8. 综合练习:迷你待办清单
这是什么:一个 100 行以内的桌面待办清单(Todo 清单):输入任务、回车添加、双击标记完成(划线变灰)、右键删除、关掉重开数据还在——这一栈全部知识点的一次总装。
为什么用:前 7 章学的是零件,真正写程序的本事是把零件拼成一个能交付的产品。这个练习刻意覆盖五件事:面向对象封装(oop 栈衔接)、输入与按钮(第 3 章)、grid 布局(第 4 章)、事件绑定(第 5 章)、菜单与对话框 + JSON 落盘(第 6 章 + 标准库)——拼完它,你有第一个"可以发给别人用"的 Python 作品。
如何用:先列需求,再抄代码跑通,然后逐块看懂它为什么长这样,最后按三个挑战自己扩展。
需求清单(先想清楚再动手)
| # | 功能 | 用到哪章 |
|---|---|---|
| 1 | 顶部输入框 + 添加按钮,输入后回车或点击都能加 | 第 3、5 章 |
| 2 | 任务列表显示在中间,占满剩余空间 | 第 4 章 grid |
| 3 | 双击任务 = 标记完成/取消完成 | 第 5 章 bind |
| 4 | 右键任务 = 删除 | 第 5 章 Button-3 |
| 5 | 退出自动存 todo.json,启动自动读回 |
第 6 章思路 + json 标准库 |
| 6 | 菜单栏:清空已完成 / 退出 | 第 6 章 menu |
完整代码
存为 todo.py 直接运行(记得 Python 3.12+,见版本基线):
import json
import tkinter as tk
from tkinter import messagebox
from pathlib import Path
DATA_FILE = Path(__file__).parent / "todo.json"
class TodoApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("迷你待办清单")
self.geometry("420x480")
self.tasks = [] # [{"text": ..., "done": bool}, ...]
self._build_ui()
self._load()
self.protocol("WM_DELETE_WINDOW", self._quit)
# ---------- 界面:第 3、4 章 ----------
def _build_ui(self):
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1)
self.entry = tk.Entry(self, font=("", 12))
self.entry.grid(row=0, column=0, sticky="ew", padx=8, pady=8)
self.entry.bind("<Return>", lambda e: self._add())
tk.Button(self, text="添加", command=self._add).grid(
row=0, column=1, padx=(0, 8))
self.listbox = tk.Listbox(self, font=("", 12), activestyle="none")
self.listbox.grid(row=1, column=0, columnspan=2,
sticky="nsew", padx=8, pady=(0, 8))
self.listbox.bind("<Double-Button-1>", self._toggle)
self.listbox.bind("<Button-3>", self._delete)
menu = tk.Menu(self)
m = tk.Menu(menu, tearoff=0)
m.add_command(label="清空已完成", command=self._clear_done)
m.add_separator()
m.add_command(label="退出", command=self._quit)
menu.add_cascade(label="任务", menu=m)
self.config(menu=menu)
self._refresh()
# ---------- 逻辑:第 5 章 ----------
def _add(self):
text = self.entry.get().strip()
if not text:
messagebox.showwarning("注意", "先写点内容再加")
return
self.tasks.append({"text": text, "done": False})
self.entry.delete(0, "end")
self._refresh()
def _toggle(self, event):
i = self.listbox.nearest(event.y)
if 0 <= i < len(self.tasks):
self.tasks[i]["done"] = not self.tasks[i]["done"]
self._refresh()
def _delete(self, event):
i = self.listbox.nearest(event.y)
if 0 <= i < len(self.tasks):
del self.tasks[i]
self._refresh()
def _clear_done(self):
self.tasks = [t for t in self.tasks if not t["done"]]
self._refresh()
# ---------- 显示同步 ----------
def _refresh(self):
self.listbox.delete(0, "end")
for t in self.tasks:
self.listbox.insert("end", ("✔ " if t["done"] else " ") + t["text"])
self.listbox.itemconfigure("end",
foreground="gray" if t["done"] else "black")
# ---------- 持久化:json 落盘(标准库,零安装) ----------
def _load(self):
if DATA_FILE.exists():
try:
self.tasks = json.loads(DATA_FILE.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
self.tasks = [] # 坏文件不拦启动
def _save(self):
try:
DATA_FILE.write_text(
json.dumps(self.tasks, ensure_ascii=False, indent=2),
encoding="utf-8")
except OSError as e:
messagebox.showerror("保存失败", str(e))
def _quit(self):
self._save()
self.destroy()
if __name__ == "__main__":
TodoApp().mainloop()
你会看到:一个完整待办应用——加任务、双击划勾变灰、右键删、菜单清空已完成;关闭再打开,数据原样回来(同目录多了个 todo.json,用记事本打开能看到结构化内容)。
看懂它的五个关键
| 位置 | 为什么这样写 |
|---|---|
class TodoApp(tk.Tk) |
继承主窗口类,把状态(self.tasks)和动作(self._add)封在一起——面向对象那栈的知识在这里兑现 |
listbox.nearest(event.y) |
Listbox 不直接告诉你"点到哪一行",用点击的 y 坐标反推行号,这是第 5 章"事件对象"的实战 |
_refresh() 重画整个列表 |
数据(self.tasks)是唯一真相,界面只是它的投影——改完数据一律全量刷新。数据量再大才需要增量更新,练习阶段先要"对" |
json.dumps(ensure_ascii=False) |
不加这个参数,中文会被转义成 \uXXXX,json 文件没法直读 |
protocol("WM_DELETE_WINDOW", ...) |
第 6 章动手改里埋的雷在这里拆:接管"点 X 关闭",才能退出前自动保存 |
三个扩展挑战
- 优先级:任务加个
level字段(高/中/低),高优先级显示红色——练itemconfigure - 编辑:双击改成"弹出
simpledialog.askstring改名",把"完成/取消"换到右键菜单(tk.Menu(tearoff=0)+tk_popup,menu.post(event.x_root, event.y_root))——进阶版右键菜单 - 交付:按第 7 章打包成 exe 发给一位朋友,收集 ta 的一句真实反馈——这一栈的毕业设计到此完成
下一步去哪
- 嫌 Tkinter 界面朴素?概念你已经全会,只差换库:CustomTkinter(Tkinter 的现代皮肤,API 几乎不变)、Flet(Flutter 引擎,一套代码桌面+网页+手机)、PySide6(Qt 官方绑定,正经商业软件主力)——三选一继续,学习成本比零基础入门低得多
- 想做命令行工具而不是窗口?转 Flask / FastAPI 进 Web 方向
- 想继续练工程能力:设计模式 回头看
_refresh这种"数据驱动视图"的写法,就是 MVC 思想的雏形
恭喜你——从"print 一行字"到"交付一个能发给别人双击运行的桌面软件",Python 带出终端,这条路你走完了。回看 Python 板块总览 🐸