Advanced Designer topics

In this section, I will discuss more advanced topics in working with BlackAdder Designer. These are: the connecting of the individual widgets in your design using signals and slots, the adding of custom widgets to the BlackAdder Designer palette, and the actual generation and use of code with BlackAdder and with the command-line utility pyuic. Finally, I will give some attention to the generation of C++ code.

Defining signals and slots in Designer

The widgets on a form often have a relationship to each other. For instance, the OK button should ask the form to close. Clicking on a button should move selected items from one QListView to another. As you have seen before, the standard way of linking widgets in PyQt is by connecting signals with slots. You can create these connections by simply drawing a line in BlackAdder designer.

The first step is to create a design. Based on the DIalog with Buttons (right) template, we add two QListBoxes and four buttons:

Initial design.

If you right-click on the form, and then choose Connections, you will see that there are already two connections made, namely between the OK button and the Cancel button. It is our task to create more connections.

The two initial connections.

The goal of the buttons is to move items from the left listbox to the right listbox, and back. Double-arrowed buttons move everything, and single-arrowed buttons move the selection. Select the connection button ( ), and draw a line from the top button to any place on the form. A dialog pops up that lets you select from the signals of the button and the slots of the form. However, there is no slot available that says something useful like slotAddAllFromLeftToRight()!

Does this mean that you are restricted to the slots as defined in the PyQt library? Fortunately, no. You can add your own slots — but only to the form, not to the individual widgets. This is actually quite logical; later, you will generate a Python class from the .ui design. You then subclass the generated Python code to add functionality. Since you will only subclass the form, the form is the only place you will be able to add slots. If you want custom slots in your widgets, you will have to add custom widgets to Designer.

Your subclass will be a descendant of the entire form, so you can only add functionality to the form, not to the widgets. Of course, you can also create custom widgets with custom signals and slots, and use those instead of the standard QListBox. I will discuss the technique for adding custom widgets in the next section.

Lets go ahead and add our custom slots to the form. This is quite easy. Select the Slots menu item from the Edit menu, and press the New Slot button. Now you can edit the text in the Slot Properties text field. Type the name of the slot, and then enter the types of the arguments the slot should be called with, between brackets. This is not useful in our case, since we will call the slots with the clicked() signal of the buttons, and these don't pass on an argument.

Define the following four slots:

All slots are defined.

Now, you can connect the clicked() signal of each button to the right slot.

All connections are made.

The Access specifier in the slot definition dialog is only important if you want to migrate your designs to C++ at some time. "Public" means that all classes in your C++ program have access to those slots; protected means that only the generated class itself and its subclasses can access the slot. ‘Protected is as if the slotname were prefixed with a double underscore in Python.

Adding your own widgets

Not only can you add your own slots to the forms you design with BlackAdder, but you can also create custom widgets, and use those widgets in other designs. The design shown in the previous section — two listboxes and a few buttons to move items from left to right, and vice-versa — is something that's quite often needed, and is a prime candidate to turn into a widget.

Open the connections.ui file, and create a new .ui file based on the widget template. Copy everything from the form to the widget, except, of course, the OK, Cancel and Help buttons. Perhaps you will have to do the layout again; if so, use a grid layout. Create the slots again, this time for the widget, and connect them.

The DoubleListBox widget design.

Choose Compile Form from the File menu. This will generate a Python file that implements your design. For now, this is enough. As I will show later, you should subclass the generated Python file and add some real logic, and perhaps a few signals.

For now, we have a custom component, designed with BlackAdder and implemented in Python. This component we will add to the BlackAdder components palette, and use it in a dialog.

Choose Edit Custom Widgets from the Custom submenu in the Tools menu. This will open a rather complicated dialog that lets you add new widgets.

Adding a custom widget.

You must type the name of the Python class — in this case DoubleListBox — in the Class text field. The headerfile text field refers ostensibly to a C++ header file; but BlackAdder assumes that it refers to a Python file. Enter wdglistbox, if that is the name you saved your custom widget under. Do not add the extension. The choice between local and global only has a meaning for C++ and defines the type of include.

The rest of the fields are less vital. You can create a pixmap that represents your widget; if you don't, the green Trolltech logo will take be used a placeholder. You can give a default size hint and size policy. For example, if you want the double listbox to take as much space as it can get, set both policies to expanding. Our double listbox cannot contain other widgets (it is not like a groupbox), and therefore we don't check the Container Widget checkbox.

In the other tabs, we can enter the slots and signals that our widget knows; this is only useful for slots and widgets that have a meaning to the outside world. The four special slots defined in the previous section are for internal use. In a subclass of DoubleListBox, we might define a few extra signals, like:

Adding signals to a custom widget.

Note that we give a listviewitem as an argument to these signals; Python signals do have arguments, but they are untyped. Slots are not relevant for this widget, and neither are properties.

If you press OK a new item will be added to the toolbars, which you can select and put on a form. If you do so, you will see that the icon is also used to represent the widget on the form, instead of a more faithful representation of the widget. When you preview the form, you won't see the widget either; but wen you generate the form, everything will be all right.

A form using the DoubleListBox custom widget..

Layout management

It is possible to design your dialogs and widgets by plonking down elements, sizing them to your liking and placing them where you want, down to the last pixel. If you fix the size of the form and the font, you can have a perfect layout — but it will also be a layout that your users won't like. People want to resize dialogs, either to have more data visible at the same time, or to minimize the amount of space the dialog takes on their already crowded screens. Visually impaired users want to change the font size to something they can see. Furthermore, there are vast differences in default fonts between Windows systems and some other systems, like KDE, which define a different default font dependent upon screen resolution. Your pixel-precise dialog won't look so good if the user views it with a font that he chooses, not in terms of pixel size, but of points per inch — where an inch can have between 75 and 120 pixels. A twelve-point Helvetica has a lot more pixels if generated for a resolution of 120 pixels to the inch, then if it were generated for 75 pixels to the inch.

All these are good reasons to let the computer manage the layout for you. There are other reasons, too. With complex forms, doing the layout yourself quickly becomes a bore. If your application is used by people with a right-to-left alphabet, like Hebrew or Arabic, the whole layout should be mirrored. From version 3, Qt can do that for you, if only you let Qt manage your layouts. The same goes for the size of labels. If you pixel-position your controls to the width of the labels, then there won't be room for languages that use ‘fichier' for ‘file', or ‘annuleren' for ‘cancel'.

All these arguments have never before swayed developers to use automatic layout management, but with PyQt and BlackAdder, layout management is ridiculously easy (and certainly easier than manual layout). This, at least, should convert the developing masses to automatic layouting!

The Designer module of BlackAdder offers three layout managers, and a helpful tool called the spacer. The layout managers are:

By nesting layouts, together with the spacer and the sizepolicies and sizehints of each individual widget, you can create almost any layout. A good rule of thumb is perhaps that if your intended layout confuses the layout managers of the Designer, then it will probably also confuse your users.

A good layout is one that can be easily taken in with one look, and that neatly groups the various bits of data in the form. A good layout will also be simple enough that the form won't take an eternity to appear. Bear in mind that Python has to load the form, lay it out, and, most importantly, fill the various fields with relevant the data. The last step can take a lot of time. I once had to create a form that brought together about sixty pieces of information from more than twenty database tables. My client was not pleased when this form wouldn't appear in the required three seconds.

I've already discussed the classes behind the horizontal, vertical and grid layout managers: QLayout, QBoxLayout, QVBoxLayout, QHBoxLayout and QGridLayout.

You can influence the layouts by selecting them in the object hierarchy window. Interesting properties include LayoutSpacing and LayoutMargin. The first determines how much room there is between widgets; the second determines how much space the layout wants between itself and the border of the window or other layouts.

Layout manager properties.

The Horizontal Layout Manager

The horizontal layout manager lays the widgets out in one row, like the individual menu's in a menu bar, or the buttons at the bottom of a form based on the Dialog With Buttons (Bottom) dialog.

There are few widgets that are customarily laid out horizontally, most often widgets are grouped in vertical columns. The columns themselves can be grouped in a horizontal layout manager.

The Vertical Layout Manager

The vertical layout manager puts widgets in one column. This can be very useful when creating a groupbox that contains radio buttons or checkboxes. You will very seldom want to layout radio buttons in a horizontal row. Another use is the column of buttons in the Dialog with Buttons (right) template.

The Grid Layout Manager

The Grid Layout managers lays out your widgets in a square or oblong grid. If you want everything in your form to be managed in a grid, then you can simply select this layout manager from the toolbar, and then click somewhere on the background of the form. The Designer module is very clever, and will try to retain your current, manually created layout as far as possible. It can even create the difficult multi-column widgets automatically.

The Spacer object

Of course, for all its cleverness, there are situations when the Designer simply cannot determine your meaning without some help. There is no Intention Layout Manager! One useful tool to let the layout manager know your intention is the spacer object. This is an invisible (at runtime) widget that pushes other widgets away. You can use a spacer either horizontally or vertically. If you use a spacer at both sides of a widget, they will push the widget to the middle. If you use only one spacer, it will push the widget to the other side.

Playing with spacers.

What widgets can do to get the space they want

Not every widget wants to hog all the space in a dialog. A combobox, for instance, has no reason to grow vertically, while a vertical scrollbar doesn't need to get any wider. You can set the horizontal and vertical sizepolicies of your widgets in the Designer module.

However, this will not always produce the results you want — in such a case, you might be reduced to setting minimum and maximum pixel widths by hand. This may be necessary if you have a listbox or combobox that expands without limit because one of the entries is as long as a fantasy trilogy without linebreaks. To curb the tendency of the listbox to usurp all the space in the dialog, you should set its maximum width to something sensible. Note also that, alas, the layout management of the forms in the designer doesn't work exactly the same as the layout management of the running forms. You can see the difference in the preview mode.

The sizepolicy works in concord with the result of calls to the sizeHint () function — this function returns the size the widget wants to be, and the minimumSizeHint() function, which returns the absolute minimum size the widget can be. The following hints can be used for setting the sizepolicy of widgets:

  • fixed — what sizeHint() says is law — smaller nor larger is acceptable.

  • minimum — the result of sizeHint() is sufficient. It cannot be smaller, might be larger, but there's no use in growing.

  • maximum — what sizeHint() returns is the max — the widget should not be expanded, but might be shrunk without detriment.

  • preferred — the sizeHint() size is best, but the widget can be smaller without problems. It might be larger, but there's no earthly reason why it should.

  • minimumExpanding — the widget wants as much space as it can get — the more it gets, the better. No way it should shrink.

  • Expanding — the widget wants as much space as it can get, but it's still useful if it get less than the result of sizeHint().

Creating a complex form

Let's try to create a really complicated form, just to see what the automatic layout engine can do.

A quite complex dialog.

This dialog was created in the following steps:

  1. Create a new form — based the simple Dialog Template.

  2. Create two pushbuttons and place them at the right top.

  3. Create a vertical spacer item, below the buttons.

  4. Collect the buttons and the spacer in a rubber band and select the vertical layout manager. Resize the layout to the height of the dialog.

  5. Create the three listboxes, and resize them to roughly about the right size; put the three line editors below.

  6. Select the listboxes and the lineedits in a rubber band, and select the grid layout — resize the complete layout about three-quarters the height of the dialog.

  7. Create a groupbox below the constellation of listboxes and edit controls, and put, roughly vertically, three radio buttons in it.

  8. Select the groupbox and click on the vertical layout manager button. Note that if you have the object browser open, you won't see this layout manager: the groupbox takes that function.

  9. Create two checkboxes, next to each other, below the groupbox.

  10. Select the listboxes, and select the horizontal layout manager.

  11. Now select the form and then the grid layout manager.

The result should be quite pleasing — take a look at how Designer created the final grid layout. Perhaps it would be better to encase the checkboxes in a groupbox, too, but this is not essential. Some GUI design guidelines urge you to envelop everything but the OK and Cancel buttons (and perhaps the Help button if its in the same row or column) in a frame with a title. Personally, I'm in favor of that recommendation, but in this you may follow the dictates of your heart (or of your primary platform).

Ultimately, the layout management offered by the Designer is useful and sufficient for most cases; in certain cases you might want to experiment with coding the layout management yourself. This is a lot more flexible, but it takes a lot more time, too.

Generating and using Python code with pyuic

We have already converted a Designer design to Python code. This can be done using either BlackAdder, with the menu option Compile Form from the File menu, or with a stand-alone utility, such as pyuic.

The stand-alone utility pyuic has an interesting option that is currently not present in the BlackAdder IDE. Using the -x parameter, a small stub is generated at the bottom of the file that enables you to run the generated code directly.

The resulting Python file has all the hallmarks of generated code. That is to say, it is a mess you won't want to edit by hand. Especially since it will be regenerated every time you change your form.

The right way to work with these generated files is to subclass them. If you have created a form, for example frmcomplex.py, that contains the generated class FrmComplex, then your next step is to create a new Python file, dlgcomplex.py, which contains the following class definition:

Example 11-1. dlgcomplex.py — a subclass of frmcomplex.py

#
# dglcomplex.py
#
import sys
from qt import *
from frmcomplex import FrmComplex                          (1)

class DlgComplex (FrmComplex):                             (2)
    def __init__(self, parent = None,name = None,modal = 0,fl = 0):
        FrmComplex.__init__(self, parent, name, fl)        (3)

    def accept(self):                                      (4)
        print "OK is pressed"
        FrmComplex.accept(self)                            (5)

    def reject(self):
        print "Cancel pressed"
        QDialog.reject(self)                               (6)

if __name__ == '__main__':                                 (7)
    a = QApplication(sys.argv)
    QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()'))
    w = DlgComplex()                                       (8)
    a.setMainWidget(w)
    w.show()
    a.exec_loop()
        
(1)
Importing the generated class
(2)
This form is a subclass of the generated class
(3)
By passing self to the parent class, all references in the parent class will be routed via the definitions in the subclass.
(4)
Any slot of method of QDialog can be reimplemented in a subclass of QDialog. In this case, the accept() and reject() methods are re-implemented to add custom behavior to the OK and Cancel actions. Remember that we have already created the connections between the clicked() signals of these buttons and these methods in the Designer.
(5)
However, if you want to make use of the default functionality of the QDialog class, you must also call the implementation of the subclassed function in the parent class.
(6)
Because the generated code in FrmComplex doesn't really add anything, calling QDialog.reject() works just as well.
(7)
This is a stub main to test the dialog.
(8)
Make sure you instantiate the right class: the DlgComplex, not the frmComplex! Cutting and pasting can lead to difficult-to-find bugs —I have all too often copied the stub from the parent file and forgot to change the classname...

The next move is extending the constructor of the derived class to set the initial values of the various widgets.

Example 11-2. Setting default values

    def __init__(self, parent = None,name = None,modal = 0,fl = 0):
        FrmComplex.__init__(self, parent, name, fl)

        self.ListBox2.insertItem("That's a turnip")
        self.ListBox2.insertItem("Further nonsense")

        self.RadioButton1.setChecked(1)
        

As you can see, it's simply a matter of remembering what names you gave each widget, and inserting stuff — no rocket science here.

Accessing the values of each widget after the user has pressed OK is just as easy. A dialog may disappear from screen when the user presses OK, but that does not mean that the dialog has disappeared from memory. As long as there is a variable that points to the dialog, you can access each and every field.

Generating C++ code with uic

Qt is originally a C++ toolkit — and if you acquire a license for Qt, be it the free, GPLed Unix/X11 version or the (non-)commercial Windows/Unix license, you can take the .ui files you have created with BlackAdder and compile them to C++ using the uic utility.

C++ is a bit more complicated than Python, and this is reflected in the more complex procedure you need to follow when converting a .ui to C++. First of all, you need to generate the header files with the uic -o dialog.h dialog.ui command. Next, you generate the actual C++ implementation with the uic -i dialog.h -o dialog.cpp dialog.ui command. The -i tells uic to include the header file, dialog.h.

From that moment on, the work is the same as with Python. You subclass the generated code, adding real implementation logic. Clever usage will include using make to autogenerate the header and implementation files to ensure that the design of the forms in the compiled app always corresponds to the latest designs.