📚 目录 第 4 章 布局管理器:pack / grid / place(把控件排整齐)

第 4 章 布局管理器:pack / grid / place(把控件排整齐)

本书红线(贯穿全教程,回顾):
1. 每个界面都用 class App: 封装,__init__ 里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带 if __name__ == "__main__": 守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。

主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。

上节回顾(第 3 章):你认识了 Button,学会了"方法即回调"——点按钮就触发类里的一个方法去改 Label 文字,程序第一次"活"了。不过到目前为止,控件都是简单地"从上往下叠"着显示,既不整齐也不灵活。

本章目标:学会用 布局管理器 把已经认识的 Label、Button 排得整整齐齐、好看又可控。我们会讲三种布局方式——pack(上下/左右堆叠)、grid(像画表格的网格)、place(绝对坐标,了解即可),并重点建立一个 OOP 好习惯:把"建控件 + 布局"收进一个私有方法 _build_widgets(self),__init__ 只搭骨架、调方法,让"界面搭建"和"事件逻辑"分离。


4.1 是什么 / 适用场景 ★

是什么:在 tkinter 里,"创建控件"和"把控件摆到窗口的什么位置"是两件事。控件创建后只是"存在内存里",必须用布局管理器告诉它"你该出现在哪里、占多大地方"。tkinter 提供三种布局管理器:

适用场景对比:

布局适合什么不适合什么
pack简单的"上标题下按钮""左边栏右边内容"复杂表单、需要精确对齐的多列表格
grid登录框、通讯录表单、任何"表格感"界面随意漂浮、需要自由重叠的布局
place需要像素级定位的极少数情况几乎所有常规界面(维护苦难)
⚠️ 一条铁律:同一个父容器里,不要混用 pack 和 grid(也不要和 place 混用)。一个父容器只能选一种布局方式,混用会直接报错或布局错乱。这是新手最常踩的坑,本章 4.6 节会再强调。

4.2 布局选项(对照官方手册)★★

三种布局各有自己的选项。下面把最常用的列成一张表(完整选项见官方 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。

4.3 常用方法速查 ★★

布局本身不是"控件",而是控件的布局方法。下表汇总三种布局的调用方式与典型写法:

方法作用示例
控件.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 则平分空间。

4.4 详细例子(循序渐进)★★★

本章红线:只用已学过的 tk.Label 和 tk.Button 演示布局(Entry 等输入控件在第 5 章才登场)。所有示例都写在 class App: 里。

例 4.1 pack 默认堆叠:标题在上、按钮在下 ★

这就是第 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()

逐行说明:

例 4.2 pack 的 side / fill / expand:做一个左右分栏 ★★

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()

逐行说明:

例 4.3 grid 两行两列:做一个像表格的"通讯录表头" ★★★

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()

逐行说明:

例 4.4 grid 的 columnspan 跨格 + _build_widgets 封装 ★★★

本章 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()

逐行说明:

例 4.5 place 绝对定位(了解即可)★

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。


4.5 OOP 落点 ★★

本章最重要的 OOP 习惯是"界面搭建"与"事件逻辑"分离:

一句话:__init__ 搭骨架,调 _build_widgets 建界面,事件写成独立方法——三个层次清清楚楚。

4.6 注意事项 / 易错清单 ★★


4.7 本章练习

  1. 用 pack 把第 3 章的"标题(Label)"排在最上面、"按钮(Button)"排在最下面,并给两者加上 pady=10 的间距。
  2. 把练习 1 改成用 grid:标题放在第 0 行(用 columnspan 横跨两列),按钮放在第 1 行第 0 列,体会两种布局写法的不同。
  3. 用 grid 做一个 3 行 2 列的"通讯录表单骨架":第 0 行是表头("姓名""电话"),第 1、2 行各放一条示例数据;并让第 0 列在窗口拉宽时跟着拉伸。
  4. 思考题:为什么"同一父容器不能同时用 pack 和 grid"?(提示:两种管理器都会去争夺对子控件位置的控制权,tkinter 无法同时满足,于是直接报错。)
下节预告:第 5 章我们引入第一个"能输入"的控件——Entry(单行输入框),并学习用 StringVar 把"输入框"和"变量"双向绑定,让程序能读出用户打的字。