From 0fb107d95295d05bee00c7dcd7930b93c930b7a5 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Tue, 18 Mar 2025 13:05:36 +0300 Subject: [PATCH] module --- boxes/CTkLoadingBox.py | 118 ++++++++++++++++++++ boxes/CTkLoginBox.py | 133 +++++++++++++++++++++++ boxes/CTkMessageBox.py | 178 +++++++++++++++++++++++++++++++ boxes/__init__.py | 3 + frames/CTkColoredConsoleFrame.py | 94 ++++++++++++++++ frames/CTkSidebarFrame.py | 141 ++++++++++++++++++++++++ frames/CTkTableFrame.py | 82 ++++++++++++++ frames/__init__.py | 3 + utils/__init__.py | 3 + utils/fonts.py | 85 +++++++++++++++ utils/misc.py | 12 +++ utils/params.py | 22 ++++ 12 files changed, 874 insertions(+) create mode 100644 boxes/CTkLoadingBox.py create mode 100644 boxes/CTkLoginBox.py create mode 100644 boxes/CTkMessageBox.py create mode 100644 boxes/__init__.py create mode 100644 frames/CTkColoredConsoleFrame.py create mode 100644 frames/CTkSidebarFrame.py create mode 100644 frames/CTkTableFrame.py create mode 100644 frames/__init__.py create mode 100644 utils/__init__.py create mode 100644 utils/fonts.py create mode 100644 utils/misc.py create mode 100644 utils/params.py diff --git a/boxes/CTkLoadingBox.py b/boxes/CTkLoadingBox.py new file mode 100644 index 0000000..837eb9e --- /dev/null +++ b/boxes/CTkLoadingBox.py @@ -0,0 +1,118 @@ +from threading import Thread +from typing import Optional, Union, Tuple + +from customtkinter import CTkToplevel, CTkProgressBar, CTkLabel, ThemeManager, CTkFont +from loguru import logger + + +class CTkLoadingBox(CTkToplevel): + + def __init__(self, + title: str = "CTkDialog", + text: str = "CTkDialog", + font: Optional[Union[tuple, CTkFont]] = None, + + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + button_text_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + parent=None): + + super().__init__(fg_color=fg_color) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) + self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) + self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) + self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) + self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) + self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) + + self._running: bool = False + self._progress = 0 + self._text = [text, ""] + self._font = font + + # self.geometry("300x100") + self.transient(parent) + self.title(title) + self.lift() # lift window on top + self.attributes("-topmost", True) # stay on top + self.protocol("WM_DELETE_WINDOW", self._on_closing) + self._create_widgets() + self.resizable(False, False) + + self.grab_set() # make other windows not clickable + + def _create_widgets(self): + # self.message_label = CTkLabel(self, text=self._text) + self.message_label = CTkLabel( + self, width=300, wraplength=300, fg_color="transparent", text=self._text[0] + ) + self.message_label.pack(pady=5) + + # Прогресс-бар + self.progress_bar = CTkProgressBar(self, mode="determinate", width=250) + self.progress_bar.pack(pady=10) + self.progress_bar.set(0) # Устанавливаем начальное значение прогресса + + # Текст с процентами + self.percent_label = CTkLabel( + self, width=300, wraplength=300, fg_color="transparent", text="0%" + ) + self.percent_label.pack(pady=5) + + def _on_closing(self): + self.grab_release() + # self.destroy() + + def __run(self, f, *args, **kwargs): + self._running = True + try: + f(*args, **kwargs) + except Exception as e: + logger.exception(e) + finally: + self._running = False + self.grab_release() + self.destroy() + self.quit() + + def process(self, func, *args, **kwargs): + Thread(target=self.__run, args=(func, self, *args), kwargs=kwargs, daemon=True).start() + self.mainloop() + + def _set_subtext(self, subtext): + self._text[1] = subtext + self.message_label.configure(text=f"{self._text[0]}: {self._text[1]}") + + def set_subtext(self, subtext, *_, **__): + self.after(0, self._set_subtext, subtext) + + def _set_text(self, text): + self._text[0] = text + self.message_label.configure(text=f"{self._text[0]}") + + def set_text(self, text, *_, **__): + self.after(0, self._set_text, text) + + def _set_progress(self, value): + if 0.0 <= value <= 1.0: + self._progress = value + self.progress_bar.set(value) # Обновляем прогресс-бар + self.percent_label.configure(text=f"{int(value * 100)}%") # Обновляем текст процентов + + def set_progress(self, value, *_, **__): + self.after(0, self._set_progress, value) + + +if __name__ == '__main__': + c = CTkLoadingBox("Title", "Message") + c.set_progress(0.475) + c.mainloop() diff --git a/boxes/CTkLoginBox.py b/boxes/CTkLoginBox.py new file mode 100644 index 0000000..fae2964 --- /dev/null +++ b/boxes/CTkLoginBox.py @@ -0,0 +1,133 @@ +from typing import Optional, Union, Tuple +import customtkinter as ctk +from customtkinter import CTkFont, ThemeManager + +from gui.utils.params import Strings + + +class CTkLoginBox(ctk.CTkToplevel): + + def __init__(self, + title: str = "LoginBox", + greeting: str = "LoginBox", + font: Optional[Union[tuple, CTkFont]] = None, + + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + button_text_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + stings=Strings, + + parent=None, + header=None): + + super().__init__(fg_color=fg_color) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(text_color) + self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) + self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) + self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) + self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) + self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) + + self.__strings = stings + self._header = header + self._greeting = greeting + self._font = font + self._input = None + + self._parent = parent + + self.transient(parent) + self.title(title) + self.lift() + self.protocol("WM_DELETE_WINDOW", self._on_closing) + self._create_widgets() + self.resizable(False, False) + self.grab_set() + + def get_parent(self): + return self._parent + + def set_greeting(self, greeting): + self._greeting = greeting + + def _on_closing(self): + self.grab_release() + self.destroy() + + def _center(self): + self.update_idletasks() + x = (self.winfo_screenwidth() - self.winfo_reqwidth()) + y = (self.winfo_screenheight() - self.winfo_reqheight()) + self.geometry('+{}+{}'.format(x, y)) + + def _create_widgets(self): + if self._header: + self._header(self, self.title(), disable_hide=True, disable_close=True, transient=True) + + # Основная рамка + main_frame = ctk.CTkFrame(self) + main_frame.focus_set() + main_frame.pack(pady=(5, 10), padx=10, fill="x") + + # Настраиваем веса столбцов: 1/3 для метки, 2/3 для ввода + main_frame.grid_columnconfigure(0, weight=1) # Первый столбец (метки) + main_frame.grid_columnconfigure(1, weight=2) # Второй столбец (поля ввода) + + # Приветствие + label_greeting = ctk.CTkLabel(main_frame, text=self._greeting, anchor="center") + label_greeting.grid(row=0, column=0, pady=5, sticky="nsew", columnspan=2) + + # Логин + label_login = ctk.CTkLabel(main_frame, text=self.__strings.login) + label_login.grid(row=1, column=0, padx=10, pady=5, sticky="w") # Выравнивание влево + + self.entry_login = ctk.CTkEntry(main_frame) + self.entry_login.grid(row=1, column=0, padx=(80, 5), pady=5, sticky="we", columnspan=2) # Выравнивание + растяжение + + # Пароль + label_password = ctk.CTkLabel(main_frame, text=self.__strings.password) + label_password.grid(row=2, column=0, padx=10, pady=5, sticky="w") # Выравнивание влево + + self.entry_password = ctk.CTkEntry(main_frame, show="・") + self.entry_password.grid(row=2, column=0, padx=(80, 5), pady=5, sticky="we", columnspan=2) # Выравнивание + растяжение + + # Кнопки + btn_ok = ctk.CTkButton(main_frame, text=self.__strings.ok, command=self._on_ok) + btn_ok.grid(row=3, column=0, padx=10, pady=10, sticky="e") # Выравнивание вправо + + btn_cancel = ctk.CTkButton(main_frame, text=self.__strings.cancel, command=self._on_cancel) + btn_cancel.grid(row=3, column=1, padx=10, pady=10, sticky="e") # Выравнивание вправо + + def _on_ok(self): + self._input = [ + self.entry_login.get(), + self.entry_password.get() + ] + self._on_closing() + + def _on_cancel(self): + self._input = None + self._on_closing() + + def get_output(self): + self._center() + self.master.wait_window(self) + return self._input + + +if __name__ == '__main__': + root = ctk.CTk() + root.withdraw() + login_box = CTkLoginBox(parent=root) + result = login_box.get_output() + print(result) + # root.mainloop() diff --git a/boxes/CTkMessageBox.py b/boxes/CTkMessageBox.py new file mode 100644 index 0000000..bbb17ec --- /dev/null +++ b/boxes/CTkMessageBox.py @@ -0,0 +1,178 @@ +from typing import Optional, Union, Tuple, Dict, Any, Literal + +import customtkinter as ctk +from customtkinter import CTkFont, ThemeManager + +from gui.utils import Strings, Icons + +class CTkMessageBox(ctk.CTkToplevel): + header_map = { + "info": Strings.info, + "warning": Strings.warning, + "error": Strings.error, + "yesno": Strings.yesno + } + icon_map = { + "info": Icons.info, + "warning": Icons.warning, + "error": Icons.error, + "yesno": Icons.yesno + } + buttons_map = { + "info": {"Ok": {"output": True, "row": 0, "column": 0, "sticky": "center"}}, + "warning": { + "Ok": {"output": True, "row": 0, "column": 0, "sticky": "e"}, + "Cancel": {"output": False, "row": 0, "column": 1, "sticky": "w"} + }, + "error": {"Ok": {"output": True, "row": 0, "column": 0, "sticky": "center"}}, + "yesno": { + "Yes": {"output": True, "row": 0, "column": 0, "sticky": "e"}, + "No": {"output": False, "row": 0, "column": 1, "sticky": "w"} + } + } + def __init__(self, + title: str = "CTkDialog", + font: Optional[Union[tuple, CTkFont]] = None, + text: str = "CTkDialog", + mode: Literal["info", "warning", "error", "yesno"] = "info", + timeout: int = 0, + + fg_color: Optional[Union[str, Tuple[str, str]]] = None, + text_color: Optional[Union[str, Tuple[str, str]]] = None, + button_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + button_hover_color: Optional[Union[str, Tuple[str, str]]] = None, + button_text_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_fg_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, + entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, + + parent=None, + header=None, + buttons: Dict[str, Any] | str = 'auto'): + + super().__init__(fg_color=fg_color) + + self._fg_color = ThemeManager.theme["CTkToplevel"]["fg_color"] if fg_color is None else self._check_color_type(fg_color) + self._text_color = ThemeManager.theme["CTkLabel"]["text_color"] if text_color is None else self._check_color_type(button_hover_color) + self._button_fg_color = ThemeManager.theme["CTkButton"]["fg_color"] if button_fg_color is None else self._check_color_type(button_fg_color) + self._button_hover_color = ThemeManager.theme["CTkButton"]["hover_color"] if button_hover_color is None else self._check_color_type(button_hover_color) + self._button_text_color = ThemeManager.theme["CTkButton"]["text_color"] if button_text_color is None else self._check_color_type(button_text_color) + self._entry_fg_color = ThemeManager.theme["CTkEntry"]["fg_color"] if entry_fg_color is None else self._check_color_type(entry_fg_color) + self._entry_border_color = ThemeManager.theme["CTkEntry"]["border_color"] if entry_border_color is None else self._check_color_type(entry_border_color) + self._entry_text_color = ThemeManager.theme["CTkEntry"]["text_color"] if entry_text_color is None else self._check_color_type(entry_text_color) + + self._running: bool = False + self._header = header + self._mode = mode + self._text = text + self._timeout = timeout + self._font = font or ("Arial", 12) + self._input = None + + _y = len(self._text.split('\n')) * 7 # Calculate height based on the number of lines in self._text + _y += 30 + + if timeout > 0: + if self._header: + header = self._header(self, self.title(), disable_hide=True, disable_close=True) + header.pack(fill="x", pady=(0, 0)) + + # self.geometry(f"150x{_y}") + self.overrideredirect(True) # remove window decorations + self.after(self._timeout, self.destroy) # close window after timeout + self._buttons = {} + else: + # self.geometry(f"150x{_y+12}") + if buttons == "auto": + self._buttons = self.buttons_map.get(self._mode, {}) + elif isinstance(buttons, str): + self._buttons = {buttons: {"output": True, "row": 0, "column": 0, "sticky": "center"}} + + self.attributes("-topmost", True) # stay on top + self.transient(parent) + self.title(title) + self.lift() # lift window on top + self.protocol("WM_DELETE_WINDOW", self._on_closing) + self._create_widgets() + self.resizable(False, False) + self._set_on_pos() + self.grab_set() # make other windows not clickable + + def _set_on_pos(self): + self.update_idletasks() + y = (self.winfo_screenheight() - self.winfo_reqheight()) // 8 + self.geometry(f"+{y}+{y}") + + def _on_closing(self): + self.grab_release() + self.destroy() + + def _create_widgets(self): + # Иконки для типов сообщений + icon_text = self.icon_map.get(self._mode, "?") + + main_frame = ctk.CTkFrame(self) + main_frame.focus_set() + main_frame.pack(pady=(10, 10), padx=10, fill="x") + + # Верхняя часть с иконкой и текстом сообщения + main_frame.grid_columnconfigure(0, weight=1) # Первый столбец + main_frame.grid_columnconfigure(1, weight=2) # Второй столбец + + # Иконка и текст сообщения + icon_label = ctk.CTkLabel(main_frame, text=icon_text, font=("Arial", 24)) + icon_label.grid(row=0, column=0, padx=(10, 0), pady=(10, 5), sticky="w") + header_label = ctk.CTkLabel(main_frame, text=self.header_map.get(self._mode, "Unknown"), font=("Arial", 16)) + header_label.grid(row=0, column=0, padx=(10+40, 10), pady=(12, 3), sticky="w", columnspan=2) + + message_label = ctk.CTkLabel(main_frame, text=self._text, font=self._font) + message_label.grid(row=1, column=0, padx=(15, 10), pady=5, sticky="w", rowspan=2) + + for t, setts in self._buttons.items(): + btn = ctk.CTkButton(main_frame, text=t, command=lambda b=setts['output']: self._on_button_click(b)) + row = setts.get("row", 0) + 3 + if setts.get("sticky") == "center": + btn.grid(row=row, column=0, padx=10, pady=10, sticky="we", columnspan=2) + else: + btn.grid(row=row, column=setts.get("column", 0), padx=10, pady=10, sticky=setts.get("sticky", "e")) + + def _on_button_click(self, button): + self._input = button + self.grab_release() + self.destroy() + + def get_output(self): + self.master.wait_window(self) + if self._timeout > 0 and not self._input: + return + return self._buttons.get(self._input, self._input) + +class messagebox: + @staticmethod + def showinfo(title: str | None, message: str, timeout: int = 0, parent=None, header=None): + return CTkMessageBox(title=title, text=message, mode="info", timeout=timeout, parent=parent).get_output() + + @staticmethod + def showwarning(title: str| None, message: str, timeout: int = 0, parent=None, header=None): + return CTkMessageBox(title=title, text=message, mode="warning", timeout=timeout, parent=parent).get_output() + + @staticmethod + def showerror(title: str | None, message: str, timeout: int = 0, parent=None, header=None): + return CTkMessageBox(title=title, text=message, mode="error", timeout=timeout, parent=parent).get_output() + + @staticmethod + def yesno(title: str | None, message: str, timeout: int = 0, parent=None, header=None): + return CTkMessageBox(title=title, text=message, mode="yesno", timeout=timeout, parent=parent).get_output() + +# if __name__ == '__main__': +# root = ctk.CTk() +# root.geometry("200x150") +# text = ctk.CTkLabel(root, text="Test the message box") +# text.pack(pady=5) +# info_button = ctk.CTkButton(root, text="Info", command=lambda: CTkMessageBox(title="Info", text="This is an info message", mode="info", timeout=1000)) +# info_button.pack(pady=5) +# warn_button = ctk.CTkButton(root, text="Warn", command=lambda: CTkMessageBox(title="Warning", text="This is a warning message", mode="warning")) +# warn_button.pack(pady=5) +# error_button = ctk.CTkButton(root, text="Error", command=lambda: CTkMessageBox(title="Error", text="This is an error message", mode="error")) +# error_button.pack(pady=5) +# root.mainloop() diff --git a/boxes/__init__.py b/boxes/__init__.py new file mode 100644 index 0000000..d5b368e --- /dev/null +++ b/boxes/__init__.py @@ -0,0 +1,3 @@ +from .CTkMessageBox import CTkMessageBox, messagebox +from .CTkLoginBox import CTkLoginBox +from .CTkLoadingBox import CTkLoadingBox diff --git a/frames/CTkColoredConsoleFrame.py b/frames/CTkColoredConsoleFrame.py new file mode 100644 index 0000000..2bfc422 --- /dev/null +++ b/frames/CTkColoredConsoleFrame.py @@ -0,0 +1,94 @@ +import tkinter as tk +from datetime import datetime +from enum import Enum + +from customtkinter import CTkTextbox, CTk +from loguru import logger + +from gui.utils import fonts + +class LogLevels(Enum): + DEBUG = 0 + ERROR = 1 + WARNING = 2 + INFO = 3 + SUCCESS = 4 + +class CTkColoredConsoleFrame(CTkTextbox): + + def __init__(self, master: CTk, corner_radius=0, font=None, _callback=None, **kwargs): + if font is None: + font = fonts.log_font() + super().__init__(master, corner_radius=corner_radius, font=font, **kwargs) + self.configure(state='disabled') + self.__callback = _callback + self._create_tags() + self._create_menu() + + def _copy(self, _=None): + self.clipboard_clear() + selected_text = self.get(tk.SEL_FIRST, tk.SEL_LAST).replace("\n\n", "\n") + self.clipboard_append(selected_text) + + def _open_menu(self, ev): + self.menu.post(ev.x_root, ev.y_root) + + def _create_menu(self): + self.menu = tk.Menu(self, tearoff=0) + self.menu.add_command(label="Copy", command=self._copy) + self.master.bind("", self._copy) + self.master.bind("", self._open_menu) + + def _create_tags(self): + self.tag_config(LogLevels.DEBUG, foreground='#0000FF') + self.tag_config(LogLevels.ERROR, foreground='#FF0000') + self.tag_config(LogLevels.WARNING, foreground='#FFA500') + self.tag_config(LogLevels.INFO, foreground='white') + self.tag_config(LogLevels.SUCCESS, foreground='#008000') + + def clear(self): + self.configure(state='normal') + self.delete(1.0, 'end') + self.configure(state='disabled') + + def log(self, level, message, _date=None): + logger.debug(f"[{self.master}] [{level.name}] {message}") + if not _date: + _date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if self.__callback: + self.__callback(level, message, _date) + self.update_idletasks() + self.configure(state='normal') + self.insert('end', f"{_date} | {level.name} | {message}" + '\n', (level, )) + self.see('end') + self.configure(state='disabled') + self.update_idletasks() + + def debug(self, message): + self.after(50, self.log, LogLevels.DEBUG, message) + + def error(self, message): + self.after(50, self.log, LogLevels.ERROR, message) + + def warning(self, message): + self.after(50, self.log, LogLevels.WARNING, message) + + def info(self, message): + self.after(50, self.log, LogLevels.INFO, message) + + def success(self, message): + self.after(50, self.log, LogLevels.SUCCESS, message) + +if __name__ == '__main__': + _root = CTk() + _root.title("CTkColoredConsoleFrame") + _root.geometry("400x300") + _root.configure(bg='white') + con = CTkColoredConsoleFrame(_root) + con.pack(fill='both', expand=True) + con.debug("Debug message") + con.error("Error message") + con.warning("Warning message") + con.info("Info message") + con.success("Success message") + _root.mainloop() diff --git a/frames/CTkSidebarFrame.py b/frames/CTkSidebarFrame.py new file mode 100644 index 0000000..35a54ee --- /dev/null +++ b/frames/CTkSidebarFrame.py @@ -0,0 +1,141 @@ +from PIL import Image +from customtkinter import CTkFrame, CTk, CTkScrollableFrame, CTkLabel, CTkButton, CTkImage +from loguru import logger + +from gui.utils import fonts + + +class CTkSidebarFrame(CTkFrame): + fg_color = "#2b2b2b" + bg_color = "#333333" + + butt = { + "enabled": "#ededed", + "disabled": "#363636", + "fg": "#3a6069", + "bg": "transparent", + "hv": "#518894", + "pressed": "#2b2b2b" + } + + def __init__(self, master: CTk, name, icon_light, icon_dark, name_font=None, + options=False, options_text="Options", options_img=None, options_command=None, + corner_radius=0, *args, **kwargs): + super().__init__(master, corner_radius=corner_radius, *args, **kwargs) + + self.__state = { + "status": { + "text": "Starting..", + "color": "#aec6cf" + } + } + self.__buttons = {} + self.active = None + + self._name = name + self._name_font = name_font or fonts.heading_font() + self._icon_light = icon_light + self._icon_dark = icon_dark + + self._add_options = options + self._options_text = options_text + if options_img is not None: + options_img = Image.open(options_img) + self._options_img = options_img + if options and options_command is None: + raise ValueError("options_command must be provided if options is True") + self._options_command = options_command + + self.grid_columnconfigure(0, weight=0) # label + self.grid_columnconfigure(1, weight=0) # dropdown + self.grid_rowconfigure(2, weight=1) # buttons + self.grid_rowconfigure(3, weight=0) # settings + + self._create_widgets() + + def _create_widgets(self): + self.update_idletasks() + + name_label = CTkLabel(self, text=self._name, fg_color=self.fg_color, bg_color=self.bg_color, corner_radius=0, font=self._name_font) + name_label.grid(row=0, column=0, columnspan=2, sticky="ew", padx=5, pady=(10, 0)) + + self.status_label = CTkLabel(self, text="Status", fg_color=self.fg_color, bg_color=self.bg_color, corner_radius=0) + self.status_label.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=(0, 3)) + self._render_status() + + self._sc_frame = CTkScrollableFrame(self, width=160, fg_color=self.fg_color, scrollbar_button_color=self.bg_color, corner_radius=0) + self._sc_frame.grid(row=2, column=0, sticky="nsew", padx=5, pady=(3, 10)) + + if self._add_options: + settings_img = None + if self._options_img is not None: + settings_img = CTkImage(self._options_img, self._options_img, (12, 12)) + self.options = CTkButton(self, + text=self._options_text, fg_color=self.fg_color, bg_color=self.bg_color, + font=fonts.button_med_font(), image=settings_img, command=self._options_command) + self.options.grid(row=3, column=0, sticky="ew", padx=5, pady=(0, 10)) + + def _render_status(self): + status = self.__state["status"] + self.status_label.configure(text=status["text"], font=fonts.subheading_font(), text_color=status["color"]) + + def _on_press(self, name): + logger.debug(f"[{name!r}] pressed") + button = self.__buttons[name] + if not button['active']: + logger.debug(f"{name} is disabled") + return + self.set_enabled(self.active) + self.set_pressed(name) + self.active = name + button['button'].configure(hover_color=self.butt['pressed']) + f = button['command'] + f_args = button['command_args'] + f(*f_args) + + def set_status(self, text, color=None): + self.__state["status"]["text"] = text + if color is not None: + self.__state["status"]["color"] = color + self._render_status() + + def add_button(self, name, text, command, command_args=(), icon=None): + self.update_idletasks() + if icon is not None: + _icon = Image.open(icon) + icon = CTkImage(_icon, _icon, (12, 12)) + button = CTkButton(self._sc_frame, text=text, font=fonts.button_med_font(), image=icon, command=lambda: self._on_press(name)) + button.pack(fill="x", padx=5, pady=5) + self.__buttons[name] = {"active": True, "button": button, "command": command, "command_args": command_args} + self.set_enabled(name) + + def set_pressed(self, name): + if name is None: + return + butt = self.__buttons[name] + if not butt['active']: + return + butt['button'].configure(fg_color=self.butt['pressed'], hover_color=self.butt['pressed']) + + def set_enabled(self, name): + self.update_idletasks() + logger.debug(f"[{name!r}] set_enabled") + if name is None: + return + butt = self.__buttons[name] + butt['active'] = True + butt['button'].configure( + fg_color=self.butt['fg'], bg_color=self.butt['bg'], text_color=self.butt['enabled'], hover_color=self.butt['hv'] + ) + + def set_disabled(self, name): + self.update_idletasks() + logger.debug(f"[{name!r}] set_disabled") + if name is None: + return + butt = self.__buttons[name] + butt['active'] = False + butt['button'].configure( + fg_color=self.butt['fg'], bg_color=self.butt['bg'], text_color=self.butt['disabled'], + hover_color=self.butt['fg'] + ) diff --git a/frames/CTkTableFrame.py b/frames/CTkTableFrame.py new file mode 100644 index 0000000..284e309 --- /dev/null +++ b/frames/CTkTableFrame.py @@ -0,0 +1,82 @@ +import customtkinter as ctk + +from gui.utils import fonts + + +class CTkTableFrame(ctk.CTkFrame): + def __init__(self, master: ctk.CTk, columns, data, callback, width=400, height=200, *args, **kwargs): + super().__init__(master, *args, **kwargs) + self.columns = columns + self.data = data + + self._callback = callback + + self.configure(width=width, height=height) + + self.scroll_frame = ctk.CTkScrollableFrame(self) + self.scroll_frame.pack(fill="both", expand=True, padx=5, pady=5) + + self._create_table() + + def _create_table(self): + # Заголовки + header_frame = ctk.CTkFrame(self.scroll_frame, fg_color="gray30") + header_frame.pack(fill="x", padx=2, pady=1) + + for col in self.columns: + header_label = ctk.CTkLabel(header_frame, text=col, font=fonts.body_med_font(), padx=5) + header_label.pack(side="left", padx=5, pady=3, expand=True) + + # Данные + for row_data in self.data: + row_frame = ctk.CTkFrame(self.scroll_frame, fg_color="gray20", corner_radius=3, height=20) + row_frame.pack(fill="x", padx=2, pady=1) + + # Обработчик двойного клика + row_frame.bind("", lambda event, r=row_data: self._callback(r)) + + for cell in row_data: + cell_label = ctk.CTkLabel(row_frame, text=cell, font=fonts.small_font(), padx=5) + cell_label.pack(side="left", padx=5, pady=2, expand=True) + + # Прокидываем клик от ячеек на строку + cell_label.bind("", lambda event, r=row_data: self._callback(r)) + + +# Пример использования +if __name__ == "__main__": + + app = ctk.CTk() + app.geometry("600x400") + + def show_modal(row_data): + modal = ctk.CTkToplevel(app) + modal.geometry("250x150") + modal.title("Детали") + + # Делаем модальное окно поверх основного + modal.grab_set() + modal.focus_force() + + label_text = f"{row_data}" + label = ctk.CTkLabel(modal, text=label_text, font=("Arial", 12)) + label.pack(pady=15) + + close_btn = ctk.CTkButton(modal, text="Закрыть", command=modal.destroy) + close_btn.pack(pady=10) + + ctk.set_appearance_mode("dark") + + columns = ["ID", "Имя", "Логин"] + data = [(i, f"Имя {i}", f"i1111-{i}") for i in range(1, 21)] # 20 строк для теста скролла + + lable = ctk.CTkLabel(app, text="Тестовая таблица", font=fonts.title_font()) + lable.pack(pady=10) + + table = CTkTableFrame(app, columns, data, show_modal, width=400, height=200) + table.pack(pady=10, padx=20, fill="both", expand=True) + + but1 = ctk.CTkButton(app, text="Закрыть", command=app.destroy) + but1.pack(pady=10) + + app.mainloop() diff --git a/frames/__init__.py b/frames/__init__.py new file mode 100644 index 0000000..106f3a5 --- /dev/null +++ b/frames/__init__.py @@ -0,0 +1,3 @@ +from .CTkSidebarFrame import CTkSidebarFrame +from .CTkColoredConsoleFrame import CTkColoredConsoleFrame +from .CTkTableFrame import CTkTableFrame diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..3b93799 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,3 @@ +from . import fonts +from .params import Strings, Icons +from .misc import base_path diff --git a/utils/fonts.py b/utils/fonts.py new file mode 100644 index 0000000..c91db37 --- /dev/null +++ b/utils/fonts.py @@ -0,0 +1,85 @@ +# https://github.com/kelltom/OS-Bot-COLOR/ +import pathlib + +import customtkinter as ctk + +fonts_path = pathlib.Path(__file__).parent +ctk.FontManager.load_font(str(fonts_path.joinpath("CascadiaCode.ttf"))) + + +def get_font(family="Trebuchet MS", size=14, weight="normal", slant="roman", underline=False): + """ + Gets a font object with the given parameters. This is a wrapper for ctk.CTkFont. Provides + defaults for app theme fonts. + """ + return ctk.CTkFont(family=family, size=size, weight=weight, slant=slant, underline=underline) + + +def title_font(): + """ + Preset for titles (largest). + """ + return get_font(size=24, weight="bold") + + +def heading_font(size=20): + """ + Preset for headings. + """ + return get_font(size=size, weight="bold") + + +def subheading_font(size=16): + """ + Preset for subheadings. + """ + return get_font(size=size, weight="bold") + + +def body_large_font(size=15): + """ + Preset for body text. + """ + return get_font(size=size) + + +def body_med_font(size=14): + """ + Preset for body text. + """ + return get_font(size=size) + + +def button_med_font(size=14): + """ + Preset for button text. + """ + return get_font(size=size, weight="bold") + + +def button_small_font(size=12): + """ + Preset for button text. + """ + return get_font(size=size, weight="bold") + + +def small_font(size=12): + """ + Preset for small text, such as captions or footnotes. + """ + return get_font(size=size) + + +def micro_font(size=10): + """ + Preset for micro text, such as version stamps. + """ + return get_font(size=size) + + +def log_font(size=12): + """ + Preset for log text. + """ + return get_font(family="Cascadia Code", size=size) diff --git a/utils/misc.py b/utils/misc.py new file mode 100644 index 0000000..d2c221d --- /dev/null +++ b/utils/misc.py @@ -0,0 +1,12 @@ +import sys +from pathlib import Path + + +def base_path(): + # PyInstaller creates a temp folder and stores path in _MEIPASS + try: + # noinspection PyUnresolvedReferences,PyProtectedMember + return Path(sys._MEIPASS).resolve() + except AttributeError: + return Path().resolve() + diff --git a/utils/params.py b/utils/params.py new file mode 100644 index 0000000..be9567a --- /dev/null +++ b/utils/params.py @@ -0,0 +1,22 @@ + +class Strings: + + # login box, + ok = "ok" + cancel = "cancel" + login = "login" + password = "password" + + # message box + info = "Info" + warning = "Warning" + error = "Error" + yesno = "Question" + +class Icons: + + # message box + info = "ℹ️" + warning = "⚠️" + error = "❌" + yesno = "?"