以前一章計算機介面為例,本篇將加入簡單的事件綁定,以實作計算機的基本功能。
產生 UI 的 SetupUi
函式中會把UI元件一一設定成 Attribute 給自己也就是 self
這個變數,因此接下來我們就可以對這些元件進行事件的綁定。
事件綁定(Event Binding)
將 Function Pointer 綁在某一元件的 Signal 上,需要用 connect
。以下示範幾種寫法綁定兩種事件:
- 按鈕(數字鍵
b0
,b1
, …)被點擊clicked
時將數字插入方程式文字列lEquation
中 - 方程式文字列
lEquation
發生改變時print
出當下的內容
方法一
最簡單的方法,就是一對一綁定。
def __init__(self):
# ...
self.b0.clicked.connect(self.addNumber0)
self.lEquation.textChanged.connect(self.equationChanged)
def addNumber0(self):
self.lEquation.insert('0')
def equationChanged(self, text):
print('Eq=', text)
方法二
如果想要寫通用一點的 Function,可以使用 lambda 帶入其他參數。
def __init__(self):
# ...
self.b0.clicked.connect(lambda: self.addNumber(0))
self.lEquation.textChanged.connect(lambda t: self.textChanged(t, 'Eq'))
def addNumber(self, number):
self.lEquation.insert(str(number))
def textChanged(self, text, src):
print(src, '=', text)
方法 N
結合方法二,但使用 dict 結構 mapping 相對應的值,另外也可以擅用 Python 內建的 getattr 取得 Class 內的 Attribute,綁定寫法完全是自由發揮~
def __init__(self):
# ...
btnName2Number = {
'b0': 0,
'b1': 1,
# ...
}
for btn, num in btnName2Number.items():
getattr(self, btn).clicked.connect(lambda: self.addNumber(num))
self.lEquation.textChanged.connect(lambda t: self.textChanged(t, 'Eq'))
def addNumber(self, number):
self.lEquation.insert(str(number))
def textChanged(self, text, src):
print(src, '=', text)
自定義事件(Custom Event)
除了內建的Event之外,也可以客製化自己喜歡的事件。例如以下範例:
- [Line#2] 首先自訂一個名為
numberAdded
的Event
,並註冊該 Event 觸發時會帶的參數類型 - [Line#9] 自訂事件設定於
addNumber
完成時,將藉由 emit 函式來觸發 - [Line#5] 透過
connect
將事件綁定到onNumberAdded
函式上然後顯示出來
class Main(QtWidgets.QMainWindow, ui.Ui_MainWindow):
numberAdded = QtCore.pyqtSignal(int)
def __init__(self):
# ...
self.numberAdded.connect(self.onNumberAdded)
def addNumber(self, number):
self.lEquation.insert(str(number))
self.numberAdded.emit(number)
def onNumberAdded(self, number):
print('Insert', number)
這邊自己綁定自己的事件只是示範用,通常用於不同 Widget 之間的溝通比較多。而且建議大家優先熟悉內建的各種 Signal 啦~不然一旦掉進 Event 陷阱程式就會 Crash 惹。
事件處理(Event Handle)
事件一旦被觸發就會跳到綁定的函示執行,因此發生問題時常常會從莫名其妙的地方 Crash 掉,而這經常都是因為我們在無意之間觸發到了不預期的事件,因此使用上必須格外小心。
觸發時機
有一種陷阱就是觸發時機,例如我們想要在方程式文字框中一邊鍵入數值一邊計算總值,可能會這樣寫:
class Main(QtWidgets.QMainWindow, ui.Ui_MainWindow):
def __init__(self):
# ...
self.lEquation.textChanged.connect(self.equationChanged)
def equationChanged(self, text):
print('Eq=', text)
value = eval(text)
self.lEqual.setText(str(value))
接著會發現按「1 + 1」按一按就 Crash 了~因為 textChanged
這個事件會在編輯的過程中不斷被觸發,此時較好的做法是綁定 editingFinished
,將只在完成編輯 blur 掉之後觸發。至於什麼事件適合怎麼使用,也只能 Try and Error XD
循環觸發(Event Loop)
另外有一種情況會發生循環被觸發的事件遞迴。例如想要在算完值之後將計算式清為 0,假設這樣寫:
class Main(QtWidgets.QMainWindow, ui.Ui_MainWindow):
def __init__(self):
# ...
self.lEquation.textChanged.connect(self.equationChanged)
self.lEqual.textChanged.connect(self.totalChanged)
def equationChanged(self, text):
print('Eq=', text)
value = eval(text)
self.lEqual.setText(str(value))
def totalChanged(self, total):
self.lEquation.setText('0')
這時試著輸入數字你會發現數字根本輸入不進去,這是因為 setText
這個 method 也會同時觸發 textChanged
的事件,最後導致數值永遠為 0,會收斂的循環倒還好,有時一不小心就會踩到 Python 遞迴的次數上限而導致程式死當。
如果想要暴力解決循環觸發的問題,可以暫時把事件屏蔽起來以避開類似的問題:
self.lEquation.blockSignals(False)
# ...
self.lEquation.blockSignals(False)
不過此時你會發現,在計算總值寫入 lEqual
之後就無法同時將計算式清為 0。這邊的例子也許有點爛,不過實際上很容易發生這種循環觸發的問題,此時就要回頭想想 GUI 的設計邏輯是否有問題,進一步思考能夠達成同樣目標的其他方法。
最後在各函式內寫上適合的邏輯條件,就可以完成簡單的計算機功能囉~完成範例:
前往下一步~
雖然事件的處理上會有許多眉眉角角需要留意,但在 GUI 的實作中,Event Binding 還是不可或缺的好幫手。
現在 GUI 終於可以動起來~接著可以嘗試為應用程式加入快捷鍵。
為 PyQt 製作的 GUI 應用程式加入快捷鍵