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

## /ctk_extender/widget/base.py

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

from ..event import EventListener
from .geometry import GeometryStrategy, GridStrategy
from .err import WidgetNotInitializedError, WidgetMountError

__all__ = [
    'Widget',
]


[документация] class Widget(ABC): """ Базовый абстрактный класс для всех элементов интерфейса ctk_extender. Автоматически управляет жизненным циклом, геометрией и декларативными подписками на события. """ __slots__ = ('_parent', '_geometry', '_ctk_instance', '_is_destroyed', '_kwargs') def __init__( self, parent: ctk.CTk | ctk.CTkFrame | ctk.CTkTabview | Any, geometry: GeometryStrategy | None = None, **kwargs: Any, ) -> None: """ :param parent: Родительский контейнер (окно или фрейм). :param geometry: Объект стратегии геометрии. Если не задан, применяется дефолтный. """ self._parent: Any = parent self._geometry: GeometryStrategy = geometry or GridStrategy() self._ctk_instance: ctk.CTkBaseClass | None = None self._is_destroyed: bool = False self._kwargs: Any = kwargs # Фаза 1: Инициализируем и собираем UI-элемент self._ctk_instance = self.build() # Фаза 2: Автоматически регистрируем все методы с декоратором @on_event EventListener.register_listeners(self) # Фаза 3: Монтируем (отрисовываем) элемент на родительском контейнере self.mount() @property def parent(self) -> Any: """Возвращает родительский контейнер виджета.""" return self._parent @property def ctk_instance(self) -> ctk.CTkBaseClass: """ Возвращает ссылку на реальный объект customtkinter. :raises WidgetNotInitializedError: Если инстанс не инициализирован. """ if self._ctk_instance is None: raise WidgetNotInitializedError(f"Widget '{self.__class__.__name__}' was not initialized through build().") return self._ctk_instance
[документация] @abstractmethod def build(self) -> ctk.CTkBaseClass: """ Абстрактный метод сборки компонента. Должен быть переопределен в наследниках. Внутри него создаются и конфигурируются реальные ctk-объекты. :return: Экземпляр реального ctk-виджета (например, ctk.CTkButton). :raises NotImplementedError: Должен быть определен в подклассе. """ raise NotImplementedError
[документация] def mount(self) -> Self: """ Размещает виджет на родительском контейнере в соответствии с заданным методом геометрии. """ try: self._geometry.mount(self.ctk_instance) except Exception as e: logger.error(f"Error in mounting geometry '{self._geometry.__class__.__name__}' for {self.__class__.__name__}: {e}") raise WidgetMountError(f"Error in mounting geometry '{self._geometry.__class__.__name__}' for {self.__class__.__name__}") from e return self
[документация] def unmount(self) -> Self: """ Скрывает виджет с экрана, извлекая его из менеджера геометрии, но сохраняя состояние объекта в памяти. """ try: self._geometry.unmount(self.ctk_instance) except Exception as e: logger.error(f"Error in unmounting geometry '{self._geometry.__class__.__name__}' for {self.__class__.__name__}: {e}") raise WidgetMountError(f"Error in unmounting geometry '{self._geometry.__class__.__name__}' for {self.__class__.__name__}") from e return self
[документация] def set_geometry(self, geometry: GeometryStrategy) -> Self: """ Позволяет динамически на лету переключить менеджер разметки виджета (например, перекинуть его из pack в grid без пересоздания объекта). :param geometry: Объект стратегии геометрии. """ self.unmount() self._geometry = geometry self.mount() return self
[документация] def focus(self) -> None: """Переводит фокус ввода операционной системы на данный виджет.""" if self._ctk_instance is not None: try: self._ctk_instance.focus_set() except Exception as e: logger.error(f"Failed to shift focus to widget {self.__class__.__name__}: {e}")
[документация] def destroy(self) -> None: """ Полностью уничтожает виджет, очищая память и удаляя его глобальные подписки. """ if getattr(self, '_is_destroyed', False): return self._is_destroyed = True # Сначала отписываем все методы от EventController, чтобы избежать утечек памяти EventListener.unregister_listeners(self) if self._ctk_instance is not None: try: self.unmount() # Убираем с экрана перед деструкцией self._ctk_instance.destroy() except Exception: pass self._ctk_instance = None if hasattr(self, '_geometry'): del self._geometry logger.debug(f"Widget {self.__class__.__name__} successfully destroyed.")
def __del__(self) -> None: """ Деструктор класса. Гарантирует автоматическую очистку подписок и ресурсов при удалении ссылки на объект или закрытии контейнера. """ try: self.destroy() except Exception: pass