PyQt5: QThreadPool vs QThread

當程式執行時間比較長的時候,會出現類似當機整個畫面不能動的情況。在有 GUI(Graphical user interface) 的程式會特別明顯,而且如果因為程式執行間發生錯誤,也會造成整個程式當機。

PyQt 有提供 multi-tasking 的 library,分別是 QThreadPoolQThread

QThreadPool

使用 QThreadPool 要先設計一個 QRunnable 的 Object (我看的範例都叫 Worker),裡面放要花很長時間執行的程式。通常會需要設計 Signals 來傳送各種訊息與其他物件溝通 (不知道表達得是否精確,反正我是這麼理解的)。

import sys
import time

from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QRunnable
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QPushButton, QLabel

class Signals(QObject):
    start = pyqtSignal(str)  # 開始訊號
    running = pyqtSignal(int, str)  # 運行中訊號
    complete = pyqtSignal(str)  # 完成訊號

    # pyqtSignal() 可以傳送各類型的變數,也可以同時傳多個變數。

class Worker(QRunnable):
    def __init__(self):
        super(Worker, self).__init__()
        self.signal = Signals()

    def run(self):
        """
        要花很長時間執行的程式
        """
        self.signal.start.emit("start")  # 發出開始訊號
        for i in range(10):
            self.signal.running.emit(i, "running")  # 發出運行中訊號
            print(i)
            time.sleep(1)

        self.signal.complete.emit("complete")  # 發出完成訊號

class App(QWidget):
    def __init__(self):
        super(App, self).__init__()

        # Layout
        layout = QVBoxLayout(self)
        self.setLayout(layout)

        # 其他物件
        self.start_btn = QPushButton("Start")
        self.label_information = QLabel("wait to start")
        layout.addWidget(self.start_btn)
        layout.addWidget(self.label_information)

        # 建立 thread pool
        self.threadpool = QThreadPool()
        self.start_btn.clicked.connect(self.start_btn_clicked)

    def start_btn_clicked(self):
        # 建立 worker
        worker = Worker()

        # 連接訊號與 function
        worker.signal.start.connect(self.work_start)
        worker.signal.running.connect(self.work_running)
        worker.signal.complete.connect(self.work_complete)

        # 將 worker 放入thread pool 中並開始運行
        self.threadpool.start(worker)

    def work_start(self, msg):
        """
        收到 start 訊號之後要執行的程式
        """
        self.label_information.setText(msg)

    def work_running(self, i, msg):
        """
        收到 running 訊號之後要執行的程式
        """
        self.label_information.setText(f"{i}: {msg}")

    def work_complete(self, msg):
        """
        收到 complete 訊號之後要執行的程式 
        """
        self.label_information.setText(msg)

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

要特別注意的是:

  • Signals() 一定要另外設計成一個 object,要不然會有錯誤。

QThread

QThread 只要設計一個 Worker,裡面包含 pyqtSignal() ,以及要花很多時間執行的程式。

import sys
import time

from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QVBoxLayout, QWidget, QPushButton, QLabel

class Worker(QThread):
    is_start = pyqtSignal(str)  # 開始訊號
    running = pyqtSignal(int, str)  # 運行中訊號
    complete = pyqtSignal(str)  # 完成訊號

    # pyqtSignal() 可以傳送各類型的變數,也可以同時傳多個變數。

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

    def run(self):
        """
        要花很長時間執行的程式
        """
        self.is_start.emit("start")  # 發出開始訊號
        for i in range(10):
            self.running.emit(i, "running")  # 發出運行中訊號
            print(i)
            time.sleep(1)

        self.complete.emit("complete")  # 發出完成訊號

class App(QWidget):
    def __init__(self):
        super(App, self).__init__()

        # Layout
        layout = QVBoxLayout(self)
        self.setLayout(layout)

        # 其他物件
        self.start_btn = QPushButton("Start")
        self.label_information = QLabel("wait to start")
        layout.addWidget(self.start_btn)
        layout.addWidget(self.label_information)

        # 建立 thread pool

        self.start_btn.clicked.connect(self.start_btn_clicked)

    def start_btn_clicked(self):
        # 建立 worker
        self.worker = Worker()
        self.worker.is_start.connect(self.work_start)
        self.worker.running.connect(self.work_running)
        self.worker.complete.connect(self.work_complete)
        self.worker.start()

    def work_start(self, msg):
        """
        收到 start 訊號之後要執行的程式
        """
        self.label_information.setText(msg)

    def work_running(self, i, msg):
        """
        收到 running 訊號之後要執行的程式
        """
        self.label_information.setText(f"{i}: {msg}")

    def work_complete(self, msg):
        """
        收到 complete 訊號之後要執行的程式
        """
        self.label_information.setText(msg)

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

要特別注意的是:

  • Worker() 裡面的 pyqtSignal() 一定要是 class variable,不能是 instance variable。
  • App() 裡面呼叫 Worker() 時,一定要用 self.worker = Worker() 不能用 worker = Worker()

QRunnable vs QThread

其實我分不太出來這兩個有什麼不一樣。但是在網路上找到的資料是說,QRunnable功能比較陽春,而QThread 功能比較完整且彈性。如果一般情況,能用 QRunnable 就用 QRunnable

參考資料

  1. https://www.reddit.com/r/learnpython/comments/azh3xu/python3_with_pyqt5_whats_a_good_way_to_implement/
  2. https://blog.daychen.tw/2017/02/pyqt5-part-4.html
  3. https://stackoverflow.com/questions/16791824/c-qt-qthread-vs-qrunnable

關鍵字: python, multithreading, pyqt, GUI, 自學, 程式設計, 醫檢師

留言

這個網誌中的熱門文章

Terminal 預設換行

血庫人的夢魘 - 令人崩潰的抗癌新藥

Thomsen‐Friedenreich antigen (T 抗原) 活化病人之輸血策略