📚 Tkinter 界面跟学日记系列
✓01 第一扇窗口:30 分钟做出你的第一个 Python 界面(入门)
✓02 让界面活起来:按钮点击背后的完整故事(基础)
03 布局三板斧:side、expand、fill 和 Frame 一次讲透(进阶 · 当前篇)
04 面向对象毕业作:用 class 写一个正经待办清单(进阶)
布局三板斧:side、expand、fill 和 Frame 一次讲透
Tkinter 跟学日记 03 · 难度:进阶
前两篇你已经能做出"能用的"界面了:窗口弹得出来,按钮点得动,数字也会跳。但当时我交上去第一版计数器,你只回了一句——"排列不好看"。那是真问题:窗口空荡荡一大片,数字死贴左边,按钮也缩在角落。今天这篇,就是专门把"为什么不好看、怎么摆才好看"的底层逻辑一次讲透。读完本篇,你会对 pack 布局建立一个完整的模型,再也不靠拍脑袋试参数。
先坦白一个事实:当时为了"看起来整齐",我把窗口做成 800x400,再给每个组件塞 padx=100 硬把东西往中间推。这招能骗过眼睛,但窗口一缩放就露馅。后来我才明白,真正的解法只有四个东西:side、expand、fill、Frame。下面我一个一个拆给你看。
第一板斧:side —— 往哪面"墙"上贴
把 pack 想成"往墙上贴便利贴"。一个窗口(或 Frame)就是一面墙,pack 就是拿胶带把组件贴上去。默认情况下,便利贴一张一张从上往下摞,这对应 side="top"(默认值,不写也行)。
Python · 默认竖排
import tkinter as tk
win = tk.Tk()
tk.Label(win, text="第一行").pack()
tk.Label(win, text="第二行").pack()
tk.Label(win, text="第三行").pack()
win.mainloop()
运行后你会看到三个标签从上到下竖直排列,紧贴窗口上边缘。这就是"往上贴、接着往下叠"。
那怎么让它们横着排?把胶带方向改成左边:side="left",组件就会从左往右排。注意一个反直觉的点:side="left" 不是"贴到左边一条",而是"这张便利贴贴在左边,下一张从它右边接着贴"。方向,决定的是下一个组件从哪边接着放。
Python · side="left" 横排
import tkinter as tk
win = tk.Tk()
tk.Button(win, text="左").pack(side="left")
tk.Button(win, text="中").pack(side="left")
tk.Button(win, text="右").pack(side="left")
win.mainloop()
运行后三个按钮从左到右横排,贴着窗口上边缘。同理 side="right" 是从右往左贴,side="bottom" 是从下往上贴。
💡 小贴士
side 只管"贴哪面墙",不管"占满不占满"。它解决的是方向,不是"居不居中""拉不拉大"。后面两板斧才管这些。别指望 side 一行就摆出居中效果——那是当时我误以为 side="left" 能办到的,结果按钮全挤在左边。
第二板斧:Frame —— 透明分组盒子
想要"一组东西一起居中",就需要 Frame。它就是一个透明的分组盒子:你看不见它,但它能把几个组件圈在一起,当成一个整体来摆。
这里有一条铁律,我当时栽过跟头:组件的 pack 只对它的"直接父容器"生效。而"谁是父容器",在创建组件的第一行参数就定死了——我管这叫"出生定父子"。
1
创建时第一个参数 tk.Button(win, ...) 决定它属于窗口;tk.Button(frame, ...) 决定它属于 Frame。
2
后面那句 .pack() 只负责"在这个父容器里摆位置",改不了归属。
看对比就懂了。第一种:三个按钮直接贴窗口,side="left" 让它们从左到右贴在左上角——结果就是"挤在左边"。
Python · 直接贴窗口(挤左边)
import tkinter as tk
win = tk.Tk()
tk.Button(win, text="A").pack(side="left")
tk.Button(win, text="B").pack(side="left")
tk.Button(win, text="C").pack(side="left")
win.mainloop()
运行后 A B C 三个按钮全挤在窗口左上角一排,右边一大片空着。这就是"排列不好看"的经典现场。
第二种:先把三个按钮装进一个 Frame(小房间),再把这个 Frame 整体 pack 出去。Frame 比窗口窄,pack 默认会把它水平居中,于是整组按钮就跟着居中了。
Python · 包进 Frame(整组居中)
import tkinter as tk
win = tk.Tk()
frame = tk.Frame(win) # 透明分组盒子
frame.pack() # 整组默认居中
tk.Button(frame, text="A").pack(side="left")
tk.Button(frame, text="B").pack(side="left")
tk.Button(frame, text="C").pack(side="left")
win.mainloop()
运行后 A B C 作为一个整体,在窗口上方水平居中。注意:按钮内部的 side="left" 只决定它们"在 Frame 里从左到右排",而 Frame 自己的居中靠的是它自己的 pack()。父子各管各的——这正是"pack 只对直接父容器生效"。
⚠️ 常见错误
frame = tk.Frame(win) 然后 tk.Button(win, ...).pack() —— 按钮还是属于窗口,Frame 白建了。
✓ 正确:创建时第一个参数写谁,组件就属于谁。tk.Button(frame, text="A") 才是把按钮放进 Frame。
第三板斧:expand 和 fill —— "包裹(parcel)"模型
前两板斧只管"贴哪面墙、怎么分组"。但"为什么数字死贴左边、为什么窗口一缩放就露馅",真正的答案藏在 expand 和 fill 里。要讲清楚它俩,我得先引入一个模型——"包裹(parcel)"模型。这也是我后来被你复问 fill/expand 时,才补上的底层理解。
把 pack 想成"往墙上贴便利贴"。但你贴的每张便利贴,外面都裹了一层透明的包裹(parcel)——它就是组件在父容器里实际占的那块地。竖排(side="top",默认)时,这个包裹有两条铁规矩:包裹的宽度天然等于窗口(或父 Frame)的宽度——便利贴横向铺满整面墙;包裹的高度等于组件自己的"所需高度"——紧紧包住内容,不多不少。
那 expand 和 fill 各自干啥?一句话记住——expand 是"要地皮",fill 是"盖楼"。expand=True 向父容器认领它周围所有的剩余空间(本来包裹只包住自己,expand 说"空着的地方也归我");fill="x"/"y"/"both" 则是在已经到手的地皮上把自己撑大(把组件拉到包裹边界)。
回到竖排的两条规矩,就能看懂一个反直觉的不对称:横向地皮天然就有(包裹宽度 = 窗口宽),所以 fill="x" 单独就有效,不需要 expand;而纵向地皮没有(包裹高度 = 组件所需),要纵向拉高,必须先 expand 认领剩余高度。所以 fill="y" 或 fill="both" 必须配 expand=True——没地皮,盖不了楼。
Python · fill="both" 单独(无效)
import tkinter as tk
win = tk.Tk()
# 没 expand,纵向地皮没认领,标签还是原高度
tk.Label(win, text="我很高").pack(fill="both")
win.mainloop()
Python · fill="both" + expand(铺满全窗)
import tkinter as tk
win = tk.Tk()
# 先 expand 认领剩余空间,再 fill 撑满
tk.Label(win, text="我铺满整窗").pack(fill="both", expand=True)
win.mainloop()
side="left" 时,规则对称反转
前面那套"不对称"只对竖排成立。一旦你用了 side="left" 横排,方向反了,包裹的两条规矩也跟着镜像:此时包裹的高度天然等于窗口高(便利贴贴满上下两面墙),而包裹的宽度等于组件所需。于是纵向地皮天然就有,fill="y" 单独就有效;反过来要横向拉宽,必须配 expand=True。一句话:竖排看 x,横排看 y,谁"天然有"谁就能单飞。
四句决策表
记不住模型没关系,照这张表抄就行(默认都在竖排场景下):
| 场景 |
配方 |
说明 |
| 输入框 / 标签要横向铺满 |
fill="x" |
竖排宽度天然够,直接铺,不用 expand |
| 只想居中、不拉伸 |
expand=True |
不配 fill,组件保持原大小,在认领到的空间里居中 |
| 列表区要占满整块 |
fill="both" + expand=True |
横纵都要撑,必须先 expand 才盖得了楼 |
| 小按钮 |
都不加 |
保持紧凑,别占多余空间 |
嵌套认领链:空间不隔层传播
最后一个坑:你把组件塞进 Frame 后,如果只给内层组件写 expand=True,而外层 Frame 自己没写,那内层根本无处认领——因为 Frame 默认是"收缩包住内容"的,它的包裹尺寸 = 里面组件所需,它本身没向窗口多要地皮,内层自然也分不到。结论:空间不隔层传播,内外两级都要写 expand。内层撑不开外层,Frame 默认收缩。
Python · 外层没 expand(内层撑不开)
frame = tk.Frame(win)
frame.pack() # 外层没 expand,收缩包住内容
tk.Label(frame, text="撑不开").pack(
fill="both", expand=True)
Python · 内外两级都 expand(成功撑满)
frame = tk.Frame(win)
frame.pack(expand=True, fill="both")
# 外层先认领地皮...
tk.Label(frame, text="撑满了").pack(
fill="both", expand=True)
实战:把"丑计数器"改头换面
回到开头那个被你判了"排列不好看"的计数器。我当初的写法是:窗口开成 800×400 显得"大气",再用 padx=100 把东西硬往中间推。先看改造前:
Python · 改造前(800x400 + padx 硬凑)
import tkinter as tk
win = tk.Tk(); win.geometry("800x400")
num = tk.IntVar(); num.set(0)
tk.Label(win, textvariable=num).pack(padx=100)
tk.Button(win, text="+1").pack(padx=100)
tk.Button(win, text="-1").pack(padx=100)
win.mainloop()
三个毛病:窗口空荡荡一大片;数字靠 padx=100 硬推才不贴边;按钮也是各自贴窗口、被 push 到奇怪位置。窗口一缩放,这些写死的像素全部错位。现在用今天三板斧重做:
Python · 改造后(300x200 + expand 居中 + Frame 成组)
import tkinter as tk
win = tk.Tk(); win.geometry("300x200")
num = tk.IntVar(); num.set(0)
tk.Label(win, textvariable=num).pack(expand=True)
btn = tk.Frame(win); btn.pack() # 按钮组整组居中
tk.Button(btn, text="+1").pack(side="left")
tk.Button(btn, text="-1").pack(side="left")
win.mainloop()
三个变化:① 窗口缩到 300x200,不再靠大尺寸充场面;② 数字用 expand=True 认领剩余空间,于是自动在窗口里垂直居中;③ 两个按钮先装进 Frame 再成组居中,side="left" 只决定它们横排。这一版无论窗口怎么缩放,数字永远居中、按钮永远成组居中。
💡 一句话口诀
想要居中就 expand,想要成组就 Frame,永远别拿大 padx 硬凑。padx/pady 只该做"组件之间小间距"的微调,一旦超过二三十像素,基本就是在用错方法遮丑了。
✏️ 动手练习
🟢 基础验证
请创建三个按钮(文字随意),让它们横排且作为一个整体在窗口中居中。提示:回想"透明分组盒子"。参考解法:
Python · 参考解法
import tkinter as tk
win = tk.Tk()
frame = tk.Frame(win); frame.pack() # 整组默认居中
tk.Button(frame, text="甲").pack(side="left")
tk.Button(frame, text="乙").pack(side="left")
tk.Button(frame, text="丙").pack(side="left")
win.mainloop()
🟡 组合应用
做一个上下结构:上方一个输入框(或标签)横向铺满整窗,下方一个 Listbox 列表占满剩下的所有空间。提示:上行用 fill="x",下行用 fill="both" + expand=True。参考解法:
Python · 参考解法
import tkinter as tk
win = tk.Tk()
tk.Entry(win).pack(fill="x") # 上行横向铺满
lb = tk.Listbox(win)
lb.pack(fill="both", expand=True) # 占满剩余
win.mainloop()
🔴 开放挑战
用 grid 布局重做本篇那个计数器(数字 + 两个按钮)。提示方向:grid 的玩法完全不同——用 grid(row=0, column=0) 这类坐标摆放;要让组件随窗口放大,得配 sticky="nsew" 和 rowconfigure/columnconfigure 的 weight。先跑通再回看 pack 和 grid 的取舍,下一篇会用上它。此题不提供完整答案,自己查资料搞定。
📖 知识回顾
side 贴墙方向
Frame 透明分组盒子
包裹(parcel)模型
expand 认领地皮
fill 盖楼拉伸
fill="x" 单独有效
side="left" 对称反转
嵌套认领链
居中口诀
下篇预告
04 面向对象毕业作:用 class 写一个正经待办清单
本篇把布局三板斧攒齐后,我们用积累的组件写一个"毕业项目":一个真能用的待办清单。你会先撞上函数版代码的"顺序尴尬"——变量必须先定义、按钮才能点;再用 class 把数据和操作打包成"图纸",靠 self 串起每个部件;还会学到 Listbox 的完整用法(插入、删除、选中、计数)。这是把前 03 篇串起来的综合实战。如果这篇对你有用,点个关注、在看,系列更新不迷路。