Boudewijn is a senior developer at Tryllian in the Netherlands. He can be contacted at [email protected].
One of the advantages of using Python is the variety of available GUI libraries. Currently, there are several GUI toolkits in active development, including Tkinter, PyQt/PyKDE, wxPython, PyGTK/Gnome-Python, FXPy, PYFLTK, and Pythonwin. Each of these has its strengths and weaknesses. Indeed, as with C or C++, there is nearly always a GUI library perfectly suited to the project in hand. In this article I'll examine PyQt, one of the most advanced libraries, and focus on the innovative signals-and-slots paradigm it offers Python developers.
PyQt (http://www.thekompany.com/projects/pykde/) is the Python binding to the Qt library, a C++ cross-platform GUI toolkit. PyQt has been developed by Phil Thompson, together with a special interface wrapper (SIP) generator and a set of bindings to the various classes offered by the KDE desktop environment, which is based on Qt.
Qt, developed by Troll Tech (http://www.trolltech.com/), is a modern cross-platform GUI toolkit that offers a rich set of widgets that perform well. Like most modern toolkits, Qt is completely themable, both through the use of pixmaps or the implementation of new drawing primitives. It offers Unicode support on all supported platforms (Windows 95/98/NT, UNIX/X11, and embedded UNIX). Qt is well documented: Every class, function, and concept is described in the online documentation for the C++ library. Fortunately, this documentation is also easy to use with PyQt. There exists both a free and a commercial edition of Qt. However, the free edition is only available for UNIX/X11.
One of the innovations Qt offers is a novel signals-and-slots mechanism for linking objects. This mechanism provides functionality similar to the classic concept of weak references, as developed in SmallTalk. However, Eirik Eng and Haavard Nord, the designers of the system, insist that it was not inspired by SmallTalk, but was an original invention.
PyQt and Qt Concepts
PyQt is a close Python binding to Qt almost every feature Qt offers for C++ development is supported by PyQt. C++ Qt implements several of its special features such as signals-and-slots and object properties with a special-purpose preprocessor, the Meta Object Compiler (MOC). Python doesn't have a preprocessor, and signals-and-slots are implemented directly in the toolkit bindings. This is done with SIP, a smaller version of the widely used SWIG wrapper generator (http://www.swig.org/). SIP is optimized for wrapping C++ libraries and has special provisions for features implemented in C++ with MOC.
SIP is actively developed by Phil Thompson, as are the PyQt and PyKDE bindings. PyKDE is a binding to the KDE desktop environment, which itself is built upon the foundations provided by Qt. SIP supports the following C++ and MOC features:
- C++ variables.
- Qt and Python signals-and-slots.
- Overloading virtual member functions with Python class methods.
- Subclassing of C++ classes in Python.
- Static member functions.
- Protected member functions.
- Abstract classes.
- Global class instances.
- Enumerated types.
Currently not supported are properties, C++ operators, and access to protected C++ variables. Both global variables and static class variables are read-only. However, these limitations may be eliminated in the future.
Currently, PyQt supports Qt 1.44 and Qt 2.x. PyKDE supports KDE 1.1.2. Now that KDE 2.0 has gone into API freeze, it will be possible to work on KDE 2.0 bindings. SIP, PyQt, and PyKDE also form the base for the new Visual Python project, which is supported by theKompany.com.
Class Hierarchy
Most Qt classes are built on the foundation class QObject, which offers the generic signal/slot functionality. This is implemented with the connect()/disconnect() pair of methods, and the blockSignals() method.
QObject.connect() connects a listening object to an object that can emit signals, and QObject.disconnect() severs that connection. If it's useful to prevent signals from being emitted, for instance at initialization time, they can be blocked with QObject.blockSignals().
Other QObject functionality involves timers (startTimer()/killTimer()), event filters (installEventFilter()/removeEventFilter()), and the constructing of object hierarchies, where one QObject is the parent of another: insertChild(), removeChild, parent(), child(), and children().
Built on QObject is QWidget, the parent class of all visible widgets, and several nonGUI classes such as QAccel, QApplication, QClipboard, QDataPump, QDragManager, QDragObject, QFileIconProvider, QLayout, QSessionManager, QStyle, QStyleSheet, QTimer, QToolTipGroup, QTranslator, QUrlOperator, and QValidator. Visible widgets include QListView, QIconView, QSplitter, and QCanvas all the usual data-entry widgets and a comprehensive set of common dialogs. Figure 1 illustrates the Treeview widget.
In addition, there are several classes in the Qt library that are not derived from QObject, such as QString, which, from Qt Version 2.0 onward, handles strings in most encoding schemes, including Unicode. Until Python 1.6 Unicode support is finished, this is a handy class to have around if you need to work with various encoding schemes: After that, it will be perfect to transfer Python Unicode data to screen widgets.
Working with PyQt gives you almost exactly the same freedom as working with C++ Qt. For instance, it is perfectly feasible to subclass any Qt class in Python. In Listing One, the constructor, the insertItem() and the setCurrentItem() methods are reimplemented to achieve a combobox that can associate an arbitrary piece of data with a certain entry in the list. Although it is not possible to create overloaded methods in Python, it is possible to call overloaded C++ methods or to reimplement overloaded C++ methods in Python. In Listing One, I subclassed QComboBox.insertItem. The C++ header for QComboBox shows the entries in Listing Two for insertItem(...).
Although subclassing Qt classes and redefining Qt member functions seldom leads to problems in practice, there are some possible complications. PyQt determines which function is called by a combination of the number of arguments and the types of the arguments. If insertItem is not subclassed, then the type of the first argument (QString or QPixmap) decides which of the overloaded C++ functions is called.
However, if insertItem is redefined, then any call to insertItem with two or three arguments will mean a call to the Pythondefined insertItem. A call to insertItem with only one argument will mean a call to one of the Qt insertItem member functions, depending upon the type of the argument (again, QString, QPixmap, or any other type will give a TypeError). However, the third Qt insertItem definition, insertItem(const QPixmap &pixmap, const QString &text, int index=-1); is now unreachable. Depending on your intention, this might or might not be a problem.
Application Structure
PyQt applications consist of a base QApplication object that owns the visible windows. (It is also possible to write a nonGUI PyQt application one that merely uses the excellent Qt facilities for Unicode handling, for instance.) Windows are based on QMainWindow, which can have a menu, one or more toolbars, a status bar, and a central area that can be occupied by a widget that offers the application-specific interface. Listing Three is a short, yet complete, PyQt application.
Since a Qt application can have more than one window, it cannot simply quit when a window is closed; but not even the closing of the last window must always mean that the application can shut down. To allow an orderly quitting, Qt generates a signal, lastWindowClosed, which can be connected to the quit() function that terminates the application. This brings us to the main topic of this article signals-and-slots.
Signals-and-Slots
Qt has been designed to be as comfortable to use as possible, not only for users, but also for developers. One source of problems in most GUI toolkits has always been the interaction between the GUI components themselves and between the GUI components and application logic. One solution is the call-back mechanism, whereby a function pointer is passed to a widget, which then uses the pointer to call the function. Older toolkits, such as Motif, Athena, or OpenLook, use the call-back mechanism extensively. However, this mechanism is not very safe, especially not when there is a data transfer from the widget to the application. Troll Tech tried to eliminate this problem by introducing the signals/slots mechanism.
This is a very neat mechanism whereby one object can emit a signal, consisting of a signal type and a data structure, that can be caught by other objects. The emitter doesn't need to have a reference to the receiving objects, nor does a recipient need to have a reference to the emitting object.
For instance, your design might call for a knob widget that can be twiddled. Every time the knob is twiddled to a new value, another widget should show the new value. In Qt, you can have the knob emit a signal, which can be caught by another widget that then shows the value.
However, one important issue is to ensure that this value is exchanged in a typesafe way between the originating and the receiving object; in this instance, the value is a simple number, but it might be anything, from a string to a complex object. Exchanging objects of the wrong type can lead to interesting bugs. Fortunately, the signal/slot mechanism passes the desired values in a type-safe way. If you try to connect an integer-emitting signal to a string-consuming slot, you get a Python TypeError.
Listing Four illustrates how signals-and-slots work in PyQt. There's one object (in this case an instance of myKnob) that originates the signal. The signal is defined using either the SIGNAL() function (for signals defined at the C++ level) or the PYSIGNAL() function (for signals defined at the Python level). It is broadcast using the QObject.emit() method. The emit() method takes two arguments: a signal (defined with PYSIGNAL or SIGNAL) and a tuple containing the arguments. The PYSIGNAL function looks like a macro, but it is just a Python function.
There are also one or more objects (here an instance of myDisplay) that provide functions that can be called in response to signals. These functions take as arguments the arguments provided by the signal.
It's a good idea to use names for signal-handling functions ("slots" in Qt terminology) that indicate how they are used. That's why I prefer to prefix those names with slot. However, these are normal member functions and can be called from any context. Also, any and all class member functions can be used as slots in Python. This contrasts with C++, where you have to put your slots in a special slots section in the class definition.
Every descendant of QObject has inherited the connect method, but it is not necessary to use the descendants connect method it is a class method, not an instance one, so QObject.connect(...) works just as well as MyInstance.connect(...). The object where the connection between two emitting and receiving objects is defined must have a reference to them, of course. In the earlier example, we connect the Python signal sigTwiddled to the function myDisplay.slotValueChanged.
That's all really. Now whenever the knob is twiddled, the display will reflect the changed value. If the instance of myView deletes the display, the knob doesn't need to be notified and a complete decoupling is achieved.
It's important to keep in mind the difference between signals defined at the C++ level and signals defined at the Python level. If you connect signals defined at the Python level, the connection is made by the SIP library; if you connect signals defined at the C++ level, you'll be using the native Qt connection mechanism, which is slightly more efficient (although the difference is hardly noticeable). If you subclass C++ classes in Python, you can still use the C++ defined signals-and-slots; you can also add Python-defined signals-and-slots to the subclassed class.
You can connect signals to C++ slots, Python functions, or to other signals, creating a chain of signals. Table 1 lists some of the possibilities. Of course, you can also emit the signals from your Python programs for the signal/slot handler to catch. Just like slots, signals can be divided into C++ signals and Python signals. As Table 2 illustrates, the first argument is the name of the signal, the second a tuple with the arguments.
Extended Use of Signals-and-Slots
While the signal/slot mechanism has been designed primarily to enable interaction between widgets, it is not limited to GUI widgets. Any descendant of the QObject can emit or receive signals, and nothing demands that QObject descendants be GUI objects. This means that the signal/slot mechanism is usable in a much wider context. For instance, it is possible to link two abstract data structures using signals-and-slots, or an abstract data structure and several GUI widgets.
This makes the mechanism ideal for Model-View-Controller or Observer architectures, where more than one view can track changes in the model, and the model can track changes from more than one input controller. The consequence of using signals and slots is that the observer doesn't need to have a reference to the object it observes, promoting decoupling.
Comparable Constructions
The Qt signals-and-slots concept approaches the Smalltalk concept of weak references in Python. One object can react to changes in the state of second object, without the second object holding a reference to the first object. Of course, these are not classic weak references in that these are references that don't influence the actions of the garbage collector, because Python doesn't have a garbage collector. But the signal/slot connections don't count as references either, so having a connection doesn't preclude destroying the object if the last reference to it is deleted.
Be aware that in contrast to C++, Python destroys objects that no longer have any references to it. This means that if you create a Qt C++ object but don't keep a reference to it (either a direct Python one, or one from another C++ object to the created object), it will disappear immediately.
GTK appears to have developed a middle road: signals and call-back pointers. However, just like a pure call-back system, this approach is not type safe. Signals-and-slots are type safe: If you try to connect a signal that delivers a number to a slot that expects a string, a Python TypeError exception is raised. Of course, only signals-and-slots defined in C++ can raise a TypeError: Pure Python signals-and-slots share the concept of weak typing with the Python language.
There have been other attempts at building a mechanism for weak references in Python, notably by Bernhard Herzog in his excellent Sketch application (http://sketch.sourceforge.net/), with the connect.py module, which has also been used in pybiblographer.
Herzog's implementation is close to PyQt's implementation, but Herzog's is pure Python. This implementation also neatly avoids adding to the reference count of the objects by keeping the Python ID of the object in the list of connections. Listing Five is an excerpt of the actual code.
SIP Implementation
Whereas Herzog implements his module in Python using a list of Python object IDs, PyQt's implementation is implemented in C++, in the SIP library. Whenever a connection is made, SIP creates a proxy object (subclassed from QObject), which is added to a linked list of proxies. Of course, SIP only creates a proxy for Python-defined signals; C++-defined signals are connected at the Qt library level.
Interestingly, SIP is not just a wrapper generator, but also a small shared library, and it is in this library that the signal/slot mechanism is implemented, meaning that SIP actually adds functionality. Indeed, it should be possible to extract the signal/slot handling part of SIP and make it available as a general Python module, offering a fast C++ implementation of weak references. However, because the SIP implementation of signals-and-slots depends on QObject, you'd still need the Qt library.
DDJ
Listing One
from constants import TRUE, FALSE from qt import * class guiComboBox(QComboBox): def __init__(self, parent): QComboBox.__init__(self, FALSE, parent) self.setAutoCompletion(TRUE) self.data2key = {} self.key2data = {} self.connect(self, SIGNAL("activated(const char *)"), self.slotItemSelected) def insertItem(self, text, key, index=-1): QComboBox.insertItem(self, text, index) self.data2key [self.count() - 1] = key self.key2data [key]=self.count() - 1 def currentKey(self): return self.data2key[self.currentItem()] def setCurrentItem(self, key): if self.key2data.has_key(key): QComboBox.setCurrentItem(self, self.key2data[key]) def slotItemSelected(self, key): item=self.currentKey() self.emit( PYSIGNAL("itemSelected"),(item,key) )
Listing Two
class Q_EXPORT QComboBox : public QWidget ... public: ... QComboBox( QWidget *parent=0, const char *name=0 ); QComboBox( bool rw, QWidget *parent=0, const char *name=0 ); ... void insertItem( const QString &text, int index=-1 ); void insertItem( const QPixmap &pixmap, int index=-1 ); void insertItem( const QPixmap &pixmap, const QString &text, int index=-1 ); ... virtual void setCurrentItem( int index ); ...
Listing Three
from qt import * import sys class ApplicationWindow(QMainWindow): def __init__(self): QMainWindow.__init__(self, None, 'main window', Qt.WidgetFlags.WDestructiveClose) self.view=QMultiLineEdit(self) self.setCentralWidget(self.view) def main(args): app = QApplication(args) mainwin = ApplicationWindow() mainwin.show() app.connect(app, SIGNAL('lastWindowClosed()'), app, SLOT('quit()')) app.exec_loop() if __name__=="__main__": main(sys.argv)
Listing Four
class myKnob(QWidget): def __init__(self,*args): self.value=1 ... def twiddle(self): self.value=self.value + 1 self.emit(PYSIGNAL("sigTwiddled"),(self.value,)) class myDisplay(QLabel): def __init__(self,*args): ... def slotValueC`hanged(self, value): if value<>self.value: self.value=value self.setText(value) class myView(QWidget): def __init__(self, *args): ... self.display=myDisplay(self) self.knob=myKnob(self) self.connect(self.knob, PYSIGNAL("sigTwiddled"), self.display.slotValueChanged)
Listing Five
# Copyright (C) 1997, 1998, 2000 by Bernhard Herzog ... class Connector: ... def Connect(self, object, channel, function, args): idx = id(object) if self.connections.has_key(idx): channels = self.connections[idx] else: channels = self.connections[idx] = {} if channels.has_key(channel): receivers = channels[channel] else: receivers = channels[channel] = [] info = (function, args) try: receivers.remove(info) except ValueError: pass receivers.append(info) ...