本书红线(贯穿全教程,回顾):
1. 每个界面都用class App:封装,__init__里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带if __name__ == "__main__":守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。
主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。
上节回顾(第 11 章):你学会了用 Treeview 在界面上展示"表格数据"——把联系人一行行列出来。但问题来了:当窗口里既有表格、又有输入框、又有按钮时,所有控件平铺在一起会越来越乱。本章引入两个"容器"控件——Frame 和 LabelFrame,帮你把界面拆成一个个清爽的"区域"。
本章目标:学会用 Frame(纯透明容器) 和 LabelFrame(带标题的框) 把界面分区。OOP 落点是——不同区域交给 App 类中不同的私有构建方法(如 _build_form、_build_list),做到"界面结构 = 代码结构"。
Frame 是什么:tk.Frame 是一个看不见的"盒子",它本身不显示任何文字或颜色(默认透明),唯一的作用就是装其它控件。你把 Label、Entry、Button 等"放"进一个 Frame,再把这个 Frame 整体摆进窗口——于是这些控件就成了一个"组合",可以一起移动、一起布局。
LabelFrame 是什么:ttk.LabelFrame 是 Frame 的"带标题加强版"——它四周有一圈边框,并且在左上角显示一段标题文字(如"联系人信息")。它特别适合用来圈住一组相关的表单控件。
适用场景:
不适用:需要"分页切换"用 Notebook(第 13 章);需要"可拖拽分隔"用 PanedWindow(第 13 章)。容器只负责"圈地",不负责"切换/拖动"。
📌 tk 与 ttk 的选择:Frame 两个版本都有(tk.Frame、ttk.Frame);LabelFrame 优先用ttk.LabelFrame(主题更统一、更好看),tk.LabelFrame也可用。本章示例统一用ttk.LabelFrame+tk.Frame演示。注意 ttk 控件没有fg/bg,要改颜色得用Style(后续章节讲),所以容器背景保持默认即可。
Frame / LabelFrame 继承 Widget,下面列出最常用选项(完整列表见官方 tkinter.Frame / tkinter.ttk.LabelFrame 文档):
| 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
text | LabelFrame 专属:框左上角的标题文字 | text="联系人信息" | ★★★★ |
labelanchor | LabelFrame 标题所在方位 | "n"/"nw"/"w"/"ne" 等 | ★★ |
width / height | 容器宽 / 高(像素) | 整数,如 200 | ★★ |
relief | 边框样式(LabelFrame 默认有边;Frame 默认 flat) | "sunken"/"groove"/"raised" | ★★ |
borderwidth | 边框粗细(像素) | 整数 | ★ |
bg / background | 仅 tk.Frame 支持背景色;ttk.Frame 用 Style | 颜色字符串 | ★★ |
padx / pady | 容器自身的外边距(与子控件间距无关) | 整数 | ★★ |
container | 设为 True 可承载其它 Tk 顶层窗口(高级,少用) | True/False | ★ |
关键认知:Frame 默认"透明无边框"——你创建它后肉眼几乎看不出它存在,但它内部的控件布局会受影响(比如pack/grid的相对关系)。LabelFrame 则自带边框+标题,圈地效果一目了然。
Frame / LabelFrame 是"容器",自身方法不多,主要是所有控件通用的那几个:
| 方法 | 作用 | 示例 |
|---|---|---|
pack() / grid() / place() | 把容器本身摆进父窗口,并把子控件摆进容器 | self.form.pack() |
config() / configure() | 运行时改选项(LabelFrame 常用它改 text 标题) | self.form.config(text="新标题") |
winfo_children() | 返回容器内所有子控件组成的列表(调试/批量操作时有用) | self.form.winfo_children() |
destroy() | 销毁容器及其内部所有子控件 | self.form.destroy() |
📌winfo_children()很实用:当你想"清空容器里所有东西重新来"时,可以遍历它逐一destroy(),比一个个记变量方便。
最朴素的用法:用两个 Frame 把窗口上下切开,一个放标题、一个放按钮。
# 第12章 例12.1:用 tk.Frame 把界面分成"标题区"和"按钮区"两块
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# ① 顶部标题区:创建一个 Frame 当容器,pack 到窗口上方、横向占满
self.top = tk.Frame(self.root)
self.top.pack(side="top", fill="x")
# 在标题区里放 Label——注意父容器是 self.top(不是 root)!
self.title_label = tk.Label(self.top, text="我的通讯录",
font=("微软雅黑", 16))
self.title_label.pack(padx=10, pady=10)
# ② 底部按钮区:另一个 Frame,pack 到窗口下方
self.bottom = tk.Frame(self.root)
self.bottom.pack(side="bottom", fill="x")
self.add_btn = ttk.Button(self.bottom, text="添加联系人")
self.add_btn.pack(side="left", padx=5, pady=5)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.top = tk.Frame(self.root):在 root 里创建一个 Frame 容器,存进 self.top。此时它"空无一物",也看不见。self.title_label = tk.Label(self.top, ...):关键一步——这个 Label 的"父"是 self.top,而不是 root。于是它就被"装进"了标题区。以后移动/隐藏 self.top,里面的 Label 跟着动。self.top.pack(side="top", fill="x"):把容器本身摆进窗口顶部,fill="x" 让它横向撑满。self.bottom,两个区互不干扰。表单控件很多时,用 LabelFrame 把它们圈起来并加上标题,用户一眼就懂这块是干嘛的。
# 第12章 例12.2:用 ttk.LabelFrame 做带标题的"基本信息"表单区
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# LabelFrame:带标题的框,text 就是左上角的标题文字
self.form = ttk.LabelFrame(self.root, text="基本信息")
self.form.pack(padx=10, pady=10, fill="x")
# 表单内第 0 行:姓名标签 + 输入框(父容器都是 self.form)
self.name_label = ttk.Label(self.form, text="姓名:")
self.name_label.grid(row=0, column=0, padx=5, pady=5, sticky="e")
self.name_entry = ttk.Entry(self.form)
self.name_entry.grid(row=0, column=1, padx=5, pady=5)
# 表单内第 1 行:电话
self.phone_label = ttk.Label(self.form, text="电话:")
self.phone_label.grid(row=1, column=0, padx=5, pady=5, sticky="e")
self.phone_entry = ttk.Entry(self.form)
self.phone_entry.grid(row=1, column=1, padx=5, pady=5)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
ttk.LabelFrame(self.root, text="基本信息"):text="基本信息" 让框左上角显示"基本信息"标题。self.form,于是它们规规矩矩待在框里、grid 布局也只在框内部生效。这是本章 OOP 落点的核心示范——把"左区"和"右区"分别交给 _build_form 和 _build_list 两个私有方法构建,主 __init__ 只负责总布局。界面结构 = 代码结构。
# 第12章 例12.3:左 LabelFrame 放表单、右 Frame 放列表,分区由私有方法构建
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器")
# 外层总布局:一个 body Frame 横向分左右两列
self.body = tk.Frame(self.root)
self.body.pack(fill="both", expand=True)
self.body.columnconfigure(0, weight=1) # 第0列(左)可伸缩
self.body.columnconfigure(1, weight=1) # 第1列(右)可伸缩
# 把两块区域分别交给两个私有方法构建(OOP 落点)
self._build_form() # 构建左区(表单)
self._build_list() # 构建右区(列表)
def _build_form(self):
# 左区:带标题的表单框,放在 body 的第 0 列
self.form = ttk.LabelFrame(self.body, text="联系人信息")
self.form.grid(row=0, column=0, padx=5, pady=5, sticky="nsew")
ttk.Label(self.form, text="姓名:").grid(row=0, column=0, padx=4, pady=4, sticky="e")
self.name_entry = ttk.Entry(self.form)
self.name_entry.grid(row=0, column=1, padx=4, pady=4)
ttk.Label(self.form, text="电话:").grid(row=1, column=0, padx=4, pady=4, sticky="e")
self.phone_entry = ttk.Entry(self.form)
self.phone_entry.grid(row=1, column=1, padx=4, pady=4)
def _build_list(self):
# 右区:一个普通 Frame 当列表容器,放在 body 的第 1 列
self.list_box = tk.Frame(self.body)
self.list_box.grid(row=0, column=1, padx=5, pady=5, sticky="nsew")
ttk.Label(self.list_box, text="联系人列表").pack(anchor="w")
for name in ("张三", "李四", "王五"):
ttk.Label(self.list_box, text=name).pack(anchor="w", padx=6, pady=2)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.body 是一个总 Frame,用 grid 把窗口分成左右两列(第 0、1 列),weight=1 让两列能随窗口伸缩。self._build_form():构建左区,内部创建一个 ttk.LabelFrame 放进 body 第 0 列,再往里 grid 姓名/电话。self._build_list():构建右区,创建一个普通 tk.Frame 放进第 1 列,里面 pack 几个名字 Label 当"列表"。__init__ 只描述"有哪些区域、怎么排",具体每块怎么填交给对应私有方法。以后要加"搜索区",就再写一个 _build_search(),互不干扰。运行例 12.3:左边是带"联系人信息"标题的表单框,右边是"联系人列表"标题下竖排的若干名字——界面清爽分区,代码结构也清爽。
本章把"容器"和"面向对象"紧密结合,核心有三点:
self 持有每个区域:把 self.form、self.list_box 存进 self,App 的其它方法能随时拿到它们(比如清空表单、刷新列表)。_build_form / _build_list 等私有方法,主 __init__ 只管"总布局"。界面结构 = 代码结构,这是 tkinter 工程化、可维护的关键一步,也为第 14 章"自定义组件"和第 20 章 MVC 埋下伏笔。一句话:"用 Frame 圈地,用私有方法分区;界面分区清晰,代码自然清晰。"
ttk.Label(self.root, ...)(父是 root),结果它跑到 Frame 外面去了。记住——进了哪个容器,父就写哪个。self.x = tk.Frame(...) 创建容器,再往里放子控件并布局,顺序反了会报错。pack、要么全用 grid,混用会布局冲突甚至卡死。例 12.2/12.3 各自内部只用了一种。bg,要用 Style 才能改;tk.Frame 才支持 bg=。不确定就用 tk.Frame 或保持默认。text 忘记写:不写 text 它也有边框但没有标题,失去"分组说明"的意义。pack/grid,却忘了把容器(如 self.body.pack(...))摆进窗口——那样整个区域都不会显示。tk.Frame 把窗口分成上下两块,上块一个 Label 显示"通讯录",下块一个 Button 显示"退出"(提示:用 command=root.destroy)。ttk.LabelFrame(text="登录信息") 圈起"用户名/密码"两个 Entry,做出一个带标题的登录框(密码 Entry 用 show="*")。ttk.LabelFrame 放"姓名/电话"两个输入,右边 tk.Frame 用 pack 竖排几个联系人名字;用 _build_form / _build_list 两个私有方法分别构建(即复现例 12.3)。_build_form 和 _build_list 里所有控件直接写进 __init__,功能会变吗?可读性/可维护性有什么差别?(提示:功能一样,但方法会越写越长、难找;分方法才是工程化做法。)下节预告:第 13 章我们用 Notebook(分页) 和 PanedWindow(可拖拽分隔) 组织"更多内容"——当单屏放不下所有功能时,用标签页切换、用拖动条分屏,界面更从容。