AnimeVideoEditot/gui/main_window.py
2026-03-14 19:52:44 +03:00

548 lines
29 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import threading
from queue import Queue, Empty
from gui.theme import setup_theme, COLORS
from gui.video_item import VideoItem
from core.video_processor import VideoProcessor, CancelledError
from core.logger import Logger
import sys
import subprocess
class MainWindow:
def __init__(self, root):
self.root = root
self.root.title("Anime Video Editor")
self.root.geometry("1000x780")
self.root.minsize(800, 600)
self.root.configure(bg=COLORS["bg"])
setup_theme(root)
self.video_items = []
self.logger = Logger()
self._processing = False
self._progress_queue = Queue()
self._progress_win = None
self._progress_text = None
self._progress_bar_var = None
self._progress_stage_var = None
self._progress_close_btn = None
self._progress_stop_btn = None
self._cancel_event = None
self._setup_menu()
self.setup_ui()
self._poll_progress()
def _setup_menu(self):
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Справка", menu=help_menu)
help_menu.add_command(label="Инструкция", command=self.show_instruction)
help_menu.add_separator()
help_menu.add_command(label="О программе", command=self.show_about)
def setup_ui(self):
main = ttk.Frame(self.root, padding=(16, 12))
main.grid(row=0, column=0, sticky="nsew")
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main.columnconfigure(0, weight=1)
main.rowconfigure(2, weight=1)
# Заголовок
header = ttk.Frame(main)
header.grid(row=0, column=0, sticky="ew", pady=(0, 12))
header.columnconfigure(0, weight=1)
ttk.Label(header, text="Anime Video Editor", style="Header.TLabel").grid(row=0, column=0, sticky="w")
ttk.Label(header, text="Объединение и нарезка видео", style="Subtext.TLabel").grid(row=1, column=0, sticky="w")
# Панель: добавить / очистить
toolbar = ttk.Frame(main)
toolbar.grid(row=1, column=0, sticky="ew", pady=(0, 8))
toolbar.columnconfigure(1, weight=1)
ttk.Button(toolbar, text=" Добавить файлы", command=self.add_video_files).grid(row=0, column=0, padx=(0, 8))
ttk.Button(toolbar, text="Очистить всё", command=self.clear_all).grid(row=0, column=1, padx=(0, 8), sticky="w")
ttk.Button(toolbar, text="❓ Справка", command=self.show_instruction).grid(row=0, column=2, sticky="w")
# Список видео (прокрутка)
list_card = ttk.LabelFrame(main, text=" Видео в проекте ", style="Card.TLabelframe", padding=(10, 8))
list_card.grid(row=2, column=0, sticky="nsew", pady=(0, 12))
list_card.columnconfigure(0, weight=1)
list_card.rowconfigure(0, weight=1)
canvas = tk.Canvas(
list_card,
bg=COLORS["surface"],
highlightthickness=0,
)
scrollbar = ttk.Scrollbar(list_card)
self.scrollable_frame = tk.Frame(canvas, bg=COLORS["surface"])
self.scrollable_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
def _on_mousewheel(event):
canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
canvas.bind("<Enter>", lambda e: canvas.bind_all("<MouseWheel>", _on_mousewheel))
canvas.bind("<Leave>", lambda e: canvas.unbind_all("<MouseWheel>"))
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Настройки вывода (расширенные)
out_card = ttk.LabelFrame(main, text=" Настройки вывода ", style="Card.TLabelframe", padding=(12, 10))
out_card.grid(row=3, column=0, sticky="ew", pady=(0, 12))
out_card.columnconfigure(0, weight=1)
row0 = ttk.Frame(out_card)
row0.grid(row=0, column=0, sticky="ew", pady=(0, 8))
row0.columnconfigure(1, weight=1)
ttk.Label(row0, text="Профиль").grid(row=0, column=0, padx=(0, 6), pady=2, sticky="w")
self.profile_var = tk.StringVar(value="custom")
self._profile_combo = ttk.Combobox(row0, textvariable=self.profile_var, width=18,
values=["Свои настройки", "YouTube", "VK Video"], state="readonly")
self._profile_combo.grid(row=0, column=1, padx=(0, 16), pady=2, sticky="w")
self._profile_combo.bind("<<ComboboxSelected>>", self._on_profile_changed)
row1 = ttk.Frame(out_card)
row1.grid(row=1, column=0, sticky="ew", pady=(0, 4))
row1.columnconfigure(1, weight=1)
ttk.Label(row1, text="Формат").grid(row=0, column=0, padx=(0, 6), pady=2, sticky="w")
self.format_var = tk.StringVar(value="mp4")
fmt_combo = ttk.Combobox(row1, textvariable=self.format_var, width=8,
values=["mp4", "avi", "mov", "mkv"], state="readonly")
fmt_combo.grid(row=0, column=1, padx=(0, 16), pady=2, sticky="w")
ttk.Label(row1, text="Качество").grid(row=0, column=2, padx=(0, 6), pady=2, sticky="w")
self.quality_var = tk.StringVar(value="high")
ttk.Combobox(row1, textvariable=self.quality_var, width=10,
values=["low", "medium", "high"], state="readonly").grid(row=0, column=3, padx=(0, 16), pady=2, sticky="w")
ttk.Label(row1, text="FPS").grid(row=0, column=4, padx=(0, 6), pady=2, sticky="w")
self.fps_var = tk.StringVar(value="Исходный")
ttk.Combobox(row1, textvariable=self.fps_var, width=10,
values=["Исходный", "24", "25", "30", "60"], state="readonly").grid(row=0, column=5, padx=(0, 16), pady=2, sticky="w")
ttk.Label(row1, text="Разрешение").grid(row=0, column=6, padx=(0, 6), pady=2, sticky="w")
self.resolution_var = tk.StringVar(value="Исходное")
ttk.Combobox(row1, textvariable=self.resolution_var, width=12,
values=["Исходное", "720p", "1080p"], state="readonly").grid(row=0, column=7, padx=(0, 16), pady=2, sticky="w")
row2 = ttk.Frame(out_card)
row2.grid(row=2, column=0, sticky="ew", pady=(10, 0))
row2.columnconfigure(1, weight=1)
ttk.Label(row2, text="Кодек (preset)").grid(row=0, column=0, padx=(0, 6), pady=2, sticky="w")
self.preset_var = tk.StringVar(value="medium")
ttk.Combobox(row2, textvariable=self.preset_var, width=12,
values=["ultrafast", "superfast", "veryfast", "faster", "fast", "medium", "slow", "slower", "veryslow"], state="readonly").grid(row=0, column=1, padx=(0, 16), pady=2, sticky="w")
ttk.Label(row2, text="Аудио битрейт").grid(row=0, column=2, padx=(0, 6), pady=2, sticky="w")
self.audio_bitrate_var = tk.StringVar(value="192k")
self._audio_combo = ttk.Combobox(row2, textvariable=self.audio_bitrate_var, width=8,
values=["128k", "192k", "256k", "320k", "384k"], state="readonly")
self._audio_combo.grid(row=0, column=3, padx=(0, 16), pady=2, sticky="w")
ttk.Label(row2, text="Имя файла").grid(row=0, column=4, padx=(0, 6), pady=2, sticky="w")
self.filename_prefix_var = tk.StringVar(value="edited_video")
ttk.Entry(row2, textvariable=self.filename_prefix_var, width=18).grid(row=0, column=5, padx=(0, 16), pady=2, sticky="w")
row3 = ttk.Frame(out_card)
row3.grid(row=3, column=0, sticky="ew", pady=(10, 0))
row3.columnconfigure(1, weight=1)
ttk.Button(row3, text="📁 Папка для сохранения", command=self.select_output_dir).grid(row=0, column=0, padx=(0, 12), pady=4)
self.output_dir_var = tk.StringVar(value=os.getcwd())
dir_label = ttk.Label(row3, textvariable=self.output_dir_var, style="Subtext.TLabel")
dir_label.grid(row=0, column=1, sticky="w", pady=4)
dir_label.configure(background=COLORS["surface"])
# Чекбоксы: открыть папку, закрыть окно выполнения, открыть лог
row4 = ttk.Frame(out_card)
row4.grid(row=4, column=0, sticky="ew", pady=(10, 0))
self.open_folder_var = tk.BooleanVar(value=True)
ttk.Checkbutton(row4, text="Открыть папку с итоговым файлом после завершения",
variable=self.open_folder_var).grid(row=0, column=0, sticky="w", padx=(0, 20))
self.close_progress_win_var = tk.BooleanVar(value=False)
ttk.Checkbutton(row4, text="Закрыть окно выполнения после успеха",
variable=self.close_progress_win_var).grid(row=0, column=1, sticky="w", padx=(0, 20))
self.open_log_var = tk.BooleanVar(value=False)
ttk.Checkbutton(row4, text="Открыть папку с логом после завершения",
variable=self.open_log_var).grid(row=0, column=2, sticky="w")
# Кнопка выполнения и прогресс
action_frame = ttk.Frame(main)
action_frame.grid(row=4, column=0, sticky="ew", pady=(0, 8))
action_frame.columnconfigure(0, weight=1)
self.run_button = ttk.Button(action_frame, text="▶ Выполнить", style="Accent.TButton", command=self.process_videos)
self.run_button.grid(row=0, column=0, pady=(0, 8))
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(action_frame, variable=self.progress_var, maximum=100)
self.progress_bar.grid(row=1, column=0, sticky="ew", pady=(0, 4))
action_frame.columnconfigure(0, weight=1)
self.status_var = tk.StringVar(value="Готов к работе")
ttk.Label(action_frame, textvariable=self.status_var, style="Subtext.TLabel").grid(row=2, column=0, sticky="w")
def add_video_files(self):
files = filedialog.askopenfilenames(
title="Выберите видео файлы",
filetypes=[
("Видео", "*.mp4 *.avi *.mov *.mkv *.wmv *.flv *.webm"),
("Все файлы", "*.*")
]
)
for path in files:
item = VideoItem(self.scrollable_frame, path, on_remove=lambda i: self.video_items.remove(i))
item.pack(fill=tk.X, padx=4, pady=4)
self.video_items.append(item)
self.update_status(f"Добавлено файлов: {len(files)}")
def clear_all(self):
for item in self.video_items:
item.destroy()
self.video_items.clear()
self.update_status("Список очищен")
def select_output_dir(self):
d = filedialog.askdirectory(title="Папка для сохранения")
if d:
self.output_dir_var.set(d)
# Рекомендуемые настройки для платформ (YouTube, VK Video)
OUTPUT_PRESETS = {
"YouTube": {
"format": "mp4",
"quality": "high",
"fps": "30",
"resolution": "1080p",
"preset": "medium",
"audio_bitrate": "384k",
"filename_prefix": "youtube",
},
"VK Video": {
"format": "mp4",
"quality": "high",
"fps": "30",
"resolution": "1080p",
"preset": "medium",
"audio_bitrate": "256k",
"filename_prefix": "vk_video",
},
}
def _on_profile_changed(self, event=None):
profile = self.profile_var.get()
if profile == "Свои настройки" or profile not in self.OUTPUT_PRESETS:
return
preset = self.OUTPUT_PRESETS.get(profile)
if not preset:
return
self.format_var.set(preset.get("format", self.format_var.get()))
self.quality_var.set(preset.get("quality", self.quality_var.get()))
self.fps_var.set(preset.get("fps", self.fps_var.get()))
self.resolution_var.set(preset.get("resolution", self.resolution_var.get()))
self.preset_var.set(preset.get("preset", self.preset_var.get()))
self.audio_bitrate_var.set(preset.get("audio_bitrate", self.audio_bitrate_var.get()))
self.filename_prefix_var.set(preset.get("filename_prefix", self.filename_prefix_var.get()))
self.update_status(f"Применён профиль: {profile}")
def update_status(self, msg):
self.status_var.set(msg)
self.root.update_idletasks()
def _open_progress_window(self):
"""Открывает окно процесса выполнения (лог + прогресс)."""
if self._progress_win is not None and self._progress_win.winfo_exists():
self._progress_win.destroy()
win = tk.Toplevel(self.root)
win.title("Выполнение")
win.geometry("480x320")
win.minsize(360, 240)
win.configure(bg=COLORS["bg"])
win.transient(self.root)
self._progress_win = win
fr = tk.Frame(win, bg=COLORS["bg"], padx=12, pady=10)
fr.pack(fill=tk.BOTH, expand=True)
tk.Label(fr, text="Процесс выполнения", font=("Tahoma", 11, "bold"),
fg=COLORS["accent"], bg=COLORS["bg"]).pack(anchor=tk.W)
self._progress_stage_var = tk.StringVar(value="Этап 1/3: Ожидание...")
tk.Label(fr, textvariable=self._progress_stage_var, font=("Tahoma", 9),
fg=COLORS["text"], bg=COLORS["bg"]).pack(anchor=tk.W, pady=(2, 4))
self._progress_bar_var = tk.DoubleVar(value=0)
pbar = ttk.Progressbar(fr, variable=self._progress_bar_var, maximum=100)
pbar.pack(fill=tk.X, pady=(0, 8))
log_label = tk.Label(fr, text="Лог:", font=("Tahoma", 9), fg=COLORS["subtext"], bg=COLORS["bg"])
log_label.pack(anchor=tk.W)
text_fr = tk.Frame(fr, bg=COLORS["surface"])
text_fr.pack(fill=tk.BOTH, expand=True, pady=(4, 8))
self._progress_text = tk.Text(text_fr, wrap=tk.WORD, font=("Consolas", 9), height=10,
bg=COLORS["surface"], fg=COLORS["text"],
insertbackground=COLORS["text"], relief=tk.FLAT, padx=8, pady=6)
scroll = ttk.Scrollbar(text_fr)
self._progress_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
self._progress_text.configure(yscrollcommand=scroll.set)
scroll.configure(command=self._progress_text.yview)
self._progress_text.insert(tk.END, "Старт обработки...\n")
self._progress_text.config(state=tk.DISABLED)
btn_fr = tk.Frame(fr, bg=COLORS["bg"])
btn_fr.pack(fill=tk.X)
self._progress_stop_btn = ttk.Button(btn_fr, text="⏹ Остановить", command=self._request_stop)
self._progress_stop_btn.pack(side=tk.LEFT, padx=(0, 12))
self._progress_close_btn = ttk.Button(btn_fr, text="Закрыть", state="disabled",
command=self._close_progress_window)
self._progress_close_btn.pack(side=tk.RIGHT)
win.protocol("WM_DELETE_WINDOW", lambda: None) # запрет закрытия крестиком до конца
def _append_progress_log(self, msg):
if self._progress_text is None:
return
try:
self._progress_text.config(state=tk.NORMAL)
self._progress_text.insert(tk.END, msg + "\n")
self._progress_text.see(tk.END)
self._progress_text.config(state=tk.DISABLED)
except Exception:
pass
def _request_stop(self):
"""Запрос остановки выполнения (устанавливает флаг для worker)."""
if self._cancel_event is not None:
self._cancel_event.set()
if self._progress_stop_btn is not None:
self._progress_stop_btn.config(state="disabled", text="Остановка...")
def _open_folder(self, path):
"""Открыть папку в проводнике / файловом менеджере ОС."""
folder = os.path.dirname(os.path.abspath(path))
if not os.path.isdir(folder):
return
if sys.platform == "win32":
os.startfile(folder)
elif sys.platform == "darwin":
subprocess.run(["open", folder], check=False)
else:
subprocess.run(["xdg-open", folder], check=False)
def _close_progress_window(self):
if self._progress_win is not None:
try:
self._progress_win.destroy()
except Exception:
pass
self._progress_win = None
self._progress_text = None
self._progress_bar_var = None
self._progress_stage_var = None
self._progress_close_btn = None
self._progress_stop_btn = None
INSTRUCTION_TEXT = """
Anime Video Editor — объединение нескольких видео в один файл с возможностью вырезать ненужные фрагменты.
══════════════════════════════════════════════════════════
1. ДОБАВЛЕНИЕ ВИДЕО
══════════════════════════════════════════════════════════
• Нажмите «➕ Добавить файлы» и выберите один или несколько видео (MP4, AVI, MOV, MKV и др.).
• Файлы появятся в списке «Видео в проекте». Порядок в списке = порядок в итоговом ролике.
• Удалить файл из проекта: кнопка «✕» справа от имени файла.
• «Очистить всё» — удаляет все файлы из списка.
══════════════════════════════════════════════════════════
2. ЧТО ВЫРЕЗАТЬ ИЗ КАЖДОГО ВИДЕО
══════════════════════════════════════════════════════════
У каждого файла есть блок «Удалить из видео (исключить эти фрагменты)».
• Укажите диапазоны времени, которые нужно ВЫРЕЗАТЬ (удалить). Всё остальное попадёт в итоговое видео.
• Формат времени: ЧЧ:ММ:СС или ММ:СС или только секунды (например: 00:01:30, 1:30, 90).
• Кнопка «+ Добавить диапазон на удаление» — добавить ещё один фрагмент на вырезку.
• Если не добавлять ни одного диапазона — в результат попадёт всё видео целиком.
• Пример: ролик 1 минута, удалить с 0:10 по 0:20 и с 0:40 по 0:50 → в итоге будут куски 0:000:10, 0:200:40, 0:501:00, склеенные подряд.
══════════════════════════════════════════════════════════
3. НАСТРОЙКИ ВЫВОДА
══════════════════════════════════════════════════════════
• Профиль: «Свои настройки», «YouTube» или «VK Video» — подставляет рекомендуемые параметры для загрузки на эти платформы (MP4, 1080p, битрейт и т.д.). После выбора профиля параметры можно менять вручную.
• Формат: MP4, AVI, MOV, MKV.
• Качество: low / medium / high (влияет на битрейт).
• FPS: Исходный или 24, 25, 30, 60.
• Разрешение: Исходное, 720p или 1080p (масштабирование по высоте).
• Кодек (preset): от ultrafast (быстро) до veryslow (медленнее, меньше размер).
• Аудио битрейт: 128k320k.
• Имя файла: префикс для итогового файла (к нему добавится дата и время).
• «📁 Папка для сохранения» — куда сохранить результат.
══════════════════════════════════════════════════════════
4. ЗАПУСК ОБРАБОТКИ
══════════════════════════════════════════════════════════
• Нажмите «▶ Выполнить».
• Обработка идёт в фоне — окно остаётся отзывчивым, можно смотреть прогресс.
• После завершения появится сообщение с путём к файлу. Рядом сохраняется лог (_log.txt).
• При большом количестве файлов (больше 3) включается режим с меньшим потреблением памяти.
"""
def show_instruction(self):
win = tk.Toplevel(self.root)
win.title("Инструкция — Anime Video Editor")
win.geometry("560x520")
win.minsize(400, 300)
win.configure(bg=COLORS["bg"])
win.transient(self.root)
win.grab_set()
fr = tk.Frame(win, bg=COLORS["bg"], padx=12, pady=12)
fr.pack(fill=tk.BOTH, expand=True)
text = tk.Text(fr, wrap=tk.WORD, font=("Tahoma", 10), bg=COLORS["surface"], fg=COLORS["text"],
insertbackground=COLORS["text"], relief=tk.FLAT, padx=10, pady=10)
scroll = ttk.Scrollbar(fr)
text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scroll.pack(side=tk.RIGHT, fill=tk.Y)
text.configure(yscrollcommand=scroll.set)
scroll.configure(command=text.yview)
text.insert(tk.END, self.INSTRUCTION_TEXT.strip())
text.config(state=tk.DISABLED)
ttk.Button(win, text="Закрыть", command=win.destroy).pack(pady=(0, 10))
def show_about(self):
messagebox.showinfo(
"О программе",
"Anime Video Editor ver. 0.0.8\n\n"
"Объединение и нарезка видео: несколько файлов в один с вырезкой указанных фрагментов.\n\n"
"Обработка в фоне, поддержка больших и множества файлов."
"Автор - stirelshka8"
"Страница проекта - https://git.tuxops.ru/stirelshka8/AnimeVideoEditot"
)
def _poll_progress(self):
"""Обработка очереди прогресса из фонового потока (только в main thread)."""
try:
while True:
msg = self._progress_queue.get_nowait()
if msg == "done":
continue
if isinstance(msg, tuple):
if len(msg) == 3 and msg[0] == "result":
_, success, payload = msg
self._on_processing_done(success, payload)
elif len(msg) == 3 and isinstance(msg[0], int):
stage, p, text = msg
self.progress_var.set(p * 100)
self.status_var.set(text)
if self._progress_bar_var is not None:
self._progress_bar_var.set(p * 100)
if self._progress_stage_var is not None:
self._progress_stage_var.set(f"Этап {stage}/3: {text}")
self._append_progress_log(text)
except Empty:
pass
if self.root.winfo_exists():
self.root.after(200, self._poll_progress)
def _on_processing_done(self, success, payload):
self._processing = False
self._cancel_event = None
self.run_button.config(state="normal", text="▶ Выполнить")
self.progress_var.set(100 if success else 0)
if success:
output_path = payload
fmt = self.format_var.get()
log_path = output_path.replace(f".{fmt}", "_log.txt")
self.logger.save_log(log_path)
self.status_var.set(f"Готово: {output_path}")
self._append_progress_log("Готово. Файл: " + output_path)
messagebox.showinfo("Успех", f"Обработка завершена.\nФайл: {output_path}")
if self.open_folder_var.get() or self.open_log_var.get():
self._open_folder(output_path)
if self.close_progress_win_var.get():
self._close_progress_window()
else:
err = payload
self.status_var.set(err)
if err == "Отменено пользователем":
self.logger.log("INFO", err)
self._append_progress_log(err)
messagebox.showinfo("Остановлено", "Выполнение остановлено пользователем.")
else:
self.logger.log("ERROR", err)
self._append_progress_log("Ошибка: " + err)
messagebox.showerror("Ошибка", err)
if self._progress_bar_var is not None:
self._progress_bar_var.set(100 if success else 0)
if self._progress_win is not None and self._progress_win.winfo_exists():
self._progress_win.protocol("WM_DELETE_WINDOW", self._close_progress_window)
self._progress_win.title("Выполнение завершено")
if self._progress_close_btn is not None:
self._progress_close_btn.config(state="normal")
if self._progress_stop_btn is not None:
self._progress_stop_btn.config(state="disabled", text="⏹ Остановить")
self.root.after(0, lambda: self.progress_var.set(0))
def process_videos(self):
if self._processing:
return
if not self.video_items:
messagebox.showerror("Ошибка", "Добавьте хотя бы один видео файл")
return
video_data = []
for item in self.video_items:
video_data.append({"path": item.file_path, "time_ranges": item.get_time_ranges()})
fps_val = self.fps_var.get()
fps = None if fps_val == "Исходный" else int(fps_val)
res_val = self.resolution_var.get()
target_height = 720 if res_val == "720p" else (1080 if res_val == "1080p" else None)
output_dir = self.output_dir_var.get()
output_format = self.format_var.get()
quality = self.quality_var.get()
preset = self.preset_var.get()
audio_bitrate = self.audio_bitrate_var.get()
filename_prefix = (self.filename_prefix_var.get() or "edited_video").strip()
progress_queue = self._progress_queue
logger = self.logger
cancel_event = threading.Event()
self._cancel_event = cancel_event
def worker():
try:
processor = VideoProcessor(logger=logger)
def on_progress(stage, progress, message):
progress_queue.put((stage, progress, message))
output_path = processor.process_videos(
video_data=video_data,
output_dir=output_dir,
output_format=output_format,
quality=quality,
progress_callback=on_progress,
fps=fps,
preset=preset,
audio_bitrate=audio_bitrate,
target_height=target_height,
filename_prefix=filename_prefix,
cancel_check=cancel_event.is_set,
)
progress_queue.put(("result", True, output_path))
except CancelledError:
progress_queue.put(("result", False, "Отменено пользователем"))
except Exception as e:
progress_queue.put(("result", False, str(e)))
finally:
progress_queue.put("done")
self._processing = True
self.run_button.config(state="disabled", text="Работает...")
self.status_var.set("Обработка...")
self.progress_var.set(0)
self._open_progress_window()
t = threading.Thread(target=worker, daemon=True)
t.start()