Tuesday, October 4, 2011

Dialogs, Windows, and Wizards - oh My!

Introduction to the Qt Gui Framework

Qt is a very large framework, and whie it extends into many aspects of software development, one of the most popular uses is as a desktop GUI application system.

In this tutorial, we'll look at the differences between the 3 main windowing widgets: the QDialog, QMainWindow and QWizard classes.



Most of the classes that you will work with in the following tutorials  will come from the PyQt4.QtGui for UI components, and the PyQt4.QtCore package for classes to augment your code.  One of the base classes that many of the ui components will inherit from is the QWidget class, so as we start talking about the different pieces to an application - you'll start to hear the term widget used a lot.  Depending your background, that can mean many things - for the purposes of these tutorials - a widget is going to be our basic user interface type.

Each of these top-level widgets will allow you to build a standalone desktop application (or embedded application into another system - more on that to come).

And while they share many of the same properties and attributes, there are some distinct differences as well that will help you decide which class best suites your application's needs.

QDialog

The QDialog class is probably the most common class that you would use, as it is the simplest of the 3, and will cover the need of most tools.

It simply creates a top level widget that will act as a window that you can load other widgets into.

Unlike a QMainWindow, the QDialog also supports the ability to run within its own event loop - which would allow you as a developer to run the dialog and wait for the user's response.  For this reason, you'll generally use dialogs to process all of your user's input information.

Warning: If your dialog is running embedded as a plugin to another system (such as Maya or XSI), this modality may not actually extend to the root application - depending on how the Qt integration to the core was implemented.

QMainWindow

The QMainWindow class is designed with the intention of being the root window for an application.  (Thats not to say you can't use one for a popup window inside a larger app, nor that you can't use a dialog as your root window)  But, generally speaking, this is more the rule than the exception.

QMainWindows cannot run wihin their own event loop, and as such would require a bit of work to use when waiting to process a users input.  They have the added benefit of having built-in support for QMenu's and QAction's however, which, when working with the Designer becomes a very powerful tool.

The window framework also supports built-in toolbars, docking areas, and status bars - allowing for a lot more of the information and flexibility that a user would expect from a desktop application already developed.

QWizard

The QWizard class is probably the most compliated of the 3, as you don't really define widgets inside of a QWizard, but rather have to create multiple separate widgets that inherit from a QWizardPage and add them to your wizard.

A simple, straightforward example is not too complex however - but if you need to get into a situation where you are creating a non-linear wizard it starts getting a little more complex.

We'll go into more depth for wizards in a future posting, we'll just note here that it becomes very useful to use wizards if you are doing something that requires a separation between specific input and shared input - for instance if you need to define specific input for one department and then can share the same input for all departments in a later page.

Creating an Example

For now, lets just create a very simple example that would showcase each of these classes so you can see how they look and feel interacting with each other.

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

from PyQt4 import QtGui

#-------------------------------------------------------
# create the wizard classes

class MoviesPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(MoviesPage, self).__init__(parent)
        self.setTitle('Movies')
        self.setSubTitle('Setup movie specific data')

class FramesPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(FramesPage, self).__init__(parent)
        self.setTitle('Frames')
        self.setSubTitle('Setup frame specific data')

class RenderSettingsPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(RenderSettingsPage, self).__init__(parent)
        self.setTitle('Render Settings')
        self.setSubTitle('Setup common render settings for all types')

#-------------------------------------------------------
# create the dialog class

class OptionsDialog(QtGui.QDialog):
    def __init__( self, parent ):
        super(OptionsDialog, self).__init__(parent)
        self.setWindowTitle('Options')

#-------------------------------------------------------
# create the main window class

class ApplicationWindow(QtGui.QMainWindow):
    def __init__( self, parent = None ):
        super(ApplicationWindow, self).__init__(parent)
        self.setWindowTitle('Main Window')

        # tie together the different settngs through the main menu
        file_menu = self.menuBar().addMenu('File')
        action = file_menu.addAction('Export Movies...')
        action.triggered.connect( self.exportMovies )
        action = file_menu.addAction('Export Frames...')
        action.triggered.connect( self.exportFrames )
        
        options_menu = self.menuBar().addMenu('Options')
        action = options_menu.addAction('Edit Preferences')
        action.triggered.connect( self.editPreferences )

    def exportMovies( self ):
        """ Launches the export movies wizard. """
        wizard = QtGui.QWizard(self)
        wizard.addPage(MoviesPage(wizard))
        wizard.addPage(RenderSettingsPage(wizard))
        wizard.exec_()

    def exportFrames( self ):
        """ Launches the export frames wizard. """
        wizard = QtGui.QWizard(self)
        wizard.addPage(FramesPage(wizard))
        wizard.addPage(RenderSettingsPage(wizard))
        wizard.exec_()

    def editPreferences( self ):
        """ Launches the edit preferences dialog for this window. """
        dlg = OptionsDialog(self)
        dlg.exec_()

if ( __name__ == '__main__' ):
    # create the application if necessary
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
    
    window = ApplicationWindow()
    window.show()
    
    # execute the application if we've created it
    if ( app ):
        app.exec_()

So, in this example we have created an example of each kind of top level widget.

If you run the example, you can see what the difference between each is.  The root window will be your QMainWindow class and will contain menu options for launching 2 different QWizards, and a QDialog.  This would be a pretty common way to structure an application - with the main window root, and actions launching other dialogs.

Running the Application

If we start off breaking down the application, we can see some similarities to our Hello, World! example -  but some differences as well.

In the Hello, World! example, we were using the QMessageBox class, which is actually a sub-class of QDialog and didn't have to manage our application instance, since the QMessageBox.information static method runs within its own event loop.

If we look at our new code:

if ( __name__ == '__main__' ):
    # create the application if necessary
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
    
    window = ApplicationWindow()
    window.show()
    
    # execute the application if we've created it
    if ( app ):
        app.exec_()

What we are doing now, is first creating our Application in the same way as before within the __main__ scope.  But this time, we're creating our own sub-classed ApplicationWindow and showing it.

The difference between showing a widget and executing a widget is where the modality and sub-event loops come into play.  As mentioned above, the QMainWindow doesn't have the ability to run within its own event loop - so it has no exec_ method.  If we had created a QDialog or a QWizard, they would be able to be executed directly - as we do in the sub-window calls.

Instead, we have to execute our root application to start the event loop running.  To do so, we're first checking to make sure that we created the QApplication ourselves.

Warning: If there already was an application running, and this script was created as an embeded application - we would not want to call the exec method twice or it would error.

Note: If you're following along with the Qt Docs - you'll notice there is no exec_ without the underscore.  What happens sometimes is that the PyQt wrapper has to add a trailing underscore to fix naming conflicts from Qt to Python.  The exec method is a protected Python keyword and so cannot be the name of a method.  If you're looking at the C++ docs (which I highly recommend you do) its pretty much 1-to-1 to Python, but if a method is comming back as undefined - try adding a trailing underscore as it might be a protected Python term.

Defining the ApplicationWindow Class

The window that we created as our base widget we created as a subclass from the QMainWindow class.

In our constructor, we took advantage of some already created widgets that comprise a window class by adding actions to our menubar.  The result is a relatively boring window, but still was very easy to create - the best part being this exact same code will create a window for us in Linux, Windows, and MacOS.  So simple.

#-------------------------------------------------------
# create the main window class

class ApplicationWindow(QtGui.QMainWindow):
    def __init__( self, parent = None ):
        super(ApplicationWindow, self).__init__(parent)
        self.setWindowTitle('Main Window')

        # tie together the different settngs through the main menu
        file_menu = self.menuBar().addMenu('File')
        action = file_menu.addAction('Export Movies...')
        action.triggered.connect( self.exportMovies )
        action = file_menu.addAction('Export Frames...')
        action.triggered.connect( self.exportFrames )
        
        options_menu = self.menuBar().addMenu('Options')
        action = options_menu.addAction('Edit Preferences')
        action.triggered.connect( self.editPreferences )

    def exportMovies( self ):
        """ Launches the export movies wizard. """
        wizard = QtGui.QWizard(self)
        wizard.addPage(MoviesPage(wizard))
        wizard.addPage(RenderSettingsPage(wizard))
        wizard.exec_()

    def exportFrames( self ):
        """ Launches the export frames wizard. """
        wizard = QtGui.QWizard(self)
        wizard.addPage(FramesPage(wizard))
        wizard.addPage(RenderSettingsPage(wizard))
        wizard.exec_()

    def editPreferences( self ):
        """ Launches the edit preferences dialog for this window. """
        dlg = OptionsDialog(self)
        dlg.exec_()

After we created our actions, we connected them to methods to launch the sub-windows.

We'll go into more depth in this in a future post, so for now we'll just say that we've assigned different methods to respond to when a user clicks on any particular action in the window.

The two different wizard methods are there to illustrate how the same QWizardPage can be used in different QWizard classes.  We were able to reuse the logic in the RenderSettingsPage class (albeit that we don't actually have any logic in there yet) for both our Export Movies wizard and our Export Frames wizard.  The idea here being that we can share the common settings that both the Movie and Frame export would require (file location, whether to farm out the export, what the settings for the job would be, etc.) and keep the specific information for a Movie vs. a Frame on separate pages.

The third method will create our custom QDialog class and run it.

Both the wizard methods and the dialog method execute the widget vs. show it.  Each has a show method, and works similarly to the exec_ method - however would not wait for the user to finish working with the dialog before continuing.

Try changing the exec_() calls to show() and see how the interaction with your window changes.

The other difference between execution and display is the way Qt manages the memory of your dialog under the hood.  We'll cover that in the next post though - it is very important and should be read!

Defining the OptionsDialog Class

The OptionsDialog class that we've created is defined in the same fashion - we simply sub-class the QDialog class and add our own properties (which, in this example, were none).

#-------------------------------------------------------
# create the dialog class

class OptionsDialog(QtGui.QDialog):
    def __init__( self, parent ):
        super(OptionsDialog, self).__init__(parent)
        self.setWindowTitle('Options')

There really isn't much to note that's special about this class - we left it very vanilla.

Defining the Wizard Pages

The third main widget class - the QWizard - we defined in a slightly different way.  Instead of subclassing the wizard class (which, you would need to do in more complex examples), we've subclassed the QWizardPage class and are defining custom pages for a standard wizard.


#-------------------------------------------------------
# create the wizard classes

class MoviesPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(MoviesPage, self).__init__(parent)
        self.setTitle('Movies')
        self.setSubTitle('Setup movie specific data')

class FramesPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(FramesPage, self).__init__(parent)
        self.setTitle('Frames')
        self.setSubTitle('Setup frame specific data')

class RenderSettingsPage(QtGui.QWizardPage):
    def __init__( self, parent ):
        super(RenderSettingsPage, self).__init__(parent)
        self.setTitle('Render Settings')
        self.setSubTitle('Setup common render settings for all types')
Doing this allows us to create separate wizards that share the same page information - and control the flow of data via the pages, vs. the root wizard itself.

The next set of tutorials will look at some of the differences between executing an application and showing one.

1 comment: