目實(shí)例)
前言學(xué) tkinter 最容易卡住的不是控件本身而是布局控件創(chuàng)建完了界面卻空空如也或者一拉窗口所有控件擠在左上角不動(dòng)。原因在于 tkinter 的控件必須交給幾何管理器才會(huì)被擺放而幾何管理器的尺寸分配規(guī)則需要顯式配置才會(huì)「自適應(yīng)」。本文用一個(gè)完整的小項(xiàng)目——聯(lián)系人管理界面——把 tkinter 布局的三件事講透用容器分層、用grid劃表格、用rowconfigure/columnconfigure控制誰(shuí)能伸縮。tkinter 是標(biāo)準(zhǔn)庫(kù)代碼可以直接復(fù)制運(yùn)行但底層 Tcl/Tk 運(yùn)行庫(kù)需要隨解釋器一并提供個(gè)別精簡(jiǎn)安裝可能缺。先立個(gè)規(guī)矩grid、pack、place都是幾何管理器同一個(gè)容器里不要混用pack和grid。官方文檔明確警告兩者以互不兼容的方式協(xié)商尺寸可能導(dǎo)致程序卡死在反復(fù)重算布局上。要同時(shí)用就把它們分別放進(jìn)不同的Frame。一、布局總原則切塊再各自管理界面布局的正確思考順序是「先切塊再填格」根窗口 Tk()├── 頂部 LabelFrame「表單」── 內(nèi)部用 grid 排「標(biāo)簽 輸入框」└── 底部 Frame「列表區(qū)」──── 內(nèi)部左側(cè) Treeview 右側(cè)按鈕條pack每個(gè)塊是一個(gè)Frame或ttk.Frame塊內(nèi)只用一個(gè)幾何管理器。這樣即使某一塊內(nèi)部用grid、另一塊內(nèi)部用pack也不會(huì)違反「同容器不混用」的約束。二、grid 的關(guān)鍵概念行列不為零才會(huì)伸縮grid把容器劃成行列網(wǎng)格。三個(gè)選項(xiàng)決定擺放效果選項(xiàng)作用row/column指定放在第幾行第幾列從 0 開(kāi)始sticky控件在單元格內(nèi)的貼邊方式可取nsew及其組合rowspan/columnspan跨行 / 跨列只寫(xiě)這些還不夠。窗口拉大時(shí)想讓某一列跟著變寬必須給這一列設(shè)置伸縮權(quán)重否則網(wǎng)格會(huì)把所有新增空間留在外面控件紋絲不動(dòng)# 適用于 Python 3.8tkinter 屬標(biāo)準(zhǔn)庫(kù)# columnconfigure / rowconfigure 就是文檔里的 grid_columnconfigure / grid_rowconfigure 的別名root.columnconfigure(1, weight1) # 第 1 列吸收橫向多余空間root.rowconfigure(2, weight1) # 第 2 行吸收縱向多余空間weight是「分配多余空間的權(quán)重」默認(rèn)為 0也就是不吸收stickyew讓控件在單元格內(nèi)橫向填滿stickynsew則是四邊填滿。三、完整項(xiàng)目聯(lián)系人管理界面下面這段代碼包含表單區(qū)grid、列表區(qū)Treeview 滾動(dòng)條、以及增刪兩個(gè)動(dòng)作。它只用標(biāo)準(zhǔn)庫(kù)。# 適用于 Python 3.8tkinter 屬標(biāo)準(zhǔn)庫(kù)import tkinter as tkfrom tkinter import ttkdef main():root tk.Tk()root.title(聯(lián)系人管理)root.geometry(620x420)root.minsize(520, 360)# 整窗兩行兩列的伸縮策略第 1 列、第 1 行吸收多余空間root.columnconfigure(0, weight1)root.rowconfigure(1, weight1)# ---------- 表單區(qū) ----------form ttk.LabelFrame(root, text新增聯(lián)系人)form.grid(row0, column0, stickyew, padx10, pady(10, 6))form.columnconfigure(1, weight1) # 輸入列伸縮form.columnconfigure(3, weight1)name_var tk.StringVar()phone_var tk.StringVar()city_var tk.StringVar()ttk.Label(form, text姓名).grid(row0, column0, stickyw, padx6, pady4)ttk.Entry(form, textvariablename_var).grid(row0, column1, stickyew, padx6, pady4)ttk.Label(form, text電話).grid(row0, column2, stickyw, padx6, pady4)ttk.Entry(form, textvariablephone_var).grid(row0, column3, stickyew, padx6, pady4)ttk.Label(form, text城市).grid(row1, column0, stickyw, padx6, pady4)city_box ttk.Combobox(form, textvariablecity_var, statereadonly,values(北京, 上海, 廣州, 深圳))city_box.grid(row1, column1, stickyew, padx6, pady4)city_box.current(0)# ---------- 列表區(qū) ----------list_frame ttk.Frame(root)list_frame.grid(row1, column0, stickynsew, padx10, pady(0, 10))list_frame.columnconfigure(0, weight1)list_frame.rowconfigure(0, weight1)columns (name, phone, city)tree ttk.Treeview(list_frame, columnscolumns, showheadings, height8)tree.heading(name, text姓名)tree.heading(phone, text電話)tree.heading(city, text城市)tree.column(name, width120, anchorw)tree.column(phone, width150, anchorw)tree.column(city, width100, anchorw)tree.grid(row0, column0, stickynsew)scroll ttk.Scrollbar(list_frame, orientvertical, commandtree.yview)scroll.grid(row0, column1, stickyns)tree.configure(yscrollcommandscroll.set)# ---------- 動(dòng)作 ----------def add_contact():name name_var.get().strip()if not name:status_var.set(姓名不能為空)returntree.insert(, end, values(name, phone_var.get().strip(), city_var.get()))name_var.set()phone_var.set()status_var.set(f已添加{name})def delete_selected():selected tree.selection()if not selected:status_var.set(請(qǐng)先選中一行)returnfor item in selected:tree.delete(item)status_var.set(已刪除)bar ttk.Frame(root)bar.grid(row2, column0, stickyew, padx10, pady(0, 10))ttk.Button(bar, text添加, commandadd_contact).pack(sideleft)ttk.Button(bar, text刪除選中, commanddelete_selected).pack(sideleft, padx6)status_var tk.StringVar(value就緒)ttk.Label(bar, textvariablestatus_var).pack(sideright)# 回車(chē)等同于點(diǎn)「添加」bind 的回調(diào)一定會(huì)收到 event 對(duì)象這里用 lambda 丟掉它root.bind(Return, lambda event: add_contact())root.mainloop()if __name__ __main__:main()逐行推演要點(diǎn)。root.columnconfigure(0, weight1)讓整窗唯一一列吸收橫向多余空間root.rowconfigure(1, weight1)讓列表所在行吸收縱向多余空間所以窗口拉大時(shí)表格跟著長(zhǎng)、表單高度不變。form.columnconfigure(1, weight1)與columnconfigure(3, weight1)讓兩個(gè)輸入框在表單變寬時(shí)也一起變寬配合stickyew才真正鋪滿單元格。tree.column(name, width120, anchorw)設(shè)置列寬和左對(duì)齊。滾動(dòng)條通過(guò)commandtree.yview與yscrollcommandscroll.set雙向綁定這是 tkinter 里滾動(dòng)條與可滾動(dòng)控件聯(lián)動(dòng)的固定套路。ttk.Combobox(..., statereadonly, values(...))里statereadonly表示只能選、不能手輸current(0)選中第一項(xiàng)。tree.insert(, end, values(...))的第二個(gè)參數(shù)end是插入位置表示插到最后tree.selection()返回當(dāng)前選中的項(xiàng) ID 元組tree.delete(item)按 ID 刪除。四、三種幾何管理器的分工小結(jié)管理器何時(shí)用關(guān)鍵技巧grid表單、參數(shù)面板、任何表格狀結(jié)構(gòu)配合columnconfigure/rowconfigure設(shè)weightpack工具欄、狀態(tài)欄、單行按鈕條sidefillexpand控制填充與吸余place疊放、精確定位、拖拽控件relx/rely用比例比絕對(duì)像素抗縮放常見(jiàn)坑點(diǎn)? 同一個(gè)容器里一部分控件pack、一部分grid。? 一個(gè)容器只用一種要混用就把它們分到不同F(xiàn)rame里各自管理。? 只寫(xiě)了grid()窗口拉大后控件不動(dòng)。? 沒(méi)設(shè)weight的行列不吸收多余空間給需要伸縮的行列columnconfigure(n, weight1)。? 創(chuàng)建了控件卻忘了調(diào)用幾何管理器。? 控件只有交給pack/grid/place才會(huì)顯示這是官方文檔點(diǎn)名的早期錯(cuò)誤。? 給ttk.Combobox不設(shè)state用戶隨手輸入一個(gè)不在候選里的值。? 要「只能選」就寫(xiě)statereadonly。? 把StringVar當(dāng)臨時(shí)變量用完就丟。? 必須保持引用存為屬性或閉包持有否則被回收后控件與變量的聯(lián)動(dòng)會(huì)斷。? 用絕對(duì)坐標(biāo)或place的像素值寫(xiě)死布局。? 優(yōu)先用grid/pack確實(shí)要用place時(shí)用relx/rely比例值換字體和 DPI 才不會(huì)錯(cuò)位。? 滾動(dòng)條忘了雙向綁定。? 既要Scrollbar(commandwidget.yview)也要widget.configure(yscrollcommandscroll.set)缺一不可。? 選中行用tree.selection()返回值直接當(dāng)字符串用。? 它返回的是項(xiàng) ID 的元組tree.delete/tree.item都按 ID 操作。總結(jié)要點(diǎn)結(jié)論分層先用Frame/LabelFrame把界面切塊再在塊內(nèi)布局伸縮行列默認(rèn)不吸收空間weight才讓它伸縮填充sticky控制控件在單元格內(nèi)貼哪一邊、是否鋪滿混用禁區(qū)同一容器不混用pack和grid變量StringVar等變量對(duì)象必須保持引用列表TreeviewScrollbar需雙向綁定布局不是「把控件擺上去」這么簡(jiǎn)單而是「告訴容器怎么分配空間」。把weight和sticky這兩個(gè)概念吃透再記住切塊思維tkinter 界面就能做到隨窗口自如伸縮不再是一坨擠在角落的控件。