import customtkinter as ctk class CTkTableFrame(ctk.CTkFrame): 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="Загрузка...", 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"} 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): """Создает заголовок таблицы.""" header_frame = ctk.CTkFrame(self, fg_color="gray30", corner_radius=10) header_frame.pack(fill="x", padx=0, pady=1) # Заголовки for col in self._columns: header_label = ctk.CTkLabel( header_frame, text=col["name"], width=col["width"], anchor=col["align"], padx=5 ) 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): _need_to_pack.extend(self._build_row(row_index, row_data)) loading.configure(text="Отрисовка данных...") 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_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): 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, draw=True): """Добавляет новую строку.""" self._data.append(new_data) if draw: self.update_rows() def redraw(self): """Перерисовывает таблицу.""" self.clean() 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)