Наше приложение получилось достаточно удобным, но давайте сделаем его ещё функциональнее. В предыдущей части мы заложили основу: работа с JSON, CRUD-операции и базовый интерфейс. Теперь пришло время добавить те самые «плюшки», которые превращают учебный проект в полноценный инструмент.
Мы добавим четыре важные функции:

  1. Редактирование задач - чтобы исправлять опечатки и уточнять формулировки.

  2. Цветовую индикацию - выполненные задачи будут зелёными, а важные (с высоким приоритетом) красными.

  3. Сортировку - невыполненные задачи всегда будут отображаться выше выполненных.

  4. Дату создания - будем показывать, когда задача была добавлена.

Это не только сделает приложение удобнее, но и познакомит нас с новыми возможностями 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, который возвращает отсортированную копию списка для отображения. Правило сортировки:

  1. Сначала невыполненные (completed == False)

  2. Затем выполненные (completed == True)

  3. Внутри каждой группы - по 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()
Наше приложение
Наше приложение

В следующий раз продолжим изучать и вспоминать:

  1. Приоритеты - выпадающий список при добавлении: высокий, средний, низкий.

  2. Фильтр - чекбоксы «Показать все/активные/выполненные».

  3. Поиск - поле для поиска задач по тексту.

  4. Экспорт - кнопка «Сохранить как CSV» или «Экспорт в PDF».