PyQt sending parameter to slot when connecting to a signal

Question:

I have a taskbar menu that when clicked is connected to a slot that gets the trigger event. Now the problem is that I want to know which menu item was clicked, but I don’t know how to send that information to the function connected to. Here is the used to connect the action to the function:

QtCore.QObject.connect(menuAction, 'triggered()', menuClickedFunc)

I know that some events return a value, but triggered() doesn’t. So how do I make this happen? Do I have to make my own signal?

Asked By: johannix

||

Answers:

Use a lambda

Here’s an example from the PyQt book:

self.connect(button3, SIGNAL("clicked()"),
    lambda who="Three": self.anyButton(who))

By the way, you can also use functools.partial, but I find the lambda method simpler and clearer.

Answered By: Eli Bendersky

In general, you should have each menu item connected to a different slot, and have each slot handle the functionality only for it’s own menu item. For example, if you have menu items like “save”, “close”, “open”, you ought to make a separate slot for each, not try to have a single slot with a case statement in it.

If you don’t want to do it that way, you could use the QObject::sender() function to get a pointer to the sender (ie: the object that emitted the signal). I’d like to hear a bit more about what you’re trying to accomplish, though.

Answered By: KeyserSoze

As already mentioned here you can use the lambda function to pass extra arguments to the method you want to execute.

In this example you can pass a string obj to the function AddControl() invoked when the button is pressed.

# 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

Answered By: Dominique Terrs

use functools.partial

otherwise you will find you cannot pass arguments dynamically when script is running, if you use lambda.

Answered By: Eric Wang

I’d also like to add that you can use the sender method if you just need to find out what widget sent the signal. For example:

def menuClickedFunc(self):
    # The sender object:
    sender = self.sender()
    # The sender object's name:
    senderName = sender.objectName()
    print senderName
Answered By: shalinar

There is a problem with the approaches suggested in other answers,

self.whatever.connect(lambda x: self.method(..., x))        # approach 1 (suboptimal)
self.whatever.connect(functools.partial(self.method, ...))  # approach 2 (suboptimal)

which is that they create a reference cycle: the self object holds a reference to (or is) the object with the signal, which holds a reference to the function or partial object, which holds a reference to the self object. The result is that (in CPython) none of these objects will be garbage collected when all other references to them disappear; they will only be collected the next time the cycle collector runs. They will in turn keep every other Python data structure that they refer to alive, and whatever Qt objects they collectively own. This is not exactly a memory leak, since everything is freed eventually, but it can be a problem.

There is no reference cycle if you write

self.whatever.connect(self.method)

because in both PyQt and PySide, connect has a special case for Python bound method objects: instead of holding a reference to the bound method, it extracts its two fields (__self__ and __func__) and holds a weak reference to __self__ and an ordinary reference to __func__. If __self__ goes away, the connection is automatically disconnected.

You can take advantage of that behavior with inline lambda functions by writing this:

self.whatever.connect((lambda obj, x: obj.method(..., x)).__get__(self))  # approach 1' (better)

__get__ is the method of function objects that creates bound-method objects.

You can make that a little less awkward by writing a replacement for functools.partial that returns an object of the correct magical type:

def partial_bound_method(bound_method, *args, **kwargs):
    f = functools.partialmethod(bound_method.__func__, *args, **kwargs)
    # NB: the seemingly redundant lambda is needed to ensure the correct result type
    return (lambda *args: f(*args)).__get__(bound_method.__self__)

...

self.whatever.connect(partial_bound_method(self.method, ...))  # approach 2' (better)

Here’s a test that this works as intended:

# replace PyQt5 with PyQt6/PySide2/PySide6 as appropriate
from PyQt5.QtCore import QObject, QCoreApplication
import functools, weakref

def partial_bound_method(bound_method, *args, **kwargs):
    f = functools.partialmethod(bound_method.__func__, *args, **kwargs)
    # NB: the seemingly redundant lambda is needed to ensure the correct result type
    return (lambda *args: f(*args)).__get__(bound_method.__self__)

app = QCoreApplication([])

class Class(QObject):
    def method(*args): pass

def test(maketarget):
    obj = Class()
    # the signal doesn't matter; this is one that happens to exist in QObject
    obj.objectNameChanged.connect(maketarget(obj))
    obj = weakref.ref(obj)
    print('not freed' if obj() else 'freed')

test(lambda obj: obj.method)
test(lambda obj: lambda *args: obj.method('x', *args))
test(lambda obj: functools.partial(obj.method, 'x'))
test(lambda obj: partial_bound_method(obj.method, 'x'))

That should print

freed
not freed
not freed
freed
Answered By: benrg
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.