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