前面第 1~14 節分別介紹了各種視窗元件,但每個範例都只示範「單一元件」的用法。實務上一支應用程式是由許多元件組合而成,元件之間還會互相影響(例如在清單中點選某一列,上方的表單要跟著顯示該筆資料)。
本節以 assets/student_crud.py 這支學生資料維護系統為例,說明如何把前面學過的元件組成一支完整、可以實際使用的程式。這支程式透過 mysql-connector-python 連接 MySQL 資料庫,對 students 資料表進行新增、查詢、修改、刪除四種操作。
資料庫的部分看這裡
本節聚焦在視窗介面。連線設定、
SQL語法、參數化查詢等資料庫觀念,請參閱 1. MYSQL 資料庫連線(特別是 1. MYSQL 資料庫連線 > 十七、整合實戰:圖形介面 CRUD)。
用到的元件一覽
這支程式幾乎用上了前面每一節的內容,可以當作全章的複習:
| 元件 / 功能 | 在程式中的用途 | 對應章節 |
|---|---|---|
tk.Tk()、mainloop() | 建立主視窗、進入事件迴圈 | 1. tkinter 套件 |
ttk.Label | 各欄位的提示文字(姓名:、性別:…) | 2. Label 標籤元件 |
pack() 版面配置 | 由上而下排列表單、按鈕列、清單 | 3. 視窗版面配置 |
ttk.Button + command | 新增/修改/刪除/清除/重新整理 | 4. Button 按鈕元件 |
ttk.Entry + textvariable | 輸入姓名、生日、Email、電話、地址 | 5. Entry 文字方塊元件 |
messagebox | 成功提示、錯誤警告、刪除確認 | 6. messagebox 對話方塊元件 |
ttk 主題元件 | 全部改用 ttk 版本,外觀較新 | 11. ttk 主題元件套件 |
ttk.LabelFrame、ttk.Frame | 把表單框起來、每一列各用一個容器 | 12. Frame 與 LabelFrame 容器元件 |
ttk.Combobox | 性別下拉選單(限定 F/M) | 13. Combobox 下拉式選單元件 |
ttk.Treeview | 以表格顯示資料表所有資料列 | 14. Treeview 清單(表格)元件 |
畫面結構
整個視窗由上而下分成三塊,每一塊都是一個容器:
┌── ttk.LabelFrame(學生資料)─────────────────────┐
│ 姓名 [Entry] 性別 [Combobox] 生日 [Entry] │ ← row0(ttk.Frame)
│ Email [Entry] 電話 [Entry] │ ← row1(ttk.Frame)
│ 地址 [Entry] │ ← row2(ttk.Frame)
└─────────────────────────────────────────────────┘
┌── ttk.Frame(按鈕列)────────────────────────────┐
│ [新增] [修改] [刪除] [清除表單] [重新整理] │
└─────────────────────────────────────────────────┘
┌── ttk.Treeview(清單)───────────────────────────┐
│ 編號 │ 姓名 │ 性別 │ 生日 │ Email │ 電話 │ 地址 │
│ 1 │ 李大毛│ M │ ... │ ... │ ... │ ... │
└─────────────────────────────────────────────────┘
用
ttk.Frame當「每一列」的容器,是讓多個元件能水平並排的關鍵。若所有元件都直接pack到主視窗,它們只會一個一個往下疊。詳見 12. Frame 與 LabelFrame 容器元件。
新觀念一:用類別(class)封裝視窗程式
前面各節的範例都是把程式碼直接寫在檔案中(俗稱「平鋪式」寫法)。當元件數量變多、還要互相溝通時,這種寫法會出現兩個問題:
- 變數滿天飛:每個元件、每個
StringVar都是全域變數,很容易命名衝突。 - 函式難以取得元件:按鈕的處理函式需要讀取輸入框的內容,得靠全域變數傳遞,程式一大就難以維護。
解法是把整個視窗包成一個類別,元件與資料都存成實例屬性(self.xxx):
class StudentApp:
def __init__(self, root):
self.root = root
self.selected_id = None # 狀態變數
root.title('學生資料維護系統')
root.geometry('820x520')
self._build_form() # 依序建立畫面
self._build_buttons()
self._build_tree()
self.refresh() # 一開始就載入資料
def _build_form(self):
...
def create(self):
...這樣寫的好處:
| 好處 | 說明 |
|---|---|
| 不污染全域 | 所有元件與變數都掛在 self 底下,彼此不衝突。 |
| 方法互相看得到 | 任何方法都能用 self.var_name.get() 取得輸入內容,不需要全域變數。 |
| 狀態集中管理 | 像 self.selected_id 這種「目前選到哪一筆」的資訊,存在物件裡最自然。 |
| 容易複製 | 想開第二個視窗,再建立一個 StudentApp 實例即可。 |
方法名稱前面的底線
_build_form()、_build_buttons()開頭的底線_是 Python 的命名慣例,表示「這是內部使用的方法,不打算給外部呼叫」。它只是約定,並不會真的阻止外部呼叫,但看到底線就該知道不要亂動。
新觀念二:if __name__ == '__main__': 進入點
程式最後這三行是視窗程式的固定收尾:
if __name__ == '__main__':
root = tk.Tk()
app = StudentApp(root)
root.mainloop()| 敘述 | 作用 |
|---|---|
if __name__ == '__main__': | 只有「直接執行這個檔案」時才成立;被別的程式 import 時不會執行。 |
root = tk.Tk() | 建立主視窗。 |
app = StudentApp(root) | 建立應用程式物件,畫面在 __init__ 中組好。 |
root.mainloop() | 進入事件迴圈,開始等待使用者操作。 |
為什麼要加這個判斷
若沒有這一行,別的程式只要
import student_crud,視窗就會立刻跳出來。加上判斷後,這個檔案既可以直接執行(跳出視窗),也可以被當成模組匯入(只借用裡面的函式,不跳視窗)。
元件與資料的雙向流動
表單欄位都用 StringVar 綁定,因此「讀取」與「填入」都非常單純:
self.var_name = tk.StringVar()
ttk.Entry(row0, textvariable=self.var_name, width=20).pack(side='left', padx=5)| 方向 | 寫法 | 使用時機 |
|---|---|---|
| 讀出(畫面 → 程式) | self.var_name.get() | 按下「新增」時取得使用者輸入 |
| 寫入(程式 → 畫面) | self.var_name.set(值) | 點選清單某列後,把資料帶回表單 |
事件驅動:三種觸發方式
視窗程式不是由上往下執行,而是等待事件發生才執行對應的函式。這支程式用到三種:
# 1. 按鈕的 command:按下按鈕時呼叫
ttk.Button(frame, text='新增', command=self.create).pack(side='left', padx=3)
# 2. bind 綁定事件:使用者在清單中選取某列時呼叫
self.tree.bind('<<TreeviewSelect>>', self.on_select)
# 3. 直接呼叫:程式自己在需要時呼叫
self.refresh()
command=self.create後面不要加括號寫
command=self.create是把「函式本身」交給按鈕,等按下時才執行。
若寫成command=self.create(),會在建立按鈕的當下就立刻執行一次,然後把回傳值(None)當成command,按鈕就失效了。這是初學最常見的錯誤之一。
狀態管理:一組表單,兩種模式
這支程式只有一組輸入表單,卻同時支援「新增」與「修改」,關鍵在一個狀態變數:
self.selected_id = Noneselected_id | 代表模式 | 按下「修改」/「刪除」時 |
|---|---|---|
None | 新增模式 | 提示「請先在清單中選擇一筆資料」 |
有值(某個 cID) | 編輯模式 | 對該筆資料執行修改或刪除 |
流程是:
- 使用者點選
Treeview某一列 → 觸發on_select() on_select()把該列資料set()回表單,並記錄self.selected_id- 按「清除表單」→
clear_form()把selected_id設回None,回到新增模式
def on_select(self, event):
selected = self.tree.selection()
if not selected:
return
cid, name, sex, birthday, email, phone, addr = self.tree.item(selected[0], 'values')
self.selected_id = cid
self.var_name.set(name)
self.var_sex.set(sex)
...清單的重繪
每次資料異動後都要讓畫面同步,作法是先清空再重新插入:
def refresh(self):
for row in self.tree.get_children(): # 取得所有列
self.tree.delete(row) # 逐一刪除
# ...接著從資料庫重新查詢並 insert若忘記先清空,每按一次「重新整理」資料就會重複疊加一份。
另外,資料庫中允許為 NULL 的欄位讀出來會是 None,直接顯示會出現「None」字樣,所以用 or '' 轉成空字串:
self.tree.insert('', 'end',
values=(cid, name, sex, birthday, email or '', phone or '', addr or ''))輸入驗證
在寫入資料庫之前先檢查欄位,可以避免資料庫丟出例外,使用者體驗也比較好:
def _validate(self):
if not self.var_name.get().strip():
messagebox.showwarning('輸入錯誤', '姓名為必填欄位')
return False
birthday = self.var_birthday.get().strip()
try:
date.fromisoformat(birthday) # 檢查是否為 YYYY-MM-DD
except ValueError:
messagebox.showwarning('輸入錯誤', '生日格式須為 YYYY-MM-DD')
return False
return True驗證的欄位對應資料表中設為 NOT NULL 的欄位(cName、cBirthday)。strip() 用來排除「只輸入空白」的情況。
危險操作要先確認
刪除是不可復原的,一定要給使用者反悔的機會:
if not messagebox.askyesno('確認刪除', f'確定要刪除編號 {self.selected_id} 的資料嗎?'):
returnaskyesno() 回傳 True/False,使用者按「否」就直接 return,完全不執行資料庫操作。詳見 6. messagebox 對話方塊元件。
每個操作的完整流程
四個按鈕的處理方式都遵循同一個樣板:
驗證輸入 → 連線資料庫 → 執行 SQL → commit → 關閉連線
→ 顯示成功訊息 → 清除表單 → 重新整理清單
以「新增」為例:
def create(self):
if not self._validate(): # ① 驗證
return
try:
conn = get_connection() # ② 連線
cur = conn.cursor()
cur.execute( # ③ 執行 SQL(參數化查詢)
'INSERT INTO students (cName, cSex, cBirthday, cEmail, cPhone, cAddr) '
'VALUES (%s, %s, %s, %s, %s, %s)',
self._form_values(),
)
conn.commit() # ④ 提交
cur.close()
conn.close()
messagebox.showinfo('成功', '新增成功')
self.clear_form() # ⑤ 清除表單
self.refresh() # ⑥ 重新整理清單
except mysql.connector.Error as e:
messagebox.showerror('資料庫錯誤', str(e))資料庫操作一定要包
try / except網路斷線、密碼錯誤、資料格式不符都會讓程式丟出例外。視窗程式若沒有攔截,畫面會直接卡住或閃退,使用者完全不知道發生什麼事。
用messagebox.showerror()把錯誤訊息顯示出來,才是友善的作法。
常見錯誤
| 症狀 | 原因 |
|---|---|
| 按鈕一建立就自動執行 | command=self.create() 多加了括號 |
| 按「重新整理」資料越來越多 | 忘記先 delete() 清空 Treeview |
清單顯示一堆 None | 沒有用 or '' 把 NULL 轉成空字串 |
| 按「修改」沒反應 | 沒有先在清單中選取,selected_id 仍是 None |
| 資料改了但清單沒變 | 忘記在操作後呼叫 refresh() |
| 視窗一閃就關閉 | 忘記 root.mainloop() |
| 元件全部擠成一直排 | 沒有用 ttk.Frame 分組,或忘了 side='left' |
練習
- 幫程式加上「依姓名搜尋」功能:新增一個
Entry與「搜尋」按鈕,只顯示符合的資料列。
⚠️SQL必須使用參數化查詢,不可以用字串拼接。 - 在
Treeview下方加一個ttk.Label,即時顯示「目前共 N 筆資料」。 - 讓
Treeview支援點擊欄位標題排序(提示:heading()可以設定command)。 - 把身高
cHeight、體重cWeight兩個欄位也加進表單與清單。 - 目前每個操作都會重新連線一次,試著改成程式啟動時連線一次、關閉視窗時才斷線,並想想這樣做的優缺點。
相關文件
| 文件 | 內容 |
|---|---|
| 1. MYSQL 資料庫連線 | 資料庫連線、SQL 語法、參數化查詢、完整教案 |
| generate_students | 學生假資料產生器,可先造測試資料再用本程式維護 |
assets/student_crud.py | 本節完整程式碼 |
🗺️ 本章學習地圖與快速查找 → 0. 章節索引