Designing forms

One important feature of BlackAdder and PyQt is the visual gui designer. You can use the designer to easily create all kinds of dialog windows, and even custom widgets.

The definition of these user-interface designs is saved in files in an XML format, and you can easily translate those to Python code. The beauty of the system is that you can just as easily translate your designs to valid C++ code, making it easy to develop your prototype in Python and, when satisfied, port the whole gui to fast, compiled C++. (C++ code, I might add, that compiles just as well on Windows, Unix/X11, OS X and embedded systems).

The version of Qt Designer that is included with Qt3 can also create complete main windows with menus and toolbars. Once pyuic has been updated to include those elements, you can use this in your Python projects, too.

Let's use Designer to design a small form that would be useful for connecting to some system, and hook it up in a small PyQt program.

A form in the gui designer

It's quite easy to work with designer — just keep in mind that you never have to place items pixel-perfect. Just bang widgets of roughly the right size in roughly the right places, add the Qt layout managers, and see them work their magic.

You add a layout manager by selecting the widgets you want to be managed, and then selecting the right layout manager from the toolbar.

In the above design, there are three layout managers: the buttons on the right are stacked, the widgets inside the bevel are in a grid, and everything in the form is in another grid. Try making the dialog larger and smaller — it will always look good. Even better, if a visually impaired user chooses a large system font, say Arial 24 points bold, the form will still look good.

You can either compile the .ui file to Python code from BlackAdder or from the command-line. The result will be something like this:

Example 6-6. frmconnect.py

# Form implementation generated from reading ui file 'frmconnect.ui'
#
# Created: Wed Feb 28 21:34:40 2001
#      by: The Python User Interface Compiler (pyuic)
#
# WARNING! All changes made in this file will be lost!


from qt import *


class frmConnect(QDialog):
    def __init__(self,parent = None,name = None,modal = 0,fl = 0):
        QDialog.__init__(self,parent,name,modal,fl)

        if name == None:
            self.setName('frmConnect')

        self.resize(547,140)
        self.setCaption(self.tr('Connecting'))
        self.setSizeGripEnabled(1)
        frmConnectLayout = QGridLayout(self)
        frmConnectLayout.setSpacing(6)
        frmConnectLayout.setMargin(11)

        Layout5 = QVBoxLayout()
        Layout5.setSpacing(6)
        Layout5.setMargin(0)

        self.buttonOk = QPushButton(self,'buttonOk')
        self.buttonOk.setText(self.tr('&OK'))
        self.buttonOk.setAutoDefault(1)
        self.buttonOk.setDefault(1)
        Layout5.addWidget(self.buttonOk)

        self.buttonCancel = QPushButton(self,'buttonCancel')
        self.buttonCancel.setText(self.tr('&Cancel'))
        self.buttonCancel.setAutoDefault(1)
        Layout5.addWidget(self.buttonCancel)

        self.buttonHelp = QPushButton(self,'buttonHelp')
        self.buttonHelp.setText(self.tr('&Help'))
        self.buttonHelp.setAutoDefault(1)
        Layout5.addWidget(self.buttonHelp)
        spacer = QSpacerItem(20,20,QSizePolicy.Minimum,QSizePolicy.Expanding)
        Layout5.addItem(spacer)

        frmConnectLayout.addLayout(Layout5,0,1)

        self.grpConnection = QGroupBox(self,'grpConnection')
        self.grpConnection.setSizePolicy(QSizePolicy(5,7,self.grpConnection. \
        sizePolicy().hasHeightForWidth()))
        
        self.grpConnection.setTitle(self.tr(''))
        self.grpConnection.setColumnLayout(0,Qt.Vertical)
        self.grpConnection.layout().setSpacing(0)
        self.grpConnection.layout().setMargin(0)
        grpConnectionLayout = QGridLayout(self.grpConnection.layout())
        grpConnectionLayout.setAlignment(Qt.AlignTop)
        grpConnectionLayout.setSpacing(6)
        grpConnectionLayout.setMargin(11)

        self.lblName = QLabel(self.grpConnection,'lblName')
        self.lblName.setText(self.tr('&Name'))

        grpConnectionLayout.addWidget(self.lblName,0,0)

        self.lblHost = QLabel(self.grpConnection,'lblHost')
        self.lblHost.setText(self.tr('&Host'))

        grpConnectionLayout.addWidget(self.lblHost,2,0)

        self.lblPasswd = QLabel(self.grpConnection,'lblPasswd')
        self.lblPasswd.setText(self.tr('&Password'))

        grpConnectionLayout.addWidget(self.lblPasswd,1,0)

        self.txtPasswd = QLineEdit(self.grpConnection,'txtPasswd')
        self.txtPasswd.setMaxLength(8)
        self.txtPasswd.setEchoMode(QLineEdit.Password)

        grpConnectionLayout.addWidget(self.txtPasswd,1,1)

        self.cmbHostnames = QComboBox(0,self.grpConnection,'cmbHostnames')

        grpConnectionLayout.addWidget(self.cmbHostnames,2,1)

        self.txtName = QLineEdit(self.grpConnection,'txtName')
        self.txtName.setMaxLength(8)

        grpConnectionLayout.addWidget(self.txtName,0,1)

        frmConnectLayout.addWidget(self.grpConnection,0,0)

        self.connect(self.buttonOk,SIGNAL('clicked()'),self,SLOT('accept()'))
        self.connect(self.buttonCancel,SIGNAL('clicked()'), \
        self,SLOT('reject()'))

        self.setTabOrder(self.txtName,self.txtPasswd)
        self.setTabOrder(self.txtPasswd,self.cmbHostnames)
        self.setTabOrder(self.cmbHostnames,self.buttonOk)
        self.setTabOrder(self.buttonOk,self.buttonCancel)
        self.setTabOrder(self.buttonCancel,self.buttonHelp)

        self.lblName.setBuddy(self.txtName)
        self.lblPasswd.setBuddy(self.txtName)
        

Now this looks pretty hideous — but fortunately you'll never have to hack it. You would lose all your changes anyway, the next time you make a change to your design and regenerate the Python code. The best thing to do is to subclass this form with code that actually fills the dialog with data and perfoms an action upon closing it. I like to keep the names of the generated form and the subclassed form related, and I tend to refer to the first as a form, and the second as dialog — hence the prefix frmXXX for generated forms and dlgXXX for the dialogs.

For example:

Example 6-7. dlgconnect.py — the subclass of the generated form

import sys
from qt import *

from frmconnect import frmConnect

class dlgConnect(frmConnect):

    def __init__(self, parent=None):
        frmConnect.__init__(self, parent)
        self.txtName.setText("Baldrick")
        for host in ["elizabeth","george", "melchett"]:
            self.cmbHostnames.insertItem(host)

    def accept(self):
        print self.txtName.text()
        print self.txtPasswd.text()
        print self.cmbHostnames.currentText()
        frmConnect.accept(self)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    QObject.connect(app, SIGNAL('lastWindowClosed()'),
                    app, SLOT('quit()'))
    win = dlgConnect()
    app.setMainWidget(win)
    win.show()
    app.exec_loop()
        

As you can see, we have subclassed the generated form. In the constructor, the various fields are filled with a bit of data. Note that we can simply use Python string objects in setText() methods. Qt uses a special string object, QString for all its textual data, but PyQt automatically translates both Python strings and Python unicode strings to these QString objects. There are some complications, which we deal with in Chapter 8, but the translation is mostly transparent.

When you press the OK button, Qt calls the accept() method of the dialog class, in this case dlgConnect, which inherits frmConnect, which inherits QDialog. The accept() method prints out the contents of the fields. Then the accept() method of the parent class — ultimately QDialog — is called, and the dialog is closed.