PySide QThread 概念&寫法 === ###### tags: `Python` `PySide` ### 目的 執行GUI時,是一個MainWindow(MW)在負責所有widget的建立、擺放、計算等等的操作 這時候如果有人需要長時間占用運算資源,那MW就會hang在那邊,視窗也會沒辦法回應,其他事情都不用做了。 所以遇到這種工作要處理時,我們可以引入thread的概念,把這些繁瑣的工作交給子程序做,讓主視窗的運算資源可以空出來做其他事情。 ### 概念 在提到thread之前,需要先介紹signal的概念。 signal是訊號的意思,用來通知別人該做什麼事情。 而監聽signal的這個腳色,就是slot,可以根據收到的signal來做對應的事情。 因此程式的主要流程就是 1. MW送出signal給thread的slot 2. slot收到signal之後,開始執行運算 3. 運算完成後,thread送出signal給MW的slot,告知widget的內容需改變成什麼樣子 以上的流程還需要其他的一些作業,像是 * MW如何建立出thread,並出送signal * signal的格式 * signal和slot如何"連接"在一起 * 程式結束時要把thread清掉 以下會一一說明 ### 寫法 #### 定義signal ```python sig = QtCore.Signal(*types) sig.emit(*args) ``` sig是一個class,代表會傳出去的東西 types定義signal傳出去的變數類型,可以是str、int、... 需要傳遞多變數時也可以用list sig.emit()則是實際打出signal的指令,args同types,可傳多種變數 :::danger signal傳送和slot接收的變數型態、數量必須相同 ::: --- 網路上有很多人的範例是把thread class繼承自QThread再定義function 但官方不建議這樣做,底下是官方推廣的用法 #### 創建Thread 一樣要建立一個QObject class,來放thread的signal和function ```python # Define work_thread object class WorkThread(QtCore.QObject): # signal of thread th_sig = QtCore.Signal() def __init__(self): super().__init__() # Slot function of thread def run_proc(self, r): for i in range(r): self.trigger.emit(i) time.sleep(0.5) # 當thread object中有需要中止無限迴圈的需求, # 則需要define stop function來預防main window無法控制WorkThread def stop(self): pass '''In MainWindow''' # New a thread instance work_thread = WorkThread() # 定義由MW管理的thread main_thread = QtCore.QThread() # 將workthread交給main_thread管理 work_thread.moveToThread(main_thread) ``` 官方建議在main_window創建一個QThread,並將work_thread移交給他管理 #### 定義MW中的function & signal ```python '''In MainWindow''' mw_sig = QtCore.Signal(int) # Send signal def start_thread(self): main_thread.start() num = 10 mw_sig.emit(num) # Slot function of MW def say_hello(self, i): print(f"{i}: Hello World!") ``` start_thread()中main_thread.start()表示建立一個subprocess 至於如何呼叫start_thread,我們可以在GUI定義一個button #### 連接MW和thread ```python # work_thread的signal連到say_hello work_thread.th_sig.connect(say_hello) # MW的signal連到work_thread的run_proc mw_sig.connect(work_thread.run_proc) ``` #### 中止正在執行的thread ```python # self.work_thread.stop() self.main_thread.quit() self.main_thread.wait() ``` :::danger 若work_thread中有infinite loop,必須先把work_thread stop掉,才不會出現被work_thread卡死main window的情況 ::: quit()結束掉thread,wait直到清除程序結束。 ### 實例 ```python class WorkThread(QtCore.QObject): trigger = QtCore.Signal(int) stop_sig = QtCore.Signal() def __init__(self): super().__init__() def run_proc(self, r): for i in range(r): self.trigger.emit(i) time.sleep(0.5) self.stop_sig.emit() class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): mw_sig = QtCore.Signal(int) def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setupUi(self) self.setup_thread() def setup_thread(self): self.main_thread = QtCore.QThread(self) self.work_thread = WorkThread() self.work_thread.moveToThread(self.main_thread) self.work_thread.trigger.connect(self.say_hello) self.regReadBtn.clicked.connect(self.start_thread) self.mw_sig.connect(self.work_thread.run_proc) self.WorkThread.stop_sig.connect(stop_thread) def start_thread(self): self.main_thread.start() range_number = 10 self.mw_sig.emit(range_number) def say_hello(self, i): self.cmdText.append(f"{i} time: Hi, {self.dataLine.text()}") def stop_thread(self): self.main_thread.quit() self.main_thread.wait() ``` ### 除錯 若是在thread出現bug了,不會顯示在console上說錯哪一行、錯誤類型等等資訊 需要加入以下程式碼來幫助除錯 ```python try: # do some heavy work here print 1 # 1 is printed in my OutputWidget print a # a is not defined and it should print an error except: (type, value, traceback) = sys.exc_info() sys.excepthook(type, value, traceback) ...decide if should exit thread or what... ``` ### 參考 [PySide6 QThread简易教程](https://www.jianshu.com/p/e955fc332007)