В прошлой части мы превратили простое приложение для заметок в удобный инструмент с цветовой индикацией, сортировкой и возможностью редактирования. Однако, когда задач становится много, даже самый красивый список может превратиться в нечитаемую ленту. Согласитесь, сложно ориентироваться, когда нужно найти одну важную задачу среди сотни выполненных.
В этой статье мы добавим инструменты профессионального управления:
Приоритеты через выпадающий список — сделаем интерфейс добавления задач ещё удобнее.
Фильтр (Все / Активные / Выполненные) — чтобы сосредоточиться только на том, что действительно требует внимания.
Поиск — мгновенный поиск задач по тексту.
Экспорт в CSV — возможность выгрузить список задач для отчёта или анализа в Excel.
Экспорт в PDF — создание профессионально оформленных отчётов для печати или архивации.
Шаг 1. Оптимизация интерфейса: выпадающий список приоритетов
На предыдущем этапе мы использовали отдельное окно для выбора приоритета при добавлении задачи. Это было функционально, но не очень эргономично. Сделаем процесс добавления более быстрым — перенесём выбор приоритета на главную панель с помощью выпадающего списка (Combobox).
Обновляем create_widgets
Добавим ttk.Combobox на верхнюю панель, чтобы пользователь мог выбрать приоритет сразу, не открывая лишних окон. Также мы уберём кнопку "Добавить", заменив её на более интуитивную реакцию на нажатие Enter.
# В начале файла добавьте импорт ttk from tkinter import ttk class TodoApp: # ... (остальной код класса) 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()) # Выпадающий список для выбора приоритета tk.Label(top_frame, text="Приоритет:").pack(side=tk.LEFT, padx=(5, 2)) self.priority_var = tk.StringVar(value="medium") priority_combo = ttk.Combobox( top_frame, textvariable=self.priority_var, values=["high", "medium", "low"], state="readonly", width=8 ) priority_combo.pack(side=tk.LEFT, padx=(0, 5)) # Кастомизируем отображение текста в выпадающем списке (опционально) priority_combo.set("medium") # Кнопка добавления self.add_button = tk.Button(top_frame, text="➕ Добавить", command=self.add_task, bg="light blue") self.add_button.pack(side=tk.RIGHT) # ... (остальной код создания списка и нижней панели без изменений)
Упрощаем метод add_task
Теперь метод добавления задачи не требует создания модального окна. Мы просто берём текст из entry и приоритет из priority_combo.
def add_task(self): """Добавляет новую задачу с использованием полей на главном окне""" text = self.entry.get().strip() if not text: messagebox.showwarning("Предупреждение", "Введите текст задачи") return next_id = get_next_id(self.tasks) new_task = { "id": next_id, "text": text, "completed": False, "created_at": datetime.now().isoformat(), "priority": self.priority_var.get() # Берём приоритет из выпадающего списка } self.tasks.append(new_task) save_tasks(self.tasks) # Очищаем поле ввода и обновляем список self.entry.delete(0, tk.END) self.entry.focus() self.refresh_task_list() messagebox.showinfo("Успех", f"Задача добавлена (ID: {next_id})")
Шаг 2. Добавляем фильтр по статусу
Фильтр позволит скрывать выполненные задачи или, наоборот, показывать только их. Для этого нам понадобятся радиокнопки (или чекбоксы), которые будут управлять режимом отображения.
Модифицируем init и добавляем переменную фильтра
Добавим в init атрибут self.filter_mode, который будет хранить текущий режим: "all", "active" или "completed".
def __init__(self, root): # ... (предыдущий код инициализации) self.tasks = load_tasks() self.filter_mode = "all" # Режим фильтрации: all, active, completed self.create_widgets() self.refresh_task_list()
Добавляем панель фильтров в create_widgets
Разместим элементы управления фильтром между списком задач и нижней панелью.
def create_widgets(self): # ... (код создания top_frame и list_frame) # Панель фильтров filter_frame = tk.Frame(self.root) filter_frame.pack(pady=5, padx=10, fill=tk.X) tk.Label(filter_frame, text="Фильтр:").pack(side=tk.LEFT, padx=(0, 10)) self.filter_var = tk.StringVar(value="all") tk.Radiobutton(filter_frame, text="Все", variable=self.filter_var, value="all", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) tk.Radiobutton(filter_frame, text="Активные", variable=self.filter_var, value="active", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) tk.Radiobutton(filter_frame, text="Выполненные", variable=self.filter_var, value="completed", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) # ... (код создания bottom_frame)
Обновляем метод get_sorted_tasks
Теперь этот метод должен учитывать не только сортировку, но и выбранный фильтр.
def get_sorted_tasks(self): """Возвращает задачи с учётом фильтрации и сортировки""" # Сначала фильтруем if self.filter_mode == "active": filtered_tasks = [task for task in self.tasks if not task["completed"]] elif self.filter_mode == "completed": filtered_tasks = [task for task in self.tasks if task["completed"]] else: # "all" filtered_tasks = self.tasks.copy() # Затем сортируем: активные сверху, выполненные снизу active_tasks = [task for task in filtered_tasks if not task["completed"]] completed_tasks = [task for task in filtered_tasks if task["completed"]] active_tasks.sort(key=lambda x: x["id"]) completed_tasks.sort(key=lambda x: x["id"]) return active_tasks + completed_tasks
Важно: Не забудьте обновить refresh_task_list, чтобы он использовал self.filter_var.get() для установки self.filter_mode (или можно передавать фильтр напрямую в get_sorted_tasks). Добавим это в начало refresh_task_list:
def refresh_task_list(self): """Обновляет отображение списка задач""" self.filter_mode = self.filter_var.get() # Обновляем режим фильтра self.task_listbox.delete(0, tk.END) sorted_tasks = self.get_sorted_tasks() # ... (остальной код без изменений)
Шаг 3. Реализуем поиск по задачам
Поиск — это функция, которая работает поверх фильтра. Мы добавим поле ввода, при вводе в которое список будет динамически обновляться, показывая только задачи, содержащие искомый текст.
Добавляем поле поиска в create_widgets
Разместим его на отдельной панели под фильтрами или объединим с ними для компактности.
def create_widgets(self): # ... (код создания filter_frame) # Панель поиска search_frame = tk.Frame(self.root) search_frame.pack(pady=(0, 5), padx=10, fill=tk.X) tk.Label(search_frame, text="Поиск:").pack(side=tk.LEFT, padx=(0, 5)) self.search_entry = tk.Entry(search_frame, font=("Arial", 10)) self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) self.search_entry.bind("<KeyRelease>", lambda event: self.refresh_task_list()) # Поиск при каждом вводе # Кнопка очистки поиска clear_btn = tk.Button(search_frame, text="✖", command=self.clear_search, width=3) clear_btn.pack(side=tk.RIGHT, padx=(5, 0)) # ... (остальной код)
Добавляем метод clear_search
def clear_search(self): """Очищает поле поиска и обновляет список""" self.search_entry.delete(0, tk.END) self.refresh_task_list()
Обновляем логику фильтрации в get_sorted_tasks
Теперь метод должен учитывать и текст поиска.
def get_sorted_tasks(self): """Возвращает задачи с учётом фильтрации, поиска и сортировки""" # 1. Фильтр по статусу if self.filter_mode == "active": filtered_tasks = [task for task in self.tasks if not task["completed"]] elif self.filter_mode == "completed": filtered_tasks = [task for task in self.tasks if task["completed"]] else: filtered_tasks = self.tasks.copy() # 2. Фильтр по тексту (поиск) search_text = self.search_entry.get().strip().lower() if search_text: filtered_tasks = [task for task in filtered_tasks if search_text in task["text"].lower()] # 3. Сортировка active_tasks = [task for task in filtered_tasks if not task["completed"]] completed_tasks = [task for task in filtered_tasks if task["completed"]] active_tasks.sort(key=lambda x: x["id"]) completed_tasks.sort(key=lambda x: x["id"]) return active_tasks + completed_tasks
Шаг 4. Экспорт задач в CSV
Добавим возможность сохранять текущий отфильтрованный и отсортированный список задач в файл CSV. Это полезно для бэкапов или анализа данных.
Добавляем кнопку экспорта в bottom_frame
def create_widgets(self): # ... (код создания bottom_frame) self.export_button = tk.Button(bottom_frame, text="📎 Экспорт в CSV", command=self.export_to_csv, bg="light cyan") self.export_button.pack(side=tk.LEFT, padx=5) # ... (остальные кнопки)
Реализуем метод export_to_csv
Для работы с CSV используем встроенный модуль csv. В начале файла добавим импорт: import csv.
def export_to_csv(self): """Экспортирует текущий список задач в CSV-файл""" from tkinter import filedialog # Получаем текущий отображаемый список (с учётом фильтров и поиска) tasks_to_export = self.get_sorted_tasks() if not tasks_to_export: messagebox.showinfo("Информация", "Нет задач для экспорта") return # Диалог выбора места сохранения filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], title="Сохранить как CSV" ) if not filename: return try: with open(filename, 'w', encoding='utf-8-sig', newline='') as f: writer = csv.writer(f, delimiter=';') # Заголовки writer.writerow(['ID', 'Текст', 'Статус', 'Приоритет', 'Дата создания']) for task in tasks_to_export: status = "Выполнена" if task["completed"] else "Активна" # Форматируем дату для читаемости try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%Y %H:%M:%S") except: date_str = task["created_at"] # Маппинг приоритетов для читаемости priority_map = {"high": "Высокий", "medium": "Средний", "low": "Низкий"} priority_display = priority_map.get(task["priority"], task["priority"]) writer.writerow([ task["id"], task["text"], status, priority_display, date_str ]) messagebox.showinfo("Успех", f"Задачи экспортированы в файл:\n{filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать задачи:\n{str(e)}")
Шаг 5. Экспорт задач в PDF
Помимо CSV-экспорта, добавим возможность создавать профессионально оформленные PDF-отчёты. Это особенно полезно для печати, отправки по электронной почте или архивации выполненных задач в читаемом формате.
Установка необходимой библиотеки
Для работы с PDF нам потребуется библиотека reportlab. Установите её через pip:
pip install reportlab
Добавляем кнопку экспорта в PDF
Добавим новую кнопку на нижнюю панель рядом с кнопкой экспорта в PDF:
def create_widgets(self): # ... (код создания bottom_frame) self.export_csv_btn = tk.Button(bottom_frame, text="📎 Экспорт в CSV", command=self.export_to_csv, bg="light cyan") self.export_csv_btn.pack(side=tk.LEFT, padx=5) self.export_pdf_btn = tk.Button(bottom_frame, text="📄 Экспорт в PDF", command=self.export_to_pdf, bg="light pink") self.export_pdf_btn.pack(side=tk.LEFT, padx=5) # ... (остальные кнопки)
Реализуем метод export_to_pdf
Для создания PDF-документа используем возможности reportlab. Создадим отчёт с заголовком, датой генерации, таблицей задач и статистикой. Ключевая особенность — использование Paragraph для автоматического переноса текста в ячейках.
def export_to_pdf(self): """Экспортирует текущий список задач в PDF-файл с форматированием и переносом текста""" from tkinter import filedialog from reportlab.lib import colors from reportlab.lib.pagesizes import A4, landscape from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont import os # Регистрируем шрифт, поддерживающий кириллицу # Пробуем использовать системные шрифты Windows font_paths = [ "C:/Windows/Fonts/arial.ttf", # Arial "C:/Windows/Fonts/times.ttf", # Times New Roman "C:/Windows/Fonts/calibri.ttf", # Calibri "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", # Linux "/System/Library/Fonts/Arial.ttf", # macOS ] font_registered = False for font_path in font_paths: if os.path.exists(font_path): try: pdfmetrics.registerFont(TTFont('RussianFont', font_path)) font_registered = True break except: continue if not font_registered: messagebox.showwarning( "Предупреждение", "Не найден шрифт для отображения русского текста.\n" "Текст может отображаться некорректно." ) # Используем стандартный шрифт (не будет отображать русский текст) font_name = 'Helvetica' else: font_name = 'RussianFont' # Получаем текущий отображаемый список tasks_to_export = self.get_sorted_tasks() if not tasks_to_export: messagebox.showinfo("Информация", "Нет задач для экспорта") return # Диалог выбора места сохранения filename = filedialog.asksaveasfilename( defaultextension=".pdf", filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")], title="Сохранить как PDF" ) if not filename: return try: # Используем альбомную ориентацию для лучшего отображения длинных текстов doc = SimpleDocTemplate(filename, pagesize=landscape(A4), rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50) # Стили styles = getSampleStyleSheet() # Создаём базовый стиль с поддержкой русского шрифта base_style = ParagraphStyle( 'BaseStyle', parent=styles['Normal'], fontName=font_name, fontSize=9, leading=12 ) # Создаём стиль для ячеек с поддержкой переноса текста cell_style = ParagraphStyle( 'CellStyle', parent=base_style, fontSize=9, leading=12, # Межстрочный интервал alignment=0, # Выравнивание по левому краю wordWrap='CJK' # Включаем перенос слов ) title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontName=font_name, fontSize=24, textColor=colors.HexColor('#2c3e50'), spaceAfter=30, alignment=1 # Центрирование ) subtitle_style = ParagraphStyle( 'CustomSubtitle', parent=styles['Normal'], fontName=font_name, fontSize=12, textColor=colors.HexColor('#7f8c8d'), spaceAfter=20, alignment=1 ) header_style = ParagraphStyle( 'HeaderStyle', parent=styles['Normal'], fontName=font_name, fontSize=10, textColor=colors.whitesmoke, alignment=0 ) normal_style = ParagraphStyle( 'NormalStyle', parent=styles['Normal'], fontName=font_name, fontSize=10 ) # Собираем контент story = [] # Заголовок story.append(Paragraph("Менеджер задач - Отчёт", title_style)) # Дата и время генерации now = datetime.now() date_string = now.strftime("%d.%m.%Y %H:%M:%S") story.append(Paragraph(f"Дата создания отчёта: {date_string}", subtitle_style)) # Информация о фильтрах filter_info = [] if self.filter_mode == "active": filter_info.append("Только активные") elif self.filter_mode == "completed": filter_info.append("Только выполненные") else: filter_info.append("Все задачи") search_text = self.search_entry.get().strip() if search_text: filter_info.append(f"Поиск: \"{search_text}\"") story.append(Paragraph(f"Фильтры: {', '.join(filter_info)}", normal_style)) story.append(Spacer(1, 20)) # Статистика total_tasks = len(tasks_to_export) completed_count = sum(1 for t in tasks_to_export if t["completed"]) active_count = total_tasks - completed_count high_priority = sum(1 for t in tasks_to_export if t["priority"] == "high" and not t["completed"]) stats_text = f""" <b>Статистика:</b><br/> • Всего задач: {total_tasks}<br/> • Активных: {active_count}<br/> • Выполненных: {completed_count}<br/> • Активных с высоким приоритетом: {high_priority} """ story.append(Paragraph(stats_text, normal_style)) story.append(Spacer(1, 20)) # Подготовка данных для таблицы table_data = [] # Заголовки таблицы headers = ["ID", "Задача", "Статус", "Приоритет", "Дата создания"] header_cells = [] for h in headers: header_cells.append(Paragraph(f"<b>{h}</b>", header_style)) table_data.append(header_cells) # Добавляем задачи for task in tasks_to_export: # Форматируем статус status_text = "✓ Выполнена" if task["completed"] else "○ Активна" status_color = "#27ae60" if task["completed"] else "#e67e22" # Форматируем приоритет с цветом priority_map = { "high": ("🔴 Высокий", "#e74c3c"), "medium": ("🟠 Средний", "#f39c12"), "low": ("⚪ Низкий", "#95a5a6") } priority_display, priority_color = priority_map.get( task["priority"], (task["priority"], "#000000") ) # Форматируем дату try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%Y %H:%M") except: date_str = task["created_at"] # Создаём ячейки с Paragraph для автоматического переноса текста id_cell = Paragraph(str(task["id"]), cell_style) task_cell = Paragraph(task["text"], cell_style) status_cell = Paragraph( f'<font color="{status_color}">{status_text}</font>', cell_style ) priority_cell = Paragraph( f'<font color="{priority_color}">{priority_display}</font>', cell_style ) date_cell = Paragraph(date_str, cell_style) table_data.append([id_cell, task_cell, status_cell, priority_cell, date_cell]) # Настраиваем ширину колонок для альбомной ориентации page_width = landscape(A4)[0] - 100 # Вычитаем поля col_widths = [ page_width * 0.08, # ID - 8% page_width * 0.52, # Задача - 52% (основное пространство) page_width * 0.12, # Статус - 12% page_width * 0.13, # Приоритет - 13% page_width * 0.15 # Дата - 15% ] # Создаём таблицу table = Table( table_data, colWidths=col_widths, repeatRows=1, # Повторять заголовок на каждой странице rowHeights=None # Автоматическая высота строк ) # Стиль таблицы table.setStyle(TableStyle([ # Заголовок ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495e')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, 0), 'CENTER'), ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 8), ('TOPPADDING', (0, 0), (-1, 0), 8), # Общие настройки для всех ячеек ('VALIGN', (0, 1), (-1, -1), 'TOP'), # Выравнивание по верхнему краю ('LEFTPADDING', (0, 0), (-1, -1), 5), ('RIGHTPADDING', (0, 0), (-1, -1), 5), ('TOPPADDING', (0, 1), (-1, -1), 6), ('BOTTOMPADDING', (0, 1), (-1, -1), 6), # Сетка ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), # Выравнивание отдельных колонок ('ALIGN', (0, 0), (0, -1), 'CENTER'), # ID по центру ('ALIGN', (2, 0), (2, -1), 'CENTER'), # Статус по центру ('ALIGN', (3, 0), (3, -1), 'CENTER'), # Приоритет по центру ('ALIGN', (4, 0), (4, -1), 'CENTER'), # Дата по центру ])) # Чередование цветов строк for i in range(1, len(table_data)): if i % 2 == 0: table.setStyle(TableStyle([ ('BACKGROUND', (0, i), (-1, i), colors.HexColor('#f8f9fa')) ])) story.append(table) # Добавляем подвал с номерами страниц def add_page_number(canvas, doc): """Добавляет номера страниц""" page_num = canvas.getPageNumber() text = f"Страница {page_num}" canvas.saveState() canvas.setFont(font_name, 8) canvas.drawCentredString(doc.width / 2, 20, text) canvas.restoreState() # Добавляем итоговую информацию story.append(Spacer(1, 30)) footer_text = f""" <font size=8> <b>Примечания:</b><br/> • Отчёт сгенерирован программой "Менеджер задач"<br/> • Всего задач в отчёте: {total_tasks}<br/> • Дата генерации: {date_string}<br/> • Формат: PDF (Portable Document Format) </font> """ story.append(Paragraph(footer_text, normal_style)) # Создаём PDF с номерами страниц doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number) messagebox.showinfo("Успех", f"PDF-отчёт успешно создан с поддержкой русского языка:\n{filename}") except ImportError: messagebox.showerror( "Ошибка", "Библиотека reportlab не установлена.\nУстановите её командой: pip install reportlab" ) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать PDF:\n{str(e)}")
Финальный код

import json import os import csv import tkinter as tk from tkinter import ttk, messagebox, filedialog 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) 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("Менеджер задач Pro") self.root.geometry("700x600") self.root.resizable(False, False) self.tasks = load_tasks() self.filter_mode = "all" 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()) tk.Label(top_frame, text="Приоритет:").pack(side=tk.LEFT, padx=(5, 2)) self.priority_var = tk.StringVar(value="medium") priority_combo = ttk.Combobox( top_frame, textvariable=self.priority_var, values=["high", "medium", "low"], state="readonly", width=8 ) priority_combo.pack(side=tk.LEFT, padx=(0, 5)) 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) self.task_listbox.bind("<Double-Button-1>", lambda event: self.edit_task()) # Панель фильтров filter_frame = tk.Frame(self.root) filter_frame.pack(pady=5, padx=10, fill=tk.X) tk.Label(filter_frame, text="Фильтр:").pack(side=tk.LEFT, padx=(0, 10)) self.filter_var = tk.StringVar(value="all") tk.Radiobutton(filter_frame, text="Все", variable=self.filter_var, value="all", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) tk.Radiobutton(filter_frame, text="Активные", variable=self.filter_var, value="active", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) tk.Radiobutton(filter_frame, text="Выполненные", variable=self.filter_var, value="completed", command=self.refresh_task_list).pack(side=tk.LEFT, padx=5) # Панель поиска search_frame = tk.Frame(self.root) search_frame.pack(pady=(0, 5), padx=10, fill=tk.X) tk.Label(search_frame, text="Поиск:").pack(side=tk.LEFT, padx=(0, 5)) self.search_entry = tk.Entry(search_frame, font=("Arial", 10)) self.search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) self.search_entry.bind("<KeyRelease>", lambda event: self.refresh_task_list()) clear_btn = tk.Button(search_frame, text="✖", command=self.clear_search, width=3) clear_btn.pack(side=tk.RIGHT, padx=(5, 0)) # Нижняя панель: кнопки действий 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.export_csv_btn = tk.Button(bottom_frame, text="📎 Экспорт в CSV", command=self.export_to_csv, bg="light cyan") self.export_csv_btn.pack(side=tk.LEFT, padx=5) self.export_pdf_btn = tk.Button(bottom_frame, text="📄 Экспорт в PDF", command=self.export_to_pdf, bg="light pink") self.export_pdf_btn.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) def get_sorted_tasks(self): """Возвращает задачи с учётом фильтрации, поиска и сортировки""" # Фильтр по статусу if self.filter_mode == "active": filtered = [task for task in self.tasks if not task["completed"]] elif self.filter_mode == "completed": filtered = [task for task in self.tasks if task["completed"]] else: filtered = self.tasks.copy() # Поиск по тексту search_text = self.search_entry.get().strip().lower() if search_text: filtered = [task for task in filtered if search_text in task["text"].lower()] # Сортировка active = [task for task in filtered if not task["completed"]] completed = [task for task in filtered if task["completed"]] active.sort(key=lambda x: x["id"]) completed.sort(key=lambda x: x["id"]) return active + completed def refresh_task_list(self): """Обновляет отображение списка задач""" self.filter_mode = self.filter_var.get() 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" self.task_listbox.itemconfig(tk.END, bg=bg_color, fg=fg_color) def add_task(self): """Добавляет новую задачу""" text = self.entry.get().strip() if not text: messagebox.showwarning("Предупреждение", "Введите текст задачи") return next_id = get_next_id(self.tasks) new_task = { "id": next_id, "text": text, "completed": False, "created_at": datetime.now().isoformat(), "priority": self.priority_var.get() } self.tasks.append(new_task) save_tasks(self.tasks) self.entry.delete(0, tk.END) self.entry.focus() self.refresh_task_list() messagebox.showinfo("Успех", f"Задача добавлена (ID: {next_id})") def edit_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_to_edit = None for task in self.tasks: if task["id"] == task_id: task_to_edit = task 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() 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").pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="🟠 Средний", variable=priority_var, value="medium", fg="orange").pack(side=tk.LEFT, padx=10) tk.Radiobutton(priority_frame, text="⚪ Низкий", variable=priority_var, value="low").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).pack(side=tk.LEFT, padx=10) tk.Button(button_frame, text="Отмена", command=edit_window.destroy, bg="light gray", width=12).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} удалена") def clear_search(self): """Очищает поле поиска и обновляет список""" self.search_entry.delete(0, tk.END) self.refresh_task_list() def export_to_csv(self): """Экспортирует текущий список задач в CSV-файл""" tasks_to_export = self.get_sorted_tasks() if not tasks_to_export: messagebox.showinfo("Информация", "Нет задач для экспорта") return filename = filedialog.asksaveasfilename( defaultextension=".csv", filetypes=[("CSV files", "*.csv"), ("All files", "*.*")], title="Сохранить как CSV" ) if not filename: return try: with open(filename, 'w', encoding='utf-8-sig', newline='') as f: writer = csv.writer(f, delimiter=';') writer.writerow(['ID', 'Текст', 'Статус', 'Приоритет', 'Дата создания']) for task in tasks_to_export: status = "Выполнена" if task["completed"] else "Активна" try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%Y %H:%M:%S") except: date_str = task["created_at"] priority_map = {"high": "Высокий", "medium": "Средний", "low": "Низкий"} priority_display = priority_map.get(task["priority"], task["priority"]) writer.writerow([ task["id"], task["text"], status, priority_display, date_str ]) messagebox.showinfo("Успех", f"Задачи экспортированы в файл:\n{filename}") except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать задачи:\n{str(e)}") def export_to_pdf(self): """Экспортирует текущий список задач в PDF-файл с форматированием и переносом текста""" from tkinter import filedialog from reportlab.lib import colors from reportlab.lib.pagesizes import A4, landscape from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer # Получаем текущий отображаемый список tasks_to_export = self.get_sorted_tasks() if not tasks_to_export: messagebox.showinfo("Информация", "Нет задач для экспорта") return # Диалог выбора места сохранения filename = filedialog.asksaveasfilename( defaultextension=".pdf", filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")], title="Сохранить как PDF" ) if not filename: return try: # Используем альбомную ориентацию для лучшего отображения длинных текстов doc = SimpleDocTemplate(filename, pagesize=landscape(A4), rightMargin=50, leftMargin=50, topMargin=50, bottomMargin=50) # Стили styles = getSampleStyleSheet() # Создаём стиль для ячеек с поддержкой переноса текста cell_style = ParagraphStyle( 'CellStyle', parent=styles['Normal'], fontSize=9, leading=12, # Межстрочный интервал alignment=0, # Выравнивание по левому краю wordWrap='CJK' # Включаем перенос слов ) title_style = ParagraphStyle( 'CustomTitle', parent=styles['Heading1'], fontSize=24, textColor=colors.HexColor('#2c3e50'), spaceAfter=30, alignment=1 # Центрирование ) subtitle_style = ParagraphStyle( 'CustomSubtitle', parent=styles['Normal'], fontSize=12, textColor=colors.HexColor('#7f8c8d'), spaceAfter=20, alignment=1 ) header_style = ParagraphStyle( 'HeaderStyle', parent=styles['Normal'], fontSize=10, textColor=colors.whitesmoke, alignment=0, fontName='Helvetica-Bold' ) # Собираем контент story = [] # Заголовок story.append(Paragraph("Менеджер задач - Отчёт", title_style)) # Дата и время генерации now = datetime.now() date_string = now.strftime("%d.%m.%Y %H:%M:%S") story.append(Paragraph(f"Дата создания отчёта: {date_string}", subtitle_style)) # Информация о фильтрах filter_info = [] if self.filter_mode == "active": filter_info.append("Только активные") elif self.filter_mode == "completed": filter_info.append("Только выполненные") else: filter_info.append("Все задачи") search_text = self.search_entry.get().strip() if search_text: filter_info.append(f"Поиск: \"{search_text}\"") story.append(Paragraph(f"Фильтры: {', '.join(filter_info)}", styles['Normal'])) story.append(Spacer(1, 20)) # Статистика total_tasks = len(tasks_to_export) completed_count = sum(1 for t in tasks_to_export if t["completed"]) active_count = total_tasks - completed_count high_priority = sum(1 for t in tasks_to_export if t["priority"] == "high" and not t["completed"]) stats_text = f""" <b>Статистика:</b><br/> • Всего задач: {total_tasks}<br/> • Активных: {active_count}<br/> • Выполненных: {completed_count}<br/> • Активных с высоким приоритетом: {high_priority} """ story.append(Paragraph(stats_text, styles['Normal'])) story.append(Spacer(1, 20)) # Подготовка данных для таблицы table_data = [] # Заголовки таблицы headers = ["ID", "Задача", "Статус", "Приоритет", "Дата создания"] table_data.append([ Paragraph(f"<b>{h}</b>", header_style) for h in headers ]) # Добавляем задачи for task in tasks_to_export: # Форматируем статус status_text = "✓ Выполнена" if task["completed"] else "○ Активна" status_color = "#27ae60" if task["completed"] else "#e67e22" # Форматируем приоритет с цветом priority_map = { "high": ("🔴 Высокий", "#e74c3c"), "medium": ("🟠 Средний", "#f39c12"), "low": ("⚪ Низкий", "#95a5a6") } priority_display, priority_color = priority_map.get( task["priority"], (task["priority"], "#000000") ) # Форматируем дату try: created = datetime.fromisoformat(task["created_at"]) date_str = created.strftime("%d.%m.%Y %H:%M") except: date_str = task["created_at"] # Создаём ячейки с Paragraph для автоматического переноса текста id_cell = Paragraph(str(task["id"]), cell_style) task_cell = Paragraph(task["text"], cell_style) status_cell = Paragraph( f'<font color="{status_color}">{status_text}</font>', cell_style ) priority_cell = Paragraph( f'<font color="{priority_color}">{priority_display}</font>', cell_style ) date_cell = Paragraph(date_str, cell_style) table_data.append([id_cell, task_cell, status_cell, priority_cell, date_cell]) # Настраиваем ширину колонок для альбомной ориентации page_width = landscape(A4)[0] - 100 # Вычитаем поля col_widths = [ page_width * 0.08, # ID - 8% page_width * 0.52, # Задача - 52% (основное пространство) page_width * 0.12, # Статус - 12% page_width * 0.13, # Приоритет - 13% page_width * 0.15 # Дата - 15% ] # Создаём таблицу table = Table( table_data, colWidths=col_widths, repeatRows=1, # Повторять заголовок на каждой странице rowHeights=None # Автоматическая высота строк ) # Стиль таблицы table.setStyle(TableStyle([ # Заголовок ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495e')), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('ALIGN', (0, 0), (-1, 0), 'CENTER'), ('VALIGN', (0, 0), (-1, 0), 'MIDDLE'), ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), ('FONTSIZE', (0, 0), (-1, 0), 10), ('BOTTOMPADDING', (0, 0), (-1, 0), 8), ('TOPPADDING', (0, 0), (-1, 0), 8), # Общие настройки для всех ячеек ('VALIGN', (0, 1), (-1, -1), 'TOP'), # Выравнивание по верхнему краю ('LEFTPADDING', (0, 0), (-1, -1), 5), ('RIGHTPADDING', (0, 0), (-1, -1), 5), ('TOPPADDING', (0, 1), (-1, -1), 6), ('BOTTOMPADDING', (0, 1), (-1, -1), 6), # Сетка ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), # Выравнивание отдельных колонок ('ALIGN', (0, 0), (0, -1), 'CENTER'), # ID по центру ('ALIGN', (2, 0), (2, -1), 'CENTER'), # Статус по центру ('ALIGN', (3, 0), (3, -1), 'CENTER'), # Приоритет по центру ('ALIGN', (4, 0), (4, -1), 'CENTER'), # Дата по центру ])) # Чередование цветов строк for i in range(1, len(table_data)): if i % 2 == 0: table.setStyle(TableStyle([ ('BACKGROUND', (0, i), (-1, i), colors.HexColor('#f8f9fa')) ])) story.append(table) # Добавляем подвал с номерами страниц def add_page_number(canvas, doc): """Добавляет номера страниц""" page_num = canvas.getPageNumber() text = f"Страница {page_num}" canvas.saveState() canvas.setFont('Helvetica', 8) canvas.drawCentredString(doc.width / 2, 20, text) canvas.restoreState() # Добавляем итоговую информацию story.append(Spacer(1, 30)) footer_text = f""" <font size=8> <b>Примечания:</b><br/> • Отчёт сгенерирован программой "Менеджер задач"<br/> • Всего задач в отчёте: {total_tasks}<br/> • Дата генерации: {date_string}<br/> • Формат: PDF (Portable Document Format) </font> """ story.append(Paragraph(footer_text, styles['Normal'])) # Создаём PDF с номерами страниц doc.build(story, onFirstPage=add_page_number, onLaterPages=add_page_number) messagebox.showinfo("Успех", f"PDF-отчёт успешно создан с переносом текста:\n{filename}") except ImportError: messagebox.showerror( "Ошибка", "Библиотека reportlab не установлена.\nУстановите её командой: pip install reportlab" ) except Exception as e: messagebox.showerror("Ошибка", f"Не удалось экспортировать PDF:\n{str(e)}") # Запуск приложения if __name__ == "__main__": root = tk.Tk() app = TodoApp(root) root.mainloop()
Мы значительно расширили функциональность менеджера задач. Теперь приложение позволяет не только управлять списком дел, но и эффективно организовывать его с помощью гибкой системы фильтрации и поиска. Возможность экспорта данных в форматы CSV и PDF открывает новые возможности.
В следующие разы сделаем exe файл и попробуем переделать это приложение под современный вид с использованием PyQt.
