Python PyQt5 QThread 用法與範例

本篇 ShengYu 介紹 Python PyQt5 QThread 用法與範例,在 GUI 程式中,如果你想要讓程式做一件很耗時的工作,例如:下載檔案、I/O 存取等等,在 UI thread 做這些事的話會讓整個 UI 卡住,出現 UI 無回應的狀態,這時你可以將這些耗時的工作另外開執行緒去做,以避免 UI thread 卡住,在 PyQT 中我們可以使用 QThread 來完成這件事,接下來就介紹如何在 PyQT5 中使用 QThread。

以下的 Python PyQt5 QHBoxLayout 用法與範例將分為這幾部分,

  • PyQt5 QThread 的基本用法
  • PyQt5 錯誤使用執行緒讓畫面卡住
  • PyQt5 在 QWidget 裡使用 QThread

那我們開始吧!

PyQt5 QThread 的基本用法

PyQt5 要使用 QThread 建立一個執行緒的話,需要新增 QThread 的一個子類,然後覆寫 QThread.run() 函式,就像下例子中的 WorkerThread 類別繼承 QThread 並且覆寫了 run 成員函式,

1
2
3
4
5
6
class WorkerThread(QThread):
def __init__(self):
super().__init__()

def run(self):
# do something

接下來就可以使用 QThread.start() 來啟動執行緒,我們先來看一個小例子,在建構完 WorkerThread 還不會去執行 run 函式,直到呼叫 QThread.start() 才會開始去執行 run 函式,另外如果主執行緒要等待這兩個執行緒執行完畢才繼續往下執行的話可以使用 QThread.wait()QThread.wait() 會等待該執行緒執行完成才會返回,如果該執行緒裡寫了一個無窮迴圈的話,那麼執行 QThread.wait() 就會進入無限等待,等到天荒地老了,

python-pyqt-qthread.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QThread


class WorkerThread(QThread):
def __init__(self):
super().__init__()

def run(self):
for i in range(3):
time.sleep(1)
print('WorkerThread::run ' + str(i))

if __name__ == '__main__':
app = QApplication(sys.argv)
print('main')

work1 = WorkerThread()
work2 = WorkerThread()
work1.start()
work2.start()
work1.wait()
work2.wait()
print('end of main')
# sys.exit(app.exec_())

在這個 run 函式裡有個 for 迴圈執行 3 次迴圈,並且每次 sleep 1 秒就輸出一個訊息,以便我們了解執行迴圈到第幾次了,以下為輸出的結果,可以發現主執行緒是等到兩個 WorkerThread 都執行完畢了以後才輸出了 end of main 訊息,證明了使用 QThread.wait() 是有效的,

1
2
3
4
5
6
7
8
main
WorkerThread::run 0
WorkerThread::run 0
WorkerThread::run 1
WorkerThread::run 1
WorkerThread::run 2
WorkerThread::run 2
end of main

接下來讓我們來修改 WorkerThread 類別,讓輸出的訊息能夠更好地分辨是哪個執行緒,在 WorkerThread 建構時帶入一個名稱,還有 sleep 的秒數,

python-pyqt-qthread2.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QThread


class WorkerThread(QThread):
thread_name = 'unknown'
sleep_seconds = 1

def __init__(self, thread_name, sleep_seconds):
super().__init__()
self.thread_name = thread_name
self.sleep_seconds = sleep_seconds

def run(self):
for i in range(3):
time.sleep(self.sleep_seconds)
print(self.thread_name + ' ' + str(i))

if __name__ == '__main__':
app = QApplication(sys.argv)
print('main')

work1 = WorkerThread('work 1', 2)
work2 = WorkerThread('work 2', 1)
work1.start()
work2.start()
work1.wait()
work2.wait()
print('end of main')
# sys.exit(app.exec_())

跟上例不同的次這次我們帶入不同的 sleep 秒數,來觀察看看是不是 work1 執行緒跟我們的預期一樣應該要比 work2 晚完成,以下為輸出的結果,果然 work1 因為 sleep 得比較久的關係所以比 work2 執行緒晚完成,

1
2
3
4
5
6
7
8
main
work 2 0
work 2 1
work 1 0
work 2 2
work 1 1
work 1 2
end of main

PyQt5 錯誤使用執行緒讓畫面卡住

新手在 PyQt5 開發過程中容易錯誤地使用執行緒導致讓畫面卡住或者畫面變黑,以一個下載檔案的程式為例,按下按鈕會去作約 10 秒的工作,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QLabel, QPushButton)


class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.setWindowTitle('my window')
self.setGeometry(50, 50, 200, 150)

layout = QVBoxLayout()
self.setLayout(layout)

self.mylabel = QLabel('press button to start download', self)
layout.addWidget(self.mylabel)

self.mybutton = QPushButton('start', self)
self.mybutton.clicked.connect(self.onButtonClick)
layout.addWidget(self.mybutton)

def onButtonClick(self):
self.mybutton.setDisabled(True)
for i in range(10):
time.sleep(1)
self.mylabel.setText('finish')
self.mybutton.setDisabled(False)

if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())

在執行的過程中按下按鈕後會發現整個 GUI 程式就無法再做其它 UI 操作,之後整個畫面卡住或者畫面變黑,如下圖所示,

這就是錯誤地使用 UI 執行緒,要如何解決這個問題呢?下一章節馬上給你介紹。

PyQt5 在 QWidget 裡使用 QThread

在 PyQT 程式中,主執行緒就是我們說的 UI 執行緒,UI 執行緒會處理所有 widget 的事務,所以如果有一耗時的工作要執行的話我們通常不會寫在 UI 執行緒裡,因為那樣會無法讓其它 widget 進行更新,導致畫面卡住或程式無回應的現象,解決的方法是另外建立一個執行緒在處理這些耗時的工作,

我們在 WorkerThread 裡新增了兩個 signal,分別為 trigger 與 finished,finished 就是完成後的訊號,而 trigger 就是我們執行過程中會發送的訊號,而要客製化 signal 訊號時使用 pysingal 來定義要發射到目標函式的函式原型,例如下例中的 trigger = pyqtSignal(str)

整個程式就是按下按鈕後,會開啟另一個執行緒,每一秒都會更新一次秒數到 label 上,透過 self.trigger.emit(str(i+1)) 來發射訊號並且傳送第幾秒參數,第 5 秒會結束這個執行緒,然後 self.finished.emit() 發射結束訊號,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import time
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout,
QLabel, QPushButton)
from PyQt5.QtCore import QThread, pyqtSignal


class WorkerThread(QThread):
trigger = pyqtSignal(str)
finished = pyqtSignal()

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

def run(self):
for i in range(5):
time.sleep(1)
self.trigger.emit(str(i+1))
print('WorkerThread::run ' + str(i))
self.finished.emit()


class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()

def initUI(self):
self.setWindowTitle('my window')
self.setGeometry(50, 50, 200, 150)

layout = QVBoxLayout()
self.setLayout(layout)

self.mylabel = QLabel('press button to start thread', self)
layout.addWidget(self.mylabel)

self.mybutton = QPushButton('start', self)
self.mybutton.clicked.connect(self.startThread)
layout.addWidget(self.mybutton)

self.work = WorkerThread()

def startThread(self):
self.mybutton.setDisabled(True)
self.work.start()
self.work.trigger.connect(self.updateLabel)
self.work.finished.connect(self.threadFinished)
self.updateLabel(str(0))

def threadFinished(self):
self.mybutton.setDisabled(False)

def updateLabel(self, text):
self.mylabel.setText(text)

if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())

結果圖如下,

如果不想要 threadFinished 函式裡僅僅地只是一行程式碼的話,想去除 threadFinished 函式的話可以改成 lambda 的寫法,如下範例所示,將 self.mybutton.setDisabled(False) 操作寫在 self.work.finished.connect() 裡的 lambda 運算式裡,更多詳細的 Python lambda 用法可以參考這篇

1
2
3
4
5
6
7
8
9
10
11
12
13
def startThread(self):
self.mybutton.setDisabled(True)
self.work.start()
self.work.trigger.connect(self.updateLabel)
# self.work.finished.connect(self.threadFinished)
self.work.finished.connect(lambda: self.mybutton.setDisabled(False))
self.updateLabel(str(0))

# def threadFinished(self):
# self.mybutton.setDisabled(False)

def updateLabel(self, text):
self.mylabel.setText(text)

以上就是 Python PyQt5 QThread 用法與範例介紹,
如果你覺得我的文章寫得不錯、對你有幫助的話記得 Facebook 按讚支持一下!

其它相關文章推薦
Python 新手入門教學懶人包
Python PyQt5 新手入門教學