Thursday, October 6, 2011

Introduction to Signals/Slots

So whether or not you realize it, so far all the examples we've worked through have been preparing you for event-driven programming.  The way that Qt is structured forces us into this model when working with widgets and their MVC (model-view-controller) system.

This is going to be an introduction to how that system works, becuase you should embrace their development paradigm as soon as you can, or you may end up fighting against it unnecessarily and banging your head against a wall.



In Qt lingo - events are managed via signals and slots.  A Qt Object will emit a signal when a particular event (such as a user clicking a button or an action being triggered or a selection change) is fired.  In your controller layer (any of the subclasses we've already worked with) you can listen for those signals and connect them to a slot, which, at the Python level, can just be any callable method.  You can connect your slots to any number of signals and signals to any number of slots.  In this way - Qt controls the flow of action based on how an object is listening for and informing about events, without needing to specifically know what is listening.  This makes development easily modular and expandible.

This tutorial is just going to be a quick overview of how the signal/slot system works in Python - but I would recommend reading through the Qt documentation as soon as possible to understand fully how the system works as it will be much more in-depth on the subject.

New vs. Old Style Signal Syntax

First off, lets take a look at an example from the code that we've already created in a previous tutorial:

        # create the connections
        self.ui_exec_act.triggered.connect( self.execDialog )
        self.ui_show_act.triggered.connect( self.showDialog )
        self.ui_count_act.triggered.connect( self.showCount )

This is an example of PyQt's new style connection syntax.  This was introduced to make the creation of connections more Pythonic vs. C/C++style.

The triggered property that we are connecting to is a pyqtSignal - and is creating a mapping for us to the C++ signal for us.

The old style syntax for creating this same connection would be much more like the C++ docs that you see from Qt:

        # create the connections
        self.connect(self.ui_exec_act,  QtCore.SIGNAL('triggered()'), self.execDialog)
        self.connect(self.ui_show_act,  QtCore.SIGNAL('triggered()'), self.showDialog)
        self.connect(self.ui_count_act, QtCore.SIGNAL('triggered()'), self.showCount)

This follows more closely to what you're going to see in the Qt documentation, and is just as valid as the new-style syntax, just a little ugler from a Python perspective.  It is useful to understand the way this system works for reading the docs, and for when you run into overloaded signals.

Multiple Signal Types

Qt can define multiple signals for the same name and provide different arguments.  In C++ you're specifically stating which particular signal you are going to access - but with the new style syntax - it is inferred.

If we look at the signals for a QComboBox for example, you'll see that there is there are two definitions for signals of the same name - currentIndexChanged exists with both an integer argument and a string argument.

In these cases, Qt will attempt to guess which signal to use in the new-style syntax, and may not always return the one you were expecting.

If we setup a quick test to illustrate this example:

#!/usr/bin/ python ~workspace/pyqt/signals/main.py
 
from PyQt4 import QtCore, QtGui
 
class TestDialog(QtGui.QDialog):
    def __init__( self ):
        super(TestDialog, self).__init__()
 
        testCombo = QtGui.QComboBox(self)
        testCombo.addItems(['item 1', 'Item 2'])
 
        # create connection
        testCombo.currentIndexChanged.connect( self.showResult )
 
    def showResult( self, current ):
        QtGui.QMessageBox.critical( self, 'Results', str(current) )
 
if ( __name__ == '__main__' ):
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
 
    dlg = TestDialog()
    dlg.show()
 
    if ( app ):
        app.exec_()

This will create a dialog that will popup a message whenever we change the selection in our combo box.  By default, the currentIndexChanged will return the int value.

What if we wanted to get the string value though?

To force a particular signal to be listened for, you'll need to use the old syntax.  If you switch the connection code to read:

        # create connection
        self.connect(testCombo, QtCore.SIGNAL('currentIndexChanged(const QString &)'), self.showResult)

Then, when you run this example, you can see we're now getting a string value for the current text, rather than the integer value of the current index.  One thing to note from this example, when you are working with the SIGNAL system, you will need to define the signal using the C++ arguments that it would expect: const QString & vs. str, which can get a little confusing if you're used to working mostly in Python.

Emitting Signals

As you can connect to listening for a signal, you can also force a signal to be sent out by emitting the signal. 

If you' re working on a top-level dialog - you won't usually need to trigger signals, however as you get more into custom widget development - it'll become a very important piece of developing a proper event-driven architecture.

Emitting a signal is pretty easy - as with the connect method - the new style syntax also has an emit method.  Here's a simple example (that really isn't very useful...just to show):

#!/usr/bin/ python ~workspace/pyqt/signals/second.py
 
from PyQt4 import QtCore, QtGui
 
class TestDialog(QtGui.QDialog):
    def __init__( self ):
        super(TestDialog, self).__init__()
 
        # create components
        testCombo = QtGui.QComboBox(self)
        testCombo.addItems(['item 1', 'Item 2'])
 
        emitButton = QtGui.QPushButton('Emit Signal', self)
 
        # create the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(testCombo)
        layout.addWidget(emitButton)
 
        self.setLayout(layout)
 
        # store the combo
        self._testCombo = testCombo
 
        # create connections
        testCombo.currentIndexChanged.connect( self.showResult )
        emitButton.clicked.connect( self.emitChangeSignal )
 
    def emitChangeSignal( self ):
        self._testCombo.currentIndexChanged.emit(self._testCombo.currentIndex())
 
    def showResult( self, current ):
        QtGui.QMessageBox.critical( self, 'Results', str(current) )
 
if ( __name__ == '__main__' ):
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
 
    dlg = TestDialog()
    dlg.show()
 
    if ( app ):
        app.exec_()

As you can see, we've created a new method to connect to the clicked signal from a button to then emit the currentIndexChanged signal for our combobox.  This will work with the new style syntax as long as you are using the inferred type (in the case of this example, an integer).  If you wanted to emit the signal for a string value, you would need to use the old style syntax for emitting and connecting, like so:

#!/usr/bin/ python ~workspace/pyqt/signals/second.py
 
from PyQt4 import QtCore, QtGui
 
class TestDialog(QtGui.QDialog):
    def __init__( self ):
        super(TestDialog, self).__init__()
 
        # create components
        testCombo = QtGui.QComboBox(self)
        testCombo.addItems(['item 1', 'Item 2'])
 
        emitButton = QtGui.QPushButton('Emit Signal', self)
 
        # create the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(testCombo)
        layout.addWidget(emitButton)
 
        self.setLayout(layout)
 
        # store the combo
        self._testCombo = testCombo
 
        # create connections
        self.connect(testCombo, 
                     QtCore.SIGNAL('currentIndexChanged(const QString &)'), 
                     self.showResult )
        emitButton.clicked.connect( self.emitChangeSignal )
 
    def emitChangeSignal( self ):
        self._testCombo.emit(
                  QtCore.SIGNAL('currentIndexChanged(const QString &)'),
                  self._testCombo.currentText())
        
    def showResult( self, current ):
        QtGui.QMessageBox.critical( self, 'Results', str(current) )
 
if ( __name__ == '__main__' ):
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
 
    dlg = TestDialog()
    dlg.show()
 
    if ( app ):
        app.exec_()

Signal Blocking

With this event-driven style of programming, it is possible to unintentially cause events and signals to fire without intending to.  If you're not careful, you can also accidentally setup an infinite loop by emitting a signal that also triggers the original signal.

One common cause of this is signals that are emitted when code is called vs. simply when a user interacts with something.  If you look at the signals for a QAbstractButton, you'll see a clicked(bool) and a toggled(bool) signal.  If you take a look at the docs for each, you'll see that the clicked(bool) signal will be emitted when a user switches the check box on or off - but if you set it on or off through code, it won't trigger the signal.  The toggled(bool) signal on the other hand is emitted for either situation.

If we look at the following example to create a radio button style interaction between two checkboxes, we'll see the recursive events taking place:

#!/usr/bin/ python ~workspace/pyqt/signals/blocks.py
 
from PyQt4 import QtCore, QtGui
 
class TestDialog(QtGui.QDialog):
    def __init__( self ):
        super(TestDialog, self).__init__()
 
        # create components
        self._checkA = QtGui.QCheckBox('Check 01', self)
        self._checkB = QtGui.QCheckBox('Check 02', self)
        
        # create layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._checkA)
        layout.addWidget(self._checkB)
 
        self.setLayout(layout)
 
        # creaet connections
        self._checkA.toggled.connect( self.setCheckA )
        self._checkB.toggled.connect( self.setCheckB )
 
    def setCheckA( self, state ):
        self._checkA.setChecked(True)
        self._checkB.setChecked(False)
 
    def setCheckB( self, state ):
        self._checkA.setChecked(False)
        self._checkB.setChecked(True)
 
if ( __name__ == '__main__' ):
    app = None
    if ( not QtGui.QApplication.instance() ):
        app = QtGui.QApplication([])
 
    dlg = TestDialog()
    dlg.show()
 
    if ( app ):
        app.exec_()

If I run the example as is - when I toggle on one of my check boxes, it will emit the toggled signal and then attempt to retoggle the checkboxes, re-emitting the original signal.  If you're running in a shell, you'll see a:

RuntimeError: maximum recursion depth exceeded

And the tool won't work the way you intended.

The way to bypass this problem is using the QObject.blockSignals method.  Toggling this on/off will allow you to control whether or not a widget is emitting changes.  Disabling this to perform a number of actions on a widget and then re-enabling it is a very useful way to split out processing code from listenting code.

If we refactor the above code to read:

    def setCheckA( self, state ):
        # block signals
        self._checkA.blockSignals(True)
        self._checkB.blockSignals(True)
        
        # modify checkboxes
        self._checkA.setChecked(True)
        self._checkB.setChecked(False)
        
        # unblock signals
        self._checkA.blockSignals(False)
        self._checkB.blockSignals(False)
 
    def setCheckB( self, state ):
        # block signals
        self._checkA.blockSignals(True)
        self._checkB.blockSignals(True)
        
        # modify checkboxes
        self._checkA.setChecked(False)
        self._checkB.setChecked(True)
        
        # unblock signals
        self._checkA.blockSignals(False)
        self._checkB.blockSignals(False)

We will now get our code working the way we would expect without recursive callbacks.  Now, we could have obviously used a QRadioButton instead of a QCheckBox and this logic would have been internalized - or used a QButtonGroup to handle this a bit smarter - but we're keeping this simple for the sake of the examples.

All signal/slot and event logic is at the base QObject level, so most all of the QtGui components will inherit from this and be able to utilize it.  As we get more into custom widget creation, we'll start using signals and slots much more.

1 comment:

  1. Over at http://pyqt.sourceforge.net/Docs/PyQt5/signals_slots.html there is an example how overloaded signals can be connected using the new style.

    Hope you'll find some time to continue your great tutorials!

    ReplyDelete