使用Lambda表达式在PyQt中连接插槽

25

我正在尝试使用lambda函数连接插槽,但结果不如预期。在下面的代码中,我成功地正确连接了前两个按钮。对于接在循环中的后两个,出现了问题。之前有人提过同样的问题(Qt - Connect slot with argument using lambda),但这个解决方案对我不起作用。我已经盯着屏幕半个小时,但我无法弄清楚我的代码有何不同。

class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(QtGui.QWidget, self).__init__()

        main_layout = QtGui.QVBoxLayout(self)

        # Works:
        self.button_1 = QtGui.QPushButton('Button 1 manual', self)
        self.button_2 = QtGui.QPushButton('Button 2 manual', self)
        main_layout.addWidget(self.button_1)
        main_layout.addWidget(self.button_2)

        self.button_1.clicked.connect(lambda x:self.button_pushed(1))
        self.button_2.clicked.connect(lambda x:self.button_pushed(2))

        # Doesn't work:
        self.buttons = []
        for idx in [3, 4]:
            button = QtGui.QPushButton('Button {} auto'.format(idx), self)
            button.clicked.connect(lambda x=idx: self.button_pushed(x))
            self.buttons.append(button)
            main_layout.addWidget(button)


    def button_pushed(self, num):
        print 'Pushed button {}'.format(num)

按下前两个按钮会显示“Pushed button 1”和“Pushed button 2”,另外两个按钮都显示“Pushed button False”,但我预期的是3和4。
我还没有完全理解lambda机制。到底连接了什么?指向由lambda生成的函数(带有替换参数)的指针,还是lambda函数在信号触发时被评估?
4个回答

64

QPushButton.clicked 信号会发出一个参数来指示按钮的状态。当你连接到 lambda slot 时,你所分配给 idx 的可选参数将被按钮的状态覆盖。

相反,将您的连接设置为:

button.clicked.connect(lambda state, x=idx: self.button_pushed(x))

这样按钮状态被忽略,正确的值将传递到您的方法中。


1
“state”这个词在编程中的真正含义是什么? - ioaniatr
3
当您连接到“clicked”信号时,该信号具有在此处描述的签名(http://doc.qt.io/qt-5/qabstractbutton.html#clicked)。正如您所看到的,在发出信号时,会提供一个参数,其中包含按钮的状态(按钮是否已选中)。我的代码中的变量可以被称为任何您想要的名称,但它必须存在,以便Qt不会用选中的状态覆盖下一个参数(`x = idx`)。 - three_pineapples
1
@grego。你可能正在使用PySide而不是PyQt。 在这种情况下,你需要稍微调整你的代码: action.triggered.connect(lambda checked=None, service=service: printService(service)),因为显然PySide在接收器的签名不匹配时不会发送checked - johnson
1
@MarshallEubanks 这是由于 Python 中的后期绑定而必要的。在您的示例中,使用的 idx 值将是循环中所有连接信号的最后一次迭代的值(包括以前的迭代)。避免这种情况的方法是将其设置为函数/lambda 的未使用关键字参数的默认值。请参见 https://dev59.com/5nA75IYBdhLWcg3wK10p 以获取稍长的解释。 - three_pineapples
这个解决方案真的帮了我很多!谢谢! - undefined
显示剩余4条评论

24

注意!一旦将信号连接到具有对self的引用的lambda槽中,您的小部件将无法进行垃圾回收!这是因为lambda创建了一个closure,其中包含另一个对小部件的不可回收引用。

因此,self.someUIwidget.someSignal.connect(lambda p:self.someMethod(p))非常危险 :)


1
我花了两天时间添加和删除代码,才发现这实际上是我正在调查的“泄漏”故障,然后我看到了你的分析,它正是我最终得出的结论。现在要更改所有现有的代码...:( - JonBrave
3
那么有什么替代方案呢? - pyjamas
2
@Esostack:这取决于你想要实现什么。如果你只是想在槽函数中检测发出信号的对象,你可以使用 QObject.sender() 方法。此外,你还可以将信息存储在小部件属性中,并在槽函数中访问它们,参见此示例:https://stackoverflow.com/questions/49446832/pyside2-unable-to-get-sender-inside-the-slot/55118468 - Daniel K.
只有在运行时显式删除和重新创建连接的对象时(即在正常的Python关闭过程之前),才有必要担心这个问题。如果您不这样做,程序将稳定地泄漏内存,因为闭包将使所有对象保持活动状态。因此,在处理对象删除/清除的代码中简单地断开信号即可。如果对象在运行时没有被删除,则断开任何东西都没有意义,因为系统将在程序关闭后回收所有内存。 - ekhumoro
1
@Rhdr 另一个值得一提的讨论在这里 https://dev59.com/X5Dea4cB1Zd3GeqPa2S3#33310118 - Grigory Makeev
显示剩余6条评论

0

我并不确定你在使用lambda时出了什么问题。我认为这是因为idx(设置自动按钮时的循环索引)超出了范围,不再包含正确的值。

但我认为你不需要这样做。看起来你使用lambda的唯一原因是为了将参数传递给button_pushed(),以标识它是哪个按钮。在button_pushed()槽中可以调用一个名为sender()的函数,该函数标识发出信号的按钮。

以下是一个示例,我认为它基本上实现了你想要的功能:

from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

import sys

class MainWindow(QWidget):
    def __init__(self):
        super(QWidget, self).__init__()

        main_layout = QVBoxLayout(self)

        self.buttons = []

        # Works:
        self.button_1 = QPushButton('Button 1 manual', self)
        main_layout.addWidget(self.button_1)
        self.buttons.append(self.button_1)
        self.button_1.clicked.connect(self.button_pushed)

        self.button_2 = QPushButton('Button 2 manual', self)
        main_layout.addWidget(self.button_2)
        self.buttons.append(self.button_2)
        self.button_2.clicked.connect(self.button_pushed)

        # Doesn't work:
        for idx in [3, 4]:
            button = QPushButton('Button {} auto'.format(idx), self)
            button.clicked.connect(self.button_pushed)
            self.buttons.append(button)
            main_layout.addWidget(button)


    def button_pushed(self):
        print('Pushed button {}'.format(self.buttons.index(self.sender())+1))


app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

显然,如果你的最终目标是这样做,那么创建所有按钮“自动”(在循环中)而不是循环外部的两个按钮和循环内部的两个按钮将是微不足道的。 - jfsturtz
谢谢您的建议,但上面的three_pineapples解决方案正是我所寻找的,并且解释了发生了什么。是的,在循环中生成所有四个按钮除了展示在循环外行为不同的示例之外没有其他原因。 - zeus300
我也很高兴了解你之前的做法存在什么问题。 - jfsturtz

-1
非常简单。检查工作代码和不工作的代码。你有一个语法错误。
工作代码:
self.button_1.clicked.connect(lambda x:self.button_pushed(1))

无效:

button.clicked.connect(lambda x=idx: self.button_pushed(x))

修复:

button.clicked.connect(lambda x: self.button_pushed(idx))

对于lambda,您正在定义一个“x”函数,并将该函数解释为“self.button_pushed(idx)”,以便在此情况下使用函数参数(idx)。只需尝试并让我知道它是否有效。

他遇到的问题是,他试图从for循环创建中获得不同的输出。不幸的是,它将最后一个值分配给任何名为button的变量,因此它会给出4作为结果。前两个工作正常,因为它们不是在for循环中创建的,而是单独创建的。

并且工作中的按钮变量名称不同,如button_1和button_2。在for循环中创建的所有按钮都将具有名称button,这导致相同的函数。

他想要做的解决方案如下,它像魅力一样运行。

from sys import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

buttons = []

def newWin():
    window = QWidget()
    window.setWindowTitle("Lambda Loop")
    window.setFixedWidth(1000)
    window.move(175, 10)
    window.setStyleSheet("background: #161219;")
    grid = QGridLayout()
    return window, grid

def newButton(text :str, margin_left, margin_right, x):
    button = QPushButton(text)
    button.setCursor(QCursor(Qt.PointingHandCursor))
    button.setFixedWidth(485)
    button.setStyleSheet(
        "*{border: 4px solid '#BC006C';" +
        "margin-left: " + str(margin_left) + "px;" +
        "margin-right: " + str(margin_right) + "px;" +
        "color: 'white';" +
        "font-family: 'Comic Sans MS';" +
        "font-size: 16px;" +
        "border-radius: 25px;" +
        "padding: 15px 0px;" +
        "margin-top: 20px;}" +
        "*:hover {background: '#BC006C'}"
    )

    def pushed():
        val = x
        text = QLabel(str(val))
        text.setAlignment(Qt.AlignRight)
        text.setStyleSheet(
            "font-size: 35px;" +
            "color: 'white';" +
            "padding: 15px 15px 15px 25px;" +
            "margin: 50px;" +
            "background: '#64A314';" +
            "border: 1px solid '#64A314';" +
            "border-radius: 0px;"
        )
        grid.addWidget(text, 1, 0)
    button.clicked.connect(pushed)
    return button

app = QApplication(argv)
window, grid = newWin()

def frame1(grid):
    for each in [3, 4]:
        button = newButton('Button {}'.format(each), 150, 150, each)
        buttons.append(button)
        pass
    b_idx = 0
    for each in buttons:
        grid.addWidget(each, 0, b_idx, 1, 2)
        b_idx += 1

frame1(grid)

window.setLayout(grid)

window.show()

exit(app.exec())

我把所有东西都放在一个地方,这样所有人都可以看到。告诉你想做什么比猜测要容易。(您还可以在Frame函数的列表中添加新变量,它将为您创建具有不同值和功能的更多按钮。)


1
避免就投票问题争论。投票是私人行为,无需解释。如果你收到了UV,你会做出同样的主张吗?显然不会,同样的规则也适用于DVs。如果你在帖子中放置无关信息,则被视为噪音。如果你的帖子长时间内表现良好,那么你将获得更多的UV而非DVs,反之亦然。 - eyllanesc
另一方面,我对你的答案没有更好的贡献,因为它是已接受答案的较差版本,例如,你的解决方案在for循环中失败,而已接受答案则涵盖了该情况。 - eyllanesc
显然你投了负面票。我不想争论,但对于我所说的真实陈述,给人们投票,特别是像我这样新注册的人,是不公平的。先试试代码。 - Huseyin Sozen
  1. 我没有投票,如果我投了票,我也不会说。这是任何用户的权利。
  2. 投票是由帖子决定的,而不是由用户,这是所有 SO 规则所指示的。在这里,我们不会根据谁创建帖子来评估它们,而是根据帖子本身的质量来评估。在这里,无论您是新用户还是老用户,是初学者还是专家,都因为帖子的贡献而受到关注。请阅读 [答案] 并查看 [导览]。
- eyllanesc

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接