本书红线(贯穿全教程,回顾):
1. 每个界面都用class App:封装,__init__里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带if __name__ == "__main__":守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。
主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。
上节回顾(第 12 章):你学会了用 Frame / LabelFrame 把界面拆成"区域",并把不同区域交给 _build_form、_build_list 等私有方法构建,做到"界面结构 = 代码结构"。但有时单屏内容太多——比如既想看"全部联系人"又想看"我的收藏",挤在一起还是乱。本章用 Notebook(分页) 和 PanedWindow(可拖拽分隔) 来组织更多内容。
本章目标:学会用 Notebook 做"标签页切换"、用 PanedWindow 做"可拖动分隔条"。OOP 落点是——每页(每窗格)内容各自一个构建方法,互不干扰。
Notebook 是什么:ttk.Notebook 是"标签页容器"。它像浏览器的标签页——上面一排页签("全部联系人""我的收藏"),点哪个就显示哪一页的内容,各页互相独立,但只占一份屏幕空间。
PanedWindow 是什么:ttk.PanedWindow 是"可拖拽分隔的窗格容器"。它把空间分成左右(或上下)几块,中间有一条能拖动的分隔条,用户可自己调整每块的宽窄。
适用场景:
不适用:只是简单分区用 Frame/LabelFrame(第 12 章)就够;要"弹窗"用 Toplevel(后续章节)。
📌 两者都是"容器中的容器"——你先在里面建子 Frame,再往子 Frame 里放真正的控件。
Notebook 常用选项(来自官方 tkinter.ttk.Notebook):
| 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
width / height | Notebook 整体宽 / 高(像素) | 整数 | ★★ |
padding | 页签与内容之间的内边距 | 整数 | ★ |
PanedWindow 常用选项(来自官方 tkinter.ttk.PanedWindow):
| 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
orient | 分隔方向:"horizontal" 左右分、"vertical" 上下分 | "horizontal"/"vertical" | ★★★ |
showhandle | 是否显示拖拽手柄小方块 | True/False | ★★ |
sashrelief | 分隔条的立体样式 | "raised" 等 | ★ |
Notebook 方法:
| 方法 | 作用 | 示例 | |
|---|---|---|---|
add(子控件, text="页签文字") | 添加一页(子控件通常是 Frame) | nb.add(f, text="全部") | ★★★★ |
tab(页, 选项=值) | 读/改某页选项;tab(0, "text") 读第 0 页标题 | nb.tab(0, text="新名") | ★★★ |
select() | 返回当前选中页对应的控件对象;select(页) 可切换 | cur = nb.select() | ★★★ |
index("current") | 返回当前页的索引(0、1…) | nb.index("current") | ★★ |
forget(页) | 删除某页 | nb.forget(0) | ★ |
PanedWindow 方法:
| 方法 | 作用 | 示例 | |
|---|---|---|---|
add(子控件) | 添加一个子窗格 | pw.add(f) | ★★★★ |
sash_place(索引, x, y) | 用代码设置某条分隔条位置 | pw.sash_place(0, 120, 0) | ★ |
panecget(子控件, 选项) | 读某窗格的选项 | pw.panecget(f, "width") | ★ |
最基础的 Notebook:建一个 Notebook,再建两个 Frame 当"页",分别 add 进去并设页签文字。
# 第13章 例13.1:用 ttk.Notebook 做"全部联系人 / 我的收藏"两页
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# ① 创建 Notebook(分页容器),pack 占满窗口
self.nb = ttk.Notebook(self.root)
self.nb.pack(fill="both", expand=True)
# ② 第一页:全部联系人(页本身是一个 Frame)
self.page_all = ttk.Frame(self.nb)
self.nb.add(self.page_all, text="全部联系人") # 加页 + 设页签文字
ttk.Label(self.page_all, text="这里是所有联系人").pack(padx=10, pady=10)
# ③ 第二页:我的收藏
self.page_fav = ttk.Frame(self.nb)
self.nb.add(self.page_fav, text="我的收藏")
ttk.Label(self.page_fav, text="这里是我收藏的联系人").pack(padx=10, pady=10)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.nb = ttk.Notebook(self.root):创建分页容器,pack(fill="both", expand=True) 让它撑满窗口并随窗口伸缩。ttk.Frame(命名如 self.page_all),再 self.nb.add(self.page_all, text="全部联系人")——add 把 Frame 加为"一页",text= 是页签上显示的标题。self.page_all / self.page_fav 里正常 pack/grid 该页内容即可。演示如何用 tab() 读页签标题、index("current") 读当前页索引、select() 拿到当前页对象。
# 第13章 例13.2:按钮显示当前页(演示 select / index / tab)
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
self.nb = ttk.Notebook(self.root)
self.nb.pack(fill="both", expand=True)
# 两页(真实工程里会交给两个构建方法,见 13.5)
self.page_all = ttk.Frame(self.nb)
self.nb.add(self.page_all, text="全部联系人")
self.page_fav = ttk.Frame(self.nb)
self.nb.add(self.page_fav, text="我的收藏")
# 一个按钮,点击后报告"当前在第几页"
self.btn = ttk.Button(self.root, text="显示当前页", command=self.show_current)
self.btn.pack(pady=5)
def show_current(self):
# nb.select() 返回当前页对应的控件对象(内部 id 字符串)
current = self.nb.select()
# nb.index("current") 返回当前页的索引(0、1…)
idx = self.nb.index("current")
# nb.tab(idx, "text") 读取该页签的标题文字
title = self.nb.tab(idx, "text")
print(f"当前页对象={current},是第 {idx} 页:{title}")
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.nb.select():不传参数时,返回"当前选中页"对应的控件对象(一个内部 id)。self.nb.index("current"):返回当前页的序号,从 0 开始计数。self.nb.tab(idx, "text"):读第 idx 页的 text 选项,即页签标题。写 nb.tab(idx, text="新名") 还能改标题。中间那条分隔条可以拖动,用户自己决定左右比例。
# 第13章 例13.3:ttk.PanedWindow 左右可拖分隔,左放表单右放列表
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# orient="horizontal":左右两块,中间一条竖直分隔条
self.pw = ttk.PanedWindow(self.root, orient="horizontal")
self.pw.pack(fill="both", expand=True)
# 左窗格:一个 Frame,add 进 PanedWindow
self.left = ttk.Frame(self.pw, width=200)
self.pw.add(self.left)
ttk.Label(self.left, text="左:表单区").pack(padx=5, pady=5)
ttk.Entry(self.left).pack(padx=5, pady=5, fill="x")
# 右窗格
self.right = ttk.Frame(self.pw)
self.pw.add(self.right)
ttk.Label(self.right, text="右:列表区").pack(padx=5, pady=5)
for n in ("张三", "李四"):
ttk.Label(self.right, text=n).pack(anchor="w", padx=8, pady=2)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
ttk.PanedWindow(self.root, orient="horizontal"):orient 决定分隔方向,horizontal 表示左右分(分隔条竖直)。self.pw.add(self.left) / self.pw.add(self.right):依次加入左右两个窗格。PanedWindow 会按顺序从左(或从上)排布它们。Notebook / PanedWindow 天生适合"每页一个方法"的写法:
__init__ 里把所有页的控件全写出来。给"全部联系人"页写 _build_page_all(),给"我的收藏"页写 _build_page_fav(),__init__ 里只负责 add 调用。各页内容互相隔离,改一页不影响另一页。self.page_all、self.page_fav 存进 self,别的回调方法(如例 13.2 的 show_current)能随时拿到它们。_build_page_fav 只显示 favorite=True 的联系人),后续接数据库/列表时结构很顺。一句话:"一页一方法,容器持有页;切页不动其它,改页互不连累。"
add 的不是 Frame 而是单个控件:add 应接收"一整页"(通常是 Frame/容器),直接 add(ttk.Label(...)) 虽然能加但难维护。规范做法是 add(某Frame)。add 页却不 self.nb.pack(...),整个分页不显示。容器本身也得 pack/grid。orient 写反:想要左右分却写成 "vertical"(会变成上下分)。记住 horizontal=左右、vertical=上下。index("current") 在没有任何页时调用:空 Notebook 调 index 可能返回异常,确保先 add 了页再读。select() 返回的是窗口系统 id 而非控件对象本身:需要拿到真实控件时,用它和 index/页引用匹配,别直接当控件用。ttk.Notebook 做一个程序,含"设置""关于"两页,每页放一句说明文字。self.nb.select(self.page_about) 切换过去。ttk.PanedWindow(orient="vertical") 做上下分隔:上块放一个 Entry、下块放一个 Label 显示"下方内容"。_build_page_all / _build_page_fav 私有构建方法(复现 13.5 思路)。下节预告:第 14 章是本版核心章——我们把"一组常一起出现的控件"(如搜索栏、表单、表格)封装成自己的控件类(自定义组件),实现复用与解耦,这是 tkinter 工程化的关键一步,也为第 20 章 MVC 铺路。