问题场景:
当用PyQt5开发一个GUI界面 ,需要执行业务逻辑时,后台逻辑执行时间长,界面就容易出现卡死、未响应等问题。
问题原因:
在PyQt中,GUI界面本身就是一个处理事件循环的主线程,当进行耗时操作时,主线程GUI需要等待操作完成后才会响应,在等待这段时间,整个GUI就处于卡死的状态。在windows下,系统会认为这个程序运行出错了,会自动显示未响应,如果这时有其他的操作,整个程序就会卡死崩溃。
解决办法:
另开一个线程来执行这个耗时操作(使用QThread)
from PyQt5.QtCore import QThread
通过继承QThread并重写run()方法的方式实现多线程代码的编写。
结构大体如下:
class Worker(QThread):
def __init__(self):
super().__init__()
def run(self):
--snip--
把耗时操作放到一个Worker线程中的run()函数下执行,在GUI类文件中绑定操作的地方,创建Worker进程实例,启动进程即可。
t = Worker()
t.start()
进阶用法
上文中的方法开启了一个新的线程去完成耗时操作;在GUI界面运行的过程中,常需要与新的线程之间保持信息的传递即“通信”,在pyqt5中这通过信号去实现,这样能保证GUI界面对后台操作进行实时的响应,例如:按钮状态的更新、文字浏览窗口的消息变化、子窗口的打开及关闭等。
信号及自定义信号
在PyQt5中,信号与槽的使用有如下一些特点:
· 一个信号可以关联多个槽函数。
· 一个信号也可以关联其他信号。
· 信号的参数可以是任何Python数据类型。
· 一个槽函数可以和多个信号关联。
· 关联可以是直接的(同步)或排队的(异步)。
· 可以在不同线程之间建立关联。
· 信号与槽也可以断开关联。
在自定义类中还可以自定义信号。使用自定义信号在程序的对象之间传递信息是非常方便的,使用PyQt5.QtCore.pyqtSignal()为一个类定义新的信号。要自定义信号,类必须是QObject类的子类。pyqtSignal()的句法是:
pyqtSignal(types[, name[, revision=0[, arguments=[]]]])
信号可以带有参数types,后面的参数都是一些可选项,基本不使用。信号需要定义为类属性,这样定义的信号是未绑定(unbound)信号。当创建类的实例后,PyQt5会自动将类的实例与信号绑定,这样就生成了绑定的(bound)信号。这与Python语言从类的函数生成绑定的方法的机制是一样的。一个绑定的信号(也就是类的实例对象的信号)具有connect()、disconnect()和emit()这3个函数,分别用于关联槽函数、断开与槽函数的关联、发射信号。
使用示例
通过信号的emit()函数发射信号。在类的某个状态发生变化,需要通知外部发生了这种变化时,发射相应的信号。如果信号关联了一个槽函数,就会执行槽函数,如果信号没有关联槽函数,就不会产生任何动作。
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'Qua_Ins.ui'
#
# Created by: PyQt5 UI code generator 5.15.4 @bill_love_3
import sys
import os
import time
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Worker(QThread, QObject): # 自定义信号,执行run()函数时,从线程发射此信号
sinOut = pyqtSignal(str)
sinEnd = pyqtSignal()
def __init__(self, obj, parent=None):
QThread.__init__(self, parent)
QObject.__init__(self, parent)
self.obj = obj
def Threading_topo(self): # CheckTopology子进程输出读取方法
--snip-- # 耗时操作,约300S左右
def run(self):
self.sinOut.emit('...正在导入数据...')
time.sleep(2)
self.sinOut.emit('...开始检查...')
time.sleep(1)
myQIns = QI(self.obj.filename)
myQIns.line_lsit.append(os.path.basename(self.obj.filename) + '\n')
# ---------------------开启Check Topology子进程 ----------------------------------------------
if self.obj.checkBox_Topology.isChecked():
self.sinOut.emit('-----------------检查中----请等待程序完成请勿关闭----\n')
self.Threading_topo()
myQIns.Complete_check()
self.sinOut.emit('检查完成。\n')
time.sleep(2)
myQIns.WriteTxt()
self.sinOut.emit('报告生成中...\n')
time.sleep(2)
myQIns.CleanGdb()
self.sinEnd.emit()
上面的片段就是一个完整的通过继承QThread, QObject类生成的自定义类,由它开启一个新的线程完成GUI程序中耗时的业务逻辑操作,保证GUI的主线程不会停下来等待,一直处于事件循环的状态。在GUI的类文件中,通过调用产生该Worker()类的实例,在需要进行该操作的信号绑定处开启线程。
class Ui_MainWindow():
def __init__(self):
self.filename = ''
self.thread_btn_start = Worker(self, None)
这里将GUI的类创建的对象本身(self)作为obj参数传递到Worker()类中,可以看到Worker类中的self.obj.filename对应GUI类中的self.filename;通过这种用法可以实现GUI主线程对Worker开启的线程的信息传递,这种用法与类的嵌套中内部类和外部类之间通信的方法是一致的。
def pushButton_start_event(self): # 检查按钮绑定事件
if self.filename:
self.thread_btn_start.sinOut.connect(self.text_browser_show)
self.thread_btn_start.sinEnd.connect(self.set_pushButton)
self.thread_btn_start.start()
这里设置Worker线程中sinOut、sinEnd信号绑定的槽函数,之后开启线程。
注意
不要尝试在Worker开启的线程中去设置GUI界面中的控件属性,因为可能会导致未知的错误;pyqt最重要的特点信号和槽函数其中的一个用法就是用于各对象之间的信息传递,所以总是应该用信号去传递信息,包括对其他控件的状态修改等诸如此类的操作。
def set_pushButton(self):
self.pushButton_start.setEnabled(True)
self.pushButton_chose.setEnabled(True)
这里通过thread_btn_start.sinEnd信号绑定的set_pushButton槽函数设置pushButton_start按钮的状态。
总结
通过自定义信号和继承重写QThread类的run()函数的方法可以很好的解决耗时操作导致界面卡死的问题。但也存在一些不足,比如增加额外的资源开销、程序整体执行时间变长了(变慢了);当然在更好的操作体验面前这些都是可以忽略的,毕竟GUI(Graphical User Interface)的定义就是图形用户界面。
其他补充
————————2023.05.10——————更新————————–
应该将信号绑定放在需要多次操作的函数外部,否则应设置操作完成后的判断条件去断开信号绑定;不然会出现信号多次绑定到同一槽函数,结果就是一次信号传递执行两次已绑定槽函数。
class Ui_MainWindow(object):
def __init__(self):
self.filename = ''
self.thread_btn_start = Worker(self, None)
self.thread_btn_start.sinOut.connect(self.text_browser_show)
self.thread_btn_start.sinEnd.connect(self.set_pushButton)
def pushButton_start_event(self): # 检查按钮绑定事件
if self.filename:
self.pushButton_start.setEnabled(False)
self.pushButton_chose.setEnabled(False)
self.thread_btn_start.start()
def set_pushButton(self):
self.pushButton_start.setEnabled(True)
self.pushButton_chose.setEnabled(True)
文章出处登录后可见!