iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道系列 第 16 篇

Day 16: 千里姻緣一線牽- 用 signal 與 slot 為 UI 接上功能

  • 分享至 

  • xImage
  •  

本篇目的:了解如何連接 QT 的 Signal & Slot,讓 UI 元件與 Python 腳本內的物件產生互動

前言

我們在先前的文章中個別提到了建構 UI 與 Python class 的方式。而 GUI 應用程式的運行邏輯是事件驅動程式設計(Event-Driven Programming),需要使用者點擊 UI 中的元件才能觸發對應 function。因此我們需要一個可以連接 UI 元件與對應 function 的方式。

傳統的 callback 連接方式會有高度耦合、無法自由斷開/產生連結等問題。在今天的文章中,我會介紹 QT 的最大優勢 ”Signal(訊號)與 Slot(槽)”的使用方式,讓 GUI 能真實的執行腳本,別再”無魂有體親像稻草人”。

傳統 UI 元件與 function 的連接方式

傳統上 UI 元件與 function 連接時,會使用 callback (回呼) 方式。以下面的 Python 的 tkinter code 為例,bark 被當作引數傳入 tk.Button 中,並且使用者按下按鈕後才會呼叫 bark 並印出”汪汪”。

這樣的連接方式有以下幾個缺點

  • 一個按鈕只能對一個功能

  • 無法直接傳入引數給 function

# 省略 import tkinter 與視窗建立

def bark():
    print('汪汪')

# bark function 被當作引數傳入 button 中
btn = tk.Button(root,
                text='讓狗狗叫',
                font=('Microsoft JhengHei',20),
                command=bark
                )

注意:被當作引數的 function 不可以加上括號,若 function 加上括號會直接執行,就像一隻隨便吠叫的壞狗狗

QT 的 Signal & Slot 特點

為了解決 callback 方式的靈活性不佳問題,QT 創立了 ”Signal & Slot” 機制。這個機制的最大特點是 UI 元件與 function 連接方式是透過連接指令,實作方式為 物件.行為.connect (function) 。物件執行某些行為時會發射訊號 (signal),而與其 connect 的物件(slot)接收到訊號後便會執行動作。

在 Python 裡任何東西都是物件,包括當作引數傳入 callback 的沒有括號之 function 也是一個物件

在 day 2 中有提到 PySide6 較 tkinter 靈活,且較容易讓我們達成模組化程式設計,這個原因就是 QT 的 Signal & Slot。Signal & Slot 連接示意圖如下(來源),由這示意圖可以看到 Signal & Slot 的幾個特色:

  • 一個 signal 可以對應多個 slot;當然,一個 slot 也可以對應多個 signal

  • 無論對應 slot 是甚麼、或根本沒有對應 slot,都不影響 signal 發射

  • connect 可以依需求連接或斷開

  • 一個物件可以同時有 slot 與 signal

從上面的特色可以知道 QT 的連結方式屬於低耦合 (low coupling),其中的物件腳色為只管執行後發射 signal、或是等著對應 signal 來才執行的 slot。這樣的連接方式會讓我們自然的將程式專案分成為負責建構 UI 的腳本、負責運算的腳本、以及負責 connect 的腳本,這就是前面提到的模組化程式設計。

https://ithelp.ithome.com.tw/upload/images/20260929/20182319wJz663uJmX.png

如何讓元件發射 signal

呼叫 Qt 元件的 signal

Qt 元件有許多內建訊號,在執行特定動作時便會發射對應訊號,使用這些訊號的方式與讀取物件的 property 相同,為 oblect.對應訊號名稱 。若以 QPushButton 為例,它就有 clicked() 、 pressed() 等訊號,另外像是 QLineEdit 這種沒有明顯按鍵的物件,也有 textChanged()、textEdited() 等訊號可用。Qt 元件訊號的指令如下:

button = QPushButton() # 創立按鍵
line = QLineEdit() # 創立輸入框

# QPushButton 常用訊號
button.clicked # clicked: 按按鍵並放開
button.pressed # pressed: 長按按鍵


# QLineEdit 常用訊號
line.textChanged # textChanged: 當文字變化時發射訊號 
line.textEdited # textEdited: 當QLineEdit處於編輯狀態時發射訊號

Qt 內建元件已經將訊號發射時機處理好了,因此不必設定 訊號.emit()

讓自訂的 class 發射 signal

自訂的 class 當然不會有內建訊號可用,因此我們需要在 class 中設定訊號 。可以發射訊號的 class 有以下幾個特點:

  • 需要繼承 QObject,這樣才能在 Qt 體系中發射訊號

  • 訊號是 class 的類別屬性,不可以放在 __init__() 裡面

若要設定該 class 的訊號,在 class 中設定 sig = Signal() 即可。實務上可以依照用途,設定不同的訊號。另外若希望訊號可以帶有引數,需要在設定時先指定引數型別,如下面的 pop_up_sig 。

# 需要先 import QObject, Signal
from PySide6.QtCore import QObject, Signal

class YieldCalculator(QObject): # 繼承 QObject

  # 注意 signal 不能放在 __init()__ 內
  # signal 一定是類別屬性 (Class Attribute)
  finish_sig = Signal()  # 完成訊號
  pop_up_sig = Signal(str, str) # 彈出式視窗訊號,內含"標題、訊息"

訊號設定好以後,即可在 class 內的 method 中設定發射訊號。以上面兩個訊號為例,pop_up_sig 設定在需要通知使用者訊息時發送、而 finish_sig 設定在正常完成時發送。另外可以看到 pop_up_sig 裡面帶有引數,因此彈出視窗會因情境顯示不同的內容。

  def run_analyzer(self, wafer_id:str):
  
    try:
      # 這裡是運算邏輯......
      
    except Exception as e:
      # 發生錯誤時顯示異常訊息
      self.pop_up_sig.emit('發生錯誤', f'{e}')
      
    else:
      # 正常結束時顯示恭喜訊息
      self.pop_up_sig.emit('已完成', '已完成計算,感謝使用')
      self.finish_sig.emit()

在官方文件中找尋可用的 signal

在 PySide6 的官方文件中(連結),可以確認該物件對應的訊號。

https://ithelp.ithome.com.tw/upload/images/20260929/20182319RvjpUDAEhW.png

另外 Qt 中的元件有明確的繼承關係,如下面的 QPushButton 關係圖。也因此 QPushButton 可以使用 QAbstractButton、QWidget 的signal。

在上一段提到”自訂 class 若想發射訊號,需要繼承 QObject ”,我們也可以在官方文件中看到所有會發射訊號的 QT 元件皆繼承了 QObject 。這是因為 QObject 中有一個 metaObject() ,它是建立 ”Signal & Slot” 機制的重要物件 (連結)。

https://ithelp.ithome.com.tw/upload/images/20260929/20182319CYcregxphw.png

官方文件中的 QPushButton 是沒有 signal 的,要到 QAbstractButton 才有 signal 可以確認

如何接收 signal

Slot (槽) 的設定

Slot 就是在收到 signal 後,要被觸發的 function。在 "QT 的 Signal & Slot 特點" 中有提到 Signal & Slot 便是為了解決 UI 模組與計算模組過度耦合的產物,因此通常做為 slot 的 function 都與 UI 元件狀態的更動有關 (弱耦合)。

Slot 的設定與一般 function 完全相同,但 slot function 可以加上一個 @Slot() 裝飾器。加上 @Slot() 的好處如下

  • 標示更明確:可以清楚知道這個 function 是一個 slot

  • 執行速度較快:QT 的底層是 C++,若 slot 有加上裝飾器,可以在腳本執行時就預先註冊至 C++ 底層物件,增加執行速度

Slot 的設定範例如下

# 接收引數的signal後,彈出視窗的 function
# @slot()裡面可以標示對應引數

@Slot(str, str)
def pop_up_message(self, title:str, content:str):
  QMessageBox.information(self, title, content)

# 接收signal後,更新UI物件狀態的 function
# 不須任何引數
@Slot()
def update_label_wfinish_msg(self):
  self.UI.label.setText('已完成計算')

純 signal 的連接方式

無論是 QT 原生物件的訊號、或是我們自己設定的訊號,連接方式都是 物件.訊號.connect(slot) 。 connect() 內的 function 一樣不可以加上括號。

# 按鈕接到對應 slot
button.clicked.connect(update_label_wfinish_msg)

# 自訂訊號接到對應 slot
# 下面的 finish_sig 就是訊號名稱,與 clicked 相同
cal_obj = YieldCalculator()
cal_obj.finish_sig.connect(update_label_wfinish_msg)

當 signal 帶有引數時的連接方式

當 signal 帶有引數時,若引數的參數、型別與 slot 設定一致即可直接連接。當訊號發射後,引數會自動帶入到 function 中。

cal_obj = YieldCalculator()

# pop_up_sig.emit(title, msg) 發射的兩個字串
# 會自動帶入 pop_up_message
cal_obj.pop_up_sig.connect(self.pop_up_message)

當 slot 需要訊號外引數的處理方式

當 function 被當作引數傳入 connect 時,無法直接為 function 設定引數 (function 加了括號會直接觸發)。因此若我們需要為 function 設定 signal 外的引數,必須使用 lambda 修改 function。使用 lambda 可以預先設定 function 的引數但不觸發 function ,當訊號發射時才傳入引數並觸發 function。

# 訊號發射時,才傳入 wafer ID 並且觸發 function
wafer_id = 'a123456779'
button.clicked.connect(lambda :cal_obj.run_analyzer(wafer_id))

不可以寫成 lambda wafer_id: function(wafer_id) 。因為 clicked 是包含 bool 的訊號,這樣寫的話 wafer_id 會被 clicked 的布林值覆蓋掉

To be continued →

接下來要進到本專欄的第四部分 ”使用者輸入資料的管理”,內容包含使用者輸入介面的設定、內容驗證、以及儲存輸入資料的方法。下一篇會說明如何讓程式擁有持久的記憶。


上一篇
Day 15:程式碼就該像生產線一樣分組 - 談 MVC 架構與關注點分離
下一篇
Day 17: 不用吃銀杏也有好記憶 - 使用 json 模組讓 GUI 程式能留存使用者資料
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言