Qt Qt Quarterly
Qt Development Frameworks | Documentation | Qt Quarterly
« Plugging into the Web » | Adaptive Coloring for Syntax Highlighting »

Designing Custom Controls with PyQt

by David Boddie

One of the hidden features of PyQt is its ability to allow custom widget plugins to be created for Qt Designer using modules written in pure Python code. Not only does this open up some interesting possibilities for Python programmers, it also lets developers and designers experiment with rapid prototyping without the hassle of building shared libraries for plugins.

When Qt Designer was redesigned and rewritten for Qt 4, one of the main aims was to make it easier for developers to add their own custom controls to the standard Qt widgets available to designers. Although the creators of Qt Designer had C++ programmers in mind when they implemented this feature, such extensibility isn't limited to just this one language—any set of language bindings that use Qt's meta-object system can join in the fun.

A PyQt widget in Qt Designer.

In this article, we will show how to use PyQt to create widgets that can be used in Qt Designer, describe the process of making plugins, and look at the mechanisms that expose Qt's meta-object system to Python.

Creating a Custom Widget

PyQt exposes the Qt APIs to Python in a fairly conservative way, making the construction of custom widgets a familiar experience to C++ programmers. New widgets are subclassed from QWidget in the usual way, as we can see with this fully-functioning widget for entering latitude and longitude values:

class GeoLocationWidget(QWidget):

  __pyqtSignals__ = ("latitudeChanged(double)",
                     "longitudeChanged(double)")

  def __init__(self, parent = None):

     QWidget.__init__(self, parent)

     latitudeLabel = QLabel(self.tr("Latitude:"))
     self.latitudeSpinBox = QDoubleSpinBox()
     self.latitudeSpinBox.setRange(-90.0, 90.0)
     self.latitudeSpinBox.setDecimals(5)

     longitudeLabel = QLabel(self.tr("Longitude:"))
     self.longitudeSpinBox = QDoubleSpinBox()
     self.longitudeSpinBox.setRange(-180.0, 180.0)
     self.longitudeSpinBox.setDecimals(5)

     self.connect(self.latitudeSpinBox,
         SIGNAL("valueChanged(double)"),
         self, SIGNAL("latitudeChanged(double)"))
     self.connect(self.longitudeSpinBox,
         SIGNAL("valueChanged(double)"),
         self, SIGNAL("longitudeChanged(double)"))

     layout = QGridLayout(self)
     layout.addWidget(latitudeLabel, 0, 0)
     layout.addWidget(self.latitudeSpinBox, 0, 1)
     layout.addWidget(longitudeLabel, 1, 0)
     layout.addWidget(self.longitudeSpinBox, 1, 1)

Two points of interest are worth noting. Firstly, unlike in Qt Jambi and Qt Script, the syntax for connecting signals and slots follows the pattern used in C++. Secondly, the signal declarations at the start of the class definition are not strictly required by PyQt—signals with arbitrary names can be emitted without prior declaration—we will return to this point later.

Although the widget is useful as it stands, it doesn't expose any high level properties. As in C++, we define these by creating getter and setter methods inside the class definition, and use some magic to expose them to Qt's meta-object system. Here are the getter and setter methods for the latitude property:

  def latitude(self):
     return self.latitudeSpinBox.value()

  @pyqtSignature("setLatitude(double)")

  def setLatitude(self, latitude):

     if latitude != self.latitudeSpinBox.value():
         self.latitudeSpinBox.setValue(latitude)
         self.emit(SIGNAL("latitudeChanged(double)"),
                   latitude)

Just as in a C++ class, we define methods called latitude() and setLatitude() to provide the property's functionality. The declaration immediately before the setter is a special Python decorator that tells Qt that setLatitude() is a slot and that it accepts double precision floating point values. Typically, this kind of declaration is not required with PyQt—any function or method can be used as a slot—but this makes interacting with Qt Designer easier later on.

The pyqtProperty() function is used to register the property with Qt:

  latitude = pyqtProperty("double", latitude, setLatitude)

Note that the latitude name is bound to the resulting property, and the method we defined earlier is no longer directly accessible. If we wanted to keep it around, we could have used different names for the getter and for the property.

Producing a Plugin

The process of making the widget work is very similar to that for C++ widgets. Each custom widget class is represented by a plugin class that creates instances of it (as described in Qt Quarterly 16). The main difference is that Qt Designer's plugin interfaces are used via special PyQt-specific plugin classes.

A PyQt widget in Qt Designer's widget box.

To keep things short, we will only look at part of the definition of the GeoLocationPlugin class:

class GeoLocationPlugin(QPyDesignerCustomWidgetPlugin):

   def __init__(self, parent = None):

      QPyDesignerCustomWidgetPlugin.__init__(self)
      self.initialized = False

The code to initialize an instance of the class should be familiar to writers of C++ widget plugins.

The initialize() method is called by Qt Designer after the plugin has been loaded. Plugins use this opportunity to install extensions to the form editor.

   def initialize(self, formEditor):

      if self.initialized:
          return

      manager = formEditor.extensionManager()
      if manager:
          self.factory = \
              GeoLocationTaskMenuFactory(manager)
          manager.registerExtensions(
              self.factory,
              "com.trolltech.Qt.Designer.TaskMenu")

      self.initialized = True

For this plugin, we install a task menu extension to let users configure our custom widget on the form via the context menu. This is done by creating and registering a task menu factory with the form editor's extension manager.

There are a number of other methods that need to be implemented in the plugin class, but the most important ones create individual widgets and provide information about the name and location of the custom widget class:

   def createWidget(self, parent):
      return GeoLocationWidget(parent)

   def name(self):
      return "GeoLocationWidget"

   def includeFile(self):
      return "QQ_Widgets.geolocationwidget"

For widgets created with Python, the includeFile() method returns the name of the module that provides the named class. In this case, the module resides within the QQ_Widgets package.

Making a Menu

Although it is quite easy to edit the properties of the custom widget in Qt Designer's property editor, we will create a dialog that the user can open to change them in a more natural way. The dialog will be available from a new entry on the form's context menu whenever the user has selected the custom widget.

The dialog itself isn't very special. It operates on a widget passed to it from elsewhere, providing another GeoLocationWidget for the user to modify and preview changes in.

class GeoLocationDialog(QDialog):

   def __init__(self, widget, parent = None):

      QDialog.__init__(self, parent)

      self.widget = widget

      self.previewWidget = GeoLocationWidget()
      self.previewWidget.latitude = widget.latitude
      self.previewWidget.longitude = widget.longitude

      buttonBox = QDialogButtonBox()
      okButton = buttonBox.addButton(buttonBox.Ok)
      cancelButton = \
         buttonBox.addButton(buttonBox.Cancel)

      self.connect(okButton, SIGNAL("clicked()"),
                   self.updateWidget)
      self.connect(cancelButton, SIGNAL("clicked()"),
                   self, SLOT("reject()"))

      layout = QGridLayout()
      layout.addWidget(self.previewWidget, 1, 0, 1, 2)
      layout.addWidget(buttonBox, 2, 0, 1, 2)
      self.setLayout(layout)

      self.setWindowTitle(self.tr("Update Location"))

The interesting part is the slot where the changes to the preview widget are committed to the widget on the form:

   def updateWidget(self):

      formWindow = \
        QDesignerFormWindowInterface.findFormWindow(
            self.widget)

      if formWindow:
          formWindow.cursor().setProperty("latitude",
              QVariant(self.previewWidget.latitude))
          formWindow.cursor().setProperty("longitude",
              QVariant(self.previewWidget.longitude))

      self.accept()

Here, we make modifications via the form window's interface to ensure that the user's changes are recorded. This way, if the user decides that they have made a mistake, they can simply undo the changes in the normal way.

The dialog is invoked from a custom task menu extension—an object that connects a menu entry with code to open a dialog. When created, this provides an Update Location... action that the form editor adds to its context menu. We connect this action to a slot so that we can open the dialog when the user selects the corresponding menu entry.

class GeoLocationMenuEntry(QPyDesignerTaskMenuExtension):

  def __init__(self, widget, parent):

      QPyDesignerTaskMenuExtension.__init__(self, parent)

      self.widget = widget
      self.editStateAction = QAction(
          self.tr("Update Location..."), self)
      self.connect(self.editStateAction,
          SIGNAL("triggered()"), self.updateLocation)

  def preferredEditAction(self):
      return self.editStateAction

  def taskActions(self):
      return [self.editStateAction]

  def updateLocation(self):
      dialog = GeoLocationDialog(self.widget)
      dialog.exec_()

Note that the updateLocation() slot is not decorated in this case—since we make the connection, there's no need to declare it. Another short cut is the use of a Python list in the taskActions() method.

Each task menu extension is created by the task menu factory that we registered in the initialize() method of our custom widget plugin. When the user opens a context menu over a custom widget, Qt Designer creates a new task menu extension by calling the factory's createExtension() method.

class GeoLocationTaskMenuFactory(QExtensionFactory):

  def __init__(self, parent = None):

      QExtensionFactory.__init__(self, parent)

  def createExtension(self, obj, iid, parent):

      if iid != "com.trolltech.Qt.Designer.TaskMenu":
          return None

      if isinstance(obj, GeoLocationWidget):
          return GeoLocationMenuEntry(obj, parent)

      return None

The createExtension() method checks that the extension requested has the appropriate interface for a task menu extension, and only returns an instance of one if the widget it is needed for is a GeoLocationWidget.

With the task menu factory creating GeoLocationTaskMenuEntry objects on demand, the GeoLocationDialog class can be used to edit instances of GeoLocationWidget when it is installed.

Putting Things in Place

On systems where Qt Designer is able to use third party plugins, and where PyQt includes the QtDesigner module, it should be possible to use plugins written with PyQt. Unlike C++ plugins, those written in Python do not have to be compiled or otherwise prepared before we install them—we can simply copy the sources to the appropriate locations so that Qt Designer can find them.

The Python modules that provide the plugin and task menu extension are typically stored together as files in the same directory. Qt Designer plugins written in C++ are usually installed in a designer subdirectory within the directory described by QLibraryInfo::PluginPath. The convention for Python plugins is to create a python directory alongside the C++ plugins and store them in there.

The widgets themselves need to be placed in a standard location so that the Python interpreter can find them. Often, it is convenient to store them in the Python installation's site-packages directory since they are going to be needed by applications that use forms containing those widgets. The installation procedure involves creating a setup.py file which we won't discuss here—the code archive accompanying this article contains a file that can be used as a starting point for your own projects.

As an alternative to installation, environment variables can be set to refer to the locations of plugins and custom widgets. Developers familiar with Python will know that PYTHONPATH can be used to add new directories to the list of known locations of modules and packages. Similarly, PYQTDESIGNERPATH can be used to add locations of Python modules containing plugins and extensions for Qt Designer.

With the plugin, extension and custom widget installed, or their locations specified using the environment variables described above, we can now run Qt Designer and use our custom widget—it should be available from the Qt Quarterly Examples section of the widget box.

Forms that contain custom widgets can be processed with the pyuic4 tool in a way that should be familiar to users of uic. Code to import the custom widgets is generated along with the code to create the form, so developers simply need to make sure that the modules containing them are available when their applications are run.

Behind the Scenes

When writing the GeoLocationWidget class, we used three features that are not always needed when writing PyQt widgets:

Since the use of each feature makes information available to other Qt components, widgets written like this can be supplied to other QObject-based plugin systems as long as there is a way to execute their Python source code.

The use of slot and property declarations are also generally useful to Python programmers. Properties defined in this way behave just like normal Python properties. The slot declarations can be used to help connect signals to slots in a class derived from a form.

   @pyqtSignature("on_pushButton_clicked()")
   def on_pushButton_clicked(self):
      ...
      self.listWidget.addItem(
            u"%i\\xb0 %i' %i\\" N, " % self.dms(lat) + \
            u"%i\\xb0 %i' %i\\" E" % self.dms(lng))

In this case, the decorator ensures that the method is only called when a push button's clicked() signal is emitted, but not when its clicked(bool) signal is emitted.

Taking Things Further

Instructions for building and installing the widget plugins described in this article are included in the examples archive, available from the Qt Quarterly Web site. We encourage you to try them out and apply the same techniques to your own widgets.

More detailed instructions about Qt Designer plugins, PyQt and the Python tools that make them work together is available from the Riverbank Computing Web site.

Site Map Accessibility Contact