问题
使用PyQt写交互式分割的小工具,加载完图像之后,需要显示图像,同时需要生成注视点图,并显示注视点图。
于是写出了下面的【第一版代码】:
1 | def open_image(self): |
运行后发现,image和fixation map显示的都很慢,image总是等待fixation map一起显示。
理想情况下,show_image在前,image应该先显示,然后再生成fixation map。image应该先显示出来,而不是等待fixation map缓慢的预测。
查阅资料发现:
PyQt中,在如果一个函数中包含多个连续操作(比如加载图像和生成注视点图),并且没有返回控制权给事件循环,界面刷新就会被阻塞,直到函数执行完毕。这是因为 PyQt 的 GUI 是单线程的,主事件循环会一直等待当前的操作完成才能更新界面。因此,长时间运行的任务会让界面冻结或延迟刷新。
解决
QTimer
对于需要短暂延迟的操作,可以使用 QTimer.singleShot() 创建非阻塞的延迟。
【第二版代码】
1 | from PyQt5.QtCore import QTimer |
QTimer.singleShot()将任务调度为一个“单独的事件”,延迟执行,从而让事件循环有机会刷新界面。
- 事件循环的原理:PyQt 应用程序运行在一个事件循环中,这个循环会不断检查是否有用户事件(如鼠标点击、键盘输入等)或任务需要处理。当你直接在函数中执行一系列操作时,这些操作是顺序执行的,直到函数完全结束,事件循环才会继续处理其他任务或更新界面。
- QTimer.singleShot() 的工作原理:
QTimer.singleShot()会在指定的时间(例如 1000 毫秒,即 1 秒)后触发一个“单次事件”。这个事件会在主事件循环中排队等待执行。由于singleShot把任务放在事件队列里并设置了延迟,当前函数会立即结束,控制权回到事件循环。这时,界面有机会刷新、显示打开的图像。- 延迟后执行任务:在延迟时间过后(如 1 秒),事件循环会检查到
singleShot添加的任务,并执行generate_fixation_map()方法来生成注视点图。这样做的好处是,图像会在生成注视点图之前显示,而不会等待整个过程完成。
这种方法虽然让image显示出来了,但是在fixation_map生成过程中,界面不允许任何操作,无法响应。
QThread
将耗时的操作(生成注视点图)放到单独的线程中,这样可以避免阻塞主线程,从而让界面保持响应。使用多线程实现这一目标有几种常见方法,比如 QThread 或 concurrent.futures,这里主要尝试了QThead。
使用
QThread步骤:
- 定义一个任务线程:创建一个专门执行耗时任务的线程类。这个类只处理图像处理任务,生成结果后再传回主线程显示。
- 将耗时任务放入线程:启动线程后,让它执行注视点生成任务,而不影响主线程的界面更新。
- 任务完成后更新界面:将生成的注视点图像传回主线程,显示在界面上。
【第三版代码】
1 | class FixationMapThread(QThread): |
- FixationMapThread:这是一个自定义线程类,继承自
QThread。它负责在后台生成注视点图,并通过信号fixation_map_generated将生成的注视点图发送到主线程。 - open_image 方法:当用户打开图像时,主线程负责显示图像,然后创建并启动
FixationMapThread,将文件名传递给线程。 - show_fixation_map 方法:该方法接收从
FixationMapThread发出的信号,获取生成的注视点图并显示在主界面上。
这种实现方法,首先界面不会卡顿:生成注视点图的任务在后台执行,不会阻塞主线程,因此图像可以立即显示。同时,更新流畅:注视点图生成完成后,通过信号更新界面,避免了界面刷新延迟。
