背景
最近想簡單粗暴的用 Python 寫一個 GUI 的小程式。因為 Tkinter 是 Python 自帶的 GUI 解決方案,為了部署方便,就直接選擇了 Tkinter。
本來覺得 GUI 發展這麼多年以來,就算功能簡陋,但也應該大差不差才對,何況我的需求本就十分簡單。但事實上 Tkinter 的簡陋仍然超出了我的想象。因此想寫幾篇文章,記錄一下踩到的坑,權當吐槽。
同系列的其它文章:
Tkinter 吐槽之一:多執行緒與 UI 互動
問題
在一些情況下,Tkinter 原生的元件因為過於簡單而無法滿足我們的需求,所以可能需要通過 Frame 自定義一些元件,並響應一些事件。
比如,自定義一個帶多個資訊的可點選的 Frame:
import tkinter as tk
class PersonFrame(tk.Frame):
def __init__(self, master, name, age):
super().__init__(master=master, width=100, borderwidth=2, padx=5)
tk.Label(self, text=name).pack(side=tk.LEFT, fill=tk.X)
tk.Label(self, text=age, foreground='red').pack(side=tk.RIGHT)
self.bind('<Button-1>', self._on_click)
def _on_click(self, _):
print('clicked')
app = tk.Tk()
frame = PersonFrame(app, 'Tom', '27').pack(fill=tk.X)
frame = PersonFrame(app, 'Jerry', '16').pack(fill=tk.X)
app.mainloop()
這個例子看上去一切都很美好,通過自定義 Frame 的方式建立了一個元件,並且繫結了元件的 滑鼠單擊
事件。
但是經過實際測試發現,只有當滑鼠點選在 Frame 的空白位置時,才會觸發 on_click
的呼叫。如果滑鼠點在文字上,並不會有任何效果。這說明, bind
操作只對 Frame 本身生效,並不會對覆蓋在其上的子元素生效,即使邏輯上存在父子關係。
因此,這個自定義的元件沒有辦法真正意義上被當作可點選的元件使用。
方案
根本原因,在於 Tkinter 並沒有一種類似 事件冒泡
的機制,從葉子節點的元件開始向上傳遞。而只有 bind
,bind_all
, bind_class
這三種形式。很顯然,這三種形式都是無法滿足要求的。
那麼思路就演變為,有沒有可能把 Frame 及 Frame 的所有子元素,都繫結上同一個事件的方法呢?
def bind_recursively(widget: tk.Misc, event: str, callback: Callable):
"""Binds event recursively with it's all children. So a widget and all
it's children will share one event and callback.
"""
widget.bind(event, callback)
for w in widget.children.values():
bind_recursively(w, event, callback)
這段程式碼就是採用類似的思路,首先找到 widget 的所有子元素,然後以遞迴的方式繫結同一個事件的 callback。這樣可以保證一個 widget 中的所有子元素都可以響應同樣的事件。從而實現在哪裡點選都一致的效果。
總結
工程設計其實就是這樣,找到一些理論可行的方法,然後通過抽象、封裝的方式轉換成一個優雅的解決方案。 Tkinter 原生提供的東西非常有限,但是可以借鑑很多其他的思路來進行擴充套件,從而滿足我們的需求。