PySide: Connecting Multiple Widgets to the Same Slot

As I learn PyQt and PySide, I am writing some tutorials to help my fellow travelers. Today we’ll be looking at how to connect multiple widgets to the same slot. In other words, we’ll be binding the widgets signals (basically events) to slots (i.e. callables like functions, methods) which are better known as “event handlers”. Yes, I know in PySide land that you don’t call it that. It’s a SLOT, not an event handler. But I digress. Anyway, there are two ways to approach this. One is to use functools and its partial class to pass parameters or to use PySide’s introspection abilities to grab that information from the widget that did the calling. There’s actually at least one other method that is very similar to functools.partial and that is to use the infamous anonymous function utility known as lambda. Let’s take a look at how all these methods can be done!

Getting Started

pyside_multipleButtons

If you don’t have PySide yet, go out and get it. If you want to try this with PyQt, make sure your version of PyQt is 4.3 or greater as the first example of partial in the following sample application won’t work in previous versions of PyQt. You can get PySide here. Once you’re all set up and ready to rock, go ahead and read the following code:

from functools import partial
from PySide.QtCore import SIGNAL
from PySide.QtGui import QApplication, QLabel, QWidget
from PySide.QtGui import QPushButton, QVBoxLayout

########################################################################
class MultiButtonDemo(QWidget):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        super(MultiButtonDemo, self).__init__()
        
        layout = QVBoxLayout()
        
        self.label = QLabel("You haven't pressed a button!")
        
        # use a normal signal / slot mechanic
        button1 = QPushButton("One")
        self.connect(button1, SIGNAL("clicked()"), self.one)
        
        # now let's use partial functions
        button2 = QPushButton("Two")
        self.connect(button2, SIGNAL("clicked()"),
                     partial(self.onButton, "Two"))
        
        button3 = QPushButton("Three")
        self.btn3Callback = partial(self.onButton, "Three")
        button3.clicked.connect(self.btn3Callback)
        
        # now let's try using a lambda function
        button4 = QPushButton("Four")
        button4.clicked.connect(lambda name="Four": self.onButton(name))
        
        layout.addWidget(self.label)
        layout.addWidget(button1)
        layout.addWidget(button2)
        layout.addWidget(button3)
        layout.addWidget(button4)
        self.setLayout(layout)
        
        self.setWindowTitle("PySide Demo")
        
    #----------------------------------------------------------------------
    def one(self):
        """"""
        self.label.setText("You pressed One!")
        
    #----------------------------------------------------------------------
    def onButton(self, lbl):
        """Change the label to the text (lbl) passed in"""
        self.label.setText("You pressed %s!" % lbl)

#----------------------------------------------------------------------
if __name__ == "__main__":
    app = QApplication([])
    form = MultiButtonDemo()
    form.show()
    app.exec_()

Let’s spend a little time breaking this down. We subclass QWidget to create our window. Then we use a QVBoxLayout to layout our widgets in a vertical row. Next we create a label and four buttons. The first button we hook up to an event handler is done in the normal way. In the one handler, it just updates the label to say that “You Pressed One!”. You could do this for the other 3 buttons, but that’s not the point of this article. Instead, we use Python’s functools library which has a very handy partial class. The partial class allows the developer to create a callable that you can give to your PySide signal. The first example creates a partial callable that is inaccessible to anything else. The 2nd example (button 3) of partial is assigned to a class variable, self.btn3Callback which you can then give to your signal. The last example (button 4) shows you how to do the same thing as partial, but by using Python’s lambda instead. This last method is especially popular with the Tkinter GUI toolkit.

Now we’re ready to find out how to make PySide itself tell us which button signaled the slot.

How to Make PySide Tell All

pyside_multipleButtons2

We’re actually going to refactor the code a bit to make the code shorter and to show you another way to layout your widgets. This time around, we’re also going to have all the buttons connect to just one slot (i.e. event handler / callable). Here’s the code:

from PySide.QtCore import SIGNAL
from PySide.QtGui import QApplication, QLabel, QWidget
from PySide.QtGui import QPushButton, QVBoxLayout

########################################################################
class MultiButtonDemo(QWidget):
    """"""

    #----------------------------------------------------------------------
    def __init__(self):
        """Constructor"""
        super(MultiButtonDemo, self).__init__()
        
        layout = QVBoxLayout()
        
        self.label = QLabel("You haven't pressed a button!")
        layout.addWidget(self.label)
        labels = ["One", "Two", "Three", "Four"]
        for label in labels:
            btn = QPushButton(label)
            btn.clicked.connect(self.clicked)
            layout.addWidget(btn)
        
        self.setLayout(layout)
        
        self.setWindowTitle("PySide Signals / Slots Demo")
        
    #----------------------------------------------------------------------
    def clicked(self):
        """
        Change label based on what button was pressed
        """
        button = self.sender()
        if isinstance(button, QPushButton):
            self.label.setText("You pressed %s!" % button.text())

#----------------------------------------------------------------------
if __name__ == "__main__":
    app = QApplication([])
    form = MultiButtonDemo()
    form.show()
    app.exec_()

Okay, the imports are a little shorter now as we no longer need functools. Down in the __init__ we create a list of button labels and then loop over them to create the four buttons. In the loop, we also connect the button to the callable (or the signal to the slot) and add the widget to the layout manager. According to Mark Summerfield, author of Rapid GUI Programming with Python and PyQt says in his book that the callable (slot) should be named the same as the signal as that is the convention of PyQt. My guess is that is the convention in PySide too, so we’re sticking with it.

In our clicked method/signal/callable/whatever we use self.sender() to grab the button object from whatever calls the method. Then, just to be safe, we make sure that the sender is an instance of QPushButton and if it is, then we grab the label of the button via its text() call and set the label appropriately. That’s it! Short and sweet!

Wrapping Up

Now you know several ways to pass arguments to a slot/handler. You have also learned how to ask PySide which widget called the callable. Now you can get out there and code up some cool stuff on your own.

Additional Reading

Download the Source