pyqt: правильный способ подключения нескольких сигналов к одной и той же функции в pyqt (QSignalMapper не применяется)
-
Я подготовил много постов о том, как подключить несколько сигналов к одному и тому же обработчику событий в python и pyqt. Например, подключение нескольких кнопок или комбо-боксов к одной и той же функции.
-
Многие примеры показывают, как это сделать с QSignalMapper, но это не применимо, когда сигнал несет параметр, как с combobox.currentIndexChanged
-
Многие люди предполагают, что его можно сделать с помощью лямбды. Это чистое и красивое решение, я согласен, но никто упоминает, что 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 ответа:
Это просто неверно, что объект не может быть удален, потому что сигнальное соединение содержит ссылку в замыкании. Qt автоматически удалит все сигнальные соединения, когда он удалит объект, который в свою очередь удалит ссылку на
Но это означает, что вы не всегда можете полагаться только на Python для удаления объектов. У каждого объекта PyQt есть две части: Часть Qt C++ и часть Python wrapper. Обе части должны быть удалены - и иногда в определенном порядке (в зависимости от того, является ли Qt или Python в настоящее время владельцем объекта). В дополнение к этому, есть также капризы сборщика мусора Python, чтобы учитывать (особенно в течение короткого периода, когда интерпретатор выключается).lambda
на стороне 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
EDIT:
Вот еще один пример отладки, который, надеюсь, сделает его еще более ясным:
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
Во многих случаях параметр, передаваемый сигналом, может быть пойман другим способом, например, если для передающего объекта задано имя объекта, поэтому можно использовать 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()