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("", 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._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("") widget.unbind("") def _disable(widget): widget.unbind("") _disable_colors(widget) def _edit_color(widget, row, color): _disable_colors(widget) col_back = darken_color_rgb(color, 30) widget.bind("", lambda e: self.__row_leave(row, e, color)) widget.bind("", 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)