[+] edit
[+] update_rows [+] focus_at_last [!] Separate build logic
This commit is contained in:
parent
bd26ece77a
commit
5e1adb9507
@ -1,96 +1,185 @@
|
|||||||
import customtkinter as ctk
|
import customtkinter as ctk
|
||||||
|
|
||||||
class CTkTableFrame(ctk.CTkFrame):
|
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)
|
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.columns = columns
|
||||||
self.data = data
|
self._data = data # Не готовые строки
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
|
self._settings_callback = settings_callback
|
||||||
|
|
||||||
self.configure(width=width, height=height)
|
self._prepare_columns()
|
||||||
self.loading_frame = ctk.CTkFrame(self)
|
self._build_header()
|
||||||
self.loading_frame.pack(fill="both", expand=True)
|
|
||||||
self.clean()
|
|
||||||
|
|
||||||
def clean(self):
|
self.loading_frame = ctk.CTkFrame(self, bg_color="gray10", fg_color="gray10")
|
||||||
_old_children = self.winfo_children()
|
ctk.CTkLabel(self.loading_frame, text="Загрузка...", font=("Arial", 12)).pack(fill="both", expand=True)
|
||||||
for i, widget in enumerate(_old_children):
|
|
||||||
widget.destroy()
|
|
||||||
self.loading_frame = ctk.CTkFrame(self)
|
|
||||||
self.loading_frame.pack(fill="both", expand=True)
|
|
||||||
|
|
||||||
def create_table(self):
|
self._scroll_frame = ctk.CTkScrollableFrame(self, bg_color="gray10", fg_color="gray10")
|
||||||
"""Создает таблицу с заголовками и данными, используя параметры из словаря."""
|
|
||||||
loading = ctk.CTkLabel(self.loading_frame, text="Загрузка...", font=("Arial", 12))
|
|
||||||
loading.pack(fill="both", expand=True)
|
|
||||||
scroll_frame = ctk.CTkScrollableFrame(self)
|
|
||||||
|
|
||||||
_need_to_pack = []
|
self.redraw()
|
||||||
|
|
||||||
|
def _prepare_columns(self):
|
||||||
# Применяем шаблон значений по умолчанию
|
# Применяем шаблон значений по умолчанию
|
||||||
default_column = {"width": 0, "align": "left", "name": "N/A"}
|
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:
|
for col in self.columns:
|
||||||
if type(col) == dict:
|
if type(col) == dict:
|
||||||
if "width" not in col:
|
if "width" not in col:
|
||||||
col["width"] = (len(col["name"]) * 7) + 14
|
col["width"] = (len(col["name"]) * 7) + 14
|
||||||
columns.append({**default_column, **col})
|
self._columns.append({**default_column, **col})
|
||||||
else:
|
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
|
# Функция преобразования align в формат anchor
|
||||||
def parse_align(align):
|
def parse_align(align):
|
||||||
return {"left": "w", "right": "e", "center": "center"}.get(align, "w")
|
return {"left": "w", "right": "e", "center": "center"}.get(align, "w")
|
||||||
|
|
||||||
# Определяем ширины: если текущая ширина меньше вычисленной, то обновляем
|
# Определяем ширины: если текущая ширина меньше вычисленной, то обновляем
|
||||||
for i, col in enumerate(columns):
|
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
|
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:
|
if col["width"] < max_data_width:
|
||||||
col["width"] = max_data_width
|
col["width"] = max_data_width
|
||||||
col["align"] = parse_align(col["align"])
|
col["align"] = parse_align(col["align"])
|
||||||
|
|
||||||
# Заголовки
|
def _build_header(self):
|
||||||
header_frame = ctk.CTkFrame(scroll_frame, fg_color="gray30")
|
"""Создает заголовок таблицы."""
|
||||||
_need_to_pack.append((header_frame, {"fill": "x", "padx": 2, "pady": 1}))
|
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_label = ctk.CTkLabel(
|
||||||
header_frame, text=col["name"], width=col["width"], anchor=col["align"], padx=5
|
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)
|
||||||
|
|
||||||
loading.configure(text="Подготовка данных...")
|
@staticmethod
|
||||||
# Данные
|
def __row_enter(frame, e):
|
||||||
for row_index, row_data in enumerate(self.data):
|
frame.configure(fg_color="gray40")
|
||||||
row_frame = ctk.CTkFrame(scroll_frame, fg_color="gray20", corner_radius=3, height=20)
|
|
||||||
|
@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}))
|
_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}))
|
def _bind(item: ctk.CTkFrame | ctk.CTkLabel):
|
||||||
for i, col in enumerate(columns):
|
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_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)
|
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}))
|
_need_to_pack.append((cell_label, {"side": "left", "padx": 2, "pady": 2}))
|
||||||
if self._callback:
|
_bind(cell_label)
|
||||||
cell_label.bind("<Double-1>", lambda event, idx=row_index, r=row_data: self._callback({"row_index": idx, "row_data": r}))
|
|
||||||
|
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="Отрисовка данных...")
|
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)
|
widget.pack(**pack_params)
|
||||||
scroll_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
self._scroll_frame.pack_propagate(True)
|
||||||
self.loading_frame.destroy()
|
|
||||||
|
|
||||||
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):
|
if 0 <= row_index < len(self._data):
|
||||||
self.data[row_index] = new_data
|
row = self._rows[row_index]
|
||||||
self.redraw()
|
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._data.append(new_data)
|
||||||
self.redraw()
|
if draw:
|
||||||
|
self.update_rows()
|
||||||
|
|
||||||
def redraw(self):
|
def redraw(self):
|
||||||
"""Перерисовывает таблицу."""
|
"""Перерисовывает таблицу."""
|
||||||
self.clean()
|
self.clean()
|
||||||
self.create_table()
|
self.create_table()
|
||||||
|
|
||||||
|
def focus_at_last(self):
|
||||||
|
self.after(125, self._scroll_frame._parent_canvas.yview_moveto, 999)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user