PyQt:在PyQt中连接多个信号到同一个函数的正确方法(不能使用QSignalMapper)

4
  1. 我已经阅读了很多关于如何在Python和PyQt中将多个信号连接到同一事件处理程序的文章。例如,将多个按钮或组合框连接到同一个函数。

  2. 许多示例展示了使用QSignalMapper来完成此操作,但是当信号携带参数时(如combobox.currentIndexChanged),它并不适用。

  3. 许多人建议使用lambda来解决这个问题。我同意,这是一种干净、漂亮的解决方案,但没有人提到lambda会创建一个闭包,其中包含引用 - 因此引用的对象无法被删除。内存泄漏你好!

证明:

from PyQt4 import QtGui, QtCore

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

        # create and set the layout
        lay_main = QtGui.QHBoxLayout()
        self.setLayout(lay_main)

        # create two comboboxes and connect them to a single handler with lambda

        combobox = QtGui.QComboBox()
        combobox.addItems('Nol Adyn Dwa Tri'.split())
        combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind))
        lay_main.addWidget(combobox)

        combobox = QtGui.QComboBox()
        combobox.addItems('Nol Adyn Dwa Tri'.split())
        combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind))
        lay_main.addWidget(combobox)

    # let the handler show which combobox was selected with which value
    def on_selected(self, cb, index):
        print '! combobox ', cb, ' index ', index

    def __del__(self):
        print 'deleted'

if __name__ == '__main__':

    import sys
    app = QtGui.QApplication(sys.argv)

    wdg = Widget()
    wdg.show()

    wdg = None

    sys.exit(app.exec_())

即使我们清除了引用,小部件并没有被删除。移除与lambda的连接 - 它将被正确删除。
因此,问题是:连接多个带参数信号到单个处理程序的正确方法是什么,而不会泄漏内存?
2个回答

5
一个信号连接持有闭包中的引用,不代表一个对象不能被删除。当Qt删除一个对象时,它会自动删除所有的信号连接,进而删除python端对lambda的引用。
但这同时也意味着你不能总是依赖Python单独来删除对象。每个PyQt对象都有两部分构成:Qt C++部分和Python包装部分。两部分都必须删除,有时还要按特定顺序(取决于Qt或Python当前拥有对象的所有权)。除此之外,还需要考虑Python垃圾回收器的各种奇怪情况(特别是在解释器关闭时的短暂期间)。
总之,在您的具体示例中,简单的修复方法是:
    # wdg = None
    wdg.deleteLater()

这将对象安排为删除状态,因此需要运行事件循环才能产生任何效果。在您的示例中,这也将自动退出应用程序(因为该对象是最后一个关闭的窗口)。

为了更清楚地了解发生了什么,您还可以尝试以下操作:

    #wdg = None
    wdg.deleteLater()

    app.exec_()

    # Python part is still alive here...
    print(wdg)
    # but the Qt part has already gone
    print(wdg.objectName())

输出:

<__main__.Widget object at 0x7fa953688510>
Traceback (most recent call last):
  File "test.py", line 45, in <module>
    print(wdg.objectName())
RuntimeError: wrapped C/C++ object of type Widget has been deleted
deleted

编辑:

这是另一个调试示例,希望能更清楚地说明问题:

    wdg = Widget()
    wdg.show()

    wdg.deleteLater()
    print 'wdg.deleteLater called'

    del wdg
    print 'del widget executed'

    wd2 = Widget()
    wd2.show()

    print 'starting event-loop'
    app.exec_()

输出:

$ python2 test.py
wdg.deleteLater called
del widget executed
starting event-loop
deleted

deleteLater() 似乎隐藏了小部件,但析构函数仍然没有被调用。在安排删除第一个小部件之后添加第二个小部件会显示第二个小部件,但没有删除第一个小部件的迹象。与以前一样,移除连接可以解决问题。wdg.deleteLater() wdg2 = Widget() wdg2.move(300,100) wdg2.show() - Grigory Makeev
@GrigoryMakeev。不,这根本不是发生的事情。显然,Python包装器不会立即被删除,因为您仍然持有对它的全局引用。但是,您只需要执行del wdg,并且一旦Qt删除了C++部分,__del__就会被调用。我在我的答案中添加了另一个调试示例,这应该更清楚地显示实际发生的情况。 - ekhumoro
确实现在它正在工作,谢谢!只有一件事对我仍然不清楚:如果我在del wdg之后立即添加gc.collect(),析构函数仍然没有被调用。你有什么想法为什么会这样吗? - Grigory Makeev
@GrigoryMakeev。请参阅Python文档中的注释,了解有关__del__的信息(http://docs.python.org/2/reference/datamodel.html#object.__del__)。Qt仍然持有引用,因此Python垃圾回收器必须等待`deleteLater`事件被处理。我在调试示例中将打印语句放在事件循环之前,以明确表示只有在事件处理开始后才会调用`__del__`。因此,Qt首先删除C++部分,然后允许垃圾回收器删除Python部分。 - ekhumoro

2
在许多情况下,通过信号传递的参数可以以另一种方式捕获,例如,如果为发送对象设置了objectName,则可以使用QSignalMapper:
    self.signalMapper = QtCore.QSignalMapper(self)
    self.signalMapper.mapped[str].connect(myFunction)  

    self.combo.currentIndexChanged.connect(self.signalMapper.map)
    self.signalMapper.setMapping(self.combo, self.combo.objectName())

   def myFunction(self, identifier):
         combo = self.findChild(QtGui.QComboBox,identifier)
         index = combo.currentIndex()
         text = combo.currentText()
         data = combo.currentData()

是的,谢谢,这是我们目前使用的解决方法。基本上它只强调了在这种情况下我们无法捕获参数信号,因此应该尝试以其他方式推断它,在这种情况下使用combo.currentIndex()。只是我们使用self.signalMapper.mapped[QtCore.QWidget]形式,所以我们不必使用findChild。 - Grigory Makeev

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