233 lines
9.5 KiB
Python
233 lines
9.5 KiB
Python
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)
|
||
|