🐸 青蛙编程网
首页 / Python / 桌面客户端 / 8. 综合练习:迷你待办清单

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 关闭",才能退出前自动保存

三个扩展挑战

  1. 优先级:任务加个 level 字段(高/中/低),高优先级显示红色——练 itemconfigure
  2. 编辑:双击改成"弹出 simpledialog.askstring 改名",把"完成/取消"换到右键菜单(tk.Menu(tearoff=0) + tk_popup,menu.post(event.x_root, event.y_root))——进阶版右键菜单
  3. 交付:按第 7 章打包成 exe 发给一位朋友,收集 ta 的一句真实反馈——这一栈的毕业设计到此完成

下一步去哪

  • 嫌 Tkinter 界面朴素?概念你已经全会,只差换库:CustomTkinter(Tkinter 的现代皮肤,API 几乎不变)、Flet(Flutter 引擎,一套代码桌面+网页+手机)、PySide6(Qt 官方绑定,正经商业软件主力)——三选一继续,学习成本比零基础入门低得多
  • 想做命令行工具而不是窗口?转 Flask / FastAPI 进 Web 方向
  • 想继续练工程能力:设计模式 回头看 _refresh 这种"数据驱动视图"的写法,就是 MVC 思想的雏形

恭喜你——从"print 一行字"到"交付一个能发给别人双击运行的桌面软件",Python 带出终端,这条路你走完了。回看 Python 板块总览 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。