Исходный код EJIO.ctk_extender.windows.popup.base

## /ctk_extender/windows/popup/base.py

import customtkinter as ctk
from abc import ABC, abstractmethod
from loguru import logger
from typing import Any

__all__ = [
    'BasePopup',
]


[документация] class BasePopup(ABC): """ Абстрактный базовый класс для всех модальных всплывающих окон (Popup). Инкапсулирует создание CTkToplevel, центрирование, фокус и каскадное уничтожение. """ __slots__ = ( '_title', '_width', '_height', '_ctk_window', '_modal', '_timeout', '_timer_id', '_main_container', '_children' ) def __init__( self, title: str, width: int = 380, height: int = 160, modal: bool = True, master: Any = None, timeout: float | None = None, ) -> None: """ :param title: Заголовок окна. :param width: Ширина окна в пикселях. :param height: Высота окна в пикселях. :param modal: Если True, блокирует родительский интерфейс через grab_set(). :param master: Родительский виджет. :param timeout: Время автоматического закрытия, с. """ self._title: str = title self._width: int = width self._height: int = height self._modal: bool = modal self._timeout: float | None = timeout self._timer_id: str | None = None self._children: list[Any] = [] # Создаем CTkToplevel self._ctk_window: ctk.CTkToplevel | None = ctk.CTkToplevel(master=master) # Мгновенно скрываем окно с экрана. self._ctk_window.withdraw() # Первоначальная базовая конфигурация окна instance = self.ctk_instance instance.title(self._title) instance.resizable(False, False) # Задаем жесткую геометрию instance.geometry(f"{self._width}x{self._height}") # Протокол перехвата системного закрытия (крестик) instance.protocol("WM_DELETE_WINDOW", self.destroy) # Нативные виджеты CustomTkinter требуют CTkFrame вместо CTkToplevel, self._main_container = ctk.CTkFrame(instance, fg_color="transparent", corner_radius=0) self._main_container.pack(fill="both", expand=True) # Фаза сборки контента (вызывается в наследниках) self.build_content() instance.update_idletasks() # Центрируем окно на экране screen_w = instance.winfo_screenwidth() screen_h = instance.winfo_screenheight() x = (screen_w // 2) - (self._width // 2) y = (screen_h // 2) - (self._height // 2) instance.geometry(f"{self._width}x{self._height}+{x}+{y}") # Выводим полностью готовое, отрендеренное окно на экран. instance.deiconify() # Включаем жесткую модальную блокировку главного окна с небольшой задержкой, # чтобы Tcl/Tk успел инициализировать дескриптор окна в ОС if self._modal: instance.after(20, instance.grab_set) if self._timeout is not None and self._timeout > 0.0: # Переводим секунды в миллисекунды для Tkinter ms = int(self._timeout * 1000) # Планируем безопасный вызов метода destroy() в потоке UI self._timer_id = instance.after(ms, self._on_timeout_expire) logger.debug(f" Popup '{self._title}' running with auto close timer: {self._timeout} sec.") @property def ctk_instance(self) -> ctk.CTkToplevel: if self._ctk_window is None: raise RuntimeError(f"Popup object already destroyed.") return self._ctk_window @property def container(self) -> ctk.CTkFrame: """Возвращает безопасный фрейм-контейнер для упаковки дочерних виджетов.""" return self._main_container
[документация] def add_widget[W](self, widget_cls: type[W], *args: Any, **kwargs: Any) -> W: """ Фабричный метод добавления виджетов внутрь попапа. Гарантирует жесткое удержание ссылки в памяти, исключая авто-удаление. """ # В качестве родителя для физического CTkLabel/CTkButton всегда передаем фрейм подложки widget_instance = widget_cls(self.container, *args, **kwargs) self._children.append(widget_instance) return widget_instance
[документация] @abstractmethod def build_content(self) -> None: """ Абстрактный метод сборки внутреннего наполнения диалога. Переопределяется в дочерних классах. :raises: NotImplementedError Переопределяется в подклассах. """ raise NotImplementedError
def _on_timeout_expire(self) -> None: """Внутренний воркер: вызывается при истечении таймера.""" logger.info(f"Waiting time of popup '{self._title}' expired. Automatic closing.") self.destroy()
[документация] def destroy(self) -> None: """Завершает работу диалога и освобождает модальный фокус.""" if self._ctk_window is not None and self._timer_id is not None: try: self._ctk_window.after_cancel(self._timer_id) except Exception: pass self._timer_id = None for child in self._children: if child is not None: try: child.destroy() except Exception: pass self._children.clear() if self._main_container is not None: try: self._main_container.destroy() except Exception: pass self._main_container = None # type: ignore[assignment] if self._ctk_window is not None: try: if self._modal: self._ctk_window.grab_release() self._ctk_window.destroy() except Exception: pass self._ctk_window = None logger.debug(f"Model dialog '{self._title}' successfully unloaded from memory.")
def __del__(self) -> None: self.destroy()