【问题标题】:pyqt: A correct way to connect multiple signals to the same function in pyqt (QSignalMapper not applicable)pyqt:在pyqt中将多个信号连接到同一个函数的正确方法(QSignalMapper不适用)
【发布时间】:2016-01-23 02:24:03
【问题描述】:
  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 的连接 - 它会被正确删除。

那么,问题是:在不泄漏内存的情况下,将多个带参数的信号连接到单个处理程序的正确方法是什么?

【问题讨论】:

    标签: python garbage-collection pyqt signals-slots qsignalmapper


    【解决方案1】:

    不能删除对象是不正确的,因为信号连接在闭包中持有引用。 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()
    • @GrigoryMakeev。不,这根本不是发生的事情。显然,Python 包装器不会立即被删除,因为您仍然持有对它的全局引用。但是您需要做的只是del wdg,一旦Qt 删除了C++ 部分,就会调用__del__。我在答案中添加了另一个调试示例,它应该更清楚地显示真正发生的事情。
    • 现在确实可以使用了,谢谢!我只有一件事还不清楚:如果我在 del wdg 之后添加 gc.collect(),析构函数不会被调用。知道为什么吗?
    • @GrigoryMakeev。对于__del__,请参阅note in the python docs。 Qt 仍然持有引用,因此 python 垃圾收集器必须等到处理 deleteLater 事件。我在调试示例中将 print 语句放在事件循环之前,以明确__del__ 只有在事件处理开始后才会被调用。所以 Qt 先删除 C++ 部分,然后允许垃圾收集器删除 python 部分。
    【解决方案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。
    猜你喜欢
    • 1970-01-01
    • 2011-04-23
    • 1970-01-01
    • 1970-01-01
    • 2011-08-16
    • 1970-01-01
    • 2011-02-07
    • 2018-02-28
    相关资源
    最近更新 更多