Introduction

Recent versions of PyQt contain something special for developers who use Qt Designer to design the user interfaces for their applications. In addition to the full range of standard Qt widgets, you can now install your own pure Python custom widgets and use them in your designs. All the signals, slots and properties defined in Python are accessible in Designer's user interface and behave just the same as for widgets written in C++.

Background Information

Qt Designer is a user interface design tool for Qt applications that makes it possible for non-programmers to create user interfaces for applications. In many respects, it separates the design of an application from its function, leaving designers in control of its look and feel and basic usability while developers write the underlying application logic.

Most of the standard Qt widgets are available to users of Qt Designer, and these can be used to create arbitrarily complex user interfaces. However, many applications are designed to use custom widgets and components, and these often need to be made available to designers. Fortunately, Qt Designer was written with this problem in mind, and there are two ways that custom widgets can be represented.

  1. Standard widgets can be promoted to act as placeholders for custom widgets. Since a custom widget's class is a subclass of a standard widget's class, that standard widget can be used as a placeholder.

  2. You can create custom widget plugins that Qt Designer loads at run time to allow custom widgets to be used directly in the user interface design process.

In C++, developers have to create an additional plugin class, based on QDesignerCustomWidgetInterface, that informs Qt Designer about the custom widget's name and which is used to create instances of it for use in forms created by the user. The custom widget and the plugin class are then compiled together to create a plugin that Qt Designer can load. The custom widget code is often compiled separately into applications that use forms which reference the custom widget.

In Python, an additional plugin class is also needed, but developers only need to put the modules containing the custom widget and plugin class on special paths so that Qt Designer can find them. Since the custom widget code does not need to be built or combined with the plugin code, very little extra work is required in order to make custom widgets work with Qt Designer.

An Example Custom Widget

The PyAnalogClock widget is a version of the Analog Clock example supplied with Qt 4 and PyQt4 which provides a property to allow the clock to show the time in different time zones, a slot to enable the time zone to be changed in response to signals from other components, and two signals of its own to report changes to the time and time zone.

designer-analog-clock-shadow.png

Instead of quoting the example in full, we will only note the changes that need to be made to the Analog Clock example in order to integrate it with Qt Designer. Refer to the original example to see how the clock performs tasks such as painting and event handling.

Adding Signals

The first change we make to the original widget is to declare the signals the widget is able to emit. Normally, this isn't something that PyQt widgets need to do — arbitrary signals can be emitted at any time — but Qt Designer needs to be know about the custom widget's signals so that it can connect them to slots in other widgets.

class PyAnalogClock(QtGui.QWidget):

    __pyqtSignals__ = ("timeChanged(QTime)", "timeZoneChanged(int)")

We use the __pyqtSignals__ class variable to define two signals that are used to inform other components of changes to the clock's time and time zone. The variable must refer to a sequence of strings - we could have used a list instead of a tuple, for example. Signals that are declared in this way are emitted in the same way as any other signal.

We change the class's __init__() method to define an attribute to hold the time zone and, as well as connecting the timer to the widget's update() slot, we also connect it to the newly added updateTime() method.

    def __init__(self, parent = None):

        QtGui.QWidget.__init__(self, parent)
        self.timeZoneOffset = 0

        timer = QtCore.QTimer(self)
        self.connect(timer, QtCore.SIGNAL("timeout()"), self, QtCore.SLOT("update()"))
        self.connect(timer, QtCore.SIGNAL("timeout()"), self.updateTime)
        timer.start(1000)

        # ...

    def updateTime(self):

        self.emit(QtCore.SIGNAL("timeChanged(QTime)"), QtCore.QTime.currentTime())

The updateTime() method simply emits the current time every second when a time out occurs.

The timeZoneChanged() signal is emitted whenever the user, or another component, changes the time zone property. This is shown in the following section.

Adding a Property

The timeZone property is implemented using the getTimeZone() getter method, the setTimeZone() setter method, and the resetTimeZone() method.

    def getTimeZone(self):

        return self.timeZoneOffset

The getter just returns the internal time zone value.

The setTimeZone() method is also defined to be a slot -- we show this in the next section.

    def setTimeZone(self, value):

        if value != self.timeZoneOffset:
            self.timeZoneOffset = value
            self.emit(QtCore.SIGNAL("timeZoneChanged(int)"), value)
            self.update()

Note that we only change the internal timeZoneOffset attribute, emit the timeZoneChanged() signal and update the widget if the new value is different to the old one. It is especially important to avoid infinite loops with signal emissions — for example, if we connected two clocks together to keep them in sync.

Qt's property system supports properties that can be reset to their original values. This method enables the timeZone property to be reset.

    def resetTimeZone(self):

        if self.timeZoneOffset != 0:
            self.timeZoneOffset = 0
            self.emit(QtCore.SIGNAL("timeZoneChanged(int)"), 0)
            self.update()

Again, we guard against unnecessary signal emission by ensuring that the time zone is not already zero before emitting the timeZoneChanged() signal and updating the widget.

Qt-style properties are defined differently to Python's properties. To declare a property, we call pyqtProperty() to specify the type and, in this case, getter, setter and resetter methods.

    timeZone = QtCore.pyqtProperty("int", getTimeZone, setTimeZone, resetTimeZone)

Adding a Slot

Since it may be useful to be able to update the clock's time zone from other input widgets, we want to make the setTimeZone() method a slot. Normally, we don't have to do anything special to use methods as slots with PyQt, but Qt Designer needs this information to allow users to select suitable slots when connecting components together.

We use the standard decorator syntax that is used to annotate methods for use with pyuic4 and PyQt4's uic module. Here's the annotated setTimeZone() method:

    @QtCore.pyqtSignature("setTimeZone(int)")
    def setTimeZone(self, value):

        if value != self.timeZoneOffset:
            self.timeZoneOffset = value
            self.emit(QtCore.SIGNAL("timeZoneChanged(int)"), value)
            self.update()

The @pyqtSignature decorator is used to tell PyQt which argument type the method expects, and is especially useful when you want to define slots with the same name that accept different argument types. This allows the method to be a Qt slot, which means that it can be found by Qt Designer (and other C++ components) via Qt's meta-object system.

Defining the Widget's Plugin Interface

Before the widget can be used in Qt Designer, we need to prepare another class that describes our custom widget and tells Qt Designer how to instantiate it. The approach used is the same as that used for C++ plugins; the only difference being that we derive our plugin class from a PyQt-specific base class. Nonetheless, we must still implement the interface required of custom widget plugins, even if we use Python instead of C++ to do so.

The __init__() method is only used to set up the plugin and define its initialized attribute.

from PyQt4 import QtGui, QtDesigner
from analogclock import PyAnalogClock

class PyAnalogClockPlugin(QtDesigner.QPyDesignerCustomWidgetPlugin):

    def __init__(self, parent = None):

        QtDesigner.QPyDesignerCustomWidgetPlugin.__init__(self)

        self.initialized = False

The initialize() and isInitialized() methods allow the plugin to set up any required resources, ensuring that this can only happen once for each plugin.

    def initialize(self, core):

        if self.initialized:
            return

        self.initialized = True

    def isInitialized(self):

        return self.initialized

The createWidget() factory method creates new instances of our custom widget with the appropriate parent.

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

The name() method returns the name of the custom widget class that is provided by the plugin.

    def name(self):
        return "PyAnalogClock"

The group() method returns the name of the group in Qt Designer's widget box that the custom widget belongs to, and the icon() method returns the icon used to represent the custom widget in Qt Designer's widget box.

    def group(self):
        return "PyQt Examples"

    def icon(self):
        return QtGui.QIcon(_logo_pixmap)

The _logo_pixmap variable refers to a QPixmap object. This is created by including an ASCII XPM image in the source code as the _logo_16x16_xpm variable, and instantiating the QPixmap in the following way:

_logo_pixmap = QtGui.QPixmap(_logo_16x16_xpm)

The toolTip() method returns a short description of the custom widget for use in a tool tip. The whatsThis() method returns a longer description of the custom widget for use in a "What's This?" help message.

    def toolTip(self):
        return ""

    def whatsThis(self):
        return ""

Qt Designer treats container widgets differently to other types of widget. If the custom widget is intended to be used as a container for other widgets, the isContainer() method should return True, and we would need to provide another plugin class in addition to this one if we wanted to add custom editing support for this widget.

    def isContainer(self):
        return False

Since our custom widget is not a specialized container widget, this method returns False instead.

The domXml() method returns an XML description of a custom widget instance that describes default values for its properties. Each custom widget created by this plugin will be configured using this description. The XML schema can be found at http://doc.trolltech.com/designer-ui-file-format.html.

    def domXml(self):
        return (
               '<widget class="PyAnalogClock" name=\"analogClock\">\n'
               " <property name=\"toolTip\" >\n"
               "  <string>The current time</string>\n"
               " </property>\n"
               " <property name=\"whatsThis\" >\n"
               "  <string>The analog clock widget displays "
               "the current time.</string>\n"
               " </property>\n"
               "</widget>\n"
               )

Here, we provide tool tip and "What's This?" property values. These will be used for widgets placed on forms in Qt Designer.

The includeFile() method returns the module containing the custom widget class. It may include a module path in cases where the module is part of a Python package.

    def includeFile(self):
        return "analogclock"

Extending a Qt widget while maintaining Designer features

For example suppose you want to extend a QStackedWidget while maintaining useful designer features like the Insert new page menu. This is easily achieved by supplying a special XML structure (described in the schema) with the domXML method.

Key concept is the usage of customWidgets and customWidget elements and extends attribute of customWidget.

Here comes a full example: we are defining a new CSlidingPanel class which inherits QStackedWidget

    def domXml(self):
        return ("""
            <ui language="c++">
                <widget class="CSlidingPanel" name="SlidingPanel">
                    <property name="toolTip">
                        <string>{0}</string>
                    </property>
                    <property name="whatsThis">
                        <string>{1}</string>
                    </property>
                    <property name="styleSheet">
                        <string>background-color: rgb(184, 184, 184);</string>
                    </property>
                </widget>
                <customwidgets>
                    <customwidget>
                        <class>CSlidingPanel</class>
                        <extends>QStackedWidget</extends>
                    </customwidget>
               </customwidgets>
            </ui>""".format(self.toolTip(), self.whatsThis())
        )

Using the Custom Widget in Qt Designer

PyQt4 includes a common plugin loader for Qt Designer that enables widgets written in Python, with corresponding plugin interfaces defined in the way shown above, to be automatically loaded by Qt Designer when it is run. However, in order for this to work, we need to place the modules containing the custom widget and its plugin class in the appropriate locations in the file system.

By default, modules containing plugin classes should be located in the python directory inside the directory containing the other Qt plugins for Qt Designer. For testing purposes, the PYQTDESIGNERPATH environment variable can be used to refer to the location of the modules containing the plugin classes.

The modules containing the custom widgets themselves only need to be located on one of the standard paths recognized by Python, and can therefore be installed in the user's site-packages directory, or the PYTHONPATH environment variable can be set to refer to their location.

PyQt/Using_Python_Custom_Widgets_in_Qt_Designer (last edited 2014-06-08 00:43:40 by DavidBoddie)

Unable to edit the page? See the FrontPage for instructions.