PyQt: when are widgets with parents deleted?

Question:

Let’s assume I want to create a dialog box, a child of my main program:

from PyQt4 import QtGui, QtCore

class WizardJournal(QtGui.QDialog):

    def __init__(self, parent):

        super(WizardJournal, self).__init__(parent)

        self.parent = parent

        self.initUI()


    def initUI(self):

        self.parent.wizard = QtGui.QWidget()

        self.ok_button = QtGui.QPushButton("OK", self)

        self.vbox_global = QtGui.QVBoxLayout(self)

        self.vbox_global.addWidget(self.ok_button)

        self.paret.wizard.setLayout(self.vbox_global)
        self.parent.wizard.show()


if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    parent = QtGui.QWidget()
    obj = WizardJournal(parent)
    sys.exit(app.exec_())

This dialog box will be opened and closed by my main program. What is better regarding memory consumption:

  • self.ok_button = QtGui.QPushButton("OK", self)
  • self.ok_button = QtGui.QPushButton("OK")

Basically, I would like to know if I should specify the parent widget when I create a widget. When I close the dialog box, will the OK button be released from memory if I don’t specify the parent widget when it’s created?

Asked By: JPFrancoia

||

Answers:

Given the way your example is currently structured, neither the dialog nor any of its child widgets will be deleted when it is closed.

You can see this by changing the end of the example to look like this:

app.exec_()
print('n'.join(repr(w) for w in app.allWidgets()))

which will give output like this (once the dialog is closed):

<__main__.WizardJournal object at 0x7fcd850f65e8>
<PyQt4.QtGui.QPushButton object at 0x7fcd850f6708>
<PyQt4.QtGui.QWidget object at 0x7fcd850f6558>
<PyQt4.QtGui.QDesktopWidget object at 0x7fcd850f6828>
<PyQt4.QtGui.QWidget object at 0x7fcd850f6678>

In PyQt, you have to be aware that there may be two kinds of reference held for a object: one on the Python side (the PyQt wrapper object) and one on the C++ side (the underlying Qt object). So to fully delete an object, you need to remove all of these references.

In general, Qt does not delete objects unless you explictly tell it to do so. This is something you need to be aware of when creating dialogs with a parent, because it is very easy to produce a memory leak otherwise. It is common to see code written like this:

def openDialog(self):
    dialog = MyDialog(self)
    dialog.show()

Which looks harmless at first glance – but the method will create a new dialog every time it is called, and Qt will end up holding on to every single one of them (because of the parent reference on the C++ side). One way to avoid this is to re-write the method so that it only keeps a reference on the Python side:

def openDialog(self):
    self.dialog = MyDialog()
    self.dialog.show()

But what to do about a modal dialog, which must have a parent? In that case, you could initialise the dialog class like this:

class MyDialog(QtGui.QDialog):
    def __init__(self, parent):
        super(MyDialog, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

And now Qt will automatically delete the dialog when it is closed, and also recursively delete all of its child objects as well. This will just leave behind an empty PyQt wrapper object, which will (eventually) be removed by the Python garbage-collector.

So for your particular example, I think I would re-write it to look something like this:

import sys
from PyQt4 import QtGui, QtCore

class WizardJournal(QtGui.QDialog):
    def __init__(self, parent):
        super(WizardJournal, self).__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.initUI()

    def initUI(self):
        self.ok_button = QtGui.QPushButton("OK", self)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.ok_button)
        self.show()

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    parent = QtGui.QWidget()
    obj = WizardJournal(parent)
    app.exec_()
    print('n'.join(repr(w) for w in app.allWidgets()))

The dialog class is now completely self-contained, and there is only one external python reference to the instance of it. (If you need to access the parent widget from within the dialog class, you can use self.parent()).

PS: when widgets are added to a layout, they will be automatically re-parented to whatever top-level widget eventually contains the layout. So, strictly speaking, it is not necessary to explicitly set a parent for such widgets in your code.

Answered By: ekhumoro