[+] update_rows
[+] focus_at_last
[!] Separate build logic
This commit is contained in:
Maxim Khomutov 2025-03-22 19:38:15 +03:00
parent bd26ece77a
commit 5e1adb9507

View File

@ -1,96 +1,185 @@
import customtkinter as ctk
class CTkTableFrame(ctk.CTkFrame):
def __init__(self, master: ctk.CTk | ctk.CTkToplevel, columns, data: list, callback, width=400, height=200, *args, **kwargs):
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._data = data # Не готовые строки
self._callback = callback
self._settings_callback = settings_callback
self.configure(width=width, height=height)
self.loading_frame = ctk.CTkFrame(self)
self.loading_frame.pack(fill="both", expand=True)
self.clean()
self._prepare_columns()
self._build_header()
def clean(self):
_old_children = self.winfo_children()
for i, widget in enumerate(_old_children):
widget.destroy()
self.loading_frame = ctk.CTkFrame(self)
self.loading_frame.pack(fill="both", expand=True)
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)
def create_table(self):
"""Создает таблицу с заголовками и данными, используя параметры из словаря."""
loading = ctk.CTkLabel(self.loading_frame, text="Загрузка...", font=("Arial", 12))
loading.pack(fill="both", expand=True)
scroll_frame = ctk.CTkScrollableFrame(self)
self._scroll_frame = ctk.CTkScrollableFrame(self, bg_color="gray10", fg_color="gray10")
_need_to_pack = []
self.redraw()
def _prepare_columns(self):
# Применяем шаблон значений по умолчанию
default_column = {"width": 0, "align": "left", "name": "N/A"}
columns = [] # [{**default_column, **col} if type(col) == dict else {**default_column, "name": col} for col in self.columns]
for col in self.columns:
if type(col) == dict:
if "width" not in col:
col["width"] = (len(col["name"]) * 7) + 14
columns.append({**default_column, **col})
self._columns.append({**default_column, **col})
else:
columns.append({**default_column, "width": (len(col) * 7) + 14, "name": col})
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(columns):
max_data_width = max((len(str(row[i])) * 7 for row in self.data if i < len(row)), default=0) + 10
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"])
# Заголовки
header_frame = ctk.CTkFrame(scroll_frame, fg_color="gray30")
_need_to_pack.append((header_frame, {"fill": "x", "padx": 2, "pady": 1}))
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 columns:
# Заголовки
for col in self._columns:
header_label = ctk.CTkLabel(
header_frame, text=col["name"], width=col["width"], anchor=col["align"], padx=5
)
_need_to_pack.append((header_label, {"side": "left", "padx": 2, "pady": 3}))
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):
row_frame = ctk.CTkFrame(scroll_frame, fg_color="gray20", corner_radius=3, height=20)
_need_to_pack.append((row_frame, {"fill": "x", "padx": 2, "pady": 1}))
if self._callback:
row_frame.bind("<Double-1>", lambda event, idx=row_index, r=row_data: self._callback({"row_index": idx, "row_data": r}))
for i, col in enumerate(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}))
if self._callback:
cell_label.bind("<Double-1>", lambda event, idx=row_index, r=row_data: self._callback({"row_index": idx, "row_data": r}))
for row_index, row_data in enumerate(self._data):
_need_to_pack.extend(self._build_row(row_index, row_data))
loading.configure(text="Отрисовка данных...")
for i, (widget, pack_params) in enumerate(_need_to_pack):
self._scroll_frame.pack_propagate(False) # Отключает перерасчет размеров
for widget, pack_params in _need_to_pack:
widget.pack(**pack_params)
scroll_frame.pack(fill="both", expand=True, padx=5, pady=5)
self.loading_frame.destroy()
self._scroll_frame.pack_propagate(True)
def edit(self, row_index, new_data):
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):
self.data[row_index] = new_data
self.redraw()
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):
def add(self, new_data, draw=True):
"""Добавляет новую строку."""
self.data.append(new_data)
self.redraw()
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)