Особенность работы lambda с сигналами/слотами
Есть большая программа с двумя окнами и большим количеством сигналов\слотов. Заметил, что при комментировании части кода с сигналам перестаёт запускаться второе окно. В итоге программа сократилась до:
import sys
from PyQt5 import QtWidgets
class Win1(QtWidgets.QMainWindow):
def __init__(self):
super(Win1, self).__init__()
self.enter = QtWidgets.QPushButton(self)
self.enter.setText('Enter')
self.enter.setDefault(True)
self.enter.clicked.connect(self.go_ahead)
def go_ahead(self):
self.hide()
app2 = Win2()
app2.show()
class Win2(QtWidgets.QMainWindow):
def __init__(self):
super(Win2, self).__init__()
self.pushButton = QtWidgets.QPushButton(self)
self.pushButton.setText('second')
self.pushButton.setDefault(True)
self.pushButton.clicked.connect(lambda: self.send())
def send(self):
print('ff')
app1 = QtWidgets.QApplication([])
start_win = Win1()
start_win.show()
sys.exit(app1.exec_())
В таком виде программа нормально запускается. При комментировании self.pushButton.clicked.connect(lambda: self.send()) перестаёт открываться Win2. Оказалось, что надо писать self.app2 = Win2(), а не app2 = Win2()(что тоже не совсем понятно).
Дальнейший анализ показал, что использование lambda в сигнале\слоте позволяет использовать запись app2 = Win2(), т.е. с .connect(lambda: self.send()) всё работает, а с .connect(self.send) второе окно не запускается. Подобное поведение не только с сигналом clicked, но и с textChanged, timeout, cursorPositionChanged да и скорее всего со всеми.
Означает ли подобное поведение, что lambda делает app2 атрибутом Win1? Если нет, то что всё таки происходит?
Ответы (2 шт):
Оказалось, что надо писать self.app2 = Win2(), а не app2 = Win2()(что тоже не совсем понятно).
Это логично, т.к. если объект не имеет ссылок, то он уничтожается, а при app2 = Win2() объект Win2 имеет ссылку только в переменной app2, а после выхода функции эта переменная выходит из своей области действия и уничтожается, а на Win2 уменьшает количество ссылок и оно становится 0, поэтому он уничтожается.
Поэтому, когда вы написали self.app2, вы в текущем объекте создали атрибут app2 и теперь он ссылается на Win2 и теперь пока существует текущий объект Win2 тоже будет существовать.
НО, можно обойтись только app2, если указывать родителя у виджета, тогда объект будет иметь ссылку на другой объект и останется в памяти
Дальнейший анализ показал, что использование lambda в сигнале\слоте позволяет использовать запись app2 = Win2(), т.е. с .connect(lambda: self.close) всё работает, с .connect(self.close) второе окно не запускается.
Вообще, разница .connect(lambda:self.close) и .connect(self.close) проявится только при выполнении клика. В первом случае self.close не будет вызван, а во-втором будет (попробуйте в коде вызвать self.close и self.close(), и увидите, что метод в первом случае не был вызван).
Означает ли подобное поведение, что lambda делает app2 атрибутом Win1? Если нет, то что всё таки происходит?
Думаю при self.pushButton.clicked.connect(lambda: self.close()) происходит замыкание и ссылка на текущий объект сохраняется в функции (лямбде) из-за чего и не происходит удаление объекта
Вы ведь прячете первое окно, а второе окно не имеет ссылок на первое, поэтому не сможет на него воздействовать.
Мне кажется, в вашем случае, самое то будет работать с QStackedWidget. Сделаете главное окно, добавите на него те два виджета и у QStackedWidget через методы setCurrentIndex и/или setCurrentWidget переключайтесь между окнами.
Немного доработал ваш код:
import sys
from PyQt5 import QtWidgets
class Win1(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.enter = QtWidgets.QPushButton(self)
self.enter.setText('Enter')
self.enter.setDefault(True)
self.enter.clicked.connect(self.go_ahead)
def go_ahead(self):
self.hide()
self.app2 = Win2(self)
self.app2.show()
class Win2(QtWidgets.QMainWindow):
def __init__(self, app1):
super().__init__()
self.app1 = app1
self.pushButton = QtWidgets.QPushButton(self)
self.pushButton.setText('second')
self.pushButton.setDefault(True)
self.pushButton.clicked.connect(self.on_click)
def on_click(self):
self.hide()
self.app1.show()
self.close()
app1 = QtWidgets.QApplication([])
start_win = Win1()
start_win.show()
sys.exit(app1.exec_())
Думаю, текущую задачу с переключением окон лучше решать через виджет, специально для этого созданного – QStackedWidget.
Попробуйте:
import sys
from PyQt5 import QtWidgets
class Win1(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.enter = QtWidgets.QPushButton(self)
self.enter.setText('Enter')
self.enter.setDefault(True)
class Win2(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.pushButton = QtWidgets.QPushButton(self)
self.pushButton.setText('second')
self.pushButton.setDefault(True)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.win1 = Win1()
self.win2 = Win2()
self.stacked_widget = QtWidgets.QStackedWidget()
self.stacked_widget.addWidget(self.win1)
self.stacked_widget.addWidget(self.win2)
# Настройка переключения между окнами
self.win1.enter.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(1))
self.win2.pushButton.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0))
self.setCentralWidget(self.stacked_widget)
if __name__ == '__main__':
app1 = QtWidgets.QApplication([])
start_win = MainWindow()
start_win.show()
sys.exit(app1.exec_())