本书红线(贯穿全教程,回顾):
1. 每个界面都用class App:封装,__init__里建控件,方法即事件回调。
2. 所有控件示例一律写在类里,不用脱离类的扁平脚本。
3. 代码块带if __name__ == "__main__":守卫,可独立运行。
4. 星级:★ 了解 · ★★ 理解 · ★★★ 掌握 · ★★★★ 重点必会。
主线项目:图形界面通讯录管理器,随章节每章新增一个控件慢慢长大。
上节回顾(第 17 章):你学会了用菜单(Menu)给通讯录管理器加上"文件 / 编辑"下拉菜单,把很多操作(打开、保存、新增、删除)收进菜单栏。菜单让程序"像真正的软件"。本章新增两个控件——Canvas(画布)和 Progressbar(进度条):Canvas 让你在窗口里"自由画图"(画头像占位、画示意图),Progressbar 则用来给用户一个"正在保存 / 加载中"的进度反馈。
本章目标:认识 Canvas(画布)与 Progressbar(进度条)。Canvas 是一块"可以任意作画"的区域,用 create_oval/create_rectangle/create_line/create_text 画圆、画矩形、画线、写文字;Progressbar 用 value/start()/stop() 显示任务进度。把绘图逻辑收进 draw_avatar(self) 这类方法里,正是 OOP 落点。
Canvas 是什么:Canvas(画布)是一块"画板"。普通的 Label/Button 只能显示现成的文字或图片,而 Canvas 让你用代码在它上面精确地画出图形——圆、矩形、线条、文字,甚至组合成小图标。它就像一块空白画布,你拿"画笔函数"往上画。
Progressbar 是什么:Progressbar(进度条)是一个只用来"显示进度"的控件。它不交互,就是一个横条,从 0 慢慢涨到 100%,告诉用户"任务进行到哪了"。
适用场景:
不适用:只是显示一段文字用 Label 就够了;想要滚动的复杂表格用 Treeview。Canvas 适合"要自己画图形"的场合。
下面两张表分别来自官方 tkinter.Canvas 与 tkinter.ttk.Progressbar 文档,挑最常用的列出。
Canvas 常用构造选项:
| 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
width / height | 画布宽 / 高(像素) | 整数,如 120 | ★★★★ |
bg / background | 画布背景色 | 颜色字符串,如 "#eef2f7" | ★★★ |
bd / borderwidth | 边框粗细 | 整数,如 2 | ★★ |
relief | 边框样式 | "sunken"/"groove" 等 | ★★ |
cursor | 鼠标移到画布上时的指针样式 | "cross" 等 | ★ |
Progressbar 常用构造选项:
| 选项 | 作用 | 常用取值 | 重要度 |
|---|---|---|---|
mode | 进度条模式 | "determinate"(确定,按数值涨)/ "indeterminate"(不确定,来回滑) | ★★★★ |
maximum | 进度最大值 | 整数,如 100 | ★★★★ |
value | 当前进度值(0 ∼ maximum) | 整数,如 30 | ★★★★ |
length | 进度条长度(像素) | 整数,如 200 | ★★★ |
orient | 方向 | "horizontal"(默认横)/ "vertical"(竖) | ★★ |
📌 像素 vs 字符:Canvas 的width/height是像素;而 Label/Button 的width/height是字符数。别混了。
Canvas 的"画笔"方法(都作用在 Canvas 实例上,返回一个图形 id):
| 方法 | 作用 | 示例 |
|---|---|---|
create_oval(x1,y1,x2,y2,...) | 画椭圆/圆(左上角与右下角坐标) | cv.create_oval(20,20,100,100, fill="blue") |
create_rectangle(x1,y1,x2,y2,...) | 画矩形 | cv.create_rectangle(10,10,90,90, fill="red") |
create_line(x1,y1,x2,y2,...) | 画直线 | cv.create_line(0,0,100,100, width=2) |
create_text(x,y, text=, ...) | 在 (x,y) 写文字 | cv.create_text(60,60, text="张", fill="white") |
itemconfig(id, ...) | 运行时改某个图形的选项 | cv.itemconfig(circle_id, fill="red") |
delete(id) | 删除某个图形 | cv.delete(circle_id) |
Progressbar 常用方法/属性:
| 方法 / 属性 | 作用 | 示例 |
|---|---|---|
value(属性,可读写) | 当前进度值 | bar["value"] = 50 或 bar["value"] |
start(interval) | 开始"不确定模式"动画 | bar.start(20)(每 20ms 滑一步) |
stop() | 停止 start() 的动画 | bar.stop() |
step(delta) | 进度增加 delta | bar.step(10) |
⚠️ Canvas 的create_*必须作用在 Canvas 实例上(如self.canvas.create_oval(...))。这是本书红线之一——不要写成create_oval(...)脱离实例。
通讯录里每条联系人可以有个"首字母头像":一个蓝色圆 + 中间写名字第一个字。我们把绘图逻辑收进 draw_avatar(self, name) 方法——这正是 OOP 落点。
# 第18章 例18.1:用 Canvas 画圆形头像占位 + 姓名首字母
import tkinter as tk
class App:
def __init__(self, root):
self.root = root
self.root.title("头像占位")
# 建一块 120x120 的画布,浅灰底
self.canvas = tk.Canvas(self.root, width=120, height=120, bg="#eef2f7")
self.canvas.pack(padx=20, pady=20)
self.draw_avatar("张三") # 调用自己的绘图方法画头像
def draw_avatar(self, name):
# create_oval 画一个圆(椭圆):参数是"左上角坐标"和"右下角坐标"
self.canvas.create_oval(20, 20, 100, 100, fill="#3a7afe", outline="")
# create_text 在圆心 (60,60) 写文字;initial 取姓名第一个字
initial = name[0] if name else "?"
self.canvas.create_text(60, 60, text=initial, fill="white",
font=("微软雅黑", 28))
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
self.canvas = tk.Canvas(self.root, width=120, height=120, bg="#eef2f7"):创建画布,width/height 是像素。存进 self.canvas,以后方法里都能用。self.draw_avatar("张三"):把"画头像"这件事交给一个方法,传入姓名。self.canvas.create_oval(20, 20, 100, 100, ...):画一个圆。(20,20) 是圆外接框的左上角,(100,100) 是右下角——两坐标之差都是 80,所以是标准圆。fill 是填充色,outline="" 表示不加边框。self.canvas.create_text(60, 60, text=initial, ...):在圆心 (60,60) 写首字母,白色字、大字号。注意 create_text 的 x,y 是文字中心位置(默认 anchor="center")。after(ms, func) 可以让进度每隔一段时间自动 +5,直到满 100。这是进度条最经典的用法。
# 第18章 例18.2:Progressbar + after,做一个自动涨到 100% 的进度条
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("保存进度")
# mode="determinate" 表示"确定进度",按 value 数值显示
self.bar = ttk.Progressbar(self.root, mode="determinate",
maximum=100, length=200)
self.bar.pack(padx=20, pady=20)
self.bar["value"] = 0 # 初始进度为 0
self.root.after(100, self.step) # 100毫秒(0.1秒)后开始第一次 step
def step(self):
if self.bar["value"] < 100: # 还没满
self.bar["value"] += 5 # 进度 +5
self.root.after(100, self.step) # 再过 0.1 秒再调一次自己(递归定时)
else:
self.bar.stop() # 满了就停止(这里其实已到顶,stop 收尾)
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()逐行说明:
ttk.Progressbar(self.root, mode="determinate", maximum=100, length=200):mode="determinate" 是"确定进度"模式——进度由 value 决定;maximum=100 表示满值是 100;length=200 是横条像素长度。self.bar["value"] = 0:进度条是个"字典式"控件,用 bar["value"] 读写当前进度(也可以用 bar.config(value=0),两者等价)。self.root.after(100, self.step):after 是定时器——100 毫秒后调用 self.step 这个方法。self.bar["value"] += 5:每次 step 进度 +5。self.root.after(100, self.step):在 step 里再次预约 0.1 秒后调自己,形成"每 0.1 秒涨 5"的动画,直到满 100。这是 tkinter 里做动画的标准套路(因为 mainloop 是单线程,不能用 time.sleep 卡住)。把两个控件合起来:左边画头像,右边一个"保存"按钮,点一下进度条从 0 涨到 100,模拟"保存中"。
# 第18章 例18.3:Canvas 头像 + Progressbar 保存进度(点按钮开始动画)
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.root.title("保存中…")
# 左侧画布画头像
self.canvas = tk.Canvas(self.root, width=100, height=100, bg="#eef2f7")
self.canvas.pack(padx=10, pady=5, side="left")
self.draw_avatar("李四")
# 右侧:进度条 + 按钮(用一个 Frame 装下)
right = ttk.Frame(self.root)
right.pack(side="left", padx=10, pady=10)
self.bar = ttk.Progressbar(right, mode="determinate", maximum=100, length=180)
self.bar.pack(pady=5)
self.bar["value"] = 0
self.btn = ttk.Button(right, text="保存", command=self.start_save)
self.btn.pack(pady=5)
def draw_avatar(self, name):
self.canvas.create_oval(15, 15, 85, 85, fill="#3a7afe", outline="")
initial = name[0] if name else "?"
self.canvas.create_text(50, 50, text=initial, fill="white",
font=("微软雅黑", 24))
def start_save(self):
self.btn.config(state="disabled") # 保存期间禁用按钮,避免重复点
self.bar["value"] = 0
self.root.after(80, self._save_step) # 启动"保存动画"
def _save_step(self):
if self.bar["value"] < 100:
self.bar["value"] += 10 # 每次涨 10
self.root.after(80, self._save_step)
else:
self.btn.config(state="normal") # 保存完恢复按钮
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()说明:点击"保存"→ start_save 禁用按钮、归零进度、用 after 启动 _save_step 动画;涨满后恢复按钮。这个"点按钮 → 进度动画 → 完成"的模式,就是真实软件里"保存中…"提示的简化版。绘图逻辑仍在 draw_avatar(self),按钮逻辑在 start_save(self),各司其职。
本章最该记住的一句 OOP 习惯:
把绘图逻辑收进一个方法(如draw_avatar(self)),而不是把一堆create_oval散在__init__里。
这样做的好处:
__init__ 负责"搭界面"(建画布、建进度条、布局),draw_avatar 负责"怎么画"。以后想换头像样式,只改这一个方法。draw_avatar(name) 接收 name 参数,通讯录里几十条联系人都能调用同一个方法、传入不同名字,画出各自的头像,不用复制粘贴代码。self 上:画布 self.canvas、进度条 self.bar 都是 self 的属性,类里任何方法都能拿到它们去画、去改进度——这正是全书"控件即 self 属性"红线的体现。create_* 写成了脱离实例的 create_oval(...):会报 NameError。必须是 self.canvas.create_oval(...)——作用在 Canvas 实例上。time.sleep() 做进度动画:会卡死整个界面(mainloop 是单线程)。进度动画要用 after(ms, func) 递归预约,绝不用 sleep。width/height 当成字符数:它们是像素。想要"字符大小"请改用 Label。create_oval(x1,y1,x2,y2) 里 (x2-x1) 和 (y2-y1) 不相等就是椭圆;想画正圆要让宽高相等。value 却忘了设 maximum:默认 maximum=100 没问题,但 mode 没设对会没反应;"determinate" 模式下一定要配合 value 改进度。create_text 位置理解错:它的 x,y 默认是文字中心,不是左上角。想让文字左上对齐要加 anchor="nw"。10,10,110,60)里面写白色文字"通知"。create_line 在 200x200 的画布上画一个"叉号"(两条对角线)。draw_avatar 接收一个名字列表,在画布上横向排开画出 3 个圆形头像占位,分别显示不同姓名首字母。self.root.after(100, self.step) 换成 time.sleep(0.1) 行不行?为什么?(提示:会卡死界面,详见第 19 章"mainloop 单线程"的讲解。)下节预告:第 19 章是"易错与进阶"——我们会集中扫清从"能跑"到"稳跑"的坑:图片PhotoImage必须保引用、after定时原理、为什么耗时任务要进子线程、布局常见坑、高 DPI 字体模糊、以及退出前如何自动保存。这是把程序做"稳"的关键一章。