diff --git a/frames/CTkTableFrame.py b/frames/CTkTableFrame.py index 5b5d4f4..1aff7ba 100644 --- a/frames/CTkTableFrame.py +++ b/frames/CTkTableFrame.py @@ -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("", lambda e: self.__row_enter(row_frame, e)) + item.bind("", lambda e: self.__row_leave(row_frame, e)) + item.bind("", 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("", 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("", 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("") + item.unbind("") + item.unbind("") + _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) +