Wednesday, October 5, 2011

Show vs. Execute

There are some important things to keep in mind when dealing with the difference between showing a window and executing one.

This tutorial will highlight some of those differences and how to protect yourself from unanticipated problems.



One of the differences that we've already seen between QDialog.show and QDialog.exec_ is the way that it blocks window execution and allows a developer to wait for user input before continuing.

What we haven't looked into is how Qt internally manages the creation and cleanup of the dialogs based on each system.

The main difference here is that when a dialog is executed, Qt determines that it does not need to preserve its allocation for that dialog, and so defaults to Python to determine when to cleanup the reference to it.  However, when a dialog is shown, Qt decides that (by default) the dialog may need to be used more than once - and so preserves its pointer and memory even if all Python references to it have been cleaned up.

Dialog Counting

Lets look at an example:

#!/usr/bin/python ~/workspace/pyqt/memory/main.py

from PyQt4 import QtCore, QtGui

class SampleDialog(QtGui.QDialog):
    def __init__(self, parent):
        super(SampleDialog, self).__init__(parent)
        self.setWindowTitle('Testing')
        self.resize(200, 100)

class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        
        # create the menu
        test_menu = self.menuBar().addMenu('Testing')
        
        # create the menu actions
        exec_act  = test_menu.addAction('Exec Dialog')
        show_act  = test_menu.addAction('Show Dialog')
        count_act = test_menu.addAction('Show Count')
        
        # create the connections
        exec_act.triggered.connect( self.execDialog )
        show_act.triggered.connect( self.showDialog )
        count_act.triggered.connect( self.showCount )
    
    def execDialog(self):
        dlg = SampleDialog(self)
        dlg.exec_()
        
    def showDialog(self):
        dlg = SampleDialog(self)
        dlg.show()
    
    def showCount(self):
        count = len(self.findChildren(QtGui.QDialog))
        QtGui.QMessageBox.information(self, 'Dialog Count', str(count))

if ( __name__ == '__main__' ):
    app = None
    if ( not app ):
        app = QtGui.QApplication([])
    
    window = MainWindow()
    window.show()
    
    if ( app ):
        app.exec_()

If you run this example and choose the Exec Dialog action a couple of times, then choose the Show Count option, you'll see the count stays steady at 0.

If you then choose the Show Dialog action a couple of times, then choose the Show Count option, you'll see the count continue to rise.

The only difference between the execDialog method and the showDialog method is how the dialog is presented to the user (from this level).

After the dlg variable exists each Python method scope - it is cleaned up from memory.  For the executed example, it is also cleaned up from Qt's memory as it has been left up to Python to decide when to clean up, but this is not the case for the shown example.

If you are unaware that this is going on - you can rack up a lot of dialogs unintentionally.

There are couple of ways to fix this problem.

Delete on Close

If you are after the same functionality that you currently see - where you want to create a dialog non-modally and you don't care how many of the same dialog are open at the same time (if you didn't notice - you can open more than 1 of the same dialog when it is not shown), then you need to set the WA_DeleteOnClose attribute on your dialog before creating it.

If you change your showDialog method to read:

    def showDialog(self):
        dlg = SampleDialog(self)
        dlg.setAttribute( QtCore.Qt.WA_DeleteOnClose )
        dlg.show()


And re-run the example - when you click on the Show Count action, it will only show you the number of dialogs that are currently open, returning to a 0 count when all dialogs are closed.

What that Window Attribute will do is let Qt know that when the user closes the dialog, it no longer will need to keep a reference to it - so it will be cleaned up from memory.

When would you use multiple-dialogs of the same type?

One example is if you have a case like showing the details on an object - you may want to let the user choose more than 1 object and popup multiple detail view dialogs for them to compare.  You won't need to access them once the user closes them, but you don't want to block interaction with your base window either.

Note: Thanks to Khang Ngo for this observation, but apparently if you are working with PySide instead of PyQt, then setting this attribute doesn't actually affect the count.  I personally haven't worked with PySide yet, so I don't know the best tips and tricks for that one, but something to be aware of.  If you know a solution to this for PySide - feel free to post a comment so others can learn!

Stored References

If what you are after is a dialog that can be shown, closed, and then re-shown - but you'll only ever need a single instance of (such as a Find dialog), then you could get around the initial problem by storing a reference to your dialog in your base class.


If you modify your constructor method to read:

    def __init__(self, parent = None):
        super(MainWindow, self).__init__(parent)
        
        # create the sample dialog
        self._sampleDialog = SampleDialog(self)
        
        # create the menu
        test_menu = self.menuBar().addMenu('Testing')
        
        # create the menu actions
        exec_act  = test_menu.addAction('Exec Dialog')
        show_act  = test_menu.addAction('Show Dialog')
        count_act = test_menu.addAction('Show Count')
        
        # create the connections
        exec_act.triggered.connect( self.execDialog )
        show_act.triggered.connect( self.showDialog )
        count_act.triggered.connect( self.showCount )

And then modify your showDialog method to read:

    def showDialog(self):
        self._sampleDialog.show()

Then what you are doing is creating a single instance of your dialog that you are reusing.  Qt and Python will both retain pointers to the dialog in memory and so can be accessed multiple times.

In this example you've split up the creation of the dialog with the showing of it.  If the dialog already is visible, then calling showDialog will have no effect, and after the user closes the dialog it will persist and can be reshown at any point.

Warning: Do NOT mix the stored dialog with the WA_DeleteOnClose property.  What will happen in this case, is Python will continue to reference the dialog, however under the hood, the C++ object will be deleted when the dialog is closed by Qt.  When the user goes to re-show the dialog, it would cause a crash.

4 comments:

  1. Just to clarify:
    Setting WA_DeleteOnClose in PySide does work.
    The issue I had with PySide was that after calling dlg.exec_() and closing it, there is no cleanup and the count remains as 1.

    ReplyDelete
  2. So setting WA_DeleteOnClose is the way to go if you want to call exec_() in PySide and have it clean up on close.

    Creating the dialog with no parent i.e. dlg = SampleDialog(None) also seems to work.

    ReplyDelete
  3. Ah, I see - I get it now - will fix the blog. Thanks!

    ReplyDelete
  4. Looks like the issue Khang Ngo discovered has been fixed in later versions of PySide.

    ReplyDelete