Transmit extra data with signals in PyQt

Signals are a neat feature of Qt that allow message-passing between different areas of your program. To use a signal you attach a function to be called in the event of the signal firing, usually accepting a small item of data about the signal state.

However, there is a limitation: the signal can only emit the data it was designed to do. So for example, a QAction has a .triggered that fires when that particular action has been activated. Unfortunately the receiving connected function only receives one thing: checked=True or False. In other words, the receiving function has no way of knowing which action triggered it.

This is usually fine. You can tie a particular action to a particular function. However, sometimes you want to trigger multiple actions off the same type of action, and treat them differently. Here’s a neat trick to do just that.

Indirection

Instead of binding the target function to the signal, you can instead bind a wrapper function that accepts the original signal, attaches some more data, then passes it on. The code to do this (using a lambda) would be:

lambda checked: self.onTriggered(checked, <object>)

Here we take the checked signal, add the object it’s come from, then pass it onto the handler. All we need to do is set the object correctly when building the connect:

action = QAction()
action.triggered.connect( lambda checked: self.onTriggered(checked, action) )

Now the onTriggered handler can receive the calling action along with the check state when it’s triggered.


Ready to build your own apps?

Create Simple GUI Applications with Python and Qt

The complete beginners guide to building cross-platform GUI applications with Python. Step by step from displaying your first window, to fully functional and useable software.

Join 300+ readers already using this book to build awesome things.

77 page ebook, 2.5 hours of video tutorials


Misdirection

Unfortunately things aren’t always that simple. If you try and build multiple actions like this by looping over a set of objects you’ll get bitten. Here we’re creating a series of actions, and trying to pass the sequence number with the signal.

for a in range(0, 10):
    action = QAction()
    action.triggered.connect( lambda checked: self.onTriggered(checked, a) )

However, when the lambda is evaluated the value of a will be set to the value it had at the end of the loop, so clicking any of them will result in the same value being sent (here 9).

The solution is to pass the value in as a named parameter. By doing this the value is bound at the time the lamdba is created, and will hold the correct value whenever it is called.

lambda checked, a=a: self.onTriggered(checked, a) )

Putting this into a loop, it would look like this:

for a in range(0, 10):
    action = QAction()
    action.triggered.connect( lambda checked, a=a: self.onTriggered(checked, a) )

Here’s an example of me doing exactly that to handle outputting a list of QAction labels into a QMenu for the visual editor in Pathomx

for wid in range( self.app.views.count() ):
    if self.app.views.widget(wid).is_floatable_view:

        ve = QAction(self.app.views.tabText(wid), o)
        ve.triggered.connect( lambda n, wid=wid: self.onAddView(n, wid) )
        vmenu.addAction(ve)

Get regular GUI tips & tutorials direct to your Inbox.

Discussion

Related posts