johannix
johannix

Reputation: 29658

PyQt sending parameter to slot when connecting to a signal

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?

Upvotes: 38

Views: 51402

Answers (6)

benrg
benrg

Reputation: 1899

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

Upvotes: 4

shalinar
shalinar

Reputation: 61

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

Upvotes: 6

Eric Wang
Eric Wang

Reputation: 1085

use functools.partial

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

Upvotes: 3

Dominique Terrs
Dominique Terrs

Reputation: 629

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

Upvotes: 23

KeyserSoze
KeyserSoze

Reputation: 2511

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.

Upvotes: 1

Eli Bendersky
Eli Bendersky

Reputation: 273366

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.

Upvotes: 54

Related Questions