Наше приложение получилось достаточно удобным, но давайте сделаем его ещё функциональнее. В предыдущей части мы заложили основу: работа с JSON, CRUD-операции и базовый интерфейс. Теперь пришло время добавить те самые «плюшки», которые превращают учебный проект в полноценный инструмент.
Мы добавим четыре важные функции:
Редактирование задач - чтобы исправлять опечатки и уточнять формулировки.
Цветовую индикацию - выполненные задачи будут зелёными, а важные (с высоким приоритетом) красными.
Сортировку - невыполненные задачи всегда будут отображаться выше выполненных.
Дату создания - будем показывать, когда задача была добавлена.
Это не только сделает приложение удобнее, но и познакомит нас с новыми возможностями Tkinter: диалоговыми окнами, настройкой цветов элементов списка и форматированием дат.
Шаг 1. Обновляем модель данных: добавляем дату и приоритет
Для новых функций нам нужно расширить структуру каждой задачи. Теперь каждая задача будет словарём с полями:
id — уникальный номер.
text — описание задачи.
completed — статус выполнения (True/False).
created_at — дата и время создания (строка в формате ISO).
priority — приоритет: "high", "medium" или "low".
Внесём изменения в функции загрузки и сохранения. Важно, чтобы старые файлы (без этих полей) продолжали корректно загружаться. Для этого добавим функцию migrate_task, которая дополнит старые задачи значениями по умолчанию
import json import os from datetime import datetime TASKS_FILE = "tasks.json" def load_tasks(): """Загружает задачи из JSON-файла с обратной совместимостью""" if not os.path.exists(TASKS_FILE): return [] with open(TASKS_FILE, "r", encoding="utf-8") as f: try: tasks = json.load(f) # Миграция старых задач (добавляем created_at и priority, если их нет) for task in tasks: if "created_at" not in task: task["created_at"] = datetime.now().isoformat() if "priority" not in task: task["priority"] = "medium" return tasks except json.JSONDecodeError: return [] def save_tasks(tasks): """Сохраняет задачи в JSON-файл""" with open(TASKS_FILE, "w", encoding="utf-8") as f: json.dump(tasks, f, ensure_ascii=False, indent=2) def get_next_id(tasks): """Возвращает следующий доступный ID для новой задачи""" if not tasks: return 1 return max(task["id"] for task in tasks) + 1
Пояснение: datetime.now().isoformat() создаёт строку вида 2025-03-22T15:30:45.123456, которая удобна для сортировки и хранения.
Шаг 2. Добавляем сортировку
Сортировка - это логика отображения. Мы не будем менять порядок задач в основном списке self.tasks, чтобы сохранить простоту. Вместо этого создадим метод get_sorted_tasks, который возвращает отсортированную копию списка для отображения. Правило сортировки:
Сначала невыполненные (completed == False)
Затем выполненные (completed == True)
Внутри каждой группы - по ID (или по дате создания, если хотим)
Добавим этот метод в класс TodoApp:
class TodoApp: def get_sorted_tasks(self): """Возвращает задачи, отсортированные для отображения: сначала активные, потом выполненные""" active_tasks = [task for task in self.tasks if not task["completed"]] completed_tasks = [task for task in self.tasks if task["completed"]] # Сортируем по ID (или можно по created_at) active_tasks.sort(key=lambda x: x["id"]) completed_tasks.sort(key=lambda x: x["id"]) return active_tasks + completed_tasks def refresh_task_list(self): """Обновляет отображение списка задач с учётом сортировки и цветовой индикации""" self.task_listbox.delete(0, tk.END) sorted_tasks = self.get_sorted_tasks() for task in sorted_tasks: # Формируем текст задачи status = "✓" if task["completed"] else "○" # Форматируем дату: показываем только день и время try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%y %H:%M") except: date_str = "дата неизвестна" display_text = f"{status} [{date_str}] {task['id']}. {task['text']}" # Вставляем задачу в список self.task_listbox.insert(tk.END, display_text) # Настраиваем цвет фона и текста в зависимости от приоритета и статуса bg_color = "white" fg_color = "black" if task["completed"]: # Выполненные задачи: зелёный текст на сером фоне fg_color = "green" bg_color = "light gray" else: # Активные задачи: цвет зависит от приоритета if task["priority"] == "high": fg_color = "red" bg_color = "#ffe6e6" # светло-красный elif task["priority"] == "medium": fg_color = "orange" bg_color = "#fff0e6" # светло-оранжевый # low — стандартные цвета # Применяем цвета к последней вставленной строке index = tk.END self.task_listbox.itemconfig(tk.END, bg=bg_color, fg=fg_color)
Пояснения:
get_sorted_tasks - создаёт новый список, не меняя исходный. Это важно, потому что мы сохраняем задачи в том порядке, в котором они были добавлены, а для отображения используем сортировку.
itemconfig(index, bg=..., fg=...) - позволяет менять цвет фона и текста для конкретной строки Listbox. Индекс
tk.ENDозначает последнюю добавленную строку.Для даты мы используем strftime - метод, который форматирует дату в читаемый вид. %d.%m.%y %H:%M даст, например, 22.03.25 15:30.
Шаг 3. Добавляем окно редактирования
Редактирование задачи потребует создания нового окна (Toplevel), в котором пользователь сможет изменить текст и приоритет. Это отличный пример работы с модальными окнами.
Добавим метод edit_task в класс TodoApp:
class TodoApp: def edit_task(self): """Открывает окно для редактирования выбранной задачи""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу для редактирования") return # Получаем реальный индекс задачи в self.tasks # Для этого нужно найти задачу по ID, потому что в Listbox другой порядок selected_index = selection[0] sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selected_index] task_id = selected_task["id"] # Находим задачу в оригинальном списке task_to_edit = None task_index = None for i, task in enumerate(self.tasks): if task["id"] == task_id: task_to_edit = task task_index = i break if not task_to_edit: return # Создаём дочернее окно edit_window = tk.Toplevel(self.root) edit_window.title(f"Редактирование задачи #{task_id}") edit_window.geometry("450x250") edit_window.resizable(False, False) edit_window.grab_set() # Делаем окно модальным # Поле для редактирования текста tk.Label(edit_window, text="Текст задачи:").pack(pady=(10, 0), anchor=tk.W, padx=10) text_entry = tk.Text(edit_window, height=4, width=50, font=("Arial", 10)) text_entry.pack(pady=5, padx=10, fill=tk.BOTH, expand=True) text_entry.insert("1.0", task_to_edit["text"]) # Выбор приоритета tk.Label(edit_window, text="Приоритет:").pack(pady=(5, 0), anchor=tk.W, padx=10) priority_var = tk.StringVar(value=task_to_edit["priority"]) priority_frame = tk.Frame(edit_window) priority_frame.pack(pady=5, padx=10, anchor=tk.W) tk.Radiobutton(priority_frame, text="Высокий", variable=priority_var, value="high", fg="red").pack(side=tk.LEFT, padx=5) tk.Radiobutton(priority_frame, text="Средний", variable=priority_var, value="medium", fg="orange").pack(side=tk.LEFT, padx=5) tk.Radiobutton(priority_frame, text="Низкий", variable=priority_var, value="low").pack(side=tk.LEFT, padx=5) # Кнопки действий button_frame = tk.Frame(edit_window) button_frame.pack(pady=15) def save_edit(): new_text = text_entry.get("1.0", "end-1c").strip() if not new_text: messagebox.showwarning("Предупреждение", "Текст задачи не может быть пустым", parent=edit_window) return task_to_edit["text"] = new_text task_to_edit["priority"] = priority_var.get() save_tasks(self.tasks) self.refresh_task_list() edit_window.destroy() messagebox.showinfo("Успех", f"Задача #{task_id} обновлена") tk.Button(button_frame, text="Сохранить", command=save_edit, bg="light green", width=10).pack(side=tk.LEFT, padx=5) tk.Button(button_frame, text="Отмена", command=edit_window.destroy, bg="light gray", width=10).pack(side=tk.LEFT, padx=5)
Теперь добавим кнопку «Редактировать» в интерфейс. В методе create_widgets в нижнюю панель добавим:
# В методе create_widgets, в bottom_frame, после кнопки "Удалить" self.edit_button = tk.Button(bottom_frame, text="✏ Редактировать", command=self.edit_task) self.edit_button.pack(side=tk.LEFT, padx=5)
Шаг 4. Обновляем метод добавления задачи
При добавлении задачи теперь нужно запрашивать приоритет и сохранять дату создания. Сделаем так: при нажатии «Добавить» будет открываться небольшое окно для выбора приоритета. Это удобнее, чем захламлять главное окно.
Перепишем метод add_task:
class TodoApp: def add_task(self): """Открывает окно для добавления новой задачи с выбором приоритета""" # Создаём диалоговое окно add_window = tk.Toplevel(self.root) add_window.title("Новая задача") add_window.geometry("400x200") add_window.resizable(False, False) add_window.grab_set() # Поле ввода текста tk.Label(add_window, text="Текст задачи:").pack(pady=(10, 0), anchor=tk.W, padx=10) text_entry = tk.Entry(add_window, width=50, font=("Arial", 10)) text_entry.pack(pady=5, padx=10, fill=tk.X) text_entry.focus() # Выбор приоритета tk.Label(add_window, text="Приоритет:").pack(pady=(5, 0), anchor=tk.W, padx=10) priority_var = tk.StringVar(value="medium") priority_frame = tk.Frame(add_window) priority_frame.pack(pady=5, padx=10, anchor=tk.W) tk.Radiobutton(priority_frame, text="Высокий", variable=priority_var, value="high", fg="red").pack(side=tk.LEFT, padx=5) tk.Radiobutton(priority_frame, text="Средний", variable=priority_var, value="medium", fg="orange").pack(side=tk.LEFT, padx=5) tk.Radiobutton(priority_frame, text="Низкий", variable=priority_var, value="low").pack(side=tk.LEFT, padx=5) # Кнопки button_frame = tk.Frame(add_window) button_frame.pack(pady=15) def save_task(): text = text_entry.get().strip() if not text: messagebox.showwarning("Предупреждение", "Введите текст задачи", parent=add_window) return next_id = get_next_id(self.tasks) new_task = { "id": next_id, "text": text, "completed": False, "created_at": datetime.now().isoformat(), "priority": priority_var.get() } self.tasks.append(new_task) save_tasks(self.tasks) self.refresh_task_list() add_window.destroy() messagebox.showinfo("Успех", f"Задача добавлена (ID: {next_id})") tk.Button(button_frame, text="Добавить", command=save_task, bg="light green", width=10).pack(side=tk.LEFT, padx=5) tk.Button(button_frame, text="Отмена", command=add_window.destroy, bg="light gray", width=10).pack(side=tk.LEFT, padx=5)
Теперь при добавлении задачи пользователь выбирает приоритет, и дата создания автоматически сохраняется.
Шаг 5. Адаптируем остальные методы
Методы mark_done и delete_task остаются практически без изменений, но нужно учесть, что curselection() теперь возвращает индекс в отсортированном списке, а не в self.tasks. Поэтому при работе с этими методами мы должны сначала найти задачу по ID, как мы сделали в edit_task.
Обновим mark_done и delete_task:
class TodoApp: def mark_done(self): """Отмечает выбранную задачу как выполненную""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу") return # Получаем задачу из отсортированного списка sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selection[0]] task_id = selected_task["id"] # Находим задачу в оригинальном списке for task in self.tasks: if task["id"] == task_id: if task["completed"]: messagebox.showinfo("Информация", "Задача уже выполнена") else: task["completed"] = True save_tasks(self.tasks) self.refresh_task_list() messagebox.showinfo("Успех", f"Задача #{task_id} отмечена как выполненная") return def delete_task(self): """Удаляет выбранную задачу""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу") return # Получаем задачу из отсортированного списка sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selection[0]] task_id = selected_task["id"] task_text = selected_task["text"] # Подтверждение удаления if messagebox.askyesno("Подтверждение", f"Удалить задачу #{task_id} \"{task_text}\"?"): # Удаляем из оригинального списка for i, task in enumerate(self.tasks): if task["id"] == task_id: del self.tasks[i] break save_tasks(self.tasks) self.refresh_task_list() messagebox.showinfo("Успех", f"Задача #{task_id} удалена")
Шаг 6. Финальный код
Теперь соберём всё вместе:
import json import os import tkinter as tk from tkinter import messagebox from datetime import datetime # Модель данных TASKS_FILE = "tasks.json" def load_tasks(): """Загружает задачи из JSON-файла с обратной совместимостью""" if not os.path.exists(TASKS_FILE): return [] with open(TASKS_FILE, "r", encoding="utf-8") as f: try: tasks = json.load(f) # Миграция старых задач (добавляем created_at и priority, если их нет) for task in tasks: if "created_at" not in task: task["created_at"] = datetime.now().isoformat() if "priority" not in task: task["priority"] = "medium" return tasks except json.JSONDecodeError: return [] def save_tasks(tasks): """Сохраняет задачи в JSON-файл""" with open(TASKS_FILE, "w", encoding="utf-8") as f: json.dump(tasks, f, ensure_ascii=False, indent=2) def get_next_id(tasks): """Возвращает следующий доступный ID для новой задачи""" if not tasks: return 1 return max(task["id"] for task in tasks) + 1 # Класс приложения class TodoApp: def __init__(self, root): self.root = root self.root.title("Менеджер задач") self.root.geometry("600x500") self.root.resizable(False, False) # Загружаем задачи self.tasks = load_tasks() # Создаём элементы интерфейса self.create_widgets() # Обновляем список задач self.refresh_task_list() def create_widgets(self): """Создаёт все виджеты окна""" # Верхняя панель: поле ввода + кнопка добавления top_frame = tk.Frame(self.root) top_frame.pack(pady=10, padx=10, fill=tk.X) self.entry = tk.Entry(top_frame, font=("Arial", 12)) self.entry.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(0, 5)) self.entry.bind("<Return>", lambda event: self.add_task()) self.add_button = tk.Button(top_frame, text="➕ Добавить", command=self.add_task, bg="light blue") self.add_button.pack(side=tk.RIGHT) # Список задач с прокруткой list_frame = tk.Frame(self.root) list_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) scrollbar = tk.Scrollbar(list_frame) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.task_listbox = tk.Listbox( list_frame, font=("Arial", 10), yscrollcommand=scrollbar.set, selectmode=tk.SINGLE, height=15, activestyle="none" ) self.task_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar.config(command=self.task_listbox.yview) # Нижняя панель: кнопки действий bottom_frame = tk.Frame(self.root) bottom_frame.pack(pady=10, padx=10, fill=tk.X) self.edit_button = tk.Button(bottom_frame, text="✏ Редактировать", command=self.edit_task, bg="light yellow") self.edit_button.pack(side=tk.LEFT, padx=5) self.done_button = tk.Button(bottom_frame, text="✅ Выполнено", command=self.mark_done, bg="light green") self.done_button.pack(side=tk.LEFT, padx=5) self.delete_button = tk.Button(bottom_frame, text="🗑 Удалить", command=self.delete_task, bg="light coral") self.delete_button.pack(side=tk.LEFT, padx=5) self.exit_button = tk.Button(bottom_frame, text="Выход", command=self.root.quit, bg="light gray") self.exit_button.pack(side=tk.RIGHT, padx=5) # Информационная метка info_label = tk.Label(self.root, text="Совет: Двойной клик по задаче - редактирование", font=("Arial", 8), fg="gray") info_label.pack(side=tk.BOTTOM, pady=5) # Привязываем двойной клик к редактированию self.task_listbox.bind("<Double-Button-1>", lambda event: self.edit_task()) def get_sorted_tasks(self): """Возвращает задачи, отсортированные для отображения: сначала активные, потом выполненные""" active_tasks = [task for task in self.tasks if not task["completed"]] completed_tasks = [task for task in self.tasks if task["completed"]] # Сортируем по ID (можно также по дате создания) active_tasks.sort(key=lambda x: x["id"]) completed_tasks.sort(key=lambda x: x["id"]) return active_tasks + completed_tasks def refresh_task_list(self): """Обновляет отображение списка задач с учётом сортировки и цветовой индикации""" self.task_listbox.delete(0, tk.END) sorted_tasks = self.get_sorted_tasks() for task in sorted_tasks: # Формируем текст задачи status = "✓" if task["completed"] else "○" # Форматируем дату try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%y %H:%M") except: date_str = "??.??.??" # Отображаем приоритет символом priority_symbol = "" if task["priority"] == "high": priority_symbol = "🔴 " elif task["priority"] == "medium": priority_symbol = "🟠 " elif task["priority"] == "low": priority_symbol = "⚪ " display_text = f"{status} {priority_symbol}[{date_str}] {task['id']}. {task['text']}" # Вставляем задачу в список self.task_listbox.insert(tk.END, display_text) # Настраиваем цвет фона и текста в зависимости от приоритета и статуса bg_color = "white" fg_color = "black" if task["completed"]: # Выполненные задачи: зелёный текст на сером фоне fg_color = "#006400" # тёмно-зелёный bg_color = "#E8E8E8" # светло-серый else: # Активные задачи: цвет зависит от приоритета if task["priority"] == "high": fg_color = "#8B0000" # тёмно-красный bg_color = "#FFE6E6" # светло-красный elif task["priority"] == "medium": fg_color = "#CC7000" # оранжевый bg_color = "#FFF0E6" # светло-оранжевый # low — стандартные цвета (чёрный на белом) # Применяем цвета к последней вставленной строке self.task_listbox.itemconfig(tk.END, bg=bg_color, fg=fg_color) def add_task(self): """Открывает окно для добавления новой задачи с выбором приоритета""" # Создаём диалоговое окно add_window = tk.Toplevel(self.root) add_window.title("Новая задача") add_window.geometry("450x250") add_window.resizable(False, False) add_window.grab_set() # Центрируем окно add_window.transient(self.root) add_window.focus_force() # Поле ввода текста tk.Label(add_window, text="Текст задачи:", font=("Arial", 10, "bold")).pack(pady=(15, 5), anchor=tk.W, padx=15) text_entry = tk.Text(add_window, height=4, width=50, font=("Arial", 10)) text_entry.pack(pady=5, padx=15, fill=tk.BOTH, expand=True) text_entry.focus() # Выбор приоритета tk.Label(add_window, text="Приоритет:", font=("Arial", 10, "bold")).pack(pady=(10, 5), anchor=tk.W, padx=15) priority_var = tk.StringVar(value="medium") priority_frame = tk.Frame(add_window) priority_frame.pack(pady=5, padx=15, anchor=tk.W) tk.Radiobutton(priority_frame, text="🔴 Высокий", variable=priority_var, value="high", fg="red", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="🟠 Средний", variable=priority_var, value="medium", fg="orange", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="⚪ Низкий", variable=priority_var, value="low", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) # Кнопки button_frame = tk.Frame(add_window) button_frame.pack(pady=20) def save_task(): text = text_entry.get("1.0", "end-1c").strip() if not text: messagebox.showwarning("Предупреждение", "Введите текст задачи", parent=add_window) return next_id = get_next_id(self.tasks) new_task = { "id": next_id, "text": text, "completed": False, "created_at": datetime.now().isoformat(), "priority": priority_var.get() } self.tasks.append(new_task) save_tasks(self.tasks) self.refresh_task_list() add_window.destroy() messagebox.showinfo("Успех", f"Задача добавлена (ID: {next_id})") tk.Button(button_frame, text="Добавить", command=save_task, bg="light green", width=12, font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=add_window.destroy, bg="light gray", width=12, font=("Arial", 10)).pack(side=tk.LEFT, padx=10) def edit_task(self): """Открывает окно для редактирования выбранной задачи""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу для редактирования") return # Получаем реальный индекс задачи в self.tasks selected_index = selection[0] sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selected_index] task_id = selected_task["id"] # Находим задачу в оригинальном списке task_to_edit = None task_index = None for i, task in enumerate(self.tasks): if task["id"] == task_id: task_to_edit = task task_index = i break if not task_to_edit: return # Создаём дочернее окно edit_window = tk.Toplevel(self.root) edit_window.title(f"Редактирование задачи #{task_id}") edit_window.geometry("500x320") edit_window.resizable(False, False) edit_window.grab_set() edit_window.transient(self.root) edit_window.focus_force() # Поле для редактирования текста tk.Label(edit_window, text="Текст задачи:", font=("Arial", 10, "bold")).pack(pady=(15, 5), anchor=tk.W, padx=15) text_entry = tk.Text(edit_window, height=5, width=55, font=("Arial", 10)) text_entry.pack(pady=5, padx=15, fill=tk.BOTH, expand=True) text_entry.insert("1.0", task_to_edit["text"]) text_entry.focus() # Информация о дате создания try: created = datetime.fromisoformat(task_to_edit["created_at"]) date_info = f"Создано: {created.strftime('%d.%m.%Y %H:%M:%S')}" except: date_info = "Дата создания неизвестна" tk.Label(edit_window, text=date_info, font=("Arial", 8), fg="gray").pack(pady=(5, 0), anchor=tk.W, padx=15) # Выбор приоритета tk.Label(edit_window, text="Приоритет:", font=("Arial", 10, "bold")).pack(pady=(10, 5), anchor=tk.W, padx=15) priority_var = tk.StringVar(value=task_to_edit["priority"]) priority_frame = tk.Frame(edit_window) priority_frame.pack(pady=5, padx=15, anchor=tk.W) tk.Radiobutton(priority_frame, text="🔴 Высокий", variable=priority_var, value="high", fg="red", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="🟠 Средний", variable=priority_var, value="medium", fg="orange", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="⚪ Низкий", variable=priority_var, value="low", font=("Arial", 10)).pack(side=tk.LEFT, padx=10) # Кнопки действий button_frame = tk.Frame(edit_window) button_frame.pack(pady=20) def save_edit(): new_text = text_entry.get("1.0", "end-1c").strip() if not new_text: messagebox.showwarning("Предупреждение", "Текст задачи не может быть пустым", parent=edit_window) return task_to_edit["text"] = new_text task_to_edit["priority"] = priority_var.get() save_tasks(self.tasks) self.refresh_task_list() edit_window.destroy() messagebox.showinfo("Успех", f"Задача #{task_id} обновлена") tk.Button(button_frame, text="Сохранить", command=save_edit, bg="light green", width=12, font=("Arial", 10)).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=edit_window.destroy, bg="light gray", width=12, font=("Arial", 10)).pack(side=tk.LEFT, padx=10) def mark_done(self): """Отмечает выбранную задачу как выполненную""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу") return # Получаем задачу из отсортированного списка sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selection[0]] task_id = selected_task["id"] # Находим задачу в оригинальном списке for task in self.tasks: if task["id"] == task_id: if task["completed"]: messagebox.showinfo("Информация", "Задача уже выполнена") else: task["completed"] = True save_tasks(self.tasks) self.refresh_task_list() messagebox.showinfo("Успех", f"Задача #{task_id} отмечена как выполненная") return def delete_task(self): """Удаляет выбранную задачу""" selection = self.task_listbox.curselection() if not selection: messagebox.showwarning("Предупреждение", "Выберите задачу") return # Получаем задачу из отсортированного списка sorted_tasks = self.get_sorted_tasks() selected_task = sorted_tasks[selection[0]] task_id = selected_task["id"] task_text = selected_task["text"] # Подтверждение удаления if messagebox.askyesno("Подтверждение", f"Удалить задачу #{task_id}\n\"{task_text[:50]}\"?"): # Удаляем из оригинального списка for i, task in enumerate(self.tasks): if task["id"] == task_id: del self.tasks[i] break save_tasks(self.tasks) self.refresh_task_list() messagebox.showinfo("Успех", f"Задача #{task_id} удалена") # Запуск приложения if __name__ == "__main__": root = tk.Tk() app = TodoApp(root) root.mainloop()

В следующий раз продолжим изучать и вспоминать:
Приоритеты - выпадающий список при добавлении: высокий, средний, низкий.
Фильтр - чекбоксы «Показать все/активные/выполненные».
Поиск - поле для поиска задач по тексту.
Экспорт - кнопка «Сохранить как CSV» или «Экспорт в PDF».
