简体   繁体   中英

PySide2: Open some Windows and close them

I want to have a main Window (class MyMainWindow ), from which you can launch an undefined number of other Windows (class MyWindow ), which you can use to get some information. Each of these other Windows is opened by pressing a button btnWindow in the main window and can be closed (with its (x)-button) when it is not needed any more.

All Windows are inherited from QMainWindow . So I have to keep a variable to point to them; otherwise they would be closed by the garbage collection. For this I use the list self.children . Since the main window stays open for hours and the user opens many windows and closes some of them, I want to keep track of the windows in use. For that I created an eventFilter which removes the windows to be closed from my list. But unfortunately this causes the entire application to crash with a SIGKILL or a SIGSEGV .

class MyMainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()                         # from QMainWindow
        self.setupUi(self)                         # from Ui_MainWindow
        self.children = []
        self.btnWindow.clicked.connect(self.onWindow)
        self.show()

    def onWindow(self):
        win = MyWindow()
        win.installEventFilter(self)
        self.children.append(win)

    def eventFilter(self, obj, event):
        if event.type() == QtCore.QEvent.Close and obj in self.children:
            self.children.remove(obj)
        return False

So my questions are:

  • Why does my application crash?
  • How can I avoid the crash?
  • Or is there a smarter way to keep track of the Windows in use and remove unused ones?

The QCloseEvent is "sent to widgets that the user wants to close". So, not only the event is triggered when the window is still open, but it is also an event that can be ignored .

What happens in your case is that, by removing the only reference to that window, you're destroying it. But, at that point, a reference still exists: eventFilter has not returned yet, you're just removing the object from the list but since obj is within the scope of eventFilter , it's still a valid Qt object.

After that, you're returning False : when the event filter returns False , it means that the event will be handled: this is normally done by the event() of the object itself. Qt will actually send the Close event to the widget, and do other things after that. The problem is that at that point, eventFilter() has already returned, python has lost the last reference to the widget, and then it will destroy it.

So, Qt will be doing those "other" things, while in the meantime the object ceases to exist; this causes the program to freeze/crash because Qt is trying to access an object that has been removed from memory.

The simple solution would be to return True in the filter:

    def eventFilter(self, obj, event):
        if event.type() == QEvent.Close and obj in self.children:
            self.children.remove(obj)
            return True
        return False

This should ensure that the event won't be processed any more, so you don't risk any memory access issue.

Also note that some Qt classes already have an overridden eventFilter() , so it's always good practice to return super().eventFilter(obj, event) when the event is not handled or shouldn't be completely discarded (which doesn't mean it will or won't be ignored).

An important warning, though. Multiple event filters can be added to an object, and that could create a problem: if another filter was already set, you go back to square one, since the object has been destroyed.

A better solution is to not rely on the deletion by the garbage collector, but allow Qt to do it. In your case, it can be done by setting the Qt.WA_DeleteOnClose attribute on the window, and connect to the destroyed signal for the actual removal of the python reference:

    def onWindow(self):
        def removeWin():
            if win in self.children:
                self.children.remove(win)
        win = MyWindow()
        win.setAttribute(Qt.WA_DeleteOnClose)
        self.children.append(win)
        win.destroyed.connect(removeWin)
        win.show()

An alternative solution, if you don't need a reference to those windows, is to set their parent.

For QMainWindow, you can do it just by adding the parent argument in the constructor :

QMainWindow sets the Qt::Window flag itself, and will hence always be created as a top-level widget.

So, the following is quite sufficient:

class MyWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        # ...

class MyMainWindow(QMainWindow):
    # ...
    def onWindow(self):
        win = MyWindow(self)
        win.setAttribute(Qt.WA_DeleteOnClose)
        win.show()

Or just do that in the constructor already:

class MyWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        # ...

class MyMainWindow(QMainWindow):
    # ...
    def onWindow(self):
        win = MyWindow(self)
        win.show()

For any other QWidget, you either set the flag by yourself, or you manually set the parent using the QObject.setParent() function call, since the QWidget.setParent() override automatically resets all window flags (making the widget also a "physical" child, aka: shown inside the parent).

Supposing that MyWindow is just a QWidget subclass:

    def onWindow(self):
        win = MyWindow(self)
        win.setAttribute(Qt.WA_DeleteOnClose)
        win.setWindowFlag(Qt.Window)
        win.show()

# or, alternatively:
    def onWindow(self):
        win = MyWindow() # <- no parent in the constructor
        QObject.setParent(win, self)
        win.setAttribute(Qt.WA_DeleteOnClose)
        win.show()

Obviously, as above, this can also be done in the __init__ of the MyWindow class:

    def onWindow(self):
        win = MyWindow(self)
        win.show()

class MyWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowFlag(Qt.Window)

# otherwise
class MyWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__()
        QObject.setParent(self, parent)
        self.setAttribute(Qt.WA_DeleteOnClose)

Note: it's normally good practice to not call self.show() in the __init__ of a widget.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM