Interface-module/frames/CTkTableFrame.py
SantaSpeen 5e1adb9507 [+] edit
[+] update_rows
[+] focus_at_last
[!] Separate build logic
2025-03-22 19:38:15 +03:00

186 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import customtkinter as ctk
class CTkTableFrame(ctk.CTkFrame):
def __init__(self, master: ctk.CTk | ctk.CTkToplevel, columns, data: list, callback, settings_callback=None, width=400, height=200, *args, **kwargs):
super().__init__(master, *args, **kwargs)
self.configure(width=width, height=height, fg_color="gray10")
self._columns = []
self._rows = [] # Готовые строки
self._rows_settings = [] # Настройки для строки {"color": None, "disable": False}
self.columns = columns
self._data = data # Не готовые строки
self._callback = callback
self._settings_callback = settings_callback
self._prepare_columns()
self._build_header()
self.loading_frame = ctk.CTkFrame(self, bg_color="gray10", fg_color="gray10")
ctk.CTkLabel(self.loading_frame, text="Загрузка...", font=("Arial", 12)).pack(fill="both", expand=True)
self._scroll_frame = ctk.CTkScrollableFrame(self, bg_color="gray10", fg_color="gray10")
self.redraw()
def _prepare_columns(self):
# Применяем шаблон значений по умолчанию
default_column = {"width": 0, "align": "left", "name": "N/A"}
for col in self.columns:
if type(col) == dict:
if "width" not in col:
col["width"] = (len(col["name"]) * 7) + 14
self._columns.append({**default_column, **col})
else:
self._columns.append({**default_column, "width": (len(col) * 7) + 14, "name": col})
# Функция преобразования align в формат anchor
def parse_align(align):
return {"left": "w", "right": "e", "center": "center"}.get(align, "w")
# Определяем ширины: если текущая ширина меньше вычисленной, то обновляем
for i, col in enumerate(self._columns):
max_data_width = max((len(str(row[i])) * 7 for row in self._data if i < len(row)), default=0) + 10
if col["width"] < max_data_width:
col["width"] = max_data_width
col["align"] = parse_align(col["align"])
def _build_header(self):
"""Создает заголовок таблицы."""
header_frame = ctk.CTkFrame(self, fg_color="gray30", corner_radius=10)
header_frame.pack(fill="x", padx=0, pady=1)
# Заголовки
for col in self._columns:
header_label = ctk.CTkLabel(
header_frame, text=col["name"], width=col["width"], anchor=col["align"], padx=5
)
header_label.pack(side="left", padx=4, pady=3)
@staticmethod
def __row_enter(frame, e):
frame.configure(fg_color="gray40")
@staticmethod
def __row_leave(frame, e):
frame.configure(fg_color="gray20")
def _build_row(self, row_index, row_data):
_need_to_pack = []
row_frame = ctk.CTkFrame(self._scroll_frame, fg_color="gray20", corner_radius=3, height=20)
self._rows[row_index] = row_frame
self._rows_settings[row_index] = {"color": None, "disable": False}
_need_to_pack.append((row_frame, {"fill": "x", "padx": 2, "pady": 1}))
def _bind(item: ctk.CTkFrame | ctk.CTkLabel):
if not self._callback:
return
item.bind("<Enter>", lambda e: self.__row_enter(row_frame, e))
item.bind("<Leave>", lambda e: self.__row_leave(row_frame, e))
item.bind("<Double-1>", lambda event, idx=row_index, r=row_data: self._callback({"row_index": idx, "row_data": r}))
_bind(row_frame)
for i, col in enumerate(self._columns):
cell_text = str(row_data[i]) if i < len(row_data) else ""
cell_label = ctk.CTkLabel(row_frame, text=cell_text, width=col["width"], anchor=col["align"], padx=5)
_need_to_pack.append((cell_label, {"side": "left", "padx": 2, "pady": 2}))
_bind(cell_label)
if self._settings_callback:
self.edit(row_index, **self._settings_callback(row_data) or {})
return _need_to_pack
def create_table(self):
"""Создает таблицу с заголовками и данными, используя параметры из словаря."""
self._rows = [None] * len(self._data)
self._rows_settings = [None] * len(self._data)
loading = self.loading_frame.winfo_children()[0]
_need_to_pack = []
loading.configure(text="Подготовка данных...")
for row_index, row_data in enumerate(self._data):
_need_to_pack.extend(self._build_row(row_index, row_data))
loading.configure(text="Отрисовка данных...")
self._scroll_frame.pack_propagate(False) # Отключает перерасчет размеров
for widget, pack_params in _need_to_pack:
widget.pack(**pack_params)
self._scroll_frame.pack_propagate(True)
self.after(25, lambda: self._scroll_frame.pack(fill="both", expand=True, padx=0, pady=(0, 0)))
self.update_idletasks()
self.loading_frame.pack_forget()
def update_rows(self):
"""Добавляет строки в таблицу."""
start_index = len(self._rows)
new_indexes = range(start_index, len(self._data))
# Если есть новые строки - добавляем их
if new_indexes:
self._rows[start_index:] = [None] * len(new_indexes)
self._rows_settings[start_index:] = [None] * len(new_indexes)
_widgets = []
for idx in new_indexes:
_widgets.extend(self._build_row(idx, self._data[idx]))
for widget, pack_params in _widgets:
widget.pack(**pack_params)
# Применяем настройки строки
for idx, settings in enumerate(self._rows_settings):
self.edit(idx, **settings)
def __clean_table(self):
self._scroll_frame.pack_propagate(False) # Отключает перерасчет размеров
self._scroll_frame.pack_forget()
for widget in self._scroll_frame.winfo_children():
widget.destroy()
def clean(self):
self.__clean_table()
self.loading_frame.winfo_children()[0].configure(text="Загрузка...")
self.loading_frame.pack(fill="both", expand=True)
self.update_idletasks()
def edit(self, row_index, new_data=None, color=None, disable=False, **__):
"""Редактирует строку по индексу."""
if 0 <= row_index < len(self._data):
row = self._rows[row_index]
if new_data:
if not isinstance(new_data, bool):
self._data[row_index] = new_data
data = self._data[row_index]
for i, widget in enumerate(row.winfo_children()):
widget.configure(text=str(data[i]))
if color:
row.configure(fg_color=color)
if disable:
def _disable(item):
item.unbind("<Double-1>")
item.unbind("<Enter>")
item.unbind("<Leave>")
_disable(row)
for widget in row.winfo_children():
_disable(widget)
self._rows_settings[row_index] = {"color": color, "disable": disable}
def add(self, new_data, draw=True):
"""Добавляет новую строку."""
self._data.append(new_data)
if draw:
self.update_rows()
def redraw(self):
"""Перерисовывает таблицу."""
self.clean()
self.create_table()
def focus_at_last(self):
self.after(125, self._scroll_frame._parent_canvas.yview_moveto, 999)