Исходный код EJIO.ctk_extender.components.table

## /ctk_extender/components/table.py

import customtkinter as ctk
from typing import Any

from ..widget import Widget, ScrollableFrame, GeometryStrategy, PackStrategy
from ..state import ReactiveList, CollectionChangedEvent
from ..event import on_event

__all__ = [
    'TableWidget',
]


[документация] class TableWidget(Widget): """ Высокоуровневый реактивный компонент таблицы на базе ScrollableFrame. Автоматически перерисовывает строки при изменении привязанного ReactiveList. Использует менеджер разметки Grid для отрисовки ячеек. """ def __init__( self, parent: Any, data_list: ReactiveList[list[str]], headers: list[str] | None = None, max_visible_rows: int | None = None, placeholder_text: str = "Нет данных для отображения", geometry: GeometryStrategy | None = None, **kwargs: Any ) -> None: """ :param parent: Родительский контейнер. :param data_list: Ссылка на реактивный список ReactiveList, содержащий строки таблицы. :param headers: Опциональный список заголовков колонок. :param max_visible_rows: Опциональное ограничение количества одновременно видимых строк. :param geometry: Стратегия размещения самой таблицы на родителе. :param placeholder_text: Текст, отображаемый при пустой таблице. :param kwargs: Дополнительные параметры стилизации фрейма таблицы. """ self.model: ReactiveList[list[str]] = data_list self._headers: list[str] | None = headers self._max_visible_rows: int | None = max_visible_rows self._placeholder_text: str = placeholder_text self._kwargs: dict[str, Any] = kwargs # Хранилище для всех динамически созданных ctk-виджетов ячеек self._cell_widgets: list[ctk.CTkLabel] = [] self._scroll_container: ScrollableFrame | None = None self._placeholder_label: ctk.CTkLabel | None = None super().__init__(parent, geometry) # Запускаем первоначальный рендеринг данных self._render_table()
[документация] def build(self) -> ctk.CTkScrollableFrame: """Строит прокручиваемый контейнер для таблицы с учетом ограничений высоты.""" # Рассчитываем примерную высоту, если задано ограничение на количество строк if self._max_visible_rows is not None: # Средняя высота одной строки в customtkinter с отступами ~32 пикселя header_offset = 1 if self._headers else 0 self._kwargs['height'] = (self._max_visible_rows + header_offset) * 34 # Создаем скроллируемый контейнер через фабрику виджетов # Используем PackStrategy внутри самого компонента, чтобы обертка заполнила весь фрейм self._scroll_container = ScrollableFrame(self.parent, geometry=PackStrategy(fill="both", expand=True), **self._kwargs) # Возвращаем физический инстанс customtkinter для базового класса Widget return self._scroll_container.ctk_instance
@on_event(CollectionChangedEvent, sender_attr="model") def _on_data_changed(self, event: CollectionChangedEvent[list[str]]) -> None: """Автоматический реактивный слушатель: перерисовывает таблицу при любых мутациях модели.""" self._render_table() def _clear_table_content(self) -> None: """Очищает физические виджеты ячеек с экрана и из памяти.""" for widget in self._cell_widgets: try: widget.destroy() except Exception: pass self._cell_widgets.clear() if self._placeholder_label is not None: self._placeholder_label.destroy() self._placeholder_label = None # Сбрасываем конфигурацию сетки container_ctk = self._scroll_container.ctk_instance # type: ignore for i in range(20): # Очищаем конфигурацию весов колонок container_ctk.grid_columnconfigure(i, weight=0) def _render_table(self) -> None: """Внутренний метод полной отрисовки структуры таблицы через Grid.""" self._clear_table_content() if self._scroll_container is None: return container_ctk = self._scroll_container.ctk_instance # --- СОСТОЯНИЕ «ПУСТО»: Рендерим плейсхолдер --- if len(self.model) == 0: # Размещаем заглушку через pack на весь внутренний контейнер self._placeholder_label = ctk.CTkLabel( container_ctk, text=self._placeholder_text, font=("Arial", 13, "italic"), text_color=("#9CA3AF", "#6B7280") # Серый цвет текста для заглушки ) self._placeholder_label.pack(expand=True, fill="both", pady=40) return # --- СОСТОЯНИЕ «ДАННЫЕ ЕСТЬ»: Переключаемся на стандартный Grid-рендер --- current_row = 0 # --- Сценарий 1: Отрисовка заголовков (если они переданы) --- if self._headers is not None: for col_idx, header_text in enumerate(self._headers): header_label = ctk.CTkLabel( container_ctk, text=header_text, font=("Arial", 12, "bold"), fg_color=("#D1D5DB", "#374151"), # Контрастный фон для заголовков (Light/Dark) corner_radius=4, padx=10, pady=6 ) # Размещаем строго по сетке grid (стандарт для таблиц) header_label.grid(row=current_row, column=col_idx, sticky="nsew", padx=2, pady=2) self._cell_widgets.append(header_label) current_row += 1 # --- Сценарий 2: Отрисовка строк с данными --- for row_data in self.model: for col_idx, cell_value in enumerate(row_data): cell_label = ctk.CTkLabel( container_ctk, text=str(cell_value), font=("Arial", 12), anchor="w", padx=10, pady=4 ) # Размещаем ячейки данных по сетке grid ниже заголовков cell_label.grid(row=current_row, column=col_idx, sticky="ew", padx=2, pady=1) self._cell_widgets.append(cell_label) current_row += 1 # Настраиваем автоматическое растяжение колонок по ширине контейнера cols_count = len(self._headers) if self._headers else (len(self.model[0]) if self.model else 1) for col_idx in range(cols_count): container_ctk.grid_columnconfigure(col_idx, weight=1)
[документация] def destroy(self) -> None: """Очищает ячейки и каскадно уничтожает контейнер таблицы.""" self._clear_table_content() if self._scroll_container is not None: self._scroll_container.destroy() self._scroll_container = None super().destroy()