主從架構
Pyqt常常使用**主從架構(Master-Workers 架構)**來避免界面卡死的情況。
Master-Workers 架構就像它的名字,一個master統領著幾個workers一起干活。其中某個worker倒下了不會導致整體任務失敗。matser不用干活,因此可以專心指揮workers。
在qt5中,master代表主線程,主要維持主界面的運行。當觸發某項耗時耗力的任務時,主線程將這項任務分配給其他線程(workers)來做。其他線程出現了災難性的錯誤,不會影響到主線程,因此程序不會完全崩潰。且主線程不承擔耗時耗力的任務,因此避免了復雜運算時主界面卡頓的問題。
進程和線程詳見1
pyqt5多線程的架構一般由三個模塊實現:
Gui.py
:只存放GUI界面,一般是Qt Designer生成的代碼,無需做任何修改Thread.py
:從線程,主要的邏輯代碼都放在這里。接收主線程的指令,并向主線程返回信號。Main.py
:主線程,負責運行GUI界面,向從線程發送指令并接收從線程返回的信號。
GUI模塊
Qt Designer保存的文件為Gui.ui
的格式,使用下面命令轉為Gui.py
。
pyuic5 Gui.ui -o Gui.py
轉換后的代碼結構如下,我們不需要對這個代碼做任何修改。
# -*- coding: utf-8 -*-# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.5.1
#
# WARNING! All changes made in this file will be lost!from PyQt5 import QtCore, QtGui, QtWidgetsclass Ui_MainWindow(object): # 注意這個類名,后面在Main.py中找到它def setupUi(self, MainWindow):MainWindow.setObjectName("MainWindow")# ------- 省略很多行... ------- #self.retranslateUi(MainWindow)QtCore.QMetaObject.connectSlotsByName(MainWindow)def retranslateUi(self, MainWindow):_translate = QtCore.QCoreApplication.translateMainWindow.setWindowTitle(_translate("MainWindow", "標題"))# ------- 省略很多行... ------- #
不同線程間的信號與槽
在討論主線程和從線程之前,首先要明確線程間傳遞信號的方法2。
主 -> 從
主線程到從線程的信號就是最基本信號與槽的機制,使用槽函數來與從線程通信。一般是Wight被clicked,然后觸發槽函數。傳遞的路徑為:1.觸發信號事件 -> 2.信號clicked -> 3.槽函數接收信號并運行
。使用以下語句綁定槽函數:
from Thread import New_thread # 從線程的引用class MainWindow(QMainWindow, Ui_MainWindow):def __init__(self, parent=None) -> None:super(MainWindow,self).__init__(parent)self.setupUi(self)# ----------------------------------------- ## ↓↓↓↓↓↓↓ 不用管上面的代碼,主要看下面 ↓↓↓↓↓↓↓ ## ----------------------------------------- #self.thread = None # 先預定義一個從線程的實例屬性,這里無需將從線程實例化# self.btn為Ui_MainWindow中定義的按鈕,這里將按鈕點擊的信號與槽函數self.func連接起來self.btn.clicked.connect(self.func) def func(self):self.thread = New_thread() # 在槽函數中實例化從線程,然后就可以操作從線程了self.thread.start()pass
有時我們需要向槽函數傳遞參數,一般使用偏函數或lambda,偏函數可參見3:
from functools import partial # 偏函數的引用
from Thread import New_thread # 從線程的引用class MainWindow(QMainWindow, Ui_MainWindow):def __init__(self, parent=None) -> None:super(MainWindow,self).__init__(parent)self.setupUi(self)# ----------------------------------------- ## ↓↓↓↓↓↓↓ 不用管上面的代碼,主要看下面 ↓↓↓↓↓↓↓ ## ----------------------------------------- #self.thread = None # 先預定義一個從線程的實例屬性,這里無需將從線程實例化# 第一種方法:這個槽函數被寫成了偏函數的形式 partial(self.func, param1, param2)self.btn1.clicked.connect(partial(self.func, param1=1, param2=2)) # 第二種方法:這個槽函數被寫成了lambda的形式 lambda:self.func(param1=1, param2=2)self.btn2.clicked.connect(lambda:self.func(param1=1, param2=2))# 這個槽函數監聽了兩個信號哦def func(self, param1, param2):# 在槽函數中實例化從線程,然后就可以操作從線程了self.thread = New_thread(param1, param2) # 從線程實例化也可以放在__init__里面,但我喜歡放在這。self.thread.start()pass
從 -> 主
從線程向主線程傳遞信號一般使用自定義信號,觸發后,從線程的自定義信號傳遞給主線程連接的槽函數。觸發的路徑為1.從線程觸發信號emit -> 2.聲明信號pyqtSignal -> 3.傳遞給主線程連接的槽函數
。使用下面代碼建立自定義信號。
先在Thread.py
中定義信號:
# Thread.py
from PyQt5.QtCore import QThread, pyqtSignalclass New_Thread(QThread):# 聲明定義信號,注意它必須是類屬性。mySignal = pyqtSignal(int,str) # 后面的參數是信號的數據類型def __init__(self) -> None:super(New_Thread, self).__init__(parent)pass def run(self):pass# ------------------------------------------ ## ↓↓↓↓↓↓↓ 上面的兩個函數不用理會,看下面 ↓↓↓↓↓↓↓ ## ------------------------------------------ ## 下面是從線程的邏輯代碼def func(self):# ------- 省略很多邏輯代碼... ------- ## 向主線程發送信號self.mySignal.emit(1,"Hello, Pyqt5")
在主線程Main.py
中監聽信號并連接到槽:
# Main.py
from Thread import New_thread # 從線程的引用class MainWindow(QMainWindow, Ui_MainWindow):def __init__(self, parent=None) -> None:super(MainWindow,self).__init__(parent)self.setupUi(self)# ----------------------------------------- ## ↓↓↓↓↓↓↓ 不用管上面的代碼,主要看下面 ↓↓↓↓↓↓↓ ## ----------------------------------------- #def func1(self):self.thread = New_thread() # 從線程實例化(也可以放在__init__里面)self.thread.start()# 監聽從線程發出的信號,并連接到槽函數func2# 記得嗎?mySignal發出了兩個數據,一個是int類型,一個str類型self.thread.mySignal.connect(self.func2) # 槽函數接收了從線程的信號def func2(self, param1:int, param2:str):pass
主線程Main模塊
主線程的作用是維護UI界面運行,下面給出Main模塊的一般架構
import sys # 顯示ui界面必要的引用
from PyQt5.QtWidgets import QMainWindow, QApplication # 顯示ui界面必要的引用
from GUI import * # 引用Qt Designer生成的GUI模塊
from Thread import New_thread # 從線程的引用# 第一個父類是PyQt5.QtWidgets.QMainWindow(取決于你在Qt Designer選擇的窗口類型)
# 第二個父類是GUI.Ui_MainWindow
class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self, parent=None) -> None:super(MainWindow,self).__init__(parent)self.setupUi(self) # 初始化UI界面self.thread = None # 先預定義一個從線程的實例屬性,這里無需將從線程實例化self.btn1.clicked.connect(self.func1) # 綁定控件的槽函數,以啟動從線程self.thread.finished.connect(self.func3) # 監聽線程是否完成任務,以結束從線程# 省略一萬行綁定槽函數的代碼...# 定義槽函數,這里可以放入從線程。def func1(self):self.thread = New_Thread() # 實例化一個從函數self.thread.start()self.thread.mySignal.connect(self.func2) # 監聽從線程的信號,并綁定槽函數# 定義響應從線程信號的槽函數def func2(self,param:int):pass# 定義結束從線程的槽函數def func3(self):self.thread.stop()# 省略一萬個槽函數...if __name__ == '__main__':# 任何一個qt應用都必須有且僅有一個QApplication對象# sys.argv是一組命令行參數的列表。# 這行代碼就是實例化一個QApplicationapp = QApplication(sys.argv) # 主線程實例化main_window = MainWindow()# 顯示窗口main_window.show()# sys.exit()是Python退出進程的函數# QApplication.exec_()的功能是“qt程序進入主循環,直到exit()被調用”# 沒有exec_()的話,程序不會進入主循環,會閃退。沒有sys.exit()的話,程序退出后進程不會結束。sys.exit(app.exec_())
從線程Thread模塊
from PyQt5.QtCore import QThread, pyqtSignal
from functools import partialclass New_Thread(QThread):# 聲明定義信號,注意它必須是類屬性。mySignal = pyqtSignal(int,str) # 后面的參數是信號的數據類型finishedSignal = pyqtSignal() # 線程完成的信號def __init__(self) -> None:super(New_Thread, self).__init__(parent)# run()是父類的方法,這里要重寫run方法# 將邏輯代碼放在run里面,當主線程調用thead.start()時會自動運行run函數。def run(self):# 省略一萬行代碼self.finishedSignal.emit()# 停止線程def stop(self):self.isRunning = False # isRunning是父類的屬性,可以停止線程。
進程和線程 ??
信號與槽函數 ??
偏函數 ??