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