📚 目录 第 18 章 画布与进度:Canvas / Progressbar(自由绘图与进度反馈)

第 18 章 画布与进度:Canvas / Progressbar(自由绘图与进度反馈)

本书红线(贯穿全教程,回顾):
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 落点。


18.1 是什么 / 适用场景 ★

Canvas 是什么:Canvas(画布)是一块"画板"。普通的 Label/Button 只能显示现成的文字或图片,而 Canvas 让你用代码在它上面精确地画出图形——圆、矩形、线条、文字,甚至组合成小图标。它就像一块空白画布,你拿"画笔函数"往上画。

Progressbar 是什么:Progressbar(进度条)是一个只用来"显示进度"的控件。它不交互,就是一个横条,从 0 慢慢涨到 100%,告诉用户"任务进行到哪了"。

适用场景:

不适用:只是显示一段文字用 Label 就够了;想要滚动的复杂表格用 Treeview。Canvas 适合"要自己画图形"的场合。


18.2 构造选项(对照官方手册)★★

下面两张表分别来自官方 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 是字符数。别混了。

18.3 常用方法速查 ★★

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)进度增加 deltabar.step(10)
⚠️ Canvas 的 create_* 必须作用在 Canvas 实例上(如 self.canvas.create_oval(...))。这是本书红线之一——不要写成 create_oval(...) 脱离实例。

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

例 18.1 在 Canvas 画一个圆形头像占位 + 姓名首字母 ★★★

通讯录里每条联系人可以有个"首字母头像":一个蓝色圆 + 中间写名字第一个字。我们把绘图逻辑收进 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()

逐行说明:

例 18.2 Progressbar 基础:用 after 做自动进度动画 ★★★

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

逐行说明:

例 18.3 综合:Canvas 头像 + Progressbar 保存进度(点按钮才开始动画)★★★★

把两个控件合起来:左边画头像,右边一个"保存"按钮,点一下进度条从 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),各司其职。


18.5 OOP 落点 ★★

本章最该记住的一句 OOP 习惯:

把绘图逻辑收进一个方法(如 draw_avatar(self)),而不是把一堆 create_oval 散在 __init__ 里。

这样做的好处:


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


18.7 本章练习

  1. 用 Canvas 画一个红色矩形(坐标 10,10,110,60)里面写白色文字"通知"。
  2. 用 create_line 在 200x200 的画布上画一个"叉号"(两条对角线)。
  3. (结合通讯录) 改写例 18.1:让 draw_avatar 接收一个名字列表,在画布上横向排开画出 3 个圆形头像占位,分别显示不同姓名首字母。
  4. 思考题:如果例 18.2 把 self.root.after(100, self.step) 换成 time.sleep(0.1) 行不行?为什么?(提示:会卡死界面,详见第 19 章"mainloop 单线程"的讲解。)
下节预告:第 19 章是"易错与进阶"——我们会集中扫清从"能跑"到"稳跑"的坑:图片 PhotoImage 必须保引用、after 定时原理、为什么耗时任务要进子线程、布局常见坑、高 DPI 字体模糊、以及退出前如何自动保存。这是把程序做"稳"的关键一章。