Исходный код 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