PYQT作为界面程序包,为Pythoner快速构建界面,提供了便利性。特别是结合Pycharm扩展工具(QTdesigner)能够通过“拖拖拽拽”的方式构建简单界面。通过UIC将UI文件快速转化为PY文件,节省了时间。
PYQT的项目实践,必须参照MVC模式,才能形成多人工作合力,关于MVC的实践,会另起它文进行总结归纳,本文重点总结归纳PYQT项目中,因耗时操作产生界面卡顿现象时,如何通过分线程的方式,解决主界面线程等待造成界面卡顿的问题。
一、问题的出现
设计了一个界面,去局域网共享文件夹中查看是否存在文件,若存在,则在textBrowser中显示文件数量,每隔几秒钟执行一次,同时将信息Append到textBrowser中。
使用os.listdir(target_path)打开共享文件夹时,由于网络故障,待反馈耗时大,由于Ui_MainWindow中的方法都在主线程中进行,造成界面的卡顿。
二、解决思路
将耗时操作分离至新线程,主界面保留资源用于交互。查询网络上现有的解决思路,大致如下:利用PYQT中的QThread类,新起一个线程,用来处理共享文件夹查阅的操作,待该线程处理完成后,主线程接收子线程处理结果,append到textBrowser中。
三、难点
利用信号与槽的机制,将主、子线程建立关联关系。
四、如何实现
1、实例化Ui_MainWindow类的MyThread对象,用于填装子线程处理任务。
self.my_thread = MyThread()
2、将事件信号与槽函数建立联系,通过count函数启用线程。
self.StopButton.clicked.connect(self.CountFilesNum)
3、Count函数:
self.textBrowser_2.append("开始检测……")
self.my_thread.is_on = True
self.my_thread.start()#启动线程
4、设计MyThread类,继承Qthread类
my_signal = pyqtSignal(str) #
tipwords = ''
def __init__(self):
super(MyThread, self).__init__()
self.tipwords = ''
self.is_on = True
定义一个信号(my_signal)用于向槽函数传递字符型变量(字符会输出到textBrowser中),调用父类的INIT方法,完成初始化设置。
5、定义线程中的主逻辑函数:
all_content = os.listdir(target_path)
……
self.my_signal.emit(str(self.tipwords))
函数执行完,释放信号。线程中的信号关联到append函数
self.my_thread.my_signal.connect(self.appendTextBrowser)
实现了线程中数据返回主线程的目的。
五、总结
除了button中的信号与槽机制外,主、子线程也是通过信号与槽机制完成的关联,可见,信号与槽机制,确实是PYQT的核心。
在使用pyqt开发界面时,遇到了一种情况,就是在点击按钮之后,响应函数中会启动一个循环,该循环会一直执行,然后就造成界面无响应,如下所示,由于我是在Linux下运行的,所以界面直接显示成灰色(windows应该显示“无响应”):
这是因为对于pyqt来说,界面线程是主线程,如果我们在主线程函数里面调用了一个耗时比较久的循环,可能就会造成主界面线程卡死在循环中,从而造成无法操作主界面或者主界面卡顿、卡死。
所以这种情况下必须使用多线程的方式来解决,即在主界面线程中在启动一个新的子线程,利用该子线程处理比较耗时的操作,然后通过signal-slot机制将子线程的数据反馈到主界面线程中,而且在子线程中不能操作界面。这就是所说的:UI只用来操作UI,子线程只用来处理数据,就是将UI的操作与耗时数据的处理进行分开处理。
在pyqt中,可以通过QThread建立一个线程,
2、使用多线程解决界面卡顿 - 方式1
下面介绍 QThread 的第一种用法:新建一个类 RunThread 继承自 QThread,然后在 RunThread 类中重写 run() 函数,在 run() 函数中进行耗时数据的处理。下面是它的用法:
#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time
# 继承QThread
class Runthread(QtCore.QThread):
# 通过类成员对象定义信号对象
_signal = pyqtSignal(str)
def __init__(self):
super(Runthread, self).__init__()
def __del__(self):
self.wait()
def run(self):
for i in range(100):
time.sleep(0.1)
self._signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同
class Example(QtWidgets.QWidget):
def __init__(self):
super(Example, self).__init__()
# 按钮初始化
self.button = QtWidgets.QPushButton('开始', self)
self.button.move(120, 80)
self.button.clicked.connect(self.start_login) # 绑定多线程触发事件
# 进度条设置
self.pbar = QtWidgets.QProgressBar(self)
self.pbar.setGeometry(50, 50, 210, 25)
self.pbar.setValue(0)
# 窗口初始化
self.setGeometry(300, 300, 300, 200)
self.show()
self.thread = None # 初始化线程
def start_login(self):
# 创建线程
self.thread = Runthread()
# 连接信号
self.thread._signal.connect(self.call_backlog) # 进程连接回传到GUI的事件
# 开始线程
self.thread.start()
def call_backlog(self, msg):
self.pbar.setValue(int(msg)) # 将线程的参数传入进度条
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = Example()
myshow.show()
sys.exit(app.exec_())
上面的代码建立一个界面,界面中只包含了一个进度条和一个按钮,点击按钮之后,进度条开始运行。
在上面的代码中,新建了一个 RunThread 类,该类继承自 QThread 类,在 RunThread 中重写了 run() 函数,并将耗时处理放在了 run() 函数中,点击按钮之后,触发 start_login() 函数,在start_login() 中,先创建了 RunThread 线程类的对象,然后将该类中的 _signal 信号与 Example 类中的 call_back() 函数建立连接,这样,就可以在run()函数运行时,将运行时的数据传递(异步,因为信号的传递与触发有一定的延时)到主机面 Example 类中并进行显示,如下所示:
3、使用多线程解决界面卡顿 - 方式2
在pyqt中多线程的使用还有另外一种方式:RunThread 类继承自 QObject,而非继承自 QThread。这种方式使用起来比第一种要复杂,但是这种方法将数据的处理与线程的创建与启动分开进行处理,在某些场景下,采用这种方式会比较方便。
下面是第二种方式的代码:
#!/usr/bin/python
# coding:UTF-8
from PyQt5 import QtWidgets, QtCore
import sys
from PyQt5.QtCore import *
import time
# 继承 QObject
class Runthread(QtCore.QObject):
# 通过类成员对象定义信号对象
signal = pyqtSignal(str)
def __init__(self):
super(Runthread, self).__init__()
self.flag = True
def __del__(self):
print(">>> __del__")
def run(self):
i = 0
while self.flag:
time.sleep(1)
if i <= 100:
self.signal.emit(str(i)) # 注意这里与_signal = pyqtSignal(str)中的类型相同
i += 1
print(">>> run end: ")
class Example(QtWidgets.QWidget):
# 通过类成员对象定义信号对象
_startThread = pyqtSignal()
def __init__(self):
super(Example, self).__init__()
# 按钮初始化
self.button_start = QtWidgets.QPushButton('开始', self)
self.button_stop = QtWidgets.QPushButton('停止', self)
self.button_start.move(60, 80)
self.button_stop.move(160, 80)
self.button_start.clicked.connect(self.start) # 绑定多线程触发事件
self.button_stop.clicked.connect(self.stop) # 绑定多线程触发事件
# 进度条设置
self.pbar = QtWidgets.QProgressBar(self)
self.pbar.setGeometry(50, 50, 210, 25)
self.pbar.setValue(0)
# 窗口初始化
self.setGeometry(300, 300, 300, 200)
self.show()
self.myT = Runthread() # 创建线程对象
self.thread = QThread(self) # 初始化QThread子线程
# 把自定义线程加入到QThread子线程中
self.myT.moveToThread(self.thread)
self._startThread.connect(self.myT.run) # 只能通过信号-槽启动线程处理函数
self.myT.signal.connect(self.call_backlog)
def start(self):
if self.thread.isRunning(): # 如果该线程正在运行,则不再重新启动
return
# 先启动QThread子线程
self.myT.flag = True
self.thread.start()
# 发送信号,启动线程处理函数
# 不能直接调用,否则会导致线程处理函数和主线程是在同一个线程,同样操作不了主界面
self._startThread.emit()
def stop(self):
if not self.thread.isRunning(): # 如果该线程已经结束,则不再重新关闭
return
self.myT.flag = False
self.stop_thread()
def call_backlog(self, msg):
self.pbar.setValue(int(msg)) # 将线程的参数传入进度条
def stop_thread(self):
print(">>> stop_thread... ")
if not self.thread.isRunning():
return
self.thread.quit() # 退出
self.thread.wait() # 回收资源
print(">>> stop_thread end... ")
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myshow = Example()
myshow.show()
sys.exit(app.exec_())
该界面包含了一个进度条、一个开始按钮、一个停止按钮。当点击“开始”按钮之后,进度条会开始运行;当点击“停止”按钮时,进度条会停止运行,如下所示:
其中 RunThread 类是线程处理函数类,该类继承自 QObject,然后通过 moveToThread 函数将该线程处理函数类添加进一个线程中。
在使用这种方式时需要注意一下几点:
self.myT=Runthread(); // 创建线程处理函数类对象,需要注意的是创建时不能指定父对象,要不然moveToThread函数会报错
self._startThread.emit(); // 虽然说,将myT添加进thread线程,并且调用thread.start(),但是,并不能通过直接调用的方式来调用RunThread类中的线程处理函数run(),如果直接调用的话,相当于还是主界面线程在调用,依然会造成主界面卡顿。此时应该使用signal-slot的方式进行调用,即在Example类中声明一个信号_startThread,并通过self._startThread.connect(self.myT.run) 将该信号与 RunThread 类中的线程处理函数建立连接,这样当调用 thread.start() 后,再调用 self._startThread.emit() 函数就可以调用 run() 函数了
self.myT.signal.connect(self.call_backlog); // 为了获取RunThread类中的处理数据,也只能通过signal-slot的方式进行获取;
当thread调用quit()和wait()函数之后,此时该线程已经停止运行,但是线程处理函数run()还未停止运行,所以,需要在run函数中的循环中添加一个判断标志位,当thread线程停止后,将该标志位置为False,这样退出循环之后run函数就退出了;
当run函数退出之后,此时RunThread类对象myT并没有销毁,因为它是Example类的成员,所以只有当Example销毁时,myT才会销毁;
4、关于connect的连接方式
在QT中,查看connect函数原型:
当然,connect有多重函数重载形式,以上只是其中的一种。其中的第五个参数type指明了signal-slot的连接方式,Qt::ConnectionType有一些几种类型:
前面三种是比较常用的,其中QueuedConnection方式是用在上面多线程的情况下。
QueuedConnection:槽函数所在线程和接收所在线程是一样的;
DirectConnection: 槽函数所在线程和发送者所在线程是一样的;
不过大多数情况下,调用connect是使用默认参数就可以了,当使用默认参数AutoConnection时:
在多线程情况下,默认使用QueuedConnection;
在单线程下,默认使用DirectConnection;
同理,在pyqt中也一样,pyqt中connect函数原型:
使用方式也一样,直接使用默认连接方式就可以了。
其实pyqt和qt差别不大,就只有语言上的差别,使用方式还都是一样的,我一般都是先查qt上资料然后在套用到pyqt上。