本书红线(贯穿全教程,回顾):
1. 每个界面都用class App:封装,__init__里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带if __name__ == "__main__":守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。
主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。
上节回顾(第 9 章):你学会了用更省空间的控件做选择与调数——Combobox 下拉选分类(记得 state="readonly")、Scale 滑块选年龄(记得 from_ 带下划线)、Spinbox 上下箭头调数。本章给通讯录加上"列表"能力——Listbox(列表框) 配合 Scrollbar(滚动条),一次性展示"一列"多条联系人姓名,支持选中与滚动。
本章目标:认识 Listbox(显示一列多行文本) 和 Scrollbar(滚动条),并学会两者双向绑定让长列表能滚动。重点是 insert/delete/get/curselection,以及用 listvariable 绑定列表、用 refresh(self) 方法从数据重绘列表——建立"数据 → 界面"的刷新思维。OOP 落点:self.listbox 作为实例属性,把"数据层"和"显示层"分开。
是什么:
Listbox:一个矩形框,里面一行一行显示文本(像 Windows 资源管理器左侧的列表)。每行一项,可单选或多选。Scrollbar:滚动条,让内容超过框高时能用滑块/滚轮查看看不到的部分。适用场景:
📌 Listbox / Scrollbar 没有 ttk 版本,直接用tk.Listbox/tk.Scrollbar(这是官方定死的,不是我们不用 ttk)。
| 控件 | 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|---|
| Listbox | height | 可见行数(不滚动时能看几行) | 8 | ★★★ |
| Listbox | selectmode | 选择模式 | "single"(单选) / "browse" / "multiple" / "extended"(框选) | ★★★ |
| Listbox | listvariable | 绑定一个 StringVar(值为列表) | tk.StringVar() | ★★★ |
| Listbox | yscrollcommand | 垂直滚动回调(绑滚动条) | sb.set | ★★★★ |
| Scrollbar | orient | 方向 | "vertical" / "horizontal" | ★★ |
| Scrollbar | command | 滚动时通知列表(绑列表) | listbox.yview | ★★★★ |
双向绑定公式(记住就行,10.4 会写):listbox.config(yscrollcommand=sb.set)+sb.config(command=listbox.yview)。两边各指对方,"谁能滚"和"滚到哪"才联动。
| 方法 | 作用 | 示例 |
|---|---|---|
insert(位置, 文本) | 加一项;tk.END 表示末尾 | lb.insert(tk.END, "张三") |
delete(起, 止) | 删除项;delete(0, tk.END) 清空全部 | lb.delete(0, tk.END) |
get(起, 止) | 取文本;get(0, tk.END) 取全部 | lb.get(0, tk.END) |
curselection() | 取选中项下标(元组,可能为空) | idx = lb.curselection() |
selection_set(下标) | 程序选中某项 | lb.selection_set(0) |
yview / xview | 滚动视图(给滚动条 command 用) | lb.yview |
⚠️curselection()返回的是下标元组(如(2,)),且可能为空——用户没选时返回空元组。用之前一定要先判断if idx:或if idx:,否则下标越界报错。
每个例子都写在 class App 里,可直接运行(本机会弹出窗口)。
把几个姓名插进列表,并支持鼠标点击选中(点按钮显示选中项)。
# 第10章 例10.1:Listbox 添加并选中显示联系人
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器 - 联系人列表")
# ① 创建 Listbox,height=8 表示不滚动时显示 8 行
self.listbox = tk.Listbox(self.root, height=8, selectmode="single")
self.listbox.pack(padx=20, pady=10, fill="both", expand=True)
# ② 用 insert(tk.END, 文本) 往末尾加几项
for name in ["张三", "李四", "王五", "赵六"]:
self.listbox.insert(tk.END, name)
# ③ 按钮:点击后显示当前选中项
self.btn = ttk.Button(self.root, text="显示选中", command=self.show_sel)
self.btn.pack(padx=20, pady=10)
self.label = ttk.Label(self.root, text="(请选择)")
self.label.pack(padx=20, pady=10)
def show_sel(self):
# ④ curselection() 返回选中项下标元组,可能为空,必须先判断
idx = self.listbox.curselection()
if idx: # 有选中才处理
name = self.listbox.get(idx[0]) # 按下标取文字
self.label.config(text=f"选中:{name}")
else:
self.label.config(text="还没选任何人")
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
tk.Listbox(self.root, height=8, selectmode="single"):一次显示 8 行,单选模式。self.listbox.insert(tk.END, name):tk.END 是 tkinter 常量,表示"插到末尾"。循环把 4 个名字都加进去。curselection() 可能为空:所以先 if idx: 判断,再 self.listbox.get(idx[0]) 取文字。直接 get(idx[0]) 在没选时会 tuple index out of range。当联系人有几十个、超过框高,就需要滚动条。关键是双向绑定两边都配。
# 第10章 例10.2:Listbox 与 Scrollbar 双向绑定实现滚动
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器 - 可滚动列表")
# 用一个 Frame 把 Listbox 和滚动条并排摆放
self.frame = ttk.Frame(self.root)
self.frame.pack(padx=20, pady=10, fill="both", expand=True)
# ① 滚动条(竖向)
self.sb = tk.Scrollbar(self.frame, orient="vertical")
self.sb.pack(side="right", fill="y")
# ② Listbox:yscrollcommand 指向滚动条的 set 方法
self.listbox = tk.Listbox(
self.frame, height=6,
yscrollcommand=self.sb.set, # 列表内容变化 → 通知滚动条更新
)
self.listbox.pack(side="left", fill="both", expand=True)
# ③ 滚动条 command 指向列表的 yview → 双向绑定完成
self.sb.config(command=self.listbox.yview)
# ④ 塞入很多项,超过 6 行就能滚动
for i in range(1, 31):
self.listbox.insert(tk.END, f"联系人_{i:02d}")
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明(双向绑定是核心):
self.sb = tk.Scrollbar(self.frame, orient="vertical"):竖向滚动条,先 pack(side="right", fill="y") 贴右边。yscrollcommand=self.sb.set:当列表内容滚动/增减时,自动调用 sb.set(...) 让滚动条滑块位置同步。self.sb.config(command=self.listbox.yview):反过来,拖滚动条时调用 listbox.yview 让列表内容跟着滚。本章重点:写一个 refresh(self) 方法,从"数据列表"重新把 Listbox 画一遍;点"删除"删掉选中项并重绘。这建立了"改数据 → 调 refresh → 界面更新"的刷新思维。
# 第10章 例10.3:用数据列表 + refresh() 方法驱动 Listbox,并删除选中项
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("通讯录管理器 - 数据驱动列表")
# ① 数据层:一个普通 Python 列表保存联系人(界面"背后"的数据)
self.contacts = ["张三", "李四", "王五", "赵六", "孙七"]
self.listbox = tk.Listbox(self.root, height=8, selectmode="single")
self.listbox.pack(padx=20, pady=10, fill="both", expand=True)
# ② 删除按钮
self.del_btn = ttk.Button(self.root, text="删除选中", command=self.delete_sel)
self.del_btn.pack(padx=20, pady=5)
self.label = ttk.Label(self.root, text="共 0 位联系人")
self.label.pack(padx=20, pady=10)
# ③ 初始先按数据重绘一次
self.refresh()
def refresh(self):
# ④ 清空列表,再从 self.contacts 逐项插入 —— "数据 → 界面" 的标准写法
self.listbox.delete(0, tk.END)
for name in self.contacts:
self.listbox.insert(tk.END, name)
self.label.config(text=f"共 {len(self.contacts)} 位联系人")
def delete_sel(self):
idx = self.listbox.curselection()
if not idx: # 没选中就提示
self.label.config(text="请先选中一位再删除")
return
# ⑤ 先按界面下标删数据,再 refresh 重绘
pos = idx[0]
self.contacts.pop(pos) # 从数据层删
self.refresh() # 重绘界面
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.contacts 是"真相来源"(数据层)。界面只是它的镜像。refresh(self):先 delete(0, tk.END) 清空,再循环 insert 重画。以后任何数据变动后调一次 refresh(),界面就重新对齐数据——这就是 GUI 的"刷新"范式。delete_sel:先 self.contacts.pop(pos) 改数据,再 self.refresh() 让界面跟着变。不要直接只改界面或只改数据,保持二者一致。self.listbox 实例属性:列表控件作为对象成员,方法间共享。self.contacts(数据)和 self.listbox(显示)分开,所有改动都先动数据、再 refresh() 重绘。这种"单一数据源 + 重绘"的思维,是后续 Treeview(第 11 章)、甚至数据库通讯录的基石。refresh(self) 方法:把"重绘逻辑"封装成一个方法,任何需要更新界面的地方调一句即可,避免重复代码、避免界面和数据不一致。listbox.config(yscrollcommand=sb.set) 和 sb.config(command=listbox.yview),缺一不可。curselection() 返回元组且可能为空:直接 idx[0] 在没选时会 IndexError。务必 if idx: 先判断。listvariable 用法易错:用 tk.StringVar() 绑定时,应 var.set(("张三","李四"))(元组/列表),不是 var.set("张三,李四")(会变成一个字符串)。本例用 insert 更直观,二者选一即可。delete(0, tk.END) 清空 vs 删单项:清空用 (0, tk.END);删单项用 delete(下标)。__init__ 里创建再 pack 子控件:本例 self.frame 先 pack,再往里放 sb/listbox,顺序别反。tk.Listbox,不要 ttk.Listbox(不存在)。if idx: 判断再 get(idx[0]))。self.contacts 数据列表保存几个联系人姓名,点"删除"按钮删掉 Listbox 中选中的那一项并重绘;用 refresh() 方法实现重绘。delete_sel 里改成直接 self.listbox.delete(idx[0]) 而不改 self.contacts、不调 refresh(),会发生什么?(提示:界面删了但数据没删,下次 refresh() 又被加回来——界面和数据不一致。务必"先改数据,再重绘"。)下节预告:第 11 章我们学 Treeview——通讯录的核心展示控件,用"表格"显示多列结构化数据(姓名/电话/分类),并支持点行回填表单。它是前几章所有控件能力的集大成者。