Connecting with signals and slots

Signals and slots come in two basic varieties: Vanilla, or C++ signals and slots (as defined in the Qt library) and Pythonic (signals and slots defined in Python). Any function of any object can be used as a slot in Python (you don't even have to inherit from QObject). This contrasts to C++, where you need to specially mark a function as a slot in order to be able to connect it to a signal (and have to inherit QObject).

Every class that descends from QObject is eligible for the sending (emitting is the technical term) and connecting of signals to its own methods. That means that if your Python class is to emit signals it has to ultimately inherit QObject.

Connections are made using the connect() method. This is a class method of QObject, and you can, according to your preference, use the method on QObject, or on the actual object you're working with.

You can connect signals to slots, but also to other signals, creating a chain of notifications. If you want to disconnect signals from slots, you can use QObject.disconnect(). If you want to emit signals from a Python object, you can use the QObject.emit() function.

The connect function can take the following parameters:

If you're connecting your signals from within a class, you can often omit the third parameter — the receiver.

PyQt defines three special functions that appear to be macros (because of their all-caps spelling, as in C++) but are in fact just functions. (In fact, there are no macros in Python). These are SLOT(), SIGNAL() and PYSIGNAL().

Two of these functions are meant for signals and slots defined in C++; the other is meant for signals defined in Python. Signals and slots defined in C++ are connected on the level of C++ (i.e., not in the sip registry) and can be a bit faster.

The first function is SLOT(), which marks its only argument, a string, as a slot defined in the Qt library, i.e. in C++. The corresponding SIGNAL, which also has one string argument, marks its argument as a signal as defined in Qt.

For instance, from the documentation of QListview we can learn that this class possesses the slot invertSelection(). From the documentation of QButton we learn that it can emit a signal clicked(). We can connect a button press to this slot as follows:

Example 7-4. Connecting a signal to a slot

#
# lsv.py - connect a button to a listview
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)

        self.mainWidget=QWidget(self);                     (1)

        self.vlayout = QVBoxLayout(self.mainWidget, 10, 5)

        self.lsv = QListView(self.mainWidget)              (2)
        self.lsv.addColumn("First column")
        self.lsv.setSelectionMode(QListView.Multi)
        self.lsv.insertItem(QListViewItem(self.lsv, "One"))
        self.lsv.insertItem(QListViewItem(self.lsv, "Two"))
        self.lsv.insertItem(QListViewItem(self.lsv, "Three"))

        self.bn = QPushButton("Push Me", self.mainWidget)  (3)
        
        self.vlayout.addWidget(self.lsv)
        self.vlayout.addWidget(self.bn)

        QObject.connect(self.bn, SIGNAL("clicked()"),      (4)
                        self.lsv, SLOT("invertSelection()"))

        self.setCentralWidget(self.mainWidget)

def main(args):
    app=QApplication(args)
    win=MainWindow()
    win.show()
    app.connect(app, SIGNAL("lastWindowClosed()")
                , app
                , SLOT("quit()")
                )
    app.exec_loop()
  
if __name__=="__main__":
        main(sys.argv)
      
(1)
We want to combine a pushbutton and a listview in a main window. So we first define a single main widget that can be managed by the layout manager of QMainWindow, and then add a new layout manager to that widget. The pushbutton and the listview then become children of the main widget, self.mainWidget.

You don't need to keep self references to the widgets, because these widgets are child objects to QMainWindow. However, if you later want to access those widgets, it is necessary to have a reference.

(2)
The QListView is a child widget to the mainWidget. It has one column and owns three listview items. In order to give the pushbutton some useful work to do, we allow a multiple selection.
(3)
A very standard pushbutton — nothing special, except that is is a child of the mainWidget.
(4)
This is the actual connection between the clicked() signal of the button and the invertSelection() of the listview. If you press the button, you'll notice the effect.

Note that the arguments of SIGNAL and SLOT are used as an index of the dictionary sip keeps of available slots and signals, and that you should match the definition of the signal and slot as given in the class documentation exactly.

A more complicated signal/slot combination can pass an integer along (or even a complete object). Let's connect the knob of a QDial to a few functions, creating an color dialer. A QDial generates the valueChanged(int) signal, which passes the current value of the dial in the form of an integer to every slot that's connected to the signal. You need to explicitly enter the types of the signal arguments, but not their names.

Example 7-5. Connection a dial to a label with signals and slots

#
# dial.py — connecting a QDial to a QLabel or two
#
import sys
from qt import *

class MainWindow(QMainWindow):

    def __init__(self, *args):
        apply(QMainWindow.__init__, (self, ) + args)

        self.vlayout = QVBoxLayout(self, 10, 5)
        self.hlayout = QHBoxLayout(None, 10, 5)
        self.labelLayout=QHBoxLayout(None, 10, 5)

        self.red = 0
        self.green = 0
        self.blue = 0

        self.dialRed = QDial(0, 255, 1, 0, self)
        self.dialRed.setBackgroundColor(QColor("red"))
        self.dialRed.setNotchesVisible(1)
        self.dialGreen = QDial(0, 255, 1, 0, self)
        self.dialGreen.setBackgroundColor(QColor("green"))
        self.dialGreen.setNotchesVisible(1)
        self.dialBlue = QDial(0, 255, 1, 0, self)
        self.dialBlue.setBackgroundColor(QColor("blue"))
        self.dialBlue.setNotchesVisible(1)

        self.hlayout.addWidget(self.dialRed)
        self.hlayout.addWidget(self.dialGreen)
        self.hlayout.addWidget(self.dialBlue)

        self.vlayout.addLayout(self.hlayout)

        self.labelRed = QLabel("Red: 0", self)
        self.labelGreen = QLabel("Green: 0", self)
        self.labelBlue = QLabel("Blue: 0", self)

        self.labelLayout.addWidget(self.labelRed)
        self.labelLayout.addWidget(self.labelGreen)
        self.labelLayout.addWidget(self.labelBlue)

        self.vlayout.addLayout(self.labelLayout)

        QObject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
                        self.slotSetRed)
        QObject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
                        self.slotSetGreen)
        QObject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
                        self.slotSetBlue)

        QObject.connect(self.dialRed, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)
        QObject.connect(self.dialGreen, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)
        QObject.connect(self.dialBlue, SIGNAL("valueChanged(int)"),
                        self.slotSetColor)

    def slotSetRed(self, value):
        self.labelRed.setText("Red: " + str(value))
        self.red = value

    def slotSetGreen(self, value):
        self.labelGreen.setText("Green: " + str(value))
        self.green = value

    def slotSetBlue(self, value):
        self.labelBlue.setText("Blue: " + str(value))
        self.blue = value

    def slotSetColor(self, value):
        self.setBackgroundColor(QColor(self.red, self.green, self.blue))
        self.labelRed.setBackgroundColor(QColor(self.red, 128, 128))
        self.labelGreen.setBackgroundColor(QColor(128, self.green, 128))
        self.labelBlue.setBackgroundColor(QColor(128, 128, self.blue))

def main(args):
    app=QApplication(args)
    win=MainWindow()
    win.show()
    app.connect(app, SIGNAL("lastWindowClosed()")
                , app
                , SLOT("quit()")
                )
    app.exec_loop()

if __name__=="__main__":
        main(sys.argv)
      

Note that we connect the C++ signals (SIGNAL), to Python functions. You simply give the function object as the slot argument— not the result of the function call. Consider the difference between:

      QObject.connect(self.dialBlue,
                      SIGNAL("valueChange(int)"),
                      self.slotSetColor())
    

which is wrong, and:

      QObject.connect(self.dialBlue,
                      SIGNAL("valueChange(int)"),
                      self.slotSetColor)
    

which is right. All that difference for two little brackets! This is a rather frequent typo or thinko. (However, to give you a glimpse of the dynamic nature of Python, if you have a function that returns the correct function to connect to the signal, you do want a function call in connect().)

Note also that the number and type of arguments of the signal and the slot you want to connect have to match. When connecting C++ signals to C++ slots, there is also a bit of type-checking done.

Python signals are indicated by the PYSIGNAL() function, which also takes a string. There is no PYSLOT() function corresponding to SLOT(), because you can use any function as a slot in Python.

The argument of PYSIGNAL() is a simple string that is unique for the class from which the signal is emitted. It performs the same function as the occasion string in the small registry.py script. The difference is that PYSIGNAL() string needs to be unique only for the class, and not the whole application.

Connecting to a Python signal doesn't differ much from connecting to a C++ signal, except that you don't have to worry so much about the type and number of arguments of the signal. To rewrite the registry.py example:

Example 7-6. Python signals and slots

#
# sigslot.py  — python signals and slots
#
from qt import *

class Button(QObject):

    def clicked(self):
        self.emit(PYSIGNAL("sigClicked"), ())

class Application(QObject):

    def __init__(self):
        QObject.__init__(self)

        self.button=Button()
        self.connect(self.button, PYSIGNAL("sigClicked"),
                        self.doAppSpecificFunction)
        self.connect(self.button, PYSIGNAL("sigClicked"),
                        self.doSecondFunction)

    def doAppSpecificFunction(self):
        print "Function called"

    def doSecondFunction(self):
        print "A second function is called."

app=Application()
app.button.clicked()
      

Running this example from the command line gives the following output:

/home/boudewijn/doc/pyqt/ch6 $ python sigslot.py
A second function is called.
Function called
    

The Button emits the Python signal. Note the construction: the second argument to the emit function is a tuple that contains the arguments you want to pass on. It must always be a tuple, even if it has to be an empty tuple, or a tuple with only one element. This is shown in the next example, in which we have to explicitly create an empty tuple, and a tuple with one element from a single argument, by enclosing the argument in brackets and adding a comma:

Example 7-7. Python signals and slots with arguments

#
# sigslot2.py  — python signals and slots with arguments
#
from qt import *

class Widget(QObject):

    def noArgument(self):
        self.emit(PYSIGNAL("sigNoArgument"), ())

    def oneArgument(self):
        self.emit(PYSIGNAL("sigOneArgument"), (1, ))

    def twoArguments(self):
        self.emit(PYSIGNAL("sigTwoArguments"), (1, "two"))

class Application(QObject):

    def __init__(self):
        QObject.__init__(self)

        self.widget = Widget()

        self.connect(self.widget, PYSIGNAL("sigNoArgument"),
                        self.printNothing)
        self.connect(self.widget, PYSIGNAL("sigOneArgument"),
                        self.printOneArgument)
        self.connect(self.widget, PYSIGNAL("sigTwoArguments"),
                        self.printTwoArguments)
        self.connect(self.widget, PYSIGNAL("sigTwoArguments"),
                        self.printVariableNumberOfArguments)

    def printNothing(self):
        print "No arguments"

    def printOneArgument(self, arg):
        print "One argument", arg

    def printTwoArguments(self, arg1, arg2):
        print "Two arguments", arg1, arg2

    def printVariableNumberOfArguments(self, *args):
        print "list of arguments", args

app=Application()
app.widget.noArgument()
app.widget.oneArgument()
app.widget.twoArguments()
      

Note the usage of the *arg argument definition. This Python construct means that a variable length list of un-named arguments can be passed to a function. Thus printVariableNumberOfArguments(self, *args) fits every signal that you care to connect it to.

It's an interesting test to run this script several times: you will notice that the order in which the signals generated by twoArguments() arrive at their destination is not fixed. This means that if a signal is connected to two or more slots, the slots are not called in any particular order. However, if two signals are connected to two separate slots, then the slots are called in the order in which the signals are emitted.

The following combinations of arguments to the connect() function are possible:

Table 7-1. Matrix of QObject.connect() combinations.

Signal Connected to Syntax (Note that you can replace QObject.connect() by self.connect() everywhere.)
C++ C++ slot of another object QObject.connect(object1, SIGNAL("qtSignal()"), object2, SLOT("qtSlot()"))
C++ C++ slot of ‘self' object QObject.connect(object1, SIGNAL("qtSignal()"), SLOT("qtSlot()"))
C++ Python slot of another object QObject.connect(object1, SIGNAL("qtSignal()"), object2, pythonFunction)
C++ Python slot of ‘self' object QObject.connect(object1, SIGNAL("qtSignal()"), self.pythonFunction)
C++ C++ signal of another object QObject.connect(object1, SIGNAL("qtSignal()"), object2, SIGNAL("qtSignal()")
C++ C++ signal of ‘self' object QObject.connect(object1, SIGNAL("qtSignal()"), self, SIGNAL("qtSignal()")
Python C++ slot of another object QObject.connect(object1, PYSIGNAL("pySignal()"), object2, SLOT("qtSlot()"))
Python C++ slot of ‘self' object QObject.connect(object1, PYSIGNAL("pySignal()"), SLOT("qtSlot()"))
Python Python slot of another object QObject.connect(object1, PYSIGNAL("pySignal()"), object2.pythonFunction))
Python Python slot of ‘self' object QObject.connect(object1, PYTHON("pySignal()"), self.pythonFunction))
Python C++ signal of another object QObject.connect(object1, OYSIGNAL("pySignal()"), object2, SIGNAL("qtSignal()")
Python C++ signal of ‘self' object QObject.connect(object1, PYSIGNAL("pySignal()"), self, SIGNAL("qtSignal()")
Python Python signal of another object QObject.connect(object1, PYSIGNAL("pySignal()"), object2, PYSIGNAL("pySignal()")
Python Python signal of ‘self' object QObject.connect(object1, PYSIGNAL("pySignal()"), PYSIGNAL("pySignal()")