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:
slotAddAll()
slotAddSelection()
slotRemoveAll()
slotRemoveSelection()
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:
sigItemAdded(ListViewItem)
sigItemDeleted(ListViewItem)
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:
horizontal
vertical
grid
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:
Create a new form — based
the simple Dialog Template.
Create two pushbuttons and place
them at the right top.
Create a vertical spacer item,
below the buttons.
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.
Create the three listboxes, and
resize them to roughly about the right size; put the
three line editors below.
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.
Create a groupbox below the
constellation of listboxes and edit controls, and put,
roughly vertically, three radio buttons in it.
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.
Create two checkboxes, next to
each other, below the groupbox.
Select the listboxes, and select
the horizontal layout manager.
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
class DlgComplex (FrmComplex):
def __init__(self, parent = None,name = None,modal = 0,fl = 0):
FrmComplex.__init__(self, parent, name, fl)
def accept(self):
print "OK is pressed"
FrmComplex.accept(self)
def reject(self):
print "Cancel pressed"
QDialog.reject(self)
if __name__ == '__main__':
a = QApplication(sys.argv)
QObject.connect(a,SIGNAL('lastWindowClosed()'),a,SLOT('quit()'))
w = DlgComplex()
a.setMainWidget(w)
w.show()
a.exec_loop()
Importing the generated
class
This form is a subclass of the
generated class
By passing
self to the parent class, all
references in the parent class will be routed
via the definitions in the
subclass.
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.
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.
Because the generated code in
FrmComplex doesn't really add anything, calling
QDialog.reject()
works just as well.
This is a stub main to test the
dialog.
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.
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.