diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 6d5d6a2..69a3975 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -8,6 +8,7 @@ from .customtkinter_progressbar import CTkProgressBar from .customtkinter_label import CTkLabel from .customtkinter_entry import CTkEntry from .customtkinter_checkbox import CTkCheckBox +from .customtkinter_radiobutton import CTkRadioButton from .customtkinter_tk import CTk from .customtkinter_canvas import CTkCanvas 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 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", "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", "CustomTkinter_shapes_font-fine.otf")) + 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", "fonts", "Roboto", "Roboto-Medium.ttf")) CTkSettings.circle_font_is_ready = pyglet.font.have_font("CustomTkinter_shapes_font") warnings.simplefilter("default") diff --git a/customtkinter/assets/CustomTkinter_shapes_font-Regular.otf b/customtkinter/assets/fonts/CustomTkinter_shapes_font-Regular.otf similarity index 100% rename from customtkinter/assets/CustomTkinter_shapes_font-Regular.otf rename to customtkinter/assets/fonts/CustomTkinter_shapes_font-Regular.otf diff --git a/customtkinter/assets/CustomTkinter_shapes_font-fine.otf b/customtkinter/assets/fonts/CustomTkinter_shapes_font-fine.otf similarity index 100% rename from customtkinter/assets/CustomTkinter_shapes_font-fine.otf rename to customtkinter/assets/fonts/CustomTkinter_shapes_font-fine.otf diff --git a/customtkinter/assets/Roboto/Roboto-Medium.ttf b/customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf similarity index 100% rename from customtkinter/assets/Roboto/Roboto-Medium.ttf rename to customtkinter/assets/fonts/Roboto/Roboto-Medium.ttf diff --git a/customtkinter/assets/Roboto/Roboto-Regular.ttf b/customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf similarity index 100% rename from customtkinter/assets/Roboto/Roboto-Regular.ttf rename to customtkinter/assets/fonts/Roboto/Roboto-Regular.ttf diff --git a/customtkinter/assets/themes/blue.json b/customtkinter/assets/themes/blue.json index 7e0ffee..f08510e 100644 --- a/customtkinter/assets/themes/blue.json +++ b/customtkinter/assets/themes/blue.json @@ -1,25 +1,25 @@ { "color": { - "window_bg_color": ["#ECECEC", "gray14"], - "button": ["#3599D6", "#1C94CF"], - "button_hover": ["#A7C2E0", "#5FB4DD"], + "window_bg_color": ["gray92", "gray12"], + "button": ["#1FA6E8", "#1C94CF"], + "button_hover": ["#1A89BF", "#1673A1"], "button_border": ["gray25", "gray86"], - "checkbox_border": ["gray20", "#ededed"], - "entry": ["white", "gray10"], - "entry_border": ["gray65", "gray35"], + "checkbox_border": ["gray40", "gray60"], + "entry": ["white", "gray24"], + "entry_border": ["gray70", "gray32"], "entry_placeholder_text": ["gray52", "gray62"], "frame_border": ["#A7C2E0", "#5FB4DD"], - "frame_low": ["#D4D5D6", "gray20"], - "frame_high": ["gray77", "gray24"], + "frame_low": ["gray87", "gray18"], + "frame_high": ["gray82", "gray22"], "label": [null, null], "text": ["gray20", "gray90"], "progressbar": ["#6B6B6B", "#222222"], "progressbar_progress": ["#3599D6", "#1C94CF"], "progressbar_border": ["gray", "gray"], "slider": ["#6B6B6B", "#222222"], - "slider_progress": ["#A5A6A5", "#555555"], + "slider_progress": ["white", "#555555"], "slider_button": ["#3599D6", "#1C94CF"], - "slider_button_hover": ["#A7C2E0", "#5FB4DD"], + "slider_button_hover": ["#1A89BF", "#1673A1"], "darken_factor": 0.8 }, @@ -43,15 +43,18 @@ "button_border_width": 0, "checkbox_corner_radius": 7, "checkbox_border_width": 3, + "radiobutton_corner_radius": 1000, + "radiobutton_border_width_unchecked": 3, + "radiobutton_border_width_checked": 6, "entry_border_width": 2, "frame_corner_radius": 10, "frame_border_width": 0, "label_corner_radius": 8, "progressbar_border_width": 0, - "progressbar_corner_radius": 100, + "progressbar_corner_radius": 1000, "slider_border_width": 6, "slider_corner_radius": 8, "slider_button_length": 0, - "slider_button_corner_radius": 100 + "slider_button_corner_radius": 1000 } } \ No newline at end of file diff --git a/customtkinter/customtkinter_canvas.py b/customtkinter/customtkinter_canvas.py index eb2aee1..63c1fcb 100644 --- a/customtkinter/customtkinter_canvas.py +++ b/customtkinter/customtkinter_canvas.py @@ -4,13 +4,7 @@ from .customtkinter_settings import CTkSettings 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 - # 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'} + radius_to_char_fine = CTkSettings.radius_to_char_fine # dict to map radius to font circle character def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/customtkinter/customtkinter_draw_engine.py b/customtkinter/customtkinter_draw_engine.py index 253e454..89673c0 100644 --- a/customtkinter/customtkinter_draw_engine.py +++ b/customtkinter/customtkinter_draw_engine.py @@ -67,7 +67,7 @@ class CTkDrawEngine: if self._rendering_method == "polygon_shapes": return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) 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": 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 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 # create border button parts if border_width > 0: if corner_radius > 0: - # create canvas border corner parts if not already created - if not self._canvas.find_withtag("border_oval_1_a"): + # 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") 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) - 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) - 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 + 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) - 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_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) + self._canvas.create_aa_circle(0, 0, 0, tags=("border_oval_3_b", "border_corner_part", "border_parts"), anchor=tkinter.CENTER, angle=180) 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: - self._canvas.delete(["border_oval_3_a", "border_oval_3_b", "border_oval_4_a", "border_oval_4_b"]) + 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.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 self._canvas.coords("border_oval_1_a", corner_radius, corner_radius, corner_radius) @@ -182,33 +191,35 @@ class CTkDrawEngine: # create inner button parts 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: 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 + 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) - 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 + 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) - 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 - 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") - 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) - 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 - 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") # change position of border corner parts diff --git a/customtkinter/customtkinter_progressbar.py b/customtkinter/customtkinter_progressbar.py index 2fc8ea9..ad9c6f0 100644 --- a/customtkinter/customtkinter_progressbar.py +++ b/customtkinter/customtkinter_progressbar.py @@ -21,7 +21,7 @@ class CTkProgressBar(tkinter.Frame): progress_color="default_theme", corner_radius="default_theme", width=200, - height=10, + height=8, border_width="default_theme", **kwargs): super().__init__(*args, **kwargs) diff --git a/customtkinter/customtkinter_radiobutton.py b/customtkinter/customtkinter_radiobutton.py new file mode 100644 index 0000000..0664e72 --- /dev/null +++ b/customtkinter/customtkinter_radiobutton.py @@ -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[] 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("", self.on_enter) + self.canvas.bind("", self.on_leave) + + self.canvas.bind("", self.invoke) + self.canvas.bind("", 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() diff --git a/customtkinter/customtkinter_settings.py b/customtkinter/customtkinter_settings.py index bebdb22..619f8a7 100644 --- a/customtkinter/customtkinter_settings.py +++ b/customtkinter/customtkinter_settings.py @@ -1,4 +1,5 @@ import sys +import platform class CTkSettings: @@ -8,6 +9,28 @@ class CTkSettings: hand_cursor_enabled = True 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 def init_drawing_method(cls): """ possible: 'polygon_shapes', 'font_shapes', 'circle_shapes' """ @@ -17,5 +40,15 @@ class CTkSettings: else: 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() diff --git a/examples/complex_example.py b/examples/complex_example.py index 8f381fd..78e29a7 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -9,15 +9,14 @@ customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "gre class App(customtkinter.CTk): - APP_NAME = "CustomTkinter complex example" WIDTH = 700 HEIGHT = 500 - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + def __init__(self): + super().__init__() - self.title(App.APP_NAME) - self.geometry(str(App.WIDTH) + "x" + str(App.HEIGHT)) + self.title("CustomTkinter complex example") + self.geometry(f"{App.WIDTH}x{App.HEIGHT}") self.minsize(App.WIDTH, App.HEIGHT) self.protocol("WM_DELETE_WINDOW", self.on_closing) @@ -26,17 +25,14 @@ class App(customtkinter.CTk): self.bind("", self.on_closing) self.createcommand('tk::mac::Quit', self.on_closing) - # ============ create two CTkFrames ============ + # ============ create two frames ============ self.frame_left = customtkinter.CTkFrame(master=self, width=180, corner_radius=0) self.frame_left.grid(row=0, column=0, sticky="nswe") - self.frame_right = customtkinter.CTkFrame(master=self, - width=420, - height=App.HEIGHT-40, - corner_radius=12) + self.frame_right = customtkinter.CTkFrame(master=self) self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20) self.grid_columnconfigure(1, weight=1) @@ -50,29 +46,25 @@ class App(customtkinter.CTk): self.label_1 = customtkinter.CTkLabel(master=self.frame_left, text="CustomTkinter", - text_font=("Roboto Medium", -16), # font name and size in px - fg_color=None) + text_font=("Roboto Medium", -16)) # font name and size in px self.label_1.grid(row=1, column=0, pady=10, padx=10) self.button_1 = customtkinter.CTkButton(master=self.frame_left, text="CTkButton 1", command=self.button_event, - fg_color=None, - border_width=2) + fg_color=("gray75", "gray30")) # <- custom tuple-color self.button_1.grid(row=2, column=0, pady=10, padx=20) self.button_2 = customtkinter.CTkButton(master=self.frame_left, text="CTkButton 2", command=self.button_event, - fg_color=None, - border_width=2) + fg_color=("gray75", "gray30")) # <- custom tuple-color self.button_2.grid(row=3, column=0, pady=10, padx=20) self.button_3 = customtkinter.CTkButton(master=self.frame_left, text="CTkButton 3", command=self.button_event, - fg_color=None, - border_width=2) + fg_color=("gray75", "gray30")) # <- custom tuple-color self.button_3.grid(row=4, column=0, pady=10, padx=20) self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_left, @@ -86,17 +78,19 @@ class App(customtkinter.CTk): # ============ frame_right ============ - self.frame_right.rowconfigure(0, weight=1) - self.frame_right.rowconfigure(3, weight=1) + for i in [0, 1, 2, 3]: + self.frame_right.rowconfigure(i, weight=1) + self.frame_right.rowconfigure(6, weight=10) self.frame_right.columnconfigure(0, weight=1) - self.frame_info = customtkinter.CTkFrame(master=self.frame_right, - width=380, - height=200) - self.frame_info.grid(row=0, column=0, columnspan=3, pady=20, padx=20, sticky="wens") + self.frame_info = customtkinter.CTkFrame(master=self.frame_right) + self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="wens") # ============ 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, text="CTkLabel: Lorem ipsum dolor sit,\n" + "amet consetetur sadipscing elitr,\n" + @@ -106,52 +100,71 @@ class App(customtkinter.CTk): height=100, fg_color=("white", "gray38"), # <- custom tuple-color 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.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 <- ============ + 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, from_=1, to=0, number_of_steps=3, command=self.progressbar.set) - self.slider_1.grid(row=1, column=0, columnspan=2, pady=10, padx=20, sticky="we") - self.slider_1.set(0.5) + self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we") + self.slider_1.set(0.7) self.slider_2 = customtkinter.CTkSlider(master=self.frame_right, - width=160, 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.label_info_2 = customtkinter.CTkLabel(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, + self.slider_button_1 = customtkinter.CTkButton(master=self.frame_right, height=25, text="CTkButton", 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, width=120, - height=30, 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, - height=30, text="CTkButton", 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) diff --git a/examples/example_background_image.py b/examples/example_background_image.py index 4a51b09..9aa9f98 100644 --- a/examples/example_background_image.py +++ b/examples/example_background_image.py @@ -42,8 +42,8 @@ class App(customtkinter.CTk): corner_radius=0) 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, - fg_color=("gray70", "gray20"), text="CustomTkinter\ninterface example") + self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60, + fg_color=("gray70", "gray35"), text="CustomTkinter\ninterface example") 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") diff --git a/test/test_button_antialiasing.py b/test/test_button_antialiasing.py index 39e874f..a421c86 100644 --- a/test/test_button_antialiasing.py +++ b/test/test_button_antialiasing.py @@ -5,7 +5,7 @@ customtkinter.set_default_color_theme("blue") customtkinter.set_appearance_mode("dark") app = customtkinter.CTk() -app.geometry("600x800") +app.geometry("600x1000") app.grid_columnconfigure(0, 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_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}", border_color="white", fg_color=None, text_color="white") 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") b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew") +customtkinter.CTkSettings.print_settings() app.mainloop() \ No newline at end of file