11

I have around 10 QAction (this number will vary in runtime) in a toolbar, which all will do same thing, but using different parameters. I am thinking to add parameter as an attribute to QAction object, and then, QAction's triggered signal will also send object's itself to the callback function, so that I could get required parameters for the function. I have actually 2 questions about this:

  • Can it be done?
  • Is there a better way of doing this?
yasar
  • 13,158
  • 28
  • 95
  • 160

5 Answers5

46

How to pass arguments to callback functions in PyQt

You can use functools.partial from standart Python library. Example with QAction:

some_action.triggered.connect(functools.partial(some_callback, param1, param2))
reclosedev
  • 9,352
  • 34
  • 51
11

You could use a lambda function associated to the GUI control's slot to pass extra arguments to the method you want to execute.

# Create the build button with its caption
self.build_button = QPushButton('&Build Greeting', self)
# Connect the button's clicked signal to AddControl
self.build_button.clicked.connect(lambda: self.AddControl('fooData'))
def AddControl(self, name):
    print name

Source: snip2code - Using Lambda Function To Pass Extra Argument in PyQt4

Dominique Terrs
  • 609
  • 8
  • 5
4

The best way to pass the arguments is to not pass them at all. You can use the dynamic nature of python, and set whatever data you need as your own properties on the widgets themselves, get the target widget in the handler using self.sender(), and then get whatever properties you need directly from the widget.

In this example five buttons are created and the required state is set on the button widget as my_own_data property:

import sys
from PyQt4 import QtGui, QtCore

class Main(QtGui.QMainWindow):

    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.centralwidget = QtGui.QWidget()
        self.vboxlayout = QtGui.QVBoxLayout()

        for idx in range(5):
            button = QtGui.QPushButton('button ' + str(idx), None)
            button.my_own_data = str(idx)  # <<< set your own property
            button.clicked.connect(self.click_handler)  # <<< no args needed
            self.vboxlayout.addWidget(button)

        self.centralwidget.setLayout(self.vboxlayout)
        self.setCentralWidget(self.centralwidget)
        self.show()

    def click_handler(self, data=None):
        if not data:
            target = self.sender()  # <<< get the event target, i.e. the button widget
            data = target.my_own_data  # <<< get your own property
        QtGui.QMessageBox.information(self, 
                                      "you clicked me!", 
                                      "my index is {0}".format(data))

app = QtGui.QApplication(sys.argv)
main = Main()
main.show()

sys.exit(app.exec_())
ccpizza
  • 28,968
  • 18
  • 162
  • 169
  • There are some downsides to this approach, which are set out in the [docs for sender](http://doc.qt.io/qt-4.8/qobject.html#sender). As you've written it, `click_handler` cannot be safely called by anything other than the specific buttons that are connected to it. – ekhumoro Dec 02 '15 at 18:04
  • @ekhumoro: Agreed; added optional data parameter, so that `click_handler()` can be used in a more generic way. – ccpizza Dec 02 '15 at 19:25
3

You can send the action object itself using a signal mapper. However, it may be better to simply send an identifier and do all the work within the signal handler.

Here's a simple demo script:

from PyQt4 import QtGui, QtCore

class Window(QtGui.QMainWindow):
    def __init__(self):
        QtGui.QMainWindow.__init__(self)
        self.mapper = QtCore.QSignalMapper(self)
        self.toolbar = self.addToolBar('Foo')
        self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
        for text in 'One Two Three'.split():
            action = QtGui.QAction(text, self)
            self.mapper.setMapping(action, text)
            action.triggered.connect(self.mapper.map)
            self.toolbar.addAction(action)
        self.mapper.mapped['QString'].connect(self.handleButton)
        self.edit = QtGui.QLineEdit(self)
        self.setCentralWidget(self.edit)

    def handleButton(self, identifier):
        if identifier == 'One':
            text = 'Do This'
        elif identifier == 'Two':
            text = 'Do That'
        elif identifier == 'Three':
            text = 'Do Other'
        self.edit.setText(text)

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.resize(300, 60)
    window.show()
    sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • This answer doesn't seem very popular. Is `QSignalMapper` not widely used? – 101 Jul 11 '15 at 08:33
  • @figs. I don't know why the OP accepted this answer, given that the more conventional `lambda/partial` solution was already given. There was once a time when `QSignalMapper` was more useful, because PyQt hasn't always supported using `lambda/partial` as a slot - but that was a long time ago. At the moment, though, I can't think of a scenario where it might be more effective (other than in C++, of course). – ekhumoro Jul 11 '15 at 09:45
1

There is still a bug in the way PyQt uses QSignalMapper. I discovered a work-around here:

http://www.riverbankcomputing.com/pipermail/pyqt/2010-March/026113.html

To fix the answered question make the following change:

        action.triggered[()].connect(self.mapper.map)

This was needed for the following version of Python:

Python 2.6.6 (r266:84292, Feb 21 2013, 19:26:11)
[GCC 4.4.7 20120313 (Red Hat 4.4.7-3)] on linux2