import ttkbootstrap as ttk from ttkbootstrap.validation import add_range_validation, add_validation, validator from ttkbootstrap.constants import * from tkinter import Frame as tkFrame from tkinter import Label as tkLabel from ttkbootstrap import utility from collections import namedtuple from ttkbootstrap import colorutils from ttkbootstrap.colorutils import RGB, HSL, HEX, HUE, SAT, LUM from PIL import ImageColor from ttkbootstrap.dialogs.colordropper import ColorDropperDialog from ttkbootstrap.tooltip import ToolTip from ttkbootstrap.localization import MessageCatalog STD_SHADES = [0.9, 0.8, 0.7, 0.4, 0.3] STD_COLORS = [ '#FF0000', '#FFC000', '#FFFF00', '#00B050', '#0070C0', '#7030A0', '#FFFFFF', '#000000' ] ColorValues = namedtuple('ColorValues', 'h s l r g b hex') ColorChoice = namedtuple('ColorChoice', 'rgb hsl hex') PEN = '✛' @validator def validate_color(event): try: ImageColor.getrgb(event.postchangetext) return True except: return False class ColorChooser(ttk.Frame): """A class which creates a color chooser widget ![](../../assets/dialogs/querybox-get-color.png) """ def __init__(self, master, initialcolor=None, padding=None): super().__init__(master, padding=padding) self.tframe = ttk.Frame(self, padding=5) self.tframe.pack(fill=X) self.bframe = ttk.Frame(self, padding=(5, 0, 5, 5)) self.bframe.pack(fill=X) self.notebook = ttk.Notebook(self.tframe) self.notebook.pack(fill=BOTH) self.style = ttk.Style.get_instance() self.colors = self.style.colors self.initialcolor = initialcolor or self.colors.bg # color variables r, g, b = ImageColor.getrgb(self.initialcolor) h, s, l = colorutils.color_to_hsl((r, g, b), RGB) hx = colorutils.color_to_hex((r, g, b), RGB) self.hue = ttk.IntVar(value=h) self.sat = ttk.IntVar(value=s) self.lum = ttk.IntVar(value=l) self.red = ttk.IntVar(value=r) self.grn = ttk.IntVar(value=g) self.blu = ttk.IntVar(value=b) self.hex = ttk.StringVar(value=hx) # widget sizes (adjusted by widget scaling) self.spectrum_height = utility.scale_size(self, 240) self.spectrum_width = utility.scale_size(self, 530) # looks better on Mac OS #self.spectrum_width = utility.scale_size(self, 480) self.spectrum_point = utility.scale_size(self, 12) # build widgets spectrum_frame = ttk.Frame(self.notebook) self.color_spectrum = self.create_spectrum(spectrum_frame) self.color_spectrum.pack(fill=X, side=TOP) self.luminance_scale = self.create_luminance_scale(self.tframe) self.luminance_scale.pack(fill=X) self.notebook.add(spectrum_frame, text=MessageCatalog.translate('Advanced')) themed_colors = [self.colors.get(c) for c in self.style.colors] self.themed_swatches = self.create_swatches( self.notebook, themed_colors) self.standard_swatches = self.create_swatches( self.notebook, STD_COLORS) self.notebook.add(self.themed_swatches, text=MessageCatalog.translate('Themed')) self.notebook.add(self.standard_swatches, text=MessageCatalog.translate('Standard')) preview_frame = self.create_preview(self.bframe) preview_frame.pack(side=LEFT, fill=BOTH, expand=YES, padx=(0, 5)) self.color_entries = self.create_value_inputs(self.bframe) self.color_entries.pack(side=RIGHT) self.create_spectrum_indicator() self.create_luminance_indicator() def create_spectrum(self, master): """Create the color spectrum canvas""" # canvas and point dimensions width = self.spectrum_width height = self.spectrum_height xf = yf = self.spectrum_point # create canvas widget and binding canvas = ttk.Canvas(master, width=width, height=height, cursor='tcross') canvas.bind("", self.on_spectrum_interaction, add="+") canvas.bind("", self.on_spectrum_interaction, add="+") # add color points for x, colorx in enumerate(range(0, width, xf)): for y, colory in enumerate(range(0, height, yf)): values = self.color_from_coords(colorx, colory) fill = values.hex bbox = [x*xf, y*yf, (x*xf)+xf, (y*yf)+yf] canvas.create_rectangle(*bbox, fill=fill, width=0) return canvas def create_spectrum_indicator(self): """Create a square indicator that displays in the position of the selected color""" s = utility.scale_size(self, 10) width = utility.scale_size(self, 2) values = self.get_variables() x1, y1 = self.coords_from_color(values.hex) colorutils.contrast_color(values.hex, 'hex') tag = ['spectrum-indicator'] self.color_spectrum.create_rectangle( x1, y1, x1+s, y1+s, width=width, tags=[tag]) self.color_spectrum.tag_lower('spectrum-indicator') # widget builder methods def create_swatches(self, master, colors): """Create a grid of color swatches""" boxpadx = 2 boxpady = 0 padxtotal = (boxpadx*15) boxwidth = int((self.spectrum_width-padxtotal)) / len(STD_COLORS) boxheight = int((self.spectrum_height-boxpady) / (len(STD_SHADES)+1)) container = ttk.Frame(master) # create color combinations color_rows = [colors] lastcol = len(colors)-1 for l in STD_SHADES: lum = int(l*LUM) row = [] for color in colors: color = colorutils.update_hsl_value( color=color, lum=lum, inmodel='hex', outmodel='hex' ) row.append(color) color_rows.append(row) # themed colors - regular colors for row in color_rows: rowframe = ttk.Frame(container) for j, color in enumerate(row): swatch = tkFrame( master=rowframe, bg=color, width=boxwidth, height=boxheight, autostyle=False ) swatch.bind('', self.on_press_swatch) if j == 0: swatch.pack(side=LEFT, padx=(0, boxpadx)) elif j == lastcol: swatch.pack(side=LEFT, padx=(boxpadx, 0)) else: swatch.pack(side=LEFT, padx=boxpadx) rowframe.pack(fill=X, expand=YES) return container def create_preview(self, master): """Create the preview frame for original and new colors""" nbstyle = self.notebook.cget('style') # set the border color to match the notebook border color bordercolor = self.style.lookup(nbstyle, 'bordercolor') container = ttk.Frame(master) # the frame and label for the original color (current) old = tkFrame( master=container, relief=FLAT, bd=2, highlightthickness=1, highlightbackground=bordercolor, bg=self.initialcolor, autostyle=False ) old.pack(side=LEFT, fill=BOTH, expand=YES, padx=(0, 2)) contrastfg = colorutils.contrast_color( color=self.initialcolor, model='hex', ) tkLabel( master=old, text=MessageCatalog.translate('Current'), background=self.initialcolor, foreground=contrastfg, autostyle=False, width=7 ).pack(anchor=NW) # the frame and label for the new color self.preview = tkFrame( master=container, relief=FLAT, bd=2, highlightthickness=1, highlightbackground=bordercolor, bg=self.initialcolor, autostyle=False ) self.preview.pack(side=LEFT, fill=BOTH, expand=YES, padx=(2, 0)) self.preview_lbl = tkLabel( master=self.preview, text=MessageCatalog.translate('New'), background=self.initialcolor, foreground=contrastfg, autostyle=False, width=7 ) self.preview_lbl.pack(anchor=NW) return container def create_value_inputs(self, master): """Create color value input widgets""" container = ttk.Frame(master) for x in range(4): container.columnconfigure(x, weight=1) # value labels lbl_cnf = {'master': container, 'anchor': E} ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Hue')}:''').grid(row=0, column=0, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Sat')}:''').grid(row=1, column=0, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Lum')}:''').grid(row=2, column=0, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Hex')}:''').grid(row=3, column=0, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Red')}:''').grid(row=0, column=2, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Green')}:''').grid(row=1, column=2, sticky=E) ttk.Label(**lbl_cnf, text=f'''{MessageCatalog.translate('Blue')}:''').grid(row=2, column=2, sticky=E) # value spinners and entry widgets rgb_cnf = {'master': container, 'from_': 0, 'to': 255, 'width': 3} sl_cnf = {'master': container, 'from_': 0, 'to': 100, 'width': 3} hue_cnf = {'master': container, 'from_': 0, 'to': 360, 'width': 3} sb_hue = ttk.Spinbox(**hue_cnf, textvariable=self.hue) sb_hue.grid(row=0, column=1, padx=4, pady=2, sticky=EW) sb_sat = ttk.Spinbox(**sl_cnf, textvariable=self.sat) sb_sat.grid(row=1, column=1, padx=4, pady=2, sticky=EW) sb_lum = ttk.Spinbox(**sl_cnf, textvariable=self.lum) sb_lum.grid(row=2, column=1, padx=4, pady=2, sticky=EW) sb_red = ttk.Spinbox(**rgb_cnf, textvariable=self.red) sb_red.grid(row=0, column=3, padx=4, pady=2, sticky=EW) sb_grn = ttk.Spinbox(**rgb_cnf, textvariable=self.grn) sb_grn.grid(row=1, column=3, padx=4, pady=2, sticky=EW) sb_blu = ttk.Spinbox(**rgb_cnf, textvariable=self.blu) sb_blu.grid(row=2, column=3, padx=4, pady=2, sticky=EW) ent_hex = ttk.Entry(container, textvariable=self.hex) ent_hex.grid(row=3, column=1, padx=4, columnspan=3, pady=2, sticky=EW) # add input validation add_validation(ent_hex, validate_color) add_range_validation(sb_hue, 0, 360) for sb in [sb_sat, sb_lum]: add_range_validation(sb, 0, 100) for sb in [sb_red, sb_grn, sb_blu]: add_range_validation(sb, 0, 255) # event binding for updating colors on value change for sb in [sb_hue, sb_sat, sb_lum]: for sequence in ['<>', '<>', '', '']: sb.bind( sequence=sequence, func=lambda _, w=sb: self.on_entry_value_change( w, HSL), add="+" ) for sb in [sb_red, sb_grn, sb_blu]: for sequence in ['<>', '<>', '', '']: sb.bind( sequence=sequence, func=lambda _, w=sb: self.on_entry_value_change( w, RGB), add="+" ) for sequence in ['', '']: ent_hex.bind( sequence=sequence, func=lambda _, w=ent_hex: self.on_entry_value_change( w, HEX), add="+" ) return container def create_luminance_scale(self, master): """Create the color luminance canvas""" # widget dimensions height = xf = self.spectrum_point width = self.spectrum_width values = self.get_variables() canvas = ttk.Canvas(master, height=height, width=width) # add color points to scale for x, l in enumerate(range(0, width, xf)): lum = l/width*LUM fill = colorutils.update_hsl_value( color=values.hex, lum=lum, inmodel='hex', outmodel='hex' ) bbox = [x*xf, 0, (x*xf)+xf, height] tag = f'color{x}' canvas.create_rectangle(*bbox, fill=fill, width=0, tags=[tag]) canvas.bind("", self.on_luminance_interaction, add="+") canvas.bind("", self.on_luminance_interaction, add="+") return canvas def create_luminance_indicator(self): """Create an indicator that displays in the position of the luminance value""" lum = 50 x1 = int(lum / LUM * self.spectrum_width) - \ ((self.spectrum_point - 2)//2) y1 = 0 x2 = x1 + self.spectrum_point y2 = self.spectrum_point - 3 tag = 'luminance-indicator' bbox = [x1, y1, x2, y2] self.luminance_scale.create_rectangle( *bbox, fill='white', outline='black', tags=[tag]) self.luminance_scale.tag_lower(tag) def coords_from_color(self, hexcolor): """Get the coordinates on the color spectrum from the color value""" h, s, _ = colorutils.color_to_hsl(hexcolor) x = (h / HUE) * self.spectrum_width y = (1-(s / SAT)) * self.spectrum_height return x, y def color_from_coords(self, x, y): """Get the color value from the mouse position in the color spectrum""" HEIGHT = self.spectrum_height WIDTH = self.spectrum_width h = int(min(HUE, max(0, (HUE/WIDTH) * x))) s = int(min(SAT, max(0, SAT - ((SAT/HEIGHT) * y)))) l = 50 hx = colorutils.color_to_hex([h, s, l], 'hsl') r, g, b = colorutils.color_to_rgb(hx) return ColorValues(h, s, l, r, g, b, hx) def set_variables(self, h, s, l, r, g, b, hx): """Update the color value variables""" self.hue.set(h) self.sat.set(s) self.lum.set(l) self.red.set(r) self.grn.set(g) self.blu.set(b) self.hex.set(hx) def get_variables(self): """Get the values of all color models and return a tuple of color values""" h = self.hue.get() s = self.sat.get() l = self.lum.get() r = self.red.get() g = self.grn.get() b = self.blu.get() hx = self.hex.get() return ColorValues(h, s, l, r, g, b, hx) def update_preview(self): """Update the color in the preview frame""" hx = self.hex.get() fg = colorutils.contrast_color( color=hx, model='hex', ) self.preview.configure(bg=hx) self.preview_lbl.configure(bg=hx, fg=fg) def update_luminance_scale(self): """Update the luminance scale with the change in hue and saturation""" values = self.get_variables() width = self.spectrum_width xf = self.spectrum_point for x, l in enumerate(range(0, width, xf)): lum = l/width*LUM fill = colorutils.update_hsl_value( color=values.hex, lum=lum, inmodel='hex', outmodel='hex' ) tag = f'color{x}' self.luminance_scale.itemconfig(tag, fill=fill) def update_luminance_indicator(self): """Update the position of the luminance indicator""" lum = self.lum.get() x = int(lum / LUM * self.spectrum_width) - \ ((self.spectrum_point - 2)//2) self.luminance_scale.moveto('luminance-indicator', x, 0) self.luminance_scale.tag_raise('luminance-indicator') def update_spectrum_indicator(self): """Move the spectrum indicator to a new location""" values = self.get_variables() x, y = self.coords_from_color(values.hex) # move to the new color location self.color_spectrum.moveto('spectrum-indicator', x, y) self.color_spectrum.tag_raise('spectrum-indicator') # adjust the outline color based on contrast of background color = colorutils.contrast_color(values.hex, 'hex') self.color_spectrum.itemconfig('spectrum-indicator', outline=color) # color events def sync_color_values(self, model): """Callback for when a color value changes. A change in one value will automatically update the other values so that all color models remain in sync.""" values = self.get_variables() if model == HEX: hx = values.hex r, g, b = colorutils.color_to_rgb(hx) h, s, l = colorutils.color_to_hsl(hx) elif model == RGB: r, g, b = values.r, values.g, values.b h, s, l = colorutils.color_to_hsl([r, g, b], 'rgb') hx = colorutils.color_to_hex([r, g, b]) elif model == HSL: h, s, l = values.h, values.s, values.l r, g, b = colorutils.color_to_rgb([h, s, l], 'hsl') hx = colorutils.color_to_hex([h, s, l], 'hsl') self.set_variables(h, s, l, r, g, b, hx) self.update_preview() self.update_luminance_indicator() def on_entry_value_change(self, widget: ttk.Spinbox, model): """Update the widget colors when the color value input is changed""" is_valid = widget.validate() if is_valid: self.sync_color_values(model) self.update_luminance_scale() self.update_spectrum_indicator() def on_press_swatch(self, event): """Update the widget colors when a color swatch is clicked.""" button: tkFrame = self.nametowidget(event.widget) color = button.cget('background') self.hex.set(color) self.sync_color_values(HEX) self.update_luminance_scale() self.update_spectrum_indicator() def on_spectrum_interaction(self, event): """Update the widget colors when the color spectrum canvas is pressed""" values = self.color_from_coords(event.x, event.y) self.hue.set(values.h) self.sat.set(values.s) self.lum.set(values.l) self.sync_color_values(HSL) self.update_luminance_scale() self.update_spectrum_indicator() def on_luminance_interaction(self, event): """Update the widget colors when the color luminance scale is pressed""" l = max(0, min(LUM, int((event.x / self.spectrum_width) * LUM))) self.lum.set(l) self.sync_color_values(HSL) from ttkbootstrap.dialogs import Dialog class ColorChooserDialog(Dialog): """A class which displays a color chooser dialog. When a color option is selected and the "OK" button is pressed, the dialog will return a namedtuple that contains the color values for rgb, hsl, and hex. These values can be accessed by indexing the tuple or by using the named fields. ![](../../assets/dialogs/querybox-get-color.png) Examples: ```python >>> cd = ColorChooserDialog() >>> cd.show() >>> colors = cd.result >>> colors.hex '#5fb04f' >>> colors[2] '#5fb04f >>> colors.rgb (95, 176, 79) >>> colors[0] (95, 176, 79) ``` """ def __init__(self, parent=None, title="Color Chooser", initialcolor=None): title = MessageCatalog.translate(title) super().__init__(parent=parent, title=title) self.initialcolor = initialcolor self.dropper = ColorDropperDialog() self.dropper.result.trace_add('write', self.trace_dropper_color) def create_body(self, master): self.colorchooser = ColorChooser(master, self.initialcolor) self.colorchooser.pack(fill=BOTH, expand=YES) def create_buttonbox(self, master): frame = ttk.Frame(master, padding=(5, 5)) # OK button ok = ttk.Button(frame, bootstyle=PRIMARY, width=6, text=MessageCatalog.translate('OK')) ok.bind("", lambda _: ok.invoke()) ok.configure(command=lambda b=ok: self.on_button_press(b)) ok.pack(padx=2, side=RIGHT) # Cancel button cancel = ttk.Button(frame, bootstyle=SECONDARY, width=6, text=MessageCatalog.translate('Cancel')) cancel.bind("", lambda _: cancel.invoke()) cancel.configure(command=lambda b=cancel: self.on_button_press(b)) cancel.pack(padx=2, side=RIGHT) # color dropper (not supported on Mac OS) if self._toplevel.winsys != 'aqua': dropper = ttk.Label(frame, text=PEN, font=('-size 16')) ToolTip(dropper, MessageCatalog.translate('color dropper')) # add tooltip dropper.pack(side=RIGHT, padx=2) dropper.bind("", self.on_show_colordropper) frame.pack(side=BOTTOM, fill=X, anchor=S) def on_show_colordropper(self, event): self.dropper.show() def trace_dropper_color(self, *_): values = self.dropper.result.get() self.colorchooser.hex.set(values[2]) self.colorchooser.sync_color_values('hex') def on_button_press(self, button): if button.cget('text') == 'OK': values = self.colorchooser.get_variables() self._result = ColorChoice( rgb=(values.r, values.g, values.b), hsl=(values.h, values.s, values.l), hex=values.hex ) self._toplevel.destroy() self._toplevel.destroy()