0.前言
在PySide6
中,有且仅有一个来处理GUI显示的线程,如果我们一些业务需要大量运算,使得运算占用GUI线程时间太久,这样会导致主窗口不能响应用户操作,导致应用程序看起来像假死了一样,这时候我们需要使用一个新的线程来运算,这样GUI线程就不会被大量运算占用,下面来介绍和学习QThread
。
1.一个简单的处理方法
大量运算的场景也许不是很常见,但是占用时间的场景一定很常见,比如等待网络,比如大量数据填充至QListWidget
中,下面首先放一个模拟场景,这种代码就会产生GUI假死(冻结)问题。
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
class MyWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("QThread学习")
self.resize(800, 600)
self.setup_ui()
self.setup_connect()
def setup_ui(self):
self.mylistwidget = QListWidget(self)
self.mylistwidget.resize(500, 500)
self.mylistwidget.move(20, 20)
self.additem_button = QPushButton(self)
self.additem_button.resize(150, 30)
self.additem_button.setText("填充QListWidget")
self.additem_button.move(530, 20)
def setup_connect(self):
self.additem_button.clicked.connect(self.additem)
def additem(self):
for i in range(5000000):
item = QListWidgetItem()
item.setText(f"第{i}项Item")
item.setIcon(QPixmap())
self.mylistwidget.addItem(item)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWindow()
window.show()
app.exec()
代码可以直接运行,运行后点击按钮,我们会发现GUI界面立刻卡死了。
其中additem
函数产生了大量的运算,导致GUI界面会假死,其实这种场景我们未必需要QThread
出场,有个简单的方法来处理这种情况。那就是使用QApplication.processEvents()
函数,将这个函数插入在循环的合适位置,来告诉Qt这时候该处理GUI产生的事件(比如用户点击、鼠标悬浮),这样GUI会在每次循环的时候来处理用户对GUI的操作,从而达到不会卡死主界面的效果。
def additem(self):
for i in range(5000000):
item = QListWidgetItem()
item.setText(f"第{i}项Item")
item.setIcon(QPixmap())
self.mylistwidget.addItem(item)
QApplication.processEvents()
这个函数很好理解,因为GUI是事件驱动的,在合适的时机让GUI去处理事件,自然就不会在运算的时候卡死界面,但是QApplication.processEvents()
也不是万能的,这个仅限每次循环用时都比较短的时候才好用,假如我们每次循环需要1秒钟呢?如果更长呢?使用sleep()
函数来模拟更长时间的单次循环。
...............
from time import sleep
...............
...............
def additem(self):
for i in range(5000000):
item = QListWidgetItem()
item.setText(f"第{i}项Item")
item.setIcon(QPixmap())
self.mylistwidget.addItem(item)
QApplication.processEvents()
sleep(0.3)
...............
程序每次循环仅仅只用了0.3S,但是这时候我们看主界面,感觉会非常的卡,因为即使使用了QApplication.processEvents()
函数,也需要0.3S才能处理一次来自GUI的事件,这时候就需要QThread
了,把运算单独放在一个线程里面,让GUI正常响应事件。
2.QThread简介
首先上官方文档!
遇事不决先看文档,当然一上来看文档估计也看不太明白,尤其是官方文档对于例子的代码也不是很全,而且英语如果不好的话学起来会很困难。
因为是简易教程,更高级的用法就不学习了,这里主要讲,如何创建一个新的线程用于运算,而且在主线程(GUI线程)和线程里传递数据。
2.1 线程之间数据的传递
首先确定一点,线程之间的数据传递是靠信号传递的,把主线程的控件产生的信号链接到线程的函数上就可以,同时信号是可以传递参数的,通过信号传递参数来传递我们自己的数据,反过来也可以同样让主线程接收来自线程的信号同时传递参数,并且对数据进行处理。
2.2 使用QThread
想正确的使用QThread
,并不建议像网络上常见的教程那样直接使用一个类来继承QThread
类,并且重写它的方法,这种使用方法已经被Qt的作者严肃批评过了,虽然这么用起来似乎也没什么问题,但是果然我们还是按照官方的要求来比较好。
首先我们需要把运算函数单独封装成一个类,这个类要继承QObject
,并且自定义一个新的信号用于接受来自主线程的参数。
先删除MyWindow
类里的additem
函数,同时新建一个类,同时写好运算函数和定义信号。
注意,信号的声明一定要做成类变量,不能放在其他地方。
这里我们让线程来返回range
出来的结果,并且每循环一次就返回一次结果。
...............
class WorkThread(QObject):
range_requested = Signal(int) # 括号里是传出的参数的类型
def __init__(self):
super().__init__()
def range_proc(self, args): # args即为从主线程接收的参数
print(args)
for i in range(5000):
self.range_requested.emit(i) # 发射信号
sleep(0.5)
...............
这样,我们的线程类就包装好了,接下来在主线程中把线程类实例化起来,并且通过线程运行,终于QThread
要出场了。删除setup_connect()
函数。
...............
class MyWindow(QMainWindow):
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("QThread学习")
self.resize(800, 600)
self.setup_ui()
self.setup_thread()
...............
def setup_thread(self):
self.thread1 = QThread(self) # 创建一个线程
self.range_thread = WorkThread() # 实例化线程类
self.range_thread.moveToThread(self.thread1) # 将类移动到线程中运行
# 线程数据传回信号,用add_item函数处理
self.range_thread.range_requested.connect(self.add_item)
self.additem_button.clicked.connect(self.start_thread)
self.additem_button.clicked.connect(self.range_thread.range_proc) # 连接到线程类的函数
def start_thread(self):
self.thread1.start()
def add_item(self, requested_number): # 线程传回参数
text = f"第{requested_number}项————Item"
item = QListWidgetItem()
item.setIcon(QPixmap())
item.setText(text)
self.mylistwidget.addItem(item)
...............
先点击运行,让我们跑一下代码看看效果,可以看出QListWidget
中缓慢填充数据,并且没有卡死GUI界面。
2.3 代码详解
可能比较熟悉Qt的人已经看明白了,但是还是打算讲解一下代码功能和实际运行中遇到的坑。
self.additem_button.clicked.connect(self.range_thread.range_proc)
把线程类的函数连接到主线程中按钮的点击信号上,这样点下按钮时,就会在线程中运行该函数。同时,会将该信号参数传递给该函数,这时候就能解释为什么点击按钮之后,会打印出false
了,因为clicked
信号本身就会传递一个参数出去,这个参数被range_thread.range_proc
接收了,这里不打算详细说clicked
传递的是什么参数,但是他传递出去的值就是false
,有兴趣的读者可以自行查看官方文档。
self.range_thread.range_requested.connect(self.add_item)
range_requested
在线程类WorkThread
里面被定义成了一个信号Signal(int)
,在我们实例化线程类WorkThread
以后,需要在主线程中把这个信号连接到一个主线程函数中,用于处理线程传回来的数据,同时Signal(int)
确定了信号传递的数据类型是int
。
self.range_requested.emit(i)
该语句表示发射信号,并且携带参数i
,根据前面的语句详解,可以得知,这个信号由主线程中的add_item
函数处理,同时参数会传给该函数。该函数在定义的时候要携带参数,例如:def add_item(self, requested_number):
,实际上requested_number
得到的数据就是i
传递来的。
2.4 Q&A
Q:信号可以随便传递任何类型的参数吗?
A:当然不是,但是常见的类型都可以传递。
Q:主线程的信号一定要连接线程类的函数吗?可以用其他主线程的函数中转吗?
A:一定要,不可以。主线程的信号不能连接到主线程的函数,即使这个函数仅有一行运行线程类函数的语句。这样做的话线程类的函数不会运行在线程中。
Q:在之前的QThread例子中,如果把range
改成5000000
,并且不sleep(0.5)
,即使用了QThread,也会卡死主界面?
A:QApplication.processEvents()
和QThread
要灵活运用,要分辨清楚究竟是GUI的刷新导致事件无法响应,还是大量运算导致事件无法响应,从而选择对应的方法。
Q:如何从主线程向线程传递参数和数据?
A:很简单,将线程传递到主线程的方法反过来就可以,在主线程中定义一个信号,然后在合适的时机发射信号并携带参数,从而让线程接收信号和数据。
3.示例代码
import sys
from time import sleep
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
class MyWindow(QMainWindow):
range_number = Signal(int)
def __init__(self) -> None:
super().__init__()
self.setWindowTitle("QThread学习")
self.resize(800, 600)
self.setup_ui()
self.setup_thread()
def setup_ui(self):
self.mylistwidget = QListWidget(self)
self.mylistwidget.resize(500, 500)
self.mylistwidget.move(20, 20)
self.additem_button = QPushButton(self)
self.additem_button.resize(150, 30)
self.additem_button.setText("填充QListWidget")
self.additem_button.move(530, 20)
def setup_thread(self):
self.thread1 = QThread(self) # 创建一个线程
self.range_thread = WorkThread() # 实例化线程类
self.range_thread.moveToThread(self.thread1) # 将类移动到线程中运行
# 线程数据传回信号,用add_item函数处理
self.range_thread.range_requested.connect(self.add_item)
self.additem_button.clicked.connect(self.start_thread)
self.range_number.connect(self.range_thread.range_proc)
# self.additem_button.clicked.connect(self.range_thread.range_proc) # 连接到线程类的函数
def start_thread(self):
self.thread1.start()
range_number = 30
self.range_number.emit(range_number) # 发射信号让线程接收需要range多少
def add_item(self, requested_number): # 线程传回参数
text = f"第{requested_number}项————Item"
item = QListWidgetItem()
item.setIcon(QPixmap())
item.setText(text)
self.mylistwidget.addItem(item)
class WorkThread(QObject):
range_requested = Signal(int) # 括号里是传出的参数的类型
def __init__(self):
super().__init__()
def range_proc(self, number): # number即为从主线程接收的参数
print(number)
for i in range(number):
self.range_requested.emit(i) # 发射信号
sleep(0.5)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MyWindow()
window.show()
app.exec()
该代码range
参数从主线程获取,线程运算range
后传回主线程,主线程负责处理运算后的数据结果。