1. 程式人生 > >PyQt5進階(二)——多執行緒:QThread & 事件處理

PyQt5進階(二)——多執行緒:QThread & 事件處理

接上篇…

2. QThread

要使用QThread開始一個執行緒,可以建立它的一個子類,然後覆蓋其QThread.run()函式

class Thread(QThread):

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

    def run(self):
        # 執行緒相關程式碼
        pass

# 建立一個新的執行緒
thread = Thread()
thread.start()

在使用執行緒時可以直接得到Thread例項,呼叫其start()函式即可啟動執行緒,執行緒啟動後,會呼叫其實現的run方法,該方法就是執行緒的執行函式,當run()退出之後執行緒基本就結束了。

QThread類中的常用方法:

方法 描述
start() 啟動執行緒
wait() 阻止執行緒
sleep(s) 強制當前執行緒睡眠s秒

QThread類中的常用訊號:

訊號 描述
started 在開始執行run()函式之前,從相關執行緒發射此訊號
finished 在程式完成業務邏輯時,從相關執行緒發射此訊號

當在視窗中顯示的資料比較簡單時,可以把讀取資料的業務邏輯放在視窗的初始化程式碼中;但如果讀取資料的時間比較長,比如網路請求資料的時間比較長,則可以把這部分邏輯放在QThread執行緒中,實現介面的資料顯示和資料讀取的分離.

  • 分離介面顯示和資料讀取
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys

class Worker(QThread):
    sinOut = pyqtSignal(str) # 自定義訊號,執行run()函式時,從相關執行緒發射此訊號

    def __init__(self, parent=None):
        super(Worker, self).__init__(parent)
        self.working = True
        self.num = 0
def __del__(self): self.working = False self.wait() def run(self): while self.working == True: file_str = 'File index {0}'.format(self.num) # str.format() self.num += 1 # 發出訊號 self.sinOut.emit(file_str) # 執行緒休眠2秒 self.sleep(2) class MainWidget(QWidget): def __init__(self, parent=None): super(MainWidget, self).__init__(parent) self.setWindowTitle("QThread 例子") # 佈局管理 self.listFile = QListWidget() self.btnStart = QPushButton('開始') layout = QGridLayout(self) layout.addWidget(self.listFile, 0, 0, 1, 2) layout.addWidget(self.btnStart, 1, 1) # 連線開始按鈕和槽函式 self.btnStart.clicked.connect(self.slotStart) # 建立新執行緒,將自定義訊號sinOut連線到slotAdd()槽函式 self.thread = Worker() self.thread.sinOut.connect(self.slotAdd) # 開始按鈕按下後使其不可用,啟動執行緒 def slotStart(self): self.btnStart.setEnabled(False) self.thread.start() # 在列表控制元件中動態新增字串條目 def slotAdd(self, file_inf): self.listFile.addItem(file_inf) if __name__ == "__main__": app = QApplication(sys.argv) demo = MainWidget() demo.show() sys.exit(app.exec_())

上一個例子中,雖然解決了介面的資料顯示和資料讀取的分離,但是如果資料的讀取非常消耗時間,則會造成介面卡死,下面是一個需要耗費很長時間讀取資料的例子

  • 介面卡死
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

global sec
sec = 0

def setTime():
    global sec
    sec += 1
    # LED顯示數字+1
    lcdNumber.display(sec)

def work():
    # 計時器每秒計數
    timer.start(1000)

    # 開始一次非常耗時的計算
    # 這裡用一個2 000 000 000次的迴圈來模擬
    for i in range(200000000):
        pass

    timer.stop()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    top = QWidget()
    top.resize(300, 120)

    # 垂直佈局類QVBoxLayout
    layout = QVBoxLayout(top)

    # 新增控制元件
    lcdNumber = QLCDNumber()
    layout.addWidget(lcdNumber)
    button = QPushButton("測試")
    layout.addWidget(button)
    timer = QTimer()

    # 每次計時結束,觸發setTime
    timer.timeout.connect(setTime)

    # 連線測試按鈕和槽函式work
    button.clicked.connect(work)

    top.show()
    sys.exit(app.exec_())

程式的執行邏輯如下:

這裡寫圖片描述

正常情況下,在點選按鈕之後,LCD上的數字會隨著時間發生變化,但是在實際執行過程中會發現點選按鈕之後,程式介面直接停止響應,直到迴圈結束才開始重新更新,於是計時器始終顯示為0。

在上面這個程式中沒有引入新的執行緒,PyQt中所有的視窗都在UI主執行緒中(就是執行了QApplication.exec()的執行緒),在這個執行緒中執行耗時的操作會阻塞UI執行緒,從而讓視窗停止響應。

為了避免出現上述問題,要使用QThread開啟一個新的執行緒,在這個執行緒中完成耗時的操作:

  • 分離UI主執行緒與工作執行緒
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *

global sec
sec = 0

# 增加了一個繼承自QThread類的類,重新寫了它的run()函式
# run()函式即是新執行緒需要執行的:執行一個迴圈;傳送計算完成的訊號。
class WorkThread(QThread):
    trigger = pyqtSignal()

    def __int__(self):
        super(WorkThread, self).__init__()

    def run(self):
        for i in range(2000000000):
            pass

        # 迴圈完畢後發出訊號
        self.trigger.emit()


def countTime():
    global sec
    sec += 1
    # LED顯示數字+1
    lcdNumber.display(sec)


def work():
    # 計時器每秒計數
    timer.start(1000)
    # 計時開始
    workThread.start()
    # 當獲得迴圈完畢的訊號時,停止計數
    workThread.trigger.connect(timeStop)


def timeStop():
    timer.stop()
    print("執行結束用時", lcdNumber.value())
    global sec
    sec = 0


if __name__ == "__main__":
    app = QApplication(sys.argv)
    top = QWidget()
    top.resize(300, 120)

    # 垂直佈局類QVBoxLayout
    layout = QVBoxLayout(top)

    # 加個顯示屏
    lcdNumber = QLCDNumber()
    layout.addWidget(lcdNumber)
    button = QPushButton("測試")
    layout.addWidget(button)

    timer = QTimer()
    workThread = WorkThread()

    button.clicked.connect(work)

    # 每次計時結束,觸發 countTime
    timer.timeout.connect(countTime)

    top.show()
    sys.exit(app.exec_())

程式執行邏輯簡單說明

按下按鈕後,計時器開始計數,並啟動一個新的執行緒,在這個執行緒裡,執行一個迴圈並在迴圈結束時傳送完成訊號,在完成訊號發出後,執行與之相關聯的槽函式,關閉定時器。

再次執行程式,介面有了響應。

3. 事件處理

對於執行很耗時的程式來說,由於PyQt需要等待程式執行完畢才能進行下一步,這個過程表現在介面上就是卡頓;而如果在執行這個耗時程式時不斷地執行QApplication.processEvents(),那麼就可以實現一邊執行耗時程式,一邊重新整理頁面的功能,會給人一種相對更流暢的感覺,QApplication.processEvents()的使用方法是,在主函式執行耗時操作的地方,加入QApplication.processEvents(),processEvents()函式的使用方法簡單來說就是重新整理頁面。

from PyQt5.QtWidgets import QWidget, QPushButton, QApplication, QListWidget, QGridLayout
import sys
import time


class WinForm(QWidget):

    def __init__(self, parent=None):
        super(WinForm, self).__init__(parent)
        self.setWindowTitle("實時重新整理介面例子")
        self.listFile = QListWidget()
        self.btnStart = QPushButton('開始')

        layout = QGridLayout(self)
        layout.addWidget(self.listFile, 0, 0, 1, 2)
        layout.addWidget(self.btnStart, 1, 1)
        self.setLayout(layout)

        self.btnStart.clicked.connect(self.slotAdd)

    def slotAdd(self):
        for n in range(10):
            str_n = 'File index {0}'.format(n)
            self.listFile.addItem(str_n)
            QApplication.processEvents()
            time.sleep(1)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    form = WinForm()
    form.show()
    sys.exit(app.exec_())

如果不新增QApplication.processEvents(),會在卡頓之後顯示輸出全部結果,新增之後,也不能保證每個都是逐行顯示,只是比不加相對流暢一點,效果是不如多執行緒的。