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)