PySide6 QThread简易教程

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简介

首先上官方文档!

https://doc.qt.io/qtforpython/PySide6/QtCore/QThread.html#

遇事不决先看文档,当然一上来看文档估计也看不太明白,尤其是官方文档对于例子的代码也不是很全,而且英语如果不好的话学起来会很困难。
因为是简易教程,更高级的用法就不学习了,这里主要讲,如何创建一个新的线程用于运算,而且在主线程(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后传回主线程,主线程负责处理运算后的数据结果。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 注明:本教程内容源自网络资源,大部分出自Vamei:http://www.cnblogs.com/vamei,小部...
    hyfine阅读 403评论 0 0
  • PyQt5入门教程 2019/12/11更新:我平时不看CSDN的,之前一时兴起发了过来,没想到反响还不错。这次就...
    资源分享吧1阅读 1,519评论 0 1
  • 本文内容来自菜鸟教程, C++教程,该篇内容仅作为笔记使用 继承 基类 & 派生类 一个类可以派生自多个类,这意味...
    leifuuu阅读 541评论 0 0
  • PyQt5:PyQt5 信号与槽(PyQt5的事件处理机制) 一、事件 在事件模型,有三个参与者:事件源、事件目标...
    gongdiwudu阅读 705评论 0 0
  • Java异步编程实战 chap1 认识异步编程 异步编程概念与作用在使用同步编程方式时,由于每个线程同时只能发起一...
    landon30阅读 1,218评论 0 0