Глава 16 · Создаём классные приложения с Tkinter

Мини-проект: список задач

Простой список строк — и уже целое маленькое приложение с сохранением между запусками.

Список задач — интеграция главы 11 (списки), главы 15 (JSON) и Tkinter (Listbox).

tasks : list[str]
отображение
Listbox
callback
add_task() / remove_task()
персистентность
todo.json
Данные, отображение и персистентность — три разных ответственности одного небольшого приложения.
todo_logika.py
import json
from pathlib import Path

def load_tasks(path):
    if not path.exists():
        return []
    with path.open("r", encoding="utf-8") as f:
        return json.load(f)

def save_tasks(path, tasks):
    with path.open("w", encoding="utf-8") as f:
        json.dump(tasks, f, ensure_ascii=False, indent=2)

def add_task(tasks, text):
    if not text.strip():
        return tasks
    return tasks + [text.strip()]

def remove_task(tasks, index):
    return tasks[:index] + tasks[index + 1:]
Список задач: три задачи, вторая выделена, поле ввода и кнопки + / −
Результат выполнения кода ниже. Пример внешнего вида на среде курса. На вашей ОС шрифты, размеры и тема могут немного отличаться.
Отдельные функции-callback без класса — источник ошибки
Callback вроде on_add(), объявленный сам по себе (не внутри другой функции), не может использовать nonlocal tasksnonlocal требует переменную из объемлющей функции, а не из глобальной области. Здесь уместнее класс приложения (глава 14) — self.tasks решает эту проблему естественно.
todo_gui.py
class TodoApp:
    def __init__(self, root, path):
        self.root = root
        self.path = path
        self.tasks = load_tasks(path)
        self.new_task_var = tk.StringVar()
        self.listbox = tk.Listbox(root, height=6)
        self.listbox.pack()
        entry = ttk.Entry(root, textvariable=self.new_task_var)
        entry.pack(side="left", fill="x", expand=True)
        ttk.Button(root, text="+", command=self.on_add).pack(side="left")
        ttk.Button(root, text="−", command=self.on_delete).pack(side="left")
        self.refresh_listbox()

    def refresh_listbox(self):
        self.listbox.delete(0, "end")
        for task in self.tasks:
            self.listbox.insert("end", task)

    def on_add(self):
        self.tasks = add_task(self.tasks, self.new_task_var.get())
        self.new_task_var.set("")
        self.refresh_listbox()
        save_tasks(self.path, self.tasks)

    def on_delete(self):
        selected = self.listbox.curselection()
        if not selected:
            return
        self.tasks = remove_task(self.tasks, selected[0])
        self.refresh_listbox()
        save_tasks(self.path, self.tasks)
Логика проверяема без единого виджета
add_task, remove_task, load_tasks, save_tasks — обычные функции со списками и словарями. Именно их и тестирует практика этого раздела — ровно то разделение, о котором шла речь в разделе 16.24.
Практика: логика списка задач
Проверяется без tkinter — на функциях работы со списком задач
Открыть практику →