如何解决在 PyQt/PySide 中连接点击信号时 lambda 和部分之间的差异
在将一组按钮的多个点击信号连接到带参数的单个槽函数时,我遇到了信号槽问题。
lambda
和 functools.partial
可以如下使用:
user = "user"
button.clicked.connect(lambda: calluser(name))
from functools import partial
user = "user"
button.clicked.connect(partial(calluser,name))
虽然在某些情况下,它们的表现不同。
以下代码显示了一个示例,该示例希望在单击时打印每个按钮的文本。
但使用 lambda
方法时,输出始终为“按钮 3”。 partial
方法按预期工作。
我怎样才能找到它们的不同点?
from PyQt5 import QtWidgets
class Program(QtWidgets.QWidget):
def __init__(self):
super(Program,self).__init__()
self.button_1 = QtWidgets.QPushButton('button 1',self)
self.button_2 = QtWidgets.QPushButton('button 2',self)
self.button_3 = QtWidgets.QPushButton('button 3',self)
from functools import partial
for n in range(3):
bh = eval("self.button_{}".format(n+1))
# lambda method : always print `button 3`
# bh.clicked.connect(lambda: self.printtext(n+1))
bh.clicked.connect(partial(self.printtext,n+1))
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.button_1)
layout.addWidget(self.button_2)
layout.addWidget(self.button_3)
def printtext(self,n):
print("button {}".format(n));
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Program()
window.show()
sys.exit(app.exec_())
附言
就我个人而言,我同意接受答案中的 ButtonGroup 方法是解决此类问题的正确且优雅的方法。
这里是关于这个问题的一些参考:
Transmitting extra data with Qt Signals
解决方法
据我所知,原因是每次“partial”都会根据带有预定义参数的“printtext”函数创建一个新函数,因此您为每个按钮传递一个不同的函数。 而在 lambda 函数中,您的参数是对变量的引用,因此当您单击按钮时,循环已经运行并且该变量等于循环中的最后一个数字并打印 3。 原来你可以不偏不倚地做同样的事情(对我有用):
from PyQt5 import QtWidgets
class Program(QtWidgets.QWidget):
@staticmethod
def create_func(n):
return lambda: print('button {}'.format(n+1))
def __init__(self):
super(Program,self).__init__()
layout = QtWidgets.QVBoxLayout(self)
for n in range(3):
layout.addWidget(QtWidgets.QPushButton('button {}'.format(n+1),self,clicked=self.create_func(n)))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Program()
window.show()
sys.exit(app.exec_())
,
在讨论 lambda/partial 和其他一些问题之间的区别之前,我将简要介绍您的代码示例的一些实用修复。
有两个主要问题。第一个是 for 循环中名称绑定的常见问题,如以下问题所述:Lambda in a loop。第二个是带有默认信号参数的 PyQt 特定问题,如本问题所述:Unable to send signal from Button created in Python loop。鉴于此,您的示例可以通过连接这样的信号来修复:
bh.clicked.connect(lambda checked,n=n: self.printtext(n+1))
因此,这会将 n
的当前值缓存在默认参数中,并添加一个 checked
参数以阻止 n
被信号发出的参数覆盖。
该示例还可以从以更惯用的方式重写以消除讨厌的 eval hack 中受益:
layout = QtWidgets.QVBoxLayout(self)
for n in range(1,4):
button = QtWidgets.QPushButton(f'button {n}')
button.clicked.connect(lambda checked,n=n: self.printtext(n))
layout.addWidget(button)
setattr(self,f'button{n}',button)
或使用 QButtonGroup:
self.buttonGroup = QtWidgets.QButtonGroup(self)
layout = QtWidgets.QVBoxLayout(self)
for n in range(1,4):
button = QtWidgets.QPushButton(f'button {n}')
layout.addWidget(button)
self.buttonGroup.addButton(button,n)
setattr(self,button)
self.buttonGroup.buttonClicked[int].connect(self.printtext)
(请注意,也可以在 Qt Designer 中通过选择按钮然后在上下文菜单中选择“分配给按钮组”来创建按钮组)。
关于 lambda 和 partial 区别的问题:主要是前者在局部变量上隐式地创建了一个 closure,而后者显式地在内部存储了传递给它的参数。
python 中闭包的常见“问题”是在循环内定义的函数不捕获封闭变量的当前值。所以当函数稍后被调用时,它只能访问最后看到的值。有一个discussion about changing this behaviour fairly recently on the python-ideas mailing list,但似乎没有得出任何确定的结论。尽管如此,python 的某些未来版本可能会删除这个小疣。 partial
函数完全避免了这个问题,因为它创建了一个 callable object,将传递给它的参数存储为只读属性。因此,在 lambda
中使用默认参数显式存储变量的解决方法是有效地使用相同的方法。
这里还有一点值得一提:PyQt 和 PySide 在连接到带有默认参数的信号时的区别。似乎 PySide 将所有插槽视为用 slot decorator 装饰;而 PyQt 以不同的方式对待未修饰的插槽。以下是差异的说明:
def __init__(self):
...
self.button_1.clicked.connect(self.slot1)
self.button_2.clicked.connect(self.slot2)
self.button_3.clicked.connect(self.slot3)
def slot1(self,n=1): print(f'slot1: {n=!r}')
def slot2(self,*args,n=1): print(f'slot2: {args=!r},{n=!r}')
def slot3(self,x,n=1): print(f'slot1: {x=!r},{n!r}')
依次点击每个按钮后,产生如下输出:
PyQt5 输出:
slot1: n=False
slot2: args=(False,),n=1
slot3: x=False,n=1
PySide2 输出:
slot1: n=False
slot2: args=(),n=1
TypeError: slot3() missing 1 required positional argument: 'x'
如果插槽用 @QtCore.pyqtSlot()
修饰,则 PyQt5 的输出与上面显示的 PySide2 输出匹配。因此,如果您需要一个适用于 PyQt 和 PySide 的 lambda(或任何其他未修饰的插槽)的解决方案,您应该使用这个:
bh.clicked.connect(lambda *args,n=n: self.printtext(n))
,
你的答案在这里: Connecting slots and signals in PyQt4 in a loop
这会起作用:
from PySide2 import QtWidgets
class Program(QtWidgets.QWidget):
def __init__(self):
super(Program,self).__init__()
layout = QtWidgets.QVBoxLayout(self)
for n in range(3):
new_b = QtWidgets.QPushButton('button {}'.format(n+1),clicked=lambda n=n: print('button {}'.format(n+1)))
layout.addWidget(new_b)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Program()
window.show()
sys.exit(app.exec_())
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。