使用 lambda 表达式连接 pyqt 中的插槽

Posted

技术标签:

【中文标题】使用 lambda 表达式连接 pyqt 中的插槽【英文标题】:Using lambda expression to connect slots in pyqt 【发布时间】:2016-06-19 13:47:32 【问题描述】:

我正在尝试将插槽与 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 函数?

【问题讨论】:

【参考方案1】:

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

相反,将您的连接设为

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

这样按钮状态被忽略,正确的值被传递给你的方法。

【讨论】:

“状态”这个词的真正含义是什么? @ioaniatr 当您连接到clicked 信号时,该信号具有描述为here 的签名。如您所见,当发出信号时,会提供一个参数,其中包含按钮的状态(按钮是否被选中)。我的代码中的变量可以随心所欲地调用,但它必须存在以便 Qt 不会覆盖下一个参数 (x=idx) 并处于选中状态。 当我尝试从 QMenu.addAction() 连接一个 QAction 时,我得到一个异常:missing 1 required positional argument: 'checked' 触发我的菜单项时。 action.triggered.connect(lambda checked, service=service: printService(service))我做错了什么? @grego,如果你还没有解决这个问题。我建议使用 functools.partial:action.triggered.connect(functools.partial(printService, service))【参考方案2】:

小心!只要您将信号连接到引用 self 的 lambda 插槽,您的小部件就不会被垃圾收集!这是因为 lambda 创建了一个闭包,其中包含对小部件的另一个无法收集的引用。

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

【讨论】:

我花了两天时间对代码进行注释和取消注释,结果发现这是我正在调查的“泄漏”错误,然后我遇到了您的分析,这正是我最终得出的结论。现在关闭以更改所有现有代码.... :( 记录一下,如果lambdaself 传递给某个外部函数,同样的问题:self.someUIwidget.someSignal.connect(lambda someNonSelfMethod(self)) 那有什么替代方案? @Esostack:取决于您想要实现的目标。如果您只想在槽中检测哪个对象发出了信号,可以使用QObject.sender() 方法。此外,您可以将信息存储在小部件属性中并在插槽函数中访问它们,请参见此示例:***.com/questions/49446832/… @Rhdr 另一个值得一提的讨论在这里***.com/questions/33304203/…【参考方案3】:

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

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

这里有一个例子,我认为它或多或少符合你的目标:

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_())

【讨论】:

(显然,创建所有按钮“自动”(在循环中)是微不足道的,而不是在循环外创建两个按钮,在循环内创建两个按钮,如果这是您的最终目标... 感谢您的建议,但上面的 three_pineapples 解决方案正是我所寻找的,并解释了发生了什么。是的,没有理由不在循环中生成所有四个按钮,除了展示循环外行为如何不同的示例。 我也很高兴知道你的做法出了什么问题。【参考方案4】:

这很简单。检查工作代码和不工作代码。您有语法错误。

工作代码:

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 循环中,所有创建的按钮都将具有相同功能的名称按钮。

他正在尝试做的解决方案如下,它就像一个魅力。

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 函数的列表中添加新变量,它会为您创建更多具有不同值和功能的按钮。)

【讨论】:

避免争论选票。投票是私人的,不需要证明是合理的。如果您收到紫外线,您会提出同样的要求吗?不,同样的规则适用于 DV。如果您在帖子中放置不相关的信息,那么这将被视为噪音。如果你的帖子很好,那么很长一段时间,你会得到比 DV 更多的紫外线,如果不是,那么你会得到相反的结果。 另一方面,我在您的答案中没有做出更好的贡献,因为它是已接受答案的不太好的版本,因为例如您的解决方案在 for 循环中失败,这与已接受的答案不同那个案子。 显然你投了反对票。我不是在争论,但投票给人们是不公平的,尤其是像我这样新注册的人,因为我已经做出了真实的陈述。先试试代码。 1.我没有投票,如果我投票了,我也不会这么说。这是任何用户的权利。 2. 投票是由帖子给出的,而不是像所有 SO 规则所表明的那样由用户给出。在这里,我们不根据创建帖子的人来评价帖子,而是根据帖子本身的质量来评价帖子。在这里,无论您是新用户还是老用户,无论您是初学者还是专家,都无关紧要,因为帖子的贡献。请阅读How to Answer 并查看tour

以上是关于使用 lambda 表达式连接 pyqt 中的插槽的主要内容,如果未能解决你的问题,请参考以下文章

Pyqt将插槽连接到具有默认参数的函数而没有lambda [重复]

使用PyQt中的QCheckBox或QComboBox更改小部件可见性

在循环中连接 PyQt4 中的插槽和信号

Qt - 使用 lambda 函数修改先前连接信号的插槽参数

PyQt5 线程、信号和插槽。连接错误

pyqt4在线程中向主线程中的插槽发出信号