## /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