/
O.S.Prog
/
DiScan
Обзор
Документация
Войти
/
O.S.Prog
/
DiScan
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
gui/help_window.py
430 строк
23 KB
O.S.Prog
Initial commit: DiScan v1.1.0
18 июл 2026, 15:51
18 июл 2026, 15:51
238a6ec
Код
Авторство
О чём код?
# gui/help_window.py """ Окно подробной инструкции с гипертекстом и оглавлением """ import tkinter as tk from tkinter import ttk class HelpWindow: """Окно инструкции с гипертекстом и оглавлением""" def __init__(self, parent): self.parent = parent # Инициализируем переменные self.content_canvas = None self.content_inner = None self.toc_canvas = None self.toc_inner = None self.toc_items = [] # Создаём окно self.window = tk.Toplevel(parent) self.window.title("📖 Инструкция — DiSc DICOM Scanner") self.window.geometry("950x700") self.window.minsize(800, 500) self.window.transient(parent) self.window.grab_set() # Центрируем self.window.update_idletasks() x = (self.window.winfo_screenwidth() // 2) - (950 // 2) y = (self.window.winfo_screenheight() // 2) - (700 // 2) self.window.geometry(f"+{x}+{y}") # Настройка стилей style = ttk.Style() style.configure('Title.TLabel', font=('Segoe UI', 16, 'bold'), foreground='#0a2463') style.configure('Heading1.TLabel', font=('Segoe UI', 13, 'bold'), foreground='#0a2463') style.configure('Heading2.TLabel', font=('Segoe UI', 11, 'bold'), foreground='#1e293b') style.configure('Body.TLabel', font=('Segoe UI', 10), wraplength=600, justify=tk.LEFT) style.configure('Code.TLabel', font=('Consolas', 9), background='#f1f5f9') style.configure('Hotkey.TLabel', font=('Consolas', 9, 'bold'), foreground='#0a2463') style.configure('Note.TLabel', font=('Segoe UI', 10), foreground='#475569') self._setup_ui() self._load_content() def _setup_ui(self): """Настройка интерфейса""" # Основной фрейм main_frame = ttk.Frame(self.window, padding="10") main_frame.pack(fill=tk.BOTH, expand=True) # Панель с заголовком и кнопкой закрытия header_frame = ttk.Frame(main_frame) header_frame.pack(fill=tk.X, pady=(0, 10)) ttk.Label( header_frame, text="🧭 DiSc — DICOM Scanner", style='Title.TLabel' ).pack(side=tk.LEFT) ttk.Button( header_frame, text="✖ Закрыть", command=self.window.destroy, width=15 ).pack(side=tk.RIGHT) # Разделитель ttk.Separator(main_frame, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=5) # Основной контейнер: оглавление слева, содержимое справа content_frame = ttk.Frame(main_frame) content_frame.pack(fill=tk.BOTH, expand=True, pady=10) # Оглавление (слева) toc_frame = ttk.Frame(content_frame, width=220) toc_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10)) toc_frame.pack_propagate(False) ttk.Label( toc_frame, text="📋 Оглавление", font=('Segoe UI', 11, 'bold'), foreground='#0a2463' ).pack(anchor=tk.W, pady=(0, 10)) # Canvas для прокрутки оглавления self.toc_canvas = tk.Canvas(toc_frame, highlightthickness=0, bg='#f8fafc') toc_scrollbar = ttk.Scrollbar(toc_frame, orient=tk.VERTICAL, command=self.toc_canvas.yview) self.toc_inner = ttk.Frame(self.toc_canvas) self.toc_inner.bind( "<Configure>", lambda e: self.toc_canvas.configure(scrollregion=self.toc_canvas.bbox("all")) ) self.toc_canvas.create_window((0, 0), window=self.toc_inner, anchor="nw") self.toc_canvas.configure(yscrollcommand=toc_scrollbar.set) self.toc_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) toc_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # Содержимое (справа) content_container = ttk.Frame(content_frame) content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) # Canvas для прокрутки содержимого self.content_canvas = tk.Canvas(content_container, highlightthickness=0) content_scrollbar = ttk.Scrollbar(content_container, orient=tk.VERTICAL, command=self.content_canvas.yview) self.content_inner = ttk.Frame(self.content_canvas) self.content_inner.bind( "<Configure>", lambda e: self.content_canvas.configure(scrollregion=self.content_canvas.bbox("all")) ) self.content_canvas.create_window((0, 0), window=self.content_inner, anchor="nw") self.content_canvas.configure(yscrollcommand=content_scrollbar.set) self.content_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) content_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) # ===== ПРИВЯЗКА КОЛЕСИКА МЫШИ ===== # К основным canvas self._bind_mousewheel(self.content_canvas) self._bind_mousewheel(self.toc_canvas) # К внутренним фреймам self._bind_mousewheel(self.content_inner) self._bind_mousewheel(self.toc_inner) # К самому окну (чтобы работало при наведении на пустое место) self.window.bind("<MouseWheel>", self._on_mousewheel) def _bind_mousewheel(self, widget): """Привязывает обработчик колесика мыши к виджету""" if not widget: return # Windows widget.bind("<MouseWheel>", self._on_mousewheel) # Linux (X11) widget.bind("<Button-4>", self._on_mousewheel_linux) widget.bind("<Button-5>", self._on_mousewheel_linux) def _on_mousewheel(self, event): """Прокрутка содержимого (Windows, macOS)""" if not self.content_canvas: return try: delta = event.delta if delta > 0: self.content_canvas.yview_scroll(-1, "units") else: self.content_canvas.yview_scroll(1, "units") except: pass def _on_mousewheel_linux(self, event): """Прокрутка содержимого (Linux)""" if not self.content_canvas: return if event.num == 4: self.content_canvas.yview_scroll(-1, "units") elif event.num == 5: self.content_canvas.yview_scroll(1, "units") def _add_toc_item(self, parent, text, target, level=0): """Добавляет пункт оглавления""" padding = 20 + level * 15 label = ttk.Label( parent, text=text, font=('Segoe UI', 9 + (1 if level == 0 else 0), 'bold' if level == 0 else 'normal'), foreground='#0a2463' if level == 0 else '#1e293b', cursor='hand2' ) label.pack(anchor=tk.W, padx=padding, pady=2) label.bind("<Button-1>", lambda e, t=target: self._scroll_to(t)) return label def _scroll_to(self, target_widget): """Прокручивает содержимое к указанному виджету""" if not self.content_canvas or not self.content_inner: return bbox = self.content_canvas.bbox(target_widget) if bbox: self.content_canvas.yview_moveto(bbox[1] / self.content_inner.winfo_height()) def _add_section(self, parent, title, icon="📌", level=1): """Добавляет секцию и возвращает её для добавления в оглавление""" frame = ttk.Frame(parent) frame.pack(anchor=tk.W, fill=tk.X, pady=(15 if level == 1 else 10, 5)) label = ttk.Label( frame, text=f"{icon} {title}", style='Heading1.TLabel' if level == 1 else 'Heading2.TLabel' ) label.pack(anchor=tk.W) return frame def _add_text(self, parent, text, style='Body.TLabel', padx=20): """Добавляет текст""" ttk.Label(parent, text=text, style=style).pack(anchor=tk.W, padx=padx, pady=2) def _add_bullet(self, parent, text, padx=30): """Добавляет пункт списка""" ttk.Label(parent, text=f"• {text}", style='Body.TLabel').pack(anchor=tk.W, padx=padx, pady=1) def _add_hotkey(self, parent, key, desc): """Добавляет строку с горячей клавишей""" frame = ttk.Frame(parent) frame.pack(anchor=tk.W, padx=30, pady=1) ttk.Label(frame, text=key, style='Hotkey.TLabel', width=18).pack(side=tk.LEFT) ttk.Label(frame, text=f"— {desc}", style='Body.TLabel').pack(side=tk.LEFT) def _add_note(self, parent, text, icon="💡"): """Добавляет заметку""" frame = ttk.Frame(parent) frame.pack(anchor=tk.W, fill=tk.X, padx=20, pady=8) ttk.Label(frame, text=f"{icon} {text}", style='Note.TLabel', wraplength=600).pack(anchor=tk.W) def _add_separator(self, parent): """Добавляет разделитель""" ttk.Separator(parent, orient=tk.HORIZONTAL).pack(fill=tk.X, pady=10) def _load_content(self): """Загружает содержимое инструкции""" # ===== ОГЛАВЛЕНИЕ ===== toc_items = [] # ===== СОДЕРЖИМОЕ ===== content = self.content_inner # ------ 1. ВВЕДЕНИЕ ------ section = self._add_section(content, "Введение", "🧭", level=1) toc_items.append(("Введение", section)) self._add_text(content, "DiSc — это портальная программа для быстрого поиска пациентов в DICOM-архивах.") self._add_text(content, "Она не требует установки, не пишет в реестр и работает с любой структурой папок.") self._add_note(content, "После первого сканирования результаты сохраняются в кэш — повторное открытие папки занимает секунды.") # ------ 2. БЫСТРЫЙ СТАРТ ------ section = self._add_section(content, "Быстрый старт", "🚀", level=1) toc_items.append(("Быстрый старт", section)) steps = [ "Запустите программу (DICOM_Explorer.exe)", "Нажмите «Обзор» и выберите папку с DICOM-файлами", "Нажмите «Сканировать» или Ctrl+S", "Дождитесь завершения — появится список пациентов" ] for step in steps: self._add_bullet(content, step) # ------ 3. ОСНОВНЫЕ ФУНКЦИИ ------ section = self._add_section(content, "Основные функции", "⚡", level=1) toc_items.append(("Основные функции", section)) functions = [ ("🔍 Сканирование", "Обходит все подпапки, находит DICOM-файлы и определяет ФИО пациента"), ("💾 Кэширование", "Сохраняет результаты в папке .dicom_cache рядом с данными"), ("📤 Экспорт пациента", "Создаёт папку с именем ФАМИЛИЯ_И.О._ДАТА и копирует все файлы"), ("📦 Пакетный экспорт", "Экспортирует всех пациентов сразу в отдельные папки"), ("🔤 Кодировка", "Поддержка кириллицы: cp1251, Mac Cyrillic, KOI8-R, UTF-8 и другие"), ("🔍 Поиск", "Фильтр по ФИО или ID пациента в реальном времени"), ("📋 Сортировка", "Клик по заголовку столбца для сортировки в обе стороны"), ] for func, desc in functions: self._add_bullet(content, f"{func}: {desc}") # ------ 4. ГОРЯЧИЕ КЛАВИШИ ------ section = self._add_section(content, "Горячие клавиши", "⌨️", level=1) toc_items.append(("Горячие клавиши", section)) hotkeys = [ ("Ctrl+O", "Открыть папку"), ("Ctrl+S", "Сканировать"), ("Ctrl+E", "Экспорт выбранного пациента"), ("Ctrl+Shift+E", "Пакетный экспорт всех пациентов"), ("Ctrl+C", "Копировать путь к папке пациента"), ("Ctrl+Shift+C", "Копировать ФИО пациента"), ("Enter", "Открыть папку пациента в проводнике"), ("Esc", "Очистить поле поиска"), ("F5", "Обновить таблицу"), ("F1", "Открыть эту инструкцию"), ] for key, desc in hotkeys: self._add_hotkey(content, key, desc) # ------ 5. РАБОТА С ТАБЛИЦЕЙ ------ section = self._add_section(content, "Работа с таблицей", "📊", level=1) toc_items.append(("Работа с таблицей", section)) self._add_text(content, "Таблица отображает найденных пациентов с колонками:") self._add_bullet(content, "ФИО пациента — имя из DICOM-файла") self._add_bullet(content, "ID пациента — идентификатор из DICOM") self._add_bullet(content, "Дата — дата исследования") self._add_bullet(content, "Снимков — количество снимков (автоопределение)") self._add_bullet(content, "Файлов — общее количество DICOM-файлов") self._add_text(content, "\nДействия с таблицей:") self._add_bullet(content, "Клик по заголовку — сортировка по столбцу (повторный клик меняет направление)") self._add_bullet(content, "ПКМ по строке — контекстное меню с действиями") self._add_bullet(content, "Двойной клик — открыть папку пациента в проводнике") # ------ 6. ЭКСПОРТ ------ section = self._add_section(content, "Экспорт пациента", "📤", level=1) toc_items.append(("Экспорт пациента", section)) self._add_text(content, "Одиночный экспорт:") steps = [ "Выделите пациента в таблице (клик по строке)", "Нажмите Ctrl+E или выберите в меню «Действия» → «Экспорт выбранного пациента»", "Выберите папку назначения", "Будет создана папка: ФАМИЛИЯ_И.О._ДАТА (например: ИВАНОВ_И.И._20260201)" ] for step in steps: self._add_bullet(content, step) self._add_note(content, "Если папка с таким именем уже существует, программа предложит: перезаписать, отменить или создать копию с суффиксом _1, _2...") # ------ 7. ПАКЕТНЫЙ ЭКСПОРТ ------ section = self._add_section(content, "Пакетный экспорт", "📦", level=1) toc_items.append(("Пакетный экспорт", section)) self._add_text(content, "Экспортирует всех пациентов из текущего списка в отдельные папки.") steps = [ "Отсканируйте папку с пациентами", "Нажмите Ctrl+Shift+E или выберите в меню «Действия» → «Пакетный экспорт всех пациентов»", "Выберите папку назначения", "Для каждого пациента будет создана отдельная папка с именем ФАМИЛИЯ_И.О._ДАТА" ] for step in steps: self._add_bullet(content, step) self._add_note(content, "Перед началом пакетного экспорта программа оценит общий объём данных и предупредит, если он слишком большой.") # ------ 8. УПРАВЛЕНИЕ ЭКСПОРТОМ ------ section = self._add_section(content, "Управление экспортом", "⏹️", level=1) toc_items.append(("Управление экспортом", section)) self._add_text(content, "Во время экспорта появляется кнопка «Остановить экспорт».") self._add_text(content, "При остановке:") self._add_bullet(content, "Текущий пациент НЕ сохраняется (неполная папка удаляется)") self._add_bullet(content, "Уже экспортированные пациенты остаются на месте") self._add_note(content, "Экспорт можно остановить в любой момент — вы не потеряете уже скопированные данные.", "🛑") # ------ 9. ПОДСЧЁТ СНИМКОВ ------ section = self._add_section(content, "Подсчёт снимков", "🖼️", level=1) toc_items.append(("Подсчёт снимков", section)) self._add_text(content, "Программа автоматически определяет тип файлов и считает снимки:") self._add_bullet(content, "Если в папке есть BMP — считает их количество (1 BMP = 1 снимок)") self._add_bullet(content, "Если BMP нет — считает уникальные SeriesInstanceUID") self._add_note(content, "Это позволяет работать с файлами от разных производителей без настройки.") # ------ 10. КОДИРОВКА ------ section = self._add_section(content, "Настройка кодировки", "🔤", level=1) toc_items.append(("Настройка кодировки", section)) self._add_text(content, "Если имена пациентов отображаются некорректно (крякозябры):") self._add_bullet(content, "Выберите другую кодировку в меню «Кодировка» или в выпадающем списке на панели") self._add_bullet(content, "Наиболее распространённые варианты: cp1251 (Windows-1251) и Mac Cyrillic") self._add_note(content, "Смена кодировки применяется сразу, без повторного сканирования.") # ------ 11. СТРУКТУРА ПАПОК ------ section = self._add_section(content, "Структура папок", "📂", level=1) toc_items.append(("Структура папок", section)) self._add_text(content, "Программа работает с любой структурой папок:") self._add_bullet(content, "Структура с подпапками: пациент → DX → файлы") self._add_bullet(content, "Плоская структура: все DICOM-файлы в одной папке") self._add_bullet(content, "Смешанная структура: комбинация обоих вариантов") self._add_note(content, "Программа рекурсивно обходит все подпапки на любой глубине.") # ------ 12. РЕШЕНИЕ ПРОБЛЕМ ------ section = self._add_section(content, "Решение проблем", "🔧", level=1) toc_items.append(("Решение проблем", section)) problems = [ ("Имена отображаются крякозябрами", "Смените кодировку в меню «Кодировка» (рекомендуется cp1251)"), ("Программа не видит DICOM-файлы", "Проверьте расширение файлов: .dcm или .dicom"), ("Сканирование длится слишком долго", "После первого сканирования результаты кэшируются — повторное открытие мгновенно"), ("При экспорте создаётся пустая папка", "Проверьте права доступа к исходной и целевой папкам"), ("Не удаётся открыть папку пациента", "Убедитесь, что папка существует и у вас есть права на чтение"), ] for problem, solution in problems: frame = ttk.Frame(content) frame.pack(anchor=tk.W, fill=tk.X, padx=20, pady=4) ttk.Label(frame, text=f"• {problem}", font=('Segoe UI', 10, 'bold'), foreground='#c0392b').pack(anchor=tk.W) ttk.Label(frame, text=f" → {solution}", style='Body.TLabel').pack(anchor=tk.W, padx=15) # ------ 13. О ПРОГРАММЕ ------ section = self._add_section(content, "О программе", "ℹ️", level=1) toc_items.append(("О программе", section)) self._add_text(content, "DiSc — DICOM Scanner") self._add_text(content, "Версия: 1.1.0") self._add_text(content, "Тип: Портативная (не требует установки)") self._add_text(content, "Лицензия: MIT") self._add_text(content, "Разработано для быстрого поиска исследований без загрузки в тяжелый DICOM-просмотрщик.") # ===== СОБИРАЕМ ОГЛАВЛЕНИЕ ===== for text, target in toc_items: self._add_toc_item(self.toc_inner, text, target) # Запоминаем ссылки для навигации self.toc_items = toc_items def show(self): """Показывает окно""" self.window.deiconify() self.window.lift() self.window.focus_force()