pyqt: правильный способ подключения нескольких сигналов к одной и той же функции в pyqt (QSignalMapper не применяется)
Я подготовил много сообщений о том, как подключить несколько сигналов к одному и тому же обработчику событий в python и pyqt. Например, подключение нескольких кнопок или комбинированных списков к одной и той же функции.
Многие примеры показывают, как это сделать с QSignalMapper, но это не применимо, когда сигнал несет параметр, как с combobox.currentIndexChanged.
Многие люди предполагают, что это можно сделать с помощью лямбды. Это чистое и симпатичное решение, я согласен, но никто не упоминает, что лямбда создает закрытие, которое содержит ссылку - таким образом, объект, на который ссылаются, не может быть удален. Привет утечка памяти!
Доказательство:
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_())
Виджет НЕ удаляется, хотя мы очищаем ссылку. Удалите соединение с лямбдой - оно удаляется корректно.
Итак, вопрос: как правильно соединить несколько сигналов с параметрами в одном обработчике без утечки памяти?
2 ответа
Просто неправда, что объект не может быть удален, потому что сигнальное соединение содержит ссылку в замыкании. Qt автоматически удалит все сигнальные соединения, когда удалит объект, что, в свою очередь, удалит ссылку на 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
Во многих случаях параметр, переносимый сигналом, может быть перехвачен другим способом, например, если для отправляющего объекта установлено 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()