Исходный код EJIO.ctk_extender.components.color_picker

## /ctk_extender/components/color_picker.py

import customtkinter as ctk
from tkinter import colorchooser
from typing import Any, Callable
from ..widget import Widget
from EJIO.utils import Color

__all__ = [
    'ColorPicker',
]


[документация] class ColorPicker(Widget): """ Интерактивный виджет выбора цвета. Поддерживает ввод HEX, RGB целых чисел и вызов палитры ОС. """ __slots__ = ( '_color', '_on_change_callback', '_preview_canvas', '_hex_entry', '_r_entry', '_g_entry', '_b_entry', '_is_updating' ) def __init__( self, parent: Any, initial_color: str | Color, on_change: Callable[[Color], None], geometry: Any = None, **kwargs: Any ) -> None: self._color: Color = initial_color if isinstance(initial_color, Color) else Color(initial_color) self._on_change_callback: Callable[[Color], None] = on_change # Флаг защиты от бесконечной рекурсии при взаимном обновлении полей ввода self._is_updating: bool = False self._preview_canvas: ctk.CTkCanvas | None = None self._hex_entry: ctk.CTkEntry | None = None self._r_entry: ctk.CTkEntry | None = None self._g_entry: ctk.CTkEntry | None = None self._b_entry: ctk.CTkEntry | None = None super().__init__(parent, geometry, **kwargs)
[документация] def build(self) -> ctk.CTkFrame: base_frame = ctk.CTkFrame(self.parent, fg_color="transparent") # 1. Квадрат-превью текущего цвета self._preview_canvas = ctk.CTkCanvas(base_frame, width=36, height=36, highlightthickness=1, bg=self._color.hex, bd=0) self._preview_canvas.pack(side="left", padx=(0, 10)) self._preview_canvas.bind("<Button-1>", lambda e: self._open_palette()) # 2. Инпут HEX формата ctk.CTkLabel(base_frame, text="#", font=("Arial", 12, "bold")).pack(side="left") self._hex_entry = ctk.CTkEntry(base_frame, width=70, font=("Consolas", 12)) self._hex_entry.insert(0, self._color.hex.lstrip('#')) self._hex_entry.pack(side="left", padx=(2, 10)) self._hex_entry.bind("<KeyRelease>", lambda e: self._on_hex_changed()) # 3. Инпуты RGB формата self._r_entry = self._create_rgb_field(base_frame, "R:", self._color.rgba[0], self._on_rgb_changed) self._g_entry = self._create_rgb_field(base_frame, "G:", self._color.rgba[1], self._on_rgb_changed) self._b_entry = self._create_rgb_field(base_frame, "B:", self._color.rgba[2], self._on_rgb_changed) # 4. Кнопка вызова палитры ОС palette_btn = ctk.CTkButton(base_frame, text="🎨", width=36, command=self._open_palette) palette_btn.pack(side="left", padx=(5, 0)) return base_frame
def _create_rgb_field( self, parent_frame: ctk.CTkFrame, label_text: str, val: int, callback: Callable[[], None] ) -> ctk.CTkEntry: """Внутренний хелпер создания полей R, G, B.""" ctk.CTkLabel(parent_frame, text=label_text, font=("Arial", 11, "bold")).pack(side="left", padx=(4, 1)) entry = ctk.CTkEntry(parent_frame, width=38, font=("Consolas", 11), justify="center") entry.insert(0, str(val)) entry.pack(side="left") entry.bind("<KeyRelease>", lambda e: callback()) return entry def _open_palette(self) -> None: """Открывает нативный диалог выбора цвета ОС.""" chosen_color = colorchooser.askcolor(initialcolor=self._color.hex, title="Выберите цвет") if chosen_color and chosen_color[1]: self._update_color_state(Color(chosen_color[1])) def _on_hex_changed(self) -> None: if self._is_updating or not self._hex_entry: return raw_text = self._hex_entry.get().strip() if len(raw_text) == 6: try: self._update_color_state(Color(f"#{raw_text}")) except Exception: pass def _on_rgb_changed(self) -> None: if self._is_updating or not self._r_entry or not self._g_entry or not self._b_entry: return try: r = int(self._r_entry.get() or 0) g = int(self._g_entry.get() or 0) b = int(self._b_entry.get() or 0) self._update_color_state(Color(r, g, b)) except Exception: pass def _update_color_state(self, new_color: Color) -> None: """Синхронизирует состояние всех полей ввода с новым цветом.""" self._color = new_color self._is_updating = True try: if self._preview_canvas: self._preview_canvas.configure(bg=self._color.hex) if self._hex_entry: self._hex_entry.delete(0, "end") self._hex_entry.insert(0, self._color.hex.lstrip('#')) if self._r_entry and self._g_entry and self._b_entry: r, g, b, _ = self._color.rgba self._r_entry.delete(0, "end") self._r_entry.insert(0, str(r)) self._g_entry.delete(0, "end") self._g_entry.insert(0, str(g)) self._b_entry.delete(0, "end") self._b_entry.insert(0, str(b)) # Прокидываем событие наверх прикладной логике self._on_change_callback(self._color) finally: self._is_updating = False