前言
此篇文章源于知乎上的一个问题,使用 PyQt5 编写 GUI 程序时,新创建的界面会闪退,本篇文章仅作记录以防以后忘记。
问题代码
python
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class Main(QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.button = QPushButton('test', self)
self.button.clicked.connect(self.start)
self.show()
def start(self):
w = QMainWindow()
w.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w1 = Main()
sys.exit(app.exec_())
在运行以上代码时,会显示一个简单的界面,界面上有一个按钮,如下图:
当点击 test 按钮时,本应该弹出一个新的窗口,但实际情况却是,有一个窗口一闪而过,重复点击新的窗口仍然一闪而过,快到我根本无法截到窗口打开的样子。
看到这里,大家可以猜测下为什么会出现这种情况,可以从变量作用域方面来猜。
问题原因
下面公布答案,关于窗口闪退的原因其实很简单,那就是 w 变量被回收了。
w 变量的作用范围只在 start 函数内,start 函数运行结束后函数内的所有变量都会被垃圾回收掉,垃圾回收时会自动执行对象中对应的 __del__
函数(又被叫做析构函数),之后对象被销毁,像没来过一样。
了解了以上知识后来解释窗口闪退的原因就很方便了,w 对象是一个窗口对象,它被创建后紧接着调用了 show 方法,show 方法会将窗口显示在屏幕上,之后 start 函数执行完毕,解释器要清理掉函数作用域内所有的局部变量,自动执行它们的 __del__
函数,这时窗口就会被回收掉,所以窗口就会自动关闭了。
由于处理器执行的速度非常快,我上面描述的流程在一瞬间就执行结束了,所以在我们看来窗口就像是"闪退" 了,实际上它只是存在的时间太短了而已。
让我们添加一个延时函数再来看看结果:
python
import sys
import time
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class Main(QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.button = QPushButton('test', self)
self.button.clicked.connect(self.start)
self.show()
def start(self):
w = QMainWindow()
w.show()
time.sleep(3)
print('start end!')
if __name__ == "__main__":
app = QApplication(sys.argv)
w1 = Main()
sys.exit(app.exec_())
执行以上代码,会发现可以正常打开第二个窗口了,但是在三秒过后窗口仍然自己关闭了,那是因为延时时间到了解释器开始了清理工作,窗口对象就被清理掉了。
而且在这三秒钟内我们会发现窗口不响应我们对窗口发出的任何动作,包括最大化、最小化和关闭按钮统统无效,这是 Windows 消息机制的原因。test 按钮绑定了事件处理函数 start,在点击 test 按钮后,按钮单击消息会被发出,放到消息队列中, 消息处理函数依次处理消息队列中的消息,当处理到 start 函数时,会遇到 sleep 函数,此时消息处理函数会等待,等待的过程中也就无法处理其他的消息,导致界面无响应,也就无法处理我们发出的任何消息(消息会被阻塞住,直到消息处理函数恢复正常)。
注:PyQt5 中有一个新的概念,将消息机制抽象为信号和槽,通过信号和槽绑定的方式来传递信号(消息),这样一来方便了后台线程和前台界面进行交互,这种抽象其实大大简化了我们的理解难度。
问题解决
理解了原理后修复这个问题就变得很容易了,既然函数结束变量会被回收,那么将窗口对象绑定到 self 上就可以了,self 会在整个程序退出时才会被销毁,这样就可以防止新打开的窗口闪退了。
python
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class Main(QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.button = QPushButton('test', self)
self.button.clicked.connect(self.start)
self.show()
self.w = None
def start(self):
self.w = QMainWindow()
self.w.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w1 = Main()
sys.exit(app.exec_())
问题验证
问题已经解决了,下面来验证下 start 函数执行完毕解释器是怎么销毁变量的,根据上面的结论我们知道,解释器在垃圾回收时会调用对象对应的 __del__
函数,那么我们重写 QMainWindow 类的 __del__
函数,观察是否会调用到我们重写的 __del__
函数。
python
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
class M(QMainWindow):
def __del__(self):
print('call __del__')
class Main(QMainWindow):
def __init__(self):
super(Main, self).__init__()
self.button = QPushButton('test', self)
self.button.clicked.connect(self.start)
self.show()
self.w = None
def start(self):
w = M()
w.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
w1 = Main()
sys.exit(app.exec_())
运行以上代码,我们发现,窗口闪退时,call __del__
被打印出来了,解释器调用了我们重写后的 __del__
函数,说明窗口对象的确是被 Python 解释器销毁了。
总结
在本次解决问题的过程中,我们可以学到 __del__
函数的用法,还了解了 Windows 的消息机制,说明多踩坑还是可以学到不少东西的,哈哈哈🤣