pyqt:在 pyqt 中将多个信号连接到同一函数的正确方法(QSignalMapper 不适用)
pyqt: A correct way to connect multiple signals to the same function in pyqt (QSignalMapper not applicable)
我已经准备了很多关于如何在 python 和 pyqt 中将多个信号连接到同一个事件处理程序的帖子。例如,将多个按钮或组合框连接到同一功能。
许多示例显示了如何使用 QSignalMapper 执行此操作,但是当信号携带参数时它不适用,如 combobox.currentIndexChanged
很多人建议可以用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 的连接 - 它会被正确删除。
所以,问题是:在不泄漏内存的情况下,将带有参数的多个信号连接到单个处理程序的正确方法是什么?
由于信号连接在闭包中持有引用而无法删除对象的说法是不正确的。 Qt在删除对象时会自动移除所有的信号连接,进而移除python端对lambda
的引用。
但这意味着您不能总是依赖Python单独来删除对象。每个 PyQt 对象都有两个部分:Qt C++ 部分和 Python 包装器部分。这两个部分都必须删除 - 有时以特定顺序删除(取决于 Qt 或 Python 当前是否拥有该对象的所有权)。除此之外,还要考虑 Python 垃圾收集器的变幻莫测(尤其是在解释器关闭的短时间内)。
无论如何,在您的具体示例中,简单的解决方法就是:
# wdg = None
wdg.deleteLater()
这会安排删除对象,因此需要 运行 事件循环才能发挥作用。在您的示例中,这也会自动退出应用程序(因为该对象是最后 window 关闭的)。
为了更清楚地看到发生了什么,您也可以试试这个:
#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
在很多情况下,信号携带的参数可以通过其他方式捕获,例如如果为发送对象设置了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()
我已经准备了很多关于如何在 python 和 pyqt 中将多个信号连接到同一个事件处理程序的帖子。例如,将多个按钮或组合框连接到同一功能。
许多示例显示了如何使用 QSignalMapper 执行此操作,但是当信号携带参数时它不适用,如 combobox.currentIndexChanged
很多人建议可以用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 的连接 - 它会被正确删除。
所以,问题是:在不泄漏内存的情况下,将带有参数的多个信号连接到单个处理程序的正确方法是什么?
由于信号连接在闭包中持有引用而无法删除对象的说法是不正确的。 Qt在删除对象时会自动移除所有的信号连接,进而移除python端对lambda
的引用。
但这意味着您不能总是依赖Python单独来删除对象。每个 PyQt 对象都有两个部分:Qt C++ 部分和 Python 包装器部分。这两个部分都必须删除 - 有时以特定顺序删除(取决于 Qt 或 Python 当前是否拥有该对象的所有权)。除此之外,还要考虑 Python 垃圾收集器的变幻莫测(尤其是在解释器关闭的短时间内)。
无论如何,在您的具体示例中,简单的解决方法就是:
# wdg = None
wdg.deleteLater()
这会安排删除对象,因此需要 运行 事件循环才能发挥作用。在您的示例中,这也会自动退出应用程序(因为该对象是最后 window 关闭的)。
为了更清楚地看到发生了什么,您也可以试试这个:
#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
在很多情况下,信号携带的参数可以通过其他方式捕获,例如如果为发送对象设置了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()