Reputation: 29658
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
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
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
Reputation: 1085
use functools.partial
otherwise you will find you cannot pass arguments dynamically when script is running, if you use lambda.
Upvotes: 3
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
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
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