font character mapping for Windows 11, added , refined CTkDrawEngineCTkRadioButton

This commit is contained in:
TomSchimansky 2022-03-05 02:01:07 +01:00
parent eab428f9ef
commit 6c976245aa
14 changed files with 500 additions and 101 deletions

View File

@ -8,6 +8,7 @@ from .customtkinter_progressbar import CTkProgressBar
from .customtkinter_label import CTkLabel from .customtkinter_label import CTkLabel
from .customtkinter_entry import CTkEntry from .customtkinter_entry import CTkEntry
from .customtkinter_checkbox import CTkCheckBox from .customtkinter_checkbox import CTkCheckBox
from .customtkinter_radiobutton import CTkRadioButton
from .customtkinter_tk import CTk from .customtkinter_tk import CTk
from .customtkinter_canvas import CTkCanvas from .customtkinter_canvas import CTkCanvas
from .customtkinter_toplevel import CTkToplevel from .customtkinter_toplevel import CTkToplevel
@ -70,9 +71,9 @@ if not sys.platform == "darwin":
# load text fonts and custom font with circle shapes for round corner rendering # load text fonts and custom font with circle shapes for round corner rendering
script_directory = os.path.dirname(os.path.abspath(__file__)) script_directory = os.path.dirname(os.path.abspath(__file__))
pyglet.font.add_file(os.path.join(script_directory, "assets", "CustomTkinter_shapes_font-fine.otf")) pyglet.font.add_file(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf"))
pyglet.font.add_file(os.path.join(script_directory, "assets", "Roboto", "Roboto-Regular.ttf")) pyglet.font.add_file(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
pyglet.font.add_file(os.path.join(script_directory, "assets", "Roboto", "Roboto-Medium.ttf")) pyglet.font.add_file(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
CTkSettings.circle_font_is_ready = pyglet.font.have_font("CustomTkinter_shapes_font") CTkSettings.circle_font_is_ready = pyglet.font.have_font("CustomTkinter_shapes_font")
warnings.simplefilter("default") warnings.simplefilter("default")

View File

@ -1,25 +1,25 @@
{ {
"color": { "color": {
"window_bg_color": ["#ECECEC", "gray14"], "window_bg_color": ["gray92", "gray12"],
"button": ["#3599D6", "#1C94CF"], "button": ["#1FA6E8", "#1C94CF"],
"button_hover": ["#A7C2E0", "#5FB4DD"], "button_hover": ["#1A89BF", "#1673A1"],
"button_border": ["gray25", "gray86"], "button_border": ["gray25", "gray86"],
"checkbox_border": ["gray20", "#ededed"], "checkbox_border": ["gray40", "gray60"],
"entry": ["white", "gray10"], "entry": ["white", "gray24"],
"entry_border": ["gray65", "gray35"], "entry_border": ["gray70", "gray32"],
"entry_placeholder_text": ["gray52", "gray62"], "entry_placeholder_text": ["gray52", "gray62"],
"frame_border": ["#A7C2E0", "#5FB4DD"], "frame_border": ["#A7C2E0", "#5FB4DD"],
"frame_low": ["#D4D5D6", "gray20"], "frame_low": ["gray87", "gray18"],
"frame_high": ["gray77", "gray24"], "frame_high": ["gray82", "gray22"],
"label": [null, null], "label": [null, null],
"text": ["gray20", "gray90"], "text": ["gray20", "gray90"],
"progressbar": ["#6B6B6B", "#222222"], "progressbar": ["#6B6B6B", "#222222"],
"progressbar_progress": ["#3599D6", "#1C94CF"], "progressbar_progress": ["#3599D6", "#1C94CF"],
"progressbar_border": ["gray", "gray"], "progressbar_border": ["gray", "gray"],
"slider": ["#6B6B6B", "#222222"], "slider": ["#6B6B6B", "#222222"],
"slider_progress": ["#A5A6A5", "#555555"], "slider_progress": ["white", "#555555"],
"slider_button": ["#3599D6", "#1C94CF"], "slider_button": ["#3599D6", "#1C94CF"],
"slider_button_hover": ["#A7C2E0", "#5FB4DD"], "slider_button_hover": ["#1A89BF", "#1673A1"],
"darken_factor": 0.8 "darken_factor": 0.8
}, },
@ -43,15 +43,18 @@
"button_border_width": 0, "button_border_width": 0,
"checkbox_corner_radius": 7, "checkbox_corner_radius": 7,
"checkbox_border_width": 3, "checkbox_border_width": 3,
"radiobutton_corner_radius": 1000,
"radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6,
"entry_border_width": 2, "entry_border_width": 2,
"frame_corner_radius": 10, "frame_corner_radius": 10,
"frame_border_width": 0, "frame_border_width": 0,
"label_corner_radius": 8, "label_corner_radius": 8,
"progressbar_border_width": 0, "progressbar_border_width": 0,
"progressbar_corner_radius": 100, "progressbar_corner_radius": 1000,
"slider_border_width": 6, "slider_border_width": 6,
"slider_corner_radius": 8, "slider_corner_radius": 8,
"slider_button_length": 0, "slider_button_length": 0,
"slider_button_corner_radius": 100 "slider_button_corner_radius": 1000
} }
} }

View File

@ -4,13 +4,7 @@ from .customtkinter_settings import CTkSettings
class CTkCanvas(tkinter.Canvas): class CTkCanvas(tkinter.Canvas):
# This dict maps a corner_radius of a circle to a specific font character, which is circle shape which fills the space radius_to_char_fine = CTkSettings.radius_to_char_fine # dict to map radius to font circle character
# of one monospace character to a specific amount from 100% to 90% (A to I).
radius_to_char = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B', 10: 'B',
9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', 11: 'C', 10: 'C',
9: 'D', 8: 'D', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H', 0: 'A'}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -67,7 +67,7 @@ class CTkDrawEngine:
if self._rendering_method == "polygon_shapes": if self._rendering_method == "polygon_shapes":
return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
elif self._rendering_method == "font_shapes": elif self._rendering_method == "font_shapes":
return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, (), symmetric_circles=True) return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
elif self._rendering_method == "circle_shapes": elif self._rendering_method == "circle_shapes":
return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius) return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius)
@ -125,33 +125,42 @@ class CTkDrawEngine:
return requires_recoloring return requires_recoloring
def _draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int, def _draw_rounded_rect_with_border_font_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int,
exclude_parts: tuple, symmetric_circles: bool = True) -> bool: exclude_parts: tuple) -> bool:
requires_recoloring = False requires_recoloring = False
# create border button parts # create border button parts
if border_width > 0: if border_width > 0:
if corner_radius > 0: if corner_radius > 0:
# create canvas border corner parts if not already created # create canvas border corner parts if not already created, but only if needed, and delete if not needed
if not self._canvas.find_withtag("border_oval_1_a"): if not self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_1_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) requires_recoloring = True
elif self._canvas.find_withtag("border_oval_1_a") and "border_oval_1" in exclude_parts:
self._canvas.delete("border_oval_1_a", "border_oval_1_b")
if not self._canvas.find_withtag("border_oval_2_a") and width > 2 * corner_radius and "border_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_2_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("border_oval_2_a") and (not width > 2 * corner_radius or "border_oval_2" in exclude_parts):
self._canvas.delete("border_oval_2_a", "border_oval_2_b")
if not self._canvas.find_withtag("border_oval_3_a") and round(corner_radius) * 2 < height: if not self._canvas.find_withtag("border_oval_3_a") and height > 2 * corner_radius \
and width > 2 * corner_radius and "border_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
if symmetric_circles:
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("border_oval_3_a") and (not (height > 2 * corner_radius
and width > 2 * corner_radius) or "border_oval_3" in exclude_parts):
self._canvas.delete("border_oval_3_a", "border_oval_3_b")
elif self._canvas.find_withtag("border_oval_3_a") and not round(corner_radius) * 2 < height: if not self._canvas.find_withtag("border_oval_4_a") and height > 2 * corner_radius and "border_oval_4" not in exclude_parts:
self._canvas.delete(["border_oval_3_a", "border_oval_3_b", "border_oval_4_a", "border_oval_4_b"]) self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_a", "border_corner_part", "border_parts"), anchor=tkinter.CENTER)
self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_4_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True
elif self._canvas.find_withtag("border_oval_4_a") and (not height > 2 * corner_radius or "border_oval_4" in exclude_parts):
self._canvas.delete("border_oval_4_a", "border_oval_4_b")
# change position of border corner parts # change position of border corner parts
self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius) self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius)
@ -182,33 +191,35 @@ class CTkDrawEngine:
# create inner button parts # create inner button parts
if inner_corner_radius > 0: if inner_corner_radius > 0:
# create canvas border corner parts if not already created # create canvas border corner parts if not already created, but only if they're needed and delete if not needed
if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts: if not self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_1_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_1_a") and "inner_oval_1" in exclude_parts:
self._canvas.delete("inner_oval_1_a", "inner_oval_1_b")
if not self._canvas.find_withtag("inner_oval_2_a") and "inner_oval_2" not in exclude_parts: if not self._canvas.find_withtag("inner_oval_2_a") and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_2" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_2_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_2_a") and (not width - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_2" in exclude_parts):
self._canvas.delete("inner_oval_2_a", "inner_oval_2_b")
if not self._canvas.find_withtag("inner_oval_3_a") and round(inner_corner_radius) * 2 < height - (2 * border_width) and "inner_oval_3" not in exclude_parts: if not self._canvas.find_withtag("inner_oval_3_a") and height - (2 * border_width) > 2 * inner_corner_radius \
and width - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_3" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_3_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_3_a") and not round(inner_corner_radius) * 2 < height - (2 * border_width): elif self._canvas.find_withtag("inner_oval_3_a") and (not (height - (2 * border_width) > 2 * inner_corner_radius
and width - (2 * border_width) > 2 * inner_corner_radius) or "inner_oval_3" in exclude_parts):
self._canvas.delete("inner_oval_3_a", "inner_oval_3_b") self._canvas.delete("inner_oval_3_a", "inner_oval_3_b")
if not self._canvas.find_withtag("inner_oval_4_a") and round(inner_corner_radius) * 2 < height - (2 * border_width) and "inner_oval_4" not in exclude_parts: if not self._canvas.find_withtag("inner_oval_4_a") and height - (2 * border_width) > 2 * inner_corner_radius and "inner_oval_4" not in exclude_parts:
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER) self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_a", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER)
if symmetric_circles: self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
self._canvas.create_aa_circle(0, 0, 0, tags=("inner_oval_4_b", "inner_corner_part", "inner_parts"), anchor=tkinter.CENTER, angle=180)
requires_recoloring = True requires_recoloring = True
elif self._canvas.find_withtag("inner_oval_4_a") and not round(inner_corner_radius) * 2 < height - (2 * border_width): elif self._canvas.find_withtag("inner_oval_4_a") and (not height - (2 * border_width) > 2 * inner_corner_radius or "inner_oval_4" in exclude_parts):
self._canvas.delete("inner_oval_4_a", "inner_oval_4_b") self._canvas.delete("inner_oval_4_a", "inner_oval_4_b")
# change position of border corner parts # change position of border corner parts

View File

@ -21,7 +21,7 @@ class CTkProgressBar(tkinter.Frame):
progress_color="default_theme", progress_color="default_theme",
corner_radius="default_theme", corner_radius="default_theme",
width=200, width=200,
height=10, height=8,
border_width="default_theme", border_width="default_theme",
**kwargs): **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@ -0,0 +1,343 @@
import tkinter
import sys
from customtkinter.customtkinter_tk import CTk
from customtkinter.customtkinter_frame import CTkFrame
from customtkinter.appearance_mode_tracker import AppearanceModeTracker
from customtkinter.customtkinter_theme_manager import CTkThemeManager
from customtkinter.customtkinter_canvas import CTkCanvas
from customtkinter.customtkinter_settings import CTkSettings
from customtkinter.customtkinter_draw_engine import CTkDrawEngine
class CTkRadioButton(tkinter.Frame):
def __init__(self, *args,
bg_color=None,
fg_color="default_theme",
hover_color="default_theme",
border_color="default_theme",
border_width_unchecked="default_theme",
border_width_checked="default_theme",
width=22,
height=22,
corner_radius="default_theme",
text_font="default_theme",
text_color="default_theme",
text="CTkRadioButton",
hover=True,
command=None,
state=tkinter.NORMAL,
value=0,
variable=None,
textvariable=None,
**kwargs):
super().__init__(*args, **kwargs)
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget too
if isinstance(self.master, (tkinter.Tk, tkinter.Frame)) and not isinstance(self.master, (CTk, CTkFrame)):
master_old_configure = self.master.config
def new_configure(*args, **kwargs):
if "bg" in kwargs:
self.configure(bg_color=kwargs["bg"])
elif "background" in kwargs:
self.configure(bg_color=kwargs["background"])
# args[0] is dict when attribute gets changed by widget[<attribut>] syntax
elif len(args) > 0 and type(args[0]) == dict:
if "bg" in args[0]:
self.configure(bg_color=args[0]["bg"])
elif "background" in args[0]:
self.configure(bg_color=args[0]["background"])
master_old_configure(*args, **kwargs)
self.master.config = new_configure
self.master.configure = new_configure
AppearanceModeTracker.add(self.set_appearance_mode, self)
self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color
self.fg_color = CTkThemeManager.theme["color"]["button"] if fg_color == "default_theme" else fg_color
self.hover_color = CTkThemeManager.theme["color"]["button_hover"] if hover_color == "default_theme" else hover_color
self.border_color = CTkThemeManager.theme["color"]["checkbox_border"] if border_color == "default_theme" else border_color
self.width = width
self.height = height
self.corner_radius = CTkThemeManager.theme["shape"]["radiobutton_corner_radius"] if corner_radius == "default_theme" else corner_radius
self.border_width_unchecked = CTkThemeManager.theme["shape"]["radiobutton_border_width_unchecked"] if border_width_unchecked == "default_theme" else border_width_unchecked
self.border_width_checked = CTkThemeManager.theme["shape"]["radiobutton_border_width_checked"] if border_width_checked == "default_theme" else border_width_checked
self.border_width = self.border_width_unchecked
if self.corner_radius*2 > self.height:
self.corner_radius = self.height/2
elif self.corner_radius*2 > self.width:
self.corner_radius = self.width/2
if self.corner_radius >= self.border_width:
self.inner_corner_radius = self.corner_radius - self.border_width
else:
self.inner_corner_radius = 0
self.text = text
self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color
self.text_font = (CTkThemeManager.theme["text"]["font"], CTkThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font
self.function = command
self.state = state
self.hover = hover
self.check_state = False
self.value = value
self.variable: tkinter.Variable = variable
self.variable_callback_blocked = False
self.textvariable = textvariable
self.variable_callback_name = None
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width,
height=self.height)
self.canvas.pack(side='left')
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
if sys.platform == "darwin" and self.state == tkinter.NORMAL and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self.state == tkinter.NORMAL and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="hand2")
if self.hover is True:
self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave)
self.canvas.bind("<Button-1>", self.invoke)
self.canvas.bind("<Button-1>", self.invoke)
self.text_label = None
self.draw() # initial draw
if self.variable is not None:
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
super().destroy()
def detect_color_of_master(self):
if isinstance(self.master, CTkFrame):
return self.master.fg_color
else:
return self.master.cget("bg")
def draw(self):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width, self.height, self.corner_radius, self.border_width)
self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
self.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.check_state is False:
self.canvas.itemconfig("border_parts",
outline=CTkThemeManager.single_color(self.border_color, self.appearance_mode),
fill=CTkThemeManager.single_color(self.border_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts",
outline=CTkThemeManager.single_color(self.fg_color, self.appearance_mode),
fill=CTkThemeManager.single_color(self.fg_color, self.appearance_mode))
self.canvas.itemconfig("inner_parts",
outline=CTkThemeManager.single_color(self.bg_color, self.appearance_mode),
fill=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
if self.text_label is None:
self.text_label = tkinter.Label(master=self,
text=self.text,
justify=tkinter.LEFT,
width=len(self.text_color),
font=self.text_font)
self.text_label.pack(side='right', padx=6)
self.text_label["anchor"] = "w"
self.text_label.configure(fg=CTkThemeManager.single_color(self.text_color, self.appearance_mode))
self.text_label.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
self.set_text(self.text)
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw()
if "text" in kwargs:
self.set_text(kwargs["text"])
del kwargs["text"]
if "state" in kwargs:
self.set_state(kwargs["state"])
del kwargs["state"]
if "fg_color" in kwargs:
self.fg_color = kwargs["fg_color"]
require_redraw = True
del kwargs["fg_color"]
if "bg_color" in kwargs:
if kwargs["bg_color"] is None:
self.bg_color = self.detect_color_of_master()
else:
self.bg_color = kwargs["bg_color"]
require_redraw = True
del kwargs["bg_color"]
if "hover_color" in kwargs:
self.hover_color = kwargs["hover_color"]
require_redraw = True
del kwargs["hover_color"]
if "text_color" in kwargs:
self.text_color = kwargs["text_color"]
require_redraw = True
del kwargs["text_color"]
if "border_color" in kwargs:
self.border_color = kwargs["border_color"]
require_redraw = True
del kwargs["border_color"]
if "border_width" in kwargs:
self.border_width = kwargs["border_width"]
require_redraw = True
del kwargs["border_width"]
if "command" in kwargs:
self.function = kwargs["command"]
del kwargs["command"]
if "variable" in kwargs:
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
self.variable = kwargs["variable"]
if self.variable is not None and self.variable != "":
self.variable_callback_name = self.variable.trace_add("write", self.variable_callback)
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
else:
self.variable = None
del kwargs["variable"]
super().configure(*args, **kwargs)
if require_redraw:
self.draw()
def set_state(self, state):
self.state = state
if self.state == tkinter.DISABLED:
self.hover = False
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("sys") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
self.hover = True
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("sys") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="hand2")
self.draw()
def set_text(self, text):
self.text = text
if self.text_label is not None:
self.text_label.configure(text=self.text, width=len(self.text))
else:
sys.stderr.write("ERROR (CTkButton): Cant change text because button has no text.")
def on_enter(self, event=0):
if self.hover is True:
self.canvas.itemconfig("border_parts",
fill=CTkThemeManager.single_color(self.hover_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.hover_color, self.appearance_mode))
def on_leave(self, event=0):
if self.hover is True:
if self.check_state is True:
self.canvas.itemconfig("border_parts",
fill=CTkThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.fg_color, self.appearance_mode))
else:
self.canvas.itemconfig("border_parts",
fill=CTkThemeManager.single_color(self.border_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.border_color, self.appearance_mode))
def variable_callback(self, var_name, index, mode):
if not self.variable_callback_blocked:
if self.variable.get() == self.value:
self.select(from_variable_callback=True)
else:
self.deselect(from_variable_callback=True)
def invoke(self, event=0):
if self.function is not None:
self.function()
if self.state == tkinter.NORMAL:
if self.check_state is False:
self.check_state = True
self.select()
def select(self, from_variable_callback=False):
self.check_state = True
self.canvas.itemconfig("border_parts",
fill=CTkThemeManager.single_color(self.fg_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.fg_color, self.appearance_mode))
self.border_width = self.border_width_checked
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set(self.value)
self.variable_callback_blocked = False
def deselect(self, from_variable_callback=False):
self.check_state = False
self.canvas.itemconfig("border_parts",
fill=CTkThemeManager.single_color(self.border_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.border_color, self.appearance_mode))
self.border_width = self.border_width_unchecked
self.draw()
if self.variable is not None and not from_variable_callback:
self.variable_callback_blocked = True
self.variable.set("")
self.variable_callback_blocked = False
def set_appearance_mode(self, mode_string):
if mode_string.lower() == "dark":
self.appearance_mode = 1
elif mode_string.lower() == "light":
self.appearance_mode = 0
if isinstance(self.master, (CTkFrame, CTk)):
self.bg_color = self.master.fg_color
else:
self.bg_color = self.master.cget("bg")
self.draw()

View File

@ -1,4 +1,5 @@
import sys import sys
import platform
class CTkSettings: class CTkSettings:
@ -8,6 +9,28 @@ class CTkSettings:
hand_cursor_enabled = True hand_cursor_enabled = True
preferred_drawing_method = None preferred_drawing_method = None
radius_to_char_fine = None
@classmethod
def init_font_character_mapping(cls):
radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B', 10: 'B',
9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', 11: 'C', 10: 'C',
9: 'D', 8: 'D', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', 11: 'D', 10: 'D',
9: 'D', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R', 0: 'A'}
if sys.platform.startswith("win"):
if sys.getwindowsversion().build > 20000: # Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_11
else: # < Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_10
else: # macOS and Linux
cls.radius_to_char_fine = radius_to_char_fine_windows_10
@classmethod @classmethod
def init_drawing_method(cls): def init_drawing_method(cls):
""" possible: 'polygon_shapes', 'font_shapes', 'circle_shapes' """ """ possible: 'polygon_shapes', 'font_shapes', 'circle_shapes' """
@ -17,5 +40,15 @@ class CTkSettings:
else: else:
cls.preferred_drawing_method = "font_shapes" cls.preferred_drawing_method = "font_shapes"
@classmethod
def print_settings(cls):
print(f"CTkSettings current values:")
print(f"scaling_factor = {cls.scaling_factor}")
print(f"circle_font_is_ready = {cls.circle_font_is_ready}")
print(f"hand_cursor_enabled = {cls.hand_cursor_enabled}")
print(f"preferred_drawing_method = {cls.preferred_drawing_method}")
print(f"radius_to_char_fine = {cls.radius_to_char_fine}")
CTkSettings.init_font_character_mapping()
CTkSettings.init_drawing_method() CTkSettings.init_drawing_method()

View File

@ -9,15 +9,14 @@ customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "gre
class App(customtkinter.CTk): class App(customtkinter.CTk):
APP_NAME = "CustomTkinter complex example"
WIDTH = 700 WIDTH = 700
HEIGHT = 500 HEIGHT = 500
def __init__(self, *args, **kwargs): def __init__(self):
super().__init__(*args, **kwargs) super().__init__()
self.title(App.APP_NAME) self.title("CustomTkinter complex example")
self.geometry(str(App.WIDTH) + "x" + str(App.HEIGHT)) self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.minsize(App.WIDTH, App.HEIGHT) self.minsize(App.WIDTH, App.HEIGHT)
self.protocol("WM_DELETE_WINDOW", self.on_closing) self.protocol("WM_DELETE_WINDOW", self.on_closing)
@ -26,17 +25,14 @@ class App(customtkinter.CTk):
self.bind("<Command-w>", self.on_closing) self.bind("<Command-w>", self.on_closing)
self.createcommand('tk::mac::Quit', self.on_closing) self.createcommand('tk::mac::Quit', self.on_closing)
# ============ create two CTkFrames ============ # ============ create two frames ============
self.frame_left = customtkinter.CTkFrame(master=self, self.frame_left = customtkinter.CTkFrame(master=self,
width=180, width=180,
corner_radius=0) corner_radius=0)
self.frame_left.grid(row=0, column=0, sticky="nswe") self.frame_left.grid(row=0, column=0, sticky="nswe")
self.frame_right = customtkinter.CTkFrame(master=self, self.frame_right = customtkinter.CTkFrame(master=self)
width=420,
height=App.HEIGHT-40,
corner_radius=12)
self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20) self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20)
self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1)
@ -50,29 +46,25 @@ class App(customtkinter.CTk):
self.label_1 = customtkinter.CTkLabel(master=self.frame_left, self.label_1 = customtkinter.CTkLabel(master=self.frame_left,
text="CustomTkinter", text="CustomTkinter",
text_font=("Roboto Medium", -16), # font name and size in px text_font=("Roboto Medium", -16)) # font name and size in px
fg_color=None)
self.label_1.grid(row=1, column=0, pady=10, padx=10) self.label_1.grid(row=1, column=0, pady=10, padx=10)
self.button_1 = customtkinter.CTkButton(master=self.frame_left, self.button_1 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 1", text="CTkButton 1",
command=self.button_event, command=self.button_event,
fg_color=None, fg_color=("gray75", "gray30")) # <- custom tuple-color
border_width=2)
self.button_1.grid(row=2, column=0, pady=10, padx=20) self.button_1.grid(row=2, column=0, pady=10, padx=20)
self.button_2 = customtkinter.CTkButton(master=self.frame_left, self.button_2 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 2", text="CTkButton 2",
command=self.button_event, command=self.button_event,
fg_color=None, fg_color=("gray75", "gray30")) # <- custom tuple-color
border_width=2)
self.button_2.grid(row=3, column=0, pady=10, padx=20) self.button_2.grid(row=3, column=0, pady=10, padx=20)
self.button_3 = customtkinter.CTkButton(master=self.frame_left, self.button_3 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 3", text="CTkButton 3",
command=self.button_event, command=self.button_event,
fg_color=None, fg_color=("gray75", "gray30")) # <- custom tuple-color
border_width=2)
self.button_3.grid(row=4, column=0, pady=10, padx=20) self.button_3.grid(row=4, column=0, pady=10, padx=20)
self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_left, self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_left,
@ -86,17 +78,19 @@ class App(customtkinter.CTk):
# ============ frame_right ============ # ============ frame_right ============
self.frame_right.rowconfigure(0, weight=1) for i in [0, 1, 2, 3]:
self.frame_right.rowconfigure(3, weight=1) self.frame_right.rowconfigure(i, weight=1)
self.frame_right.rowconfigure(6, weight=10)
self.frame_right.columnconfigure(0, weight=1) self.frame_right.columnconfigure(0, weight=1)
self.frame_info = customtkinter.CTkFrame(master=self.frame_right, self.frame_info = customtkinter.CTkFrame(master=self.frame_right)
width=380, self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="wens")
height=200)
self.frame_info.grid(row=0, column=0, columnspan=3, pady=20, padx=20, sticky="wens")
# ============ frame_right -> frame_info ============ # ============ frame_right -> frame_info ============
self.frame_info.rowconfigure(0, weight=1)
self.frame_info.columnconfigure(0, weight=1)
self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info, self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info,
text="CTkLabel: Lorem ipsum dolor sit,\n" + text="CTkLabel: Lorem ipsum dolor sit,\n" +
"amet consetetur sadipscing elitr,\n" + "amet consetetur sadipscing elitr,\n" +
@ -106,52 +100,71 @@ class App(customtkinter.CTk):
height=100, height=100,
fg_color=("white", "gray38"), # <- custom tuple-color fg_color=("white", "gray38"), # <- custom tuple-color
justify=tkinter.LEFT) justify=tkinter.LEFT)
self.label_info_1.place(relx=0.5, rely=0.15, anchor=tkinter.N) self.label_info_1.grid(column=0, row=0, sticky="nwe", padx=15, pady=15)
self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info, width=240) self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info, width=240)
self.progressbar.place(relx=0.5, rely=0.85, anchor=tkinter.S) self.progressbar.grid(row=1, column=0, sticky="ew", padx=15, pady=15)
# ============ frame_right <- ============ # ============ frame_right <- ============
self.radio_var = tkinter.IntVar(value=0)
self.label_radio_group = customtkinter.CTkLabel(master=self.frame_right,
fg_color=("white", "gray30"), # <- custom tuple-color
text="CTkRadioButton Group:")
self.label_radio_group.grid(row=0, column=2, columnspan=1, pady=0, padx=20, sticky="wes")
self.radio_button_1 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=0)
self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="")
self.radio_button_2 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=1)
self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="")
self.radio_button_3 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=2)
self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="")
#self.radio_button_1.select()
#self.radio_button_1.deselect()
self.slider_1 = customtkinter.CTkSlider(master=self.frame_right, self.slider_1 = customtkinter.CTkSlider(master=self.frame_right,
from_=1, from_=1,
to=0, to=0,
number_of_steps=3, number_of_steps=3,
command=self.progressbar.set) command=self.progressbar.set)
self.slider_1.grid(row=1, column=0, columnspan=2, pady=10, padx=20, sticky="we") self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we")
self.slider_1.set(0.5) self.slider_1.set(0.7)
self.slider_2 = customtkinter.CTkSlider(master=self.frame_right, self.slider_2 = customtkinter.CTkSlider(master=self.frame_right,
width=160,
command=self.progressbar.set) command=self.progressbar.set)
self.slider_2.grid(row=2, column=0, columnspan=2, pady=10, padx=20, sticky="we") self.slider_2.grid(row=5, column=0, columnspan=2, pady=10, padx=20, sticky="we")
self.slider_2.set(0.7) self.slider_2.set(0.7)
self.label_info_2 = customtkinter.CTkLabel(master=self.frame_right, self.slider_button_1 = customtkinter.CTkButton(master=self.frame_right,
text="CTkLabel: Lorem ipsum",
fg_color=None,
width=180,
height=20,
justify=tkinter.CENTER)
self.label_info_2.grid(row=1, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.button_4 = customtkinter.CTkButton(master=self.frame_right,
height=25, height=25,
text="CTkButton", text="CTkButton",
command=self.button_event) command=self.button_event)
self.button_4.grid(row=2, column=2, columnspan=1, pady=10, padx=20, sticky="we") self.slider_button_1.grid(row=4, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.slider_button_2 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
command=self.button_event)
self.slider_button_2.grid(row=5, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.entry = customtkinter.CTkEntry(master=self.frame_right, self.entry = customtkinter.CTkEntry(master=self.frame_right,
width=120, width=120,
height=30,
placeholder_text="CTkEntry") placeholder_text="CTkEntry")
self.entry.grid(row=4, column=0, columnspan=2, pady=20, padx=20, sticky="we") self.entry.grid(row=7, column=0, columnspan=2, pady=20, padx=20, sticky="we")
self.button_5 = customtkinter.CTkButton(master=self.frame_right, self.button_5 = customtkinter.CTkButton(master=self.frame_right,
height=30,
text="CTkButton", text="CTkButton",
command=self.button_event) command=self.button_event)
self.button_5.grid(row=4, column=2, columnspan=1, pady=20, padx=20, sticky="we") self.button_5.grid(row=7, column=2, columnspan=1, pady=20, padx=20, sticky="we")
self.progressbar.set(0.5) self.progressbar.set(0.5)

View File

@ -42,8 +42,8 @@ class App(customtkinter.CTk):
corner_radius=0) corner_radius=0)
self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER) self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
self.label_1 = customtkinter.CTkLabel(master=self.frame, corner_radius=6, width=200, height=60, self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60,
fg_color=("gray70", "gray20"), text="CustomTkinter\ninterface example") fg_color=("gray70", "gray35"), text="CustomTkinter\ninterface example")
self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER) self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER)
self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=20, width=200, placeholder_text="username") self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=20, width=200, placeholder_text="username")

View File

@ -5,7 +5,7 @@ customtkinter.set_default_color_theme("blue")
customtkinter.set_appearance_mode("dark") customtkinter.set_appearance_mode("dark")
app = customtkinter.CTk() app = customtkinter.CTk()
app.geometry("600x800") app.geometry("600x1000")
app.grid_columnconfigure(0, weight=1) app.grid_columnconfigure(0, weight=1)
app.grid_columnconfigure(1, weight=1) app.grid_columnconfigure(1, weight=1)
@ -28,7 +28,7 @@ f4 = customtkinter.CTkFrame(app, fg_color="gray90", corner_radius=0)
f4.grid(row=0, column=3, rowspan=1, columnspan=1, sticky="nsew") f4.grid(row=0, column=3, rowspan=1, columnspan=1, sticky="nsew")
f4.grid_columnconfigure(0, weight=1) f4.grid_columnconfigure(0, weight=1)
for i in range(0, 18, 1): for i in range(0, 21, 1):
b = customtkinter.CTkButton(f1, corner_radius=i, height=34, border_width=2, text=f"{i} {i-2}", b = customtkinter.CTkButton(f1, corner_radius=i, height=34, border_width=2, text=f"{i} {i-2}",
border_color="white", fg_color=None, text_color="white") border_color="white", fg_color=None, text_color="white")
b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew") b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
@ -45,4 +45,5 @@ for i in range(0, 18, 1):
border_color="gray10", fg_color="#228da8") border_color="gray10", fg_color="#228da8")
b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew") b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")
customtkinter.CTkSettings.print_settings()
app.mainloop() app.mainloop()