本书红线(贯穿全教程,回顾):
1. 每个界面都用class App:封装,__init__里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带if __name__ == "__main__":守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。
主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。
上节回顾(第 3 章):你认识了 Button,学会了"方法即回调"——点按钮就触发类里的一个方法去改 Label 文字,程序第一次"活"了。不过到目前为止,控件都是简单地"从上往下叠"着显示,既不整齐也不灵活。
本章目标:学会用 布局管理器 把已经认识的 Label、Button 排得整整齐齐、好看又可控。我们会讲三种布局方式——pack(上下/左右堆叠)、grid(像画表格的网格)、place(绝对坐标,了解即可),并重点建立一个 OOP 好习惯:把"建控件 + 布局"收进一个私有方法 _build_widgets(self),__init__ 只搭骨架、调方法,让"界面搭建"和"事件逻辑"分离。
是什么:在 tkinter 里,"创建控件"和"把控件摆到窗口的什么位置"是两件事。控件创建后只是"存在内存里",必须用布局管理器告诉它"你该出现在哪里、占多大地方"。tkinter 提供三种布局管理器:
pack():把控件按"块"堆叠(上/下/左/右),适合简单的上下或左右排列。最简单直观。grid():把父容器想象成一张表格,用 row(行)和 column(列)把控件放进格子里,像用 Excel 排表单。最常用、最灵活。place():用绝对坐标 (x, y) 精确到像素摆放。灵活但难维护,一般只用于特殊需求,本书仅作了解。适用场景对比:
| 布局 | 适合什么 | 不适合什么 |
|---|---|---|
pack | 简单的"上标题下按钮""左边栏右边内容" | 复杂表单、需要精确对齐的多列表格 |
grid | 登录框、通讯录表单、任何"表格感"界面 | 随意漂浮、需要自由重叠的布局 |
place | 需要像素级定位的极少数情况 | 几乎所有常规界面(维护苦难) |
⚠️ 一条铁律:同一个父容器里,不要混用pack和grid(也不要和place混用)。一个父容器只能选一种布局方式,混用会直接报错或布局错乱。这是新手最常踩的坑,本章 4.6 节会再强调。
三种布局各有自己的选项。下面把最常用的列成一张表(完整选项见官方 tkinter 文档的 pack / grid / place 说明):
| 选项(含所属管理器) | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
pack: side | 控件贴父容器的哪条边堆叠 | "top"(默认上)/"bottom"/"left"/"right" | ★★★★ |
pack: fill | 沿哪个方向撑满剩余空间 | "none"(默认)/"x"(横向)/"y"(纵向)/"both" | ★★★ |
pack: expand | 父容器变大时,是否把多出的空间分给它 | True/False(默认) | ★★★ |
pack: padx / pady | 控件外部的左右 / 上下间距(像素) | 整数,如 10 | ★★★ |
pack: anchor | 控件在自己格子里靠哪边 | "n"/"s"/"w"/"e"/"center" | ★★ |
grid: row / column | 放在第几行 / 第几列(从 0 数起) | 整数 0,1,2… | ★★★★ |
grid: sticky | 控件在格子里贴哪边、是否撑满 | "n"/"s"/"w"/"e" 组合,如 "ew"(左右撑满) | ★★★ |
grid: columnspan / rowspan | 跨几列 / 跨几行 | 整数,如 2 | ★★★ |
grid: padx / pady | 格子外部的左右 / 上下间距(像素) | 整数,如 5 | ★★★ |
grid: ipadx / ipady | 控件内部的左右 / 上下留白(像素) | 整数 | ★★ |
place: x / y | 控件左上角的绝对坐标(像素) | 整数,如 20 | ★★ |
place: width / height | 控件的绝对宽 / 高(像素) | 整数 | ★★ |
place: relx / rely | 相对父容器宽高的比例位置(0~1) | 小数,如 0.5(正中) | ★ |
📌padx/pady(外部间距)和ipadx/ipady(内部留白)的区别:前者是控件"和邻居之间"的距离,后者是"控件边框和自身内容之间"的距离。pack 和 grid 都支持padx/pady;只有 grid 常用ipadx/ipady。
布局本身不是"控件",而是控件的布局方法。下表汇总三种布局的调用方式与典型写法:
| 方法 | 作用 | 示例 |
|---|---|---|
控件.pack(...) | 用 pack 方式布局 | label.pack(side="top", pady=10) |
控件.grid(...) | 用 grid 方式布局 | btn.grid(row=1, column=0, sticky="ew") |
控件.place(...) | 用 place 方式布局(绝对坐标) | label.place(x=20, y=20) |
父容器.grid_columnconfigure(列, weight=1) | 让某一列随窗口拉伸(grid 专属) | root.grid_columnconfigure(0, weight=1) |
父容器.grid_rowconfigure(行, weight=1) | 让某一行随窗口拉伸(grid 专属) | root.grid_rowconfigure(1, weight=1) |
🔑weight(权重)是 grid 的精髓:默认情况下列/行不会随窗口变大而变宽。想让某一列"吃掉"多余空间,就给那一列设weight=1。多列都设weight=1则平分空间。
本章红线:只用已学过的tk.Label和tk.Button演示布局(Entry 等输入控件在第 5 章才登场)。所有示例都写在class App:里。
这就是第 3 章那种"从上往下叠"的效果,但我们用更规范的方式写出来,并加上 padx/pady 让间距更舒服:
# 第4章 例4.1:用 pack 把"标题(Label)"放上面、"按钮(Button)"放下面
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# 标题 Label:pack 默认 side="top",所以会排在最上面
self.title_label = tk.Label(
self.root, text="我的通讯录", font=("微软雅黑", 16)
)
self.title_label.pack(pady=10) # pady=10:上下各留 10 像素间距
# 按钮:同样用 pack,默认排到标题下面
self.add_btn = tk.Button(self.root, text="添加联系人", command=self.on_add)
self.add_btn.pack(pady=10)
def on_add(self):
# 这里先不实现真正功能,只打印,证明按钮点击仍正常
print("点击了添加联系人")
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.title_label.pack(pady=10):pack 不写 side 时默认 side="top",控件从上往下依次堆叠。这里标题先 pack,所以它在最上面。self.add_btn.pack(pady=10):按钮后 pack,自然排到标题下方。pady=10 让它与标题之间、与窗口底边之间都有 10 像素呼吸感。pack() 让控件真正出现在窗口里,且有了间距。side 控制堆叠方向,fill+expand 控制"是否撑满空间"。下面做一个"左侧栏 + 右侧内容"的经典布局:
# 第4章 例4.2:用 pack 的 side/fill/expand 做简单的左右分栏
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
self.root.geometry("400x300") # 先给窗口一个固定大小,方便观察 fill/expand 效果
# 左侧栏:贴左边,上下撑满(fill="y"),并随窗口拉宽而分空间(expand)
self.sidebar = tk.Label(
self.root, text="功能菜单", bg="#3a7afe", fg="white"
)
self.sidebar.pack(side="left", fill="y", padx=5, pady=5)
# 右侧主区:贴右边,上下左右都撑满(fill="both"),并参与瓜分剩余空间
self.main_area = tk.Label(
self.root, text="联系人列表区", bg="#eeeeee"
)
self.main_area.pack(side="right", fill="both", expand=True, padx=5, pady=5)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
side="left" 与 side="right":让两个 Label 分别贴左、贴右排列,形成左右分栏。fill="y":左侧栏在"垂直方向"撑满父容器高度。fill="both":右侧在"横纵两个方向"都撑满。expand=True:告诉 pack"窗口变大的多余空间,分给我一份"。如果不写 expand,fill 只能撑满"当前已有的"空间,窗口拉大时控件不会跟着变大。fill+expand 的功劳。grid 最适合做表单。下面用 Label 摆出一张 2 行 2 列的网格(表头 + 一条示例数据),并让第 0 列随窗口拉伸:
# 第4章 例4.3:用 grid 排一个两行两列的"通讯录小表格"(只用 Label/Button)
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# 第 0 行:表头(姓名 | 电话)
self.head_name = tk.Label(self.root, text="姓名", bg="#dddddd", width=12)
self.head_name.grid(row=0, column=0, sticky="ew", padx=2, pady=2)
self.head_phone = tk.Label(self.root, text="电话", bg="#dddddd", width=16)
self.head_phone.grid(row=0, column=1, sticky="ew", padx=2, pady=2)
# 第 1 行:一条示例数据
self.row_name = tk.Label(self.root, text="张三", width=12)
self.row_name.grid(row=1, column=0, sticky="ew", padx=2, pady=2)
self.row_phone = tk.Label(self.root, text="13800000000", width=16)
self.row_phone.grid(row=1, column=1, sticky="ew", padx=2, pady=2)
# 让第 0 列在窗口变宽时跟着拉伸(weight=1 表示"吃"多余空间)
self.root.grid_columnconfigure(0, weight=1)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
row=0, column=0:把"姓名"表头放进第 0 行第 0 列(行列编号都从 0 开始)。row=0, column=1 是第 0 行第 1 列,正好在它右边。sticky="ew":"e"(东/右)+"w"(西/左)组合,意思是"在格子里左右贴边撑满"。这样格子变宽时 Label 也跟着变宽,而不是缩在中间。padx=2, pady=2:格子之间的微小间距,让表格不那么挤。self.root.grid_columnconfigure(0, weight=1):让第 0 列拥有拉伸权重。窗口拉宽时,第 0 列会变大。如果不写这行,列宽固定,窗口变宽也白搭。本章 OOP 落点:把"建控件 + 布局"收进私有方法 _build_widgets(self),__init__ 只做骨架。下面做一个"标题占满整行 + 下方两列按钮"的界面,并演示 columnspan 跨列:
# 第4章 例4.4:用 grid 的 columnspan 跨列,并把"建控件+布局"收进 _build_widgets
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# __init__ 只搭骨架:保存 root,然后调用私有方法去真正建界面
self._build_widgets()
def _build_widgets(self):
# ① 顶部标题:占满第 0 行,横跨 2 列(columnspan=2)
self.title_label = tk.Label(
self.root, text="我的通讯录", font=("微软雅黑", 16), bg="#3a7afe", fg="white"
)
self.title_label.grid(row=0, column=0, columnspan=2, sticky="ew", pady=5)
# ② 下方两个按钮:分别放在第 1 行的第 0 列、第 1 列
self.add_btn = tk.Button(self.root, text="添加", command=self.on_add)
self.add_btn.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
self.del_btn = tk.Button(self.root, text="删除", command=self.on_del)
self.del_btn.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
# 让两列都参与拉伸,按钮才能左右撑满
self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=1)
def on_add(self):
print("添加")
def on_del(self):
print("删除")
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self._build_widgets():名字以单下划线开头,是 Python 约定俗成的"私有/内部方法"写法,表示"这是类自己用的,外部别随便调"。把建界面逻辑搬进来后,__init__ 干干净净。columnspan=2:标题从第 0 列开始,横跨 2 列,所以它能盖住下面两个按钮的整行宽度。sticky="ew" 让它左右撑满这 2 列。_build_widgets 里加几行;事件逻辑 on_add/on_del 单独写,互不干扰——界面搭建与事件逻辑分离。place 用像素坐标精确摆放,灵活但维护难。仅作了解,将来基本用不到:
# 第4章 例4.5:用 place 把控件放到绝对坐标(了解即可,不推荐常规使用)
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
self.root.geometry("300x200")
# 在 (20, 20) 像素处放一个标题
self.title_label = tk.Label(self.root, text="我的通讯录")
self.title_label.place(x=20, y=20)
# 在 (20, 60) 像素处放一个按钮,并固定它宽 100 高 30
self.btn = tk.Button(self.root, text="添加", command=lambda: print("hi"))
self.btn.place(x=20, y=60, width=100, height=30)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()说明:x=20, y=20 是控件左上角相对父容器左上角的像素坐标。place 不随窗口大小自动调整,所以窗口一缩放,控件位置就"错位"了——这正是它难维护的地方,日常开发请优先用 pack 或 grid。
本章最重要的 OOP 习惯是"界面搭建"与"事件逻辑"分离:
_build_widgets(self) 私有方法:把"创建控件 + 调用布局"全收进这个方法。类名前的单下划线 _ 是 Python 的"内部使用"约定。这样 __init__ 只负责"初始化骨架"(保存 root、设标题、调用 _build_widgets),一眼就能看清类做了什么。self 持有所有控件:无论用 pack 还是 grid,控件都存成 self.xxx,方法里随时能通过 self 拿到并操作它们——和前两章一脉相承。一句话:__init__搭骨架,调_build_widgets建界面,事件写成独立方法——三个层次清清楚楚。
label.pack() 又 btn.grid(...),tkinter 会抛 _tkinter.TclError: cannot use geometry manager pack inside ... already managed by grid。一个父容器只选一种布局管理器。pack/grid/place,控件就不会显示。row=1, column=1 是第 2 行第 2 列,不是第 1 行第 1 列。新手常因此"控件没出现在想要的位置"。weight,窗口拉大控件不变:列/行默认 weight=0,不随窗口拉伸。想让某列变宽,必须 grid_columnconfigure(列, weight=1)。sticky 写错方向:想左右撑满要 sticky="ew"(不是 "we",但等价;注意是 e 和 w 组合),想上下撑满用 sticky="ns"。漏写 sticky 控件会缩在格子中间。pack 的 expand 和 fill 配合:只写 fill 不写 expand,窗口变大时控件也不会跟着长大;两者通常一起用。columnspan/rowspan 跨格后忘了 sticky:跨了格但不设 sticky,控件仍缩在跨格区域的某一角。pack 把第 3 章的"标题(Label)"排在最上面、"按钮(Button)"排在最下面,并给两者加上 pady=10 的间距。grid:标题放在第 0 行(用 columnspan 横跨两列),按钮放在第 1 行第 0 列,体会两种布局写法的不同。grid 做一个 3 行 2 列的"通讯录表单骨架":第 0 行是表头("姓名""电话"),第 1、2 行各放一条示例数据;并让第 0 列在窗口拉宽时跟着拉伸。下节预告:第 5 章我们引入第一个"能输入"的控件——Entry(单行输入框),并学习用 StringVar 把"输入框"和"变量"双向绑定,让程序能读出用户打的字。