Interface-module/frames/CTkTableFrame.py
2025-03-26 18:29:34 +03:00

233 lines
9.5 KiB
Python
Raw Permalink 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
def darken_color_rgb(hex_color, amount=30):
"""Затемняет цвет, вычитая значение из каждого компонента RGB"""
try:
hex_color = hex_color.lstrip("#")
r, g, b = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
# Уменьшаем компоненты, не давая им уйти в минус
r, g, b = max(0, r - amount), max(0, g - amount), max(0, b - amount)
except Exception as e:
print(e, hex_color)
raise e
return f"#{r:02X}{g:02X}{b:02X}"
class CTkTableFrame(ctk.CTkFrame):
loading_title = "Loading"
loading_phase = "Loading: "
loading_data = "preparing rows..."
loading_draw = "drawing rows..."
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=self.loading_title, 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"}
self._columns.clear()
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):
"""Создает заголовок таблицы."""
self.header_frame = ctk.CTkFrame(self, fg_color="gray30", corner_radius=10)
self.header_frame.pack(fill="x", padx=0, pady=1)
# Заголовки
for col in self._columns:
header_label = ctk.CTkLabel(
self.header_frame, text=col["name"], width=col["width"], anchor=col["align"], padx=5
)
header_label.pack(side="left", padx=4, pady=3)
def _update_header(self):
"""Обновляет заголовок таблицы."""
for i, col in enumerate(self._columns):
header_label = self.header_frame.winfo_children()[i]
header_label.configure(width=col["width"], anchor=col["align"])
@staticmethod
def __row_enter(frame, e, color="gray40"):
frame.configure(fg_color=color)
@staticmethod
def __row_leave(frame, e, color="gray20"):
frame.configure(fg_color=color)
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._prepare_columns()
self._update_header()
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=self.loading_phase + self.loading_data)
for row_index, row_data in enumerate(self._data):
_need_to_pack.extend(self._build_row(row_index, row_data))
loading.configure(text=self.loading_phase + self.loading_draw)
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_title)
self.loading_frame.pack(fill="both", expand=True)
self.update_idletasks()
def edit(self, row_index, new_data=None, color=None, disable=False, **__):
"""Редактирует строку по индексу."""
if row_index == -1:
row_index = len(self._data) - 1
def _disable_colors(widget):
widget.unbind("<Enter>")
widget.unbind("<Leave>")
def _disable(widget):
widget.unbind("<Double-1>")
_disable_colors(widget)
def _edit_color(widget, row, color):
_disable_colors(widget)
col_back = darken_color_rgb(color, 30)
widget.bind("<Leave>", lambda e: self.__row_leave(row, e, color))
widget.bind("<Enter>", lambda e: self.__row_enter(row, e, col_back))
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:
for widget in (*row.winfo_children(), row):
_edit_color(widget, row, color)
row.configure(fg_color=color)
if disable:
for widget in (*row.winfo_children(), row):
_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.update_idletasks()
self.after(20, self.create_table)
def focus_at_last(self):
self.focus_at(999)
def focus_at(self, position: int):
self.after(125, self._scroll_frame._parent_canvas.yview_moveto, position)