PySide6 QThread簡易教程

0.前言

PySide6中,有且僅有一個來處理GUI顯示的線程,如果我們一些業(yè)務(wù)需要大量運(yùn)算,使得運(yùn)算占用GUI線程時間太久,這樣會導(dǎo)致主窗口不能響應(yīng)用戶操作,導(dǎo)致應(yīng)用程序看起來像假死了一樣,這時候我們需要使用一個新的線程來運(yùn)算,這樣GUI線程就不會被大量運(yùn)算占用,下面來介紹和學(xué)習(xí)QThread。

1.一個簡單的處理方法

大量運(yùn)算的場景也許不是很常見,但是占用時間的場景一定很常見,比如等待網(wǎng)絡(luò),比如大量數(shù)據(jù)填充至QListWidget中,下面首先放一個模擬場景,這種代碼就會產(chǎn)生GUI假死(凍結(jié))問題。

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學(xué)習(xí)")
        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()

代碼可以直接運(yùn)行,運(yùn)行后點擊按鈕,我們會發(fā)現(xiàn)GUI界面立刻卡死了。
其中additem函數(shù)產(chǎn)生了大量的運(yùn)算,導(dǎo)致GUI界面會假死,其實這種場景我們未必需要QThread出場,有個簡單的方法來處理這種情況。那就是使用QApplication.processEvents()函數(shù),將這個函數(shù)插入在循環(huán)的合適位置,來告訴Qt這時候該處理GUI產(chǎn)生的事件(比如用戶點擊、鼠標(biāo)懸浮),這樣GUI會在每次循環(huán)的時候來處理用戶對GUI的操作,從而達(dá)到不會卡死主界面的效果。

    def additem(self):
        for i in range(5000000):
            item = QListWidgetItem()
            item.setText(f"第{i}項Item")
            item.setIcon(QPixmap())
            self.mylistwidget.addItem(item)
            QApplication.processEvents()

這個函數(shù)很好理解,因為GUI是事件驅(qū)動的,在合適的時機(jī)讓GUI去處理事件,自然就不會在運(yùn)算的時候卡死界面,但是QApplication.processEvents()也不是萬能的,這個僅限每次循環(huán)用時都比較短的時候才好用,假如我們每次循環(huán)需要1秒鐘呢?如果更長呢?使用sleep()函數(shù)來模擬更長時間的單次循環(huán)。

...............
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)
...............

程序每次循環(huán)僅僅只用了0.3S,但是這時候我們看主界面,感覺會非常的卡,因為即使使用了QApplication.processEvents()函數(shù),也需要0.3S才能處理一次來自GUI的事件,這時候就需要QThread了,把運(yùn)算單獨放在一個線程里面,讓GUI正常響應(yīng)事件。

2.QThread簡介

首先上官方文檔!

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

遇事不決先看文檔,當(dāng)然一上來看文檔估計也看不太明白,尤其是官方文檔對于例子的代碼也不是很全,而且英語如果不好的話學(xué)起來會很困難。
因為是簡易教程,更高級的用法就不學(xué)習(xí)了,這里主要講,如何創(chuàng)建一個新的線程用于運(yùn)算,而且在主線程(GUI線程)和線程里傳遞數(shù)據(jù)。

2.1 線程之間數(shù)據(jù)的傳遞

首先確定一點,線程之間的數(shù)據(jù)傳遞是靠信號傳遞的,把主線程的控件產(chǎn)生的信號鏈接到線程的函數(shù)上就可以,同時信號是可以傳遞參數(shù)的,通過信號傳遞參數(shù)來傳遞我們自己的數(shù)據(jù),反過來也可以同樣讓主線程接收來自線程的信號同時傳遞參數(shù),并且對數(shù)據(jù)進(jìn)行處理。

2.2 使用QThread

想正確的使用QThread,并不建議像網(wǎng)絡(luò)上常見的教程那樣直接使用一個類來繼承QThread類,并且重寫它的方法,這種使用方法已經(jīng)被Qt的作者嚴(yán)肅批評過了,雖然這么用起來似乎也沒什么問題,但是果然我們還是按照官方的要求來比較好。
首先我們需要把運(yùn)算函數(shù)單獨封裝成一個類,這個類要繼承QObject,并且自定義一個新的信號用于接受來自主線程的參數(shù)。

先刪除MyWindow類里的additem函數(shù),同時新建一個類,同時寫好運(yùn)算函數(shù)和定義信號。
注意,信號的聲明一定要做成類變量,不能放在其他地方。
這里我們讓線程來返回range出來的結(jié)果,并且每循環(huán)一次就返回一次結(jié)果。

...............
class WorkThread(QObject):
    range_requested = Signal(int)  # 括號里是傳出的參數(shù)的類型

    def __init__(self):
        super().__init__()

    def range_proc(self, args):  # args即為從主線程接收的參數(shù)
        print(args)
        for i in range(5000):
            self.range_requested.emit(i)  # 發(fā)射信號
            sleep(0.5)
...............

這樣,我們的線程類就包裝好了,接下來在主線程中把線程類實例化起來,并且通過線程運(yùn)行,終于QThread要出場了。刪除setup_connect()函數(shù)。

...............
class MyWindow(QMainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setWindowTitle("QThread學(xué)習(xí)")
        self.resize(800, 600)
        self.setup_ui()
        self.setup_thread()
...............
    def setup_thread(self):
        self.thread1 = QThread(self)  # 創(chuàng)建一個線程
        self.range_thread = WorkThread()  # 實例化線程類
        self.range_thread.moveToThread(self.thread1)  # 將類移動到線程中運(yùn)行
        # 線程數(shù)據(jù)傳回信號,用add_item函數(shù)處理
        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)  # 連接到線程類的函數(shù)

    def start_thread(self):
        self.thread1.start()

    def add_item(self, requested_number):  # 線程傳回參數(shù)
        text = f"第{requested_number}項————Item"
        item = QListWidgetItem()
        item.setIcon(QPixmap())
        item.setText(text)
        self.mylistwidget.addItem(item)
...............

先點擊運(yùn)行,讓我們跑一下代碼看看效果,可以看出QListWidget中緩慢填充數(shù)據(jù),并且沒有卡死GUI界面。

2.3 代碼詳解

可能比較熟悉Qt的人已經(jīng)看明白了,但是還是打算講解一下代碼功能和實際運(yùn)行中遇到的坑。

self.additem_button.clicked.connect(self.range_thread.range_proc)
把線程類的函數(shù)連接到主線程中按鈕的點擊信號上,這樣點下按鈕時,就會在線程中運(yùn)行該函數(shù)。同時,會將該信號參數(shù)傳遞給該函數(shù),這時候就能解釋為什么點擊按鈕之后,會打印出false了,因為clicked信號本身就會傳遞一個參數(shù)出去,這個參數(shù)被range_thread.range_proc接收了,這里不打算詳細(xì)說clicked傳遞的是什么參數(shù),但是他傳遞出去的值就是false,有興趣的讀者可以自行查看官方文檔。

self.range_thread.range_requested.connect(self.add_item)
range_requested在線程類WorkThread里面被定義成了一個信號Signal(int),在我們實例化線程類WorkThread以后,需要在主線程中把這個信號連接到一個主線程函數(shù)中,用于處理線程傳回來的數(shù)據(jù),同時Signal(int)確定了信號傳遞的數(shù)據(jù)類型是int

self.range_requested.emit(i)
該語句表示發(fā)射信號,并且攜帶參數(shù)i,根據(jù)前面的語句詳解,可以得知,這個信號由主線程中的add_item函數(shù)處理,同時參數(shù)會傳給該函數(shù)。該函數(shù)在定義的時候要攜帶參數(shù),例如:def add_item(self, requested_number):,實際上requested_number得到的數(shù)據(jù)就是i傳遞來的。

2.4 Q&A

Q:信號可以隨便傳遞任何類型的參數(shù)嗎?
A:當(dāng)然不是,但是常見的類型都可以傳遞。

Q:主線程的信號一定要連接線程類的函數(shù)嗎?可以用其他主線程的函數(shù)中轉(zhuǎn)嗎?
A:一定要,不可以。主線程的信號不能連接到主線程的函數(shù),即使這個函數(shù)僅有一行運(yùn)行線程類函數(shù)的語句。這樣做的話線程類的函數(shù)不會運(yùn)行在線程中。

Q:在之前的QThread例子中,如果把range改成5000000,并且不sleep(0.5),即使用了QThread,也會卡死主界面?
A:QApplication.processEvents()QThread要靈活運(yùn)用,要分辨清楚究竟是GUI的刷新導(dǎo)致事件無法響應(yīng),還是大量運(yùn)算導(dǎo)致事件無法響應(yīng),從而選擇對應(yīng)的方法。

Q:如何從主線程向線程傳遞參數(shù)和數(shù)據(jù)?
A:很簡單,將線程傳遞到主線程的方法反過來就可以,在主線程中定義一個信號,然后在合適的時機(jī)發(fā)射信號并攜帶參數(shù),從而讓線程接收信號和數(shù)據(jù)。

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學(xué)習(xí)")
        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)  # 創(chuàng)建一個線程
        self.range_thread = WorkThread()  # 實例化線程類
        self.range_thread.moveToThread(self.thread1)  # 將類移動到線程中運(yùn)行
        # 線程數(shù)據(jù)傳回信號,用add_item函數(shù)處理
        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)  # 連接到線程類的函數(shù)

    def start_thread(self):
        self.thread1.start()
        range_number = 30
        self.range_number.emit(range_number)  # 發(fā)射信號讓線程接收需要range多少

    def add_item(self, requested_number):  # 線程傳回參數(shù)
        text = f"第{requested_number}項————Item"
        item = QListWidgetItem()
        item.setIcon(QPixmap())
        item.setText(text)
        self.mylistwidget.addItem(item)


class WorkThread(QObject):
    range_requested = Signal(int)  # 括號里是傳出的參數(shù)的類型

    def __init__(self):
        super().__init__()

    def range_proc(self, number):  # number即為從主線程接收的參數(shù)
        print(number)
        for i in range(number):
            self.range_requested.emit(i)  # 發(fā)射信號
            sleep(0.5)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MyWindow()
    window.show()
    app.exec()

該代碼range參數(shù)從主線程獲取,線程運(yùn)算range后傳回主線程,主線程負(fù)責(zé)處理運(yùn)算后的數(shù)據(jù)結(jié)果。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 注明:本教程內(nèi)容源自網(wǎng)絡(luò)資源,大部分出自Vamei:http://www.cnblogs.com/vamei,小部...
    hyfine閱讀 477評論 0 0
  • PyQt5入門教程 2019/12/11更新:我平時不看CSDN的,之前一時興起發(fā)了過來,沒想到反響還不錯。這次就...
    資源分享吧1閱讀 1,632評論 0 1
  • 本文內(nèi)容來自菜鳥教程, C++教程,該篇內(nèi)容僅作為筆記使用 繼承 基類 & 派生類 一個類可以派生自多個類,這意味...
    leifuuu閱讀 555評論 0 0
  • PyQt5:PyQt5 信號與槽(PyQt5的事件處理機(jī)制) 一、事件 在事件模型,有三個參與者:事件源、事件目標(biāo)...
    gongdiwudu閱讀 753評論 0 0
  • Java異步編程實戰(zhàn) chap1 認(rèn)識異步編程 異步編程概念與作用在使用同步編程方式時,由于每個線程同時只能發(fā)起一...
    landon30閱讀 1,286評論 0 0

友情鏈接更多精彩內(nèi)容