Friday, October 7, 2011

Introduction to Layouts

Layouts are a powerful feature of Qt's GUI system - what they allow you to do is completely avoid having to deal with resizing logic.

If you have ever had to write your own code to determine how to resize the contents of a desktop application - you will be thrilled to never have to deal with that ever again.

This tutorial will take you through the basics of working with laying widgets out in Qt.



Overview

I wont't be able to say it any better, so straight from the horses mouth:

"Qt includes a set of layout management classes that are used to describe how widgets are laid out in an application's user interface. These layouts automatically position and resize widgets when the amount of space available for them changes, ensuring that they are consistently arranged and that the user interface as a whole remains usable."

Qt's got some very good, straightforward docs about their layout system here.

The classes that I use 90% of the time are the QHBoxLayout, QVBoxLayout and QGridLayout.

The HBox layout allows you to organize your widgets along a horizontal row, the VBox layout allows you to organize your widgets in a vertical column, and the grid layout allows you to organize your widgets in a table form using rows and columns.

Usually, when I'm dynamically generating widgets, I use primarily the box layouts as they're easier to work with programatically - I don't have to figure out cells for the row and column manually.

Laying out Widgets vs. Setting a Widget's Layout

There are 2 steps to using the layout system in Qt: organizing widgets into a layout, and applying a layout to a widget.

Certain widgets, known as container widgets, will be able to accept a layout instance to organize the child widgets that wil be contained within it.

Other widgets should only be assigned in a layout, and that layout would somehow be applied up into a container.

Visualizing the Layouts

For this example, we'll take our previous test - and add some buttons to the window to also trigger the same events.

#!/usr/bin/python ~/workspace/pyqt/layouts/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 tool buttons
        exec_btn  = QtGui.QToolButton(self)
        show_btn  = QtGui.QToolButton(self)
        count_btn = QtGui.QToolButton(self)
        
        # layout the buttons horizontally
        layout = QtGui.QHBoxLayout()
        layout.addWidget(exec_btn)
        layout.addWidget(show_btn)
        layout.addWidget(count_btn)
        
        # create the central container widget
        widget = QtGui.QWidget(self)
        widget.setLayout(layout)
        
        self.setCentralWidget(widget)
        
        # assign the actions
        exec_btn.setDefaultAction(exec_act)
        show_btn.setDefaultAction(show_act)
        count_btn.setDefaultAction(count_act)
        
        # 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.setAttribute( QtCore.Qt.WA_DeleteOnClose )
        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_()

This is the same code for the most part from our previous example. The difference is in our constructor, we have reused our actions as buttons in our central widget.

Analyzing the New Code

There's a couple of new concepts that we just introduced, so we'll go over them one by one.

First off, we're starting to add child widgets to our main window - we're doing this by creating the 3 toolbuttons and parenting them to our self instance.

        # create the tool buttons
        exec_btn  = QtGui.QToolButton(self)
        show_btn  = QtGui.QToolButton(self)
        count_btn = QtGui.QToolButton(self)


Next, we're laying out our buttons horizontally in a QHBoxLayout.

        # layout the buttons horizontally
        layout = QtGui.QHBoxLayout()
        layout.addWidget(exec_btn)
        layout.addWidget(show_btn)
        layout.addWidget(count_btn)

This will let Qt know that we want to spread our buttons out horizontally over the available space.  If you want to lay them out vertically, try switching the layout class from QtGui.QHBoxLayout() to QtGui.QVBoxLayout() and see what the difference is.

Doing this is not enough however.  There still is no container widget that this layout is a part of.  If our base class was a QDialog we could just assign the layout to the dialog itself.  However, QMainWindow's are a little more complex since they already encorporate a few different widgets like the menu bar and tool bars.  For the main window, we need to create a new container widget, and set it as the central widget for the window.


        # create the central container widget
        widget = QtGui.QWidget(self)
        widget.setLayout(layout)
        
        self.setCentralWidget(widget)

This will create an empty QWidget to assign our layout to for its contents.  This will stretch our layout about the window as it resizes the central widget.

The last bit of code will link the actions that we created in our menu to our buttons, so if someone clicks on the button or chooses the action from the menu, it will trigger the action event.

        # assign the actions
        exec_btn.setDefaultAction(exec_act)
        show_btn.setDefaultAction(show_act)
        count_btn.setDefaultAction(count_act)

This has now linked our actions and buttons together - a useful function of the QToolButton class vs. the QPushButton class.

Improving the Layout

While we now have some buttons in our window, and have them resizing - this isn't really behaving the way that we would hope.  The buttons get stretched out wide from each other and are centered to the window.

What if we want to have our buttons stay on the top-left of our window?

What we can do is nest layouts together and assign stretch values to them to push our buttons around the way we want.

First, lets push our buttons to the left hand side of our dialog.  If you change the layout logic of the constructor to read:

        # layout the buttons horizontally
        layout = QtGui.QHBoxLayout()
        layout.addWidget(exec_btn)
        layout.addWidget(show_btn)
        layout.addWidget(count_btn)
        layout.addStretch()

And then re-run the example, you should now see some left-aligned buttons.  The addStretch method is telling the layout to expand that last section as much as possible - using up all the blank space in the layout.

The next step we want to do, is make sure our buttons stick to the top of our dialog.

To do this, we can nest our horizontal layout inside a vertical layout, and then add another stretch to the vertical layout to take up all vertical space.

Lets change the central widget logic to read:

        # create vertical layout and stretch
        vlayout = QtGui.QVBoxLayout()
        vlayout.addLayout(layout)
        vlayout.addStretch()
        
        widget.setLayout(vlayout)
        
        self.setCentralWidget(widget)
        layout.addStretch()

Note that we're now setting the vlayout layout to our container widget, not the original layout instance.

Now when we resize our dialog, our buttons stick to the top-left of our dialog.  Of course, this is quite boring...since as we resize, the buttons don't actually move anymore.

For fun...see if you can get it to stick to the bottom right instead.

Grid vs. Box Layouts

The same structure could be achieved using a single grid layout vs. using 2 box layouts - though you would have to manually set the X and Y value for each widget's cell location, as well as column spand and row span values.

Overall, I have found that when generating layouts in code that nesting layouts is an easier approach, and unless you have a crazy complex set of nesting is just as fast.

Odds are, if you have such a complex structure - then you would benefit from using the Qt designer in the first place - which we will talk about in the next lesson.

Final Example

Did you get your buttons to align to the bottom-right?  Here's the final code for this tutorial with the changes we made through the course - with the added bit of bottom-right aligning the buttons:

#!/usr/bin/python ~/workspace/pyqt/layouts/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 tool buttons
        exec_btn  = QtGui.QToolButton(self)
        show_btn  = QtGui.QToolButton(self)
        count_btn = QtGui.QToolButton(self)
        
        # layout the buttons horizontally
        layout = QtGui.QHBoxLayout()
        layout.addStretch()
        layout.addWidget(exec_btn)
        layout.addWidget(show_btn)
        layout.addWidget(count_btn)
        
        # create the central container widget
        widget = QtGui.QWidget(self)
        
        # create vertical layout and stretch
        vlayout = QtGui.QVBoxLayout()
        vlayout.addStretch()
        vlayout.addLayout(layout)
        
        widget.setLayout(vlayout)
        
        self.setCentralWidget(widget)
        
        # assign the actions
        exec_btn.setDefaultAction(exec_act)
        show_btn.setDefaultAction(show_act)
        count_btn.setDefaultAction(count_act)
        
        # 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.setAttribute( QtCore.Qt.WA_DeleteOnClose )
        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 notice the difference - we simply are adding our stretch before our widgets and nested layout.  The box layout classes will control order when drawing based on the order that each object was added to the layout.

Next, we'll start taking a look at the Qt Designer and how it can be used to ease the creation of your widgets.

No comments:

Post a Comment