diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 4cdeb9c..a73b378 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,18 +1,19 @@ __version__ = "3.12" -from .widgets.ctk_input_dialog import CTkInputDialog from .widgets.ctk_button import CTkButton +from .widgets.ctk_checkbox import CTkCheckBox +from .widgets.ctk_entry import CTkEntry from .widgets.ctk_slider import CTkSlider from .widgets.ctk_frame import CTkFrame from .widgets.ctk_progressbar import CTkProgressBar from .widgets.ctk_label import CTkLabel -from .widgets.ctk_entry import CTkEntry -from .widgets.ctk_checkbox import CTkCheckBox from .widgets.ctk_radiobutton import CTkRadioButton -from .widgets.ctk_tk import CTk from .widgets.ctk_canvas import CTkCanvas from .widgets.ctk_switch import CTkSwitch -from .widgets.ctk_toplevel import CTkToplevel + +from .windows.ctk_tk import CTk +from .windows.ctk_toplevel import CTkToplevel +from .windows.ctk_input_dialog import CTkInputDialog from .ctk_settings import CTkSettings from .appearance_mode_tracker import AppearanceModeTracker diff --git a/customtkinter/appearance_mode_tracker.py b/customtkinter/appearance_mode_tracker.py index fc4987d..35f3808 100644 --- a/customtkinter/appearance_mode_tracker.py +++ b/customtkinter/appearance_mode_tracker.py @@ -1,12 +1,16 @@ import sys import tkinter from distutils.version import StrictVersion as Version -import darkdetect -if Version(darkdetect.__version__) < Version("0.3.1"): - sys.stderr.write("WARNING: You have to update the darkdetect library: pip3 install --upgrade darkdetect\n") - if sys.platform != "darwin": - exit() +try: + import darkdetect + + if Version(darkdetect.__version__) < Version("0.3.1"): + sys.stderr.write("WARNING: You have to update the darkdetect library: pip3 install --upgrade darkdetect\n") + if sys.platform != "darwin": + exit() +except: + pass class AppearanceModeTracker: diff --git a/customtkinter/ctk_draw_engine.py b/customtkinter/ctk_draw_engine.py index 304d9f4..f12188c 100644 --- a/customtkinter/ctk_draw_engine.py +++ b/customtkinter/ctk_draw_engine.py @@ -56,19 +56,13 @@ class CTkDrawEngine: returns bool if recoloring is necessary """ - print("before", width, height) - - width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only + width = math.floor(width / 2) * 2 # round (floor) current_width and current_height and restrict them to even values only height = math.floor(height / 2) * 2 corner_radius = round(corner_radius) - print("after", width, height) - if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger corner_radius = min(width / 2, height / 2) - print("corner", corner_radius) - border_width = round(border_width) corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding) @@ -357,7 +351,7 @@ class CTkDrawEngine: returns bool if recoloring is necessary """ - width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only + width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only height = math.floor(height / 2) * 2 if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger @@ -521,7 +515,7 @@ class CTkDrawEngine: button_length: Union[float, int], button_corner_radius: Union[float, int], slider_value: float, orientation: str) -> bool: - width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only + width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only height = math.floor(height / 2) * 2 if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger diff --git a/customtkinter/ctk_settings.py b/customtkinter/ctk_settings.py index 442a580..7438902 100644 --- a/customtkinter/ctk_settings.py +++ b/customtkinter/ctk_settings.py @@ -4,10 +4,13 @@ import sys class CTkSettings: circle_font_is_ready = False - hand_cursor_enabled = True preferred_drawing_method = None radius_to_char_fine = None + cursor_manipulation_enabled = True + deactivate_macos_window_header_manipulation = False + deactivate_windows_window_header_manipulation = False + @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', @@ -41,7 +44,7 @@ class CTkSettings: def print_settings(cls): print(f"CTkSettings current values:") print(f"circle_font_is_ready = {cls.circle_font_is_ready}") - print(f"hand_cursor_enabled = {cls.hand_cursor_enabled}") + print(f"hand_cursor_enabled = {cls.cursor_manipulation_enabled}") print(f"preferred_drawing_method = {cls.preferred_drawing_method}") print(f"radius_to_char_fine = {cls.radius_to_char_fine}") diff --git a/customtkinter/scaling_tracker.py b/customtkinter/scaling_tracker.py index e38544a..2363748 100644 --- a/customtkinter/scaling_tracker.py +++ b/customtkinter/scaling_tracker.py @@ -6,23 +6,42 @@ class ScalingTracker: window_widgets_dict = {} # contains window objects as keys with list of widget callbacks as elements - window_dpi_scaling_dict = {} # contains window objects as keys and corresponding DPI values - user_scaling = 1 # scale change of all widgets and windows + window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors + + widget_scaling = 1 # user values which multiply to detected window scaling factor + window_scaling = 1 + spacing_scaling = 1 update_loop_running = False @classmethod def get_widget_scaling(cls, widget): window_root = cls.get_window_root_of_widget(widget) - return cls.window_dpi_scaling_dict[window_root] * cls.user_scaling + return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling + + @classmethod + def get_spacing_scaling(cls, widget): + window_root = cls.get_window_root_of_widget(widget) + return cls.window_dpi_scaling_dict[window_root] * cls.spacing_scaling @classmethod def get_window_scaling(cls, window): - return cls.window_dpi_scaling_dict[window] * cls.user_scaling + window_root = cls.get_window_root_of_widget(window) + return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling @classmethod - def set_user_scaling(cls, user_scaling_factor): - cls.user_scaling = user_scaling_factor + def set_widget_scaling(cls, widget_scaling_factor): + cls.widget_scaling = widget_scaling_factor + cls.update_scaling_callbacks() + + @classmethod + def set_spacing_scaling(cls, spacing_scaling_factor): + cls.spacing_scaling = spacing_scaling_factor + cls.update_scaling_callbacks() + + @classmethod + def set_window_scaling(cls, window_scaling_factor): + cls.window_scaling = window_scaling_factor cls.update_scaling_callbacks() @classmethod @@ -39,7 +58,9 @@ class ScalingTracker: def update_scaling_callbacks(cls): for window, callback_list in cls.window_widgets_dict.items(): for callback in callback_list: - callback(cls.window_dpi_scaling_dict[window]) + callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling, + cls.window_dpi_scaling_dict[window] * cls.spacing_scaling, + cls.window_dpi_scaling_dict[window] * cls.window_scaling) @classmethod def add_widget(cls, widget_callback, widget): @@ -51,7 +72,7 @@ class ScalingTracker: cls.window_widgets_dict[window_root].append(widget_callback) if window_root not in cls.window_dpi_scaling_dict: - cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_value(window_root) + cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root) if not cls.update_loop_running: window_root.after(100, cls.check_dpi_scaling) @@ -65,10 +86,10 @@ class ScalingTracker: cls.window_widgets_dict[window].append(window_callback) if window not in cls.window_dpi_scaling_dict: - cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_value(window) + cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_scaling(window) @classmethod - def get_window_dpi_value(cls, window): + def get_window_dpi_scaling(cls, window): if sys.platform == "darwin": return 1 # scaling works automatically on macOS diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py index 2ce783f..e64f9a8 100644 --- a/customtkinter/widgets/ctk_button.py +++ b/customtkinter/widgets/ctk_button.py @@ -1,5 +1,6 @@ import tkinter import sys +import math from .ctk_canvas import CTkCanvas from ..theme_manager import CTkThemeManager @@ -65,8 +66,8 @@ class CTkButton(CTkBaseClass): self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -87,11 +88,24 @@ class CTkButton(CTkBaseClass): self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1) + def set_scaling(self, *args, **kwargs): + super().set_scaling( *args, **kwargs) + + if self.text_label is not None: + self.text_label.destroy() + self.text_label = None + if self.image_label is not None: + self.image_label.destroy() + self.image_label = None + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling) + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width)) if no_color_updates is False or requires_recoloring: @@ -175,35 +189,35 @@ class CTkButton(CTkBaseClass): # create grid layout with just an image given if self.image_label is not None and self.text_label is None: - self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", pady=self.border_width * self.scaling) + self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", pady=self.apply_widget_scaling(self.border_width)) # create grid layout with just text given if self.image_label is None and self.text_label is not None: self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", - padx=self.corner_radius * self.scaling, pady=self.border_width * self.scaling) + padx=self.apply_widget_scaling(self.corner_radius), pady=self.apply_widget_scaling(self.border_width) + 1) # create grid layout of image and text label in 2x2 grid system with given compound if self.image_label is not None and self.text_label is not None: if self.compound == tkinter.LEFT or self.compound == "left": self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self.corner_radius * self.scaling, self.border_width * self.scaling), 2), pady=self.border_width * self.scaling) + padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), pady=self.apply_widget_scaling(self.border_width)) self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self.corner_radius * self.scaling, self.border_width * self.scaling)), pady=self.border_width * self.scaling) + padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), pady=self.apply_widget_scaling(self.border_width)) elif self.compound == tkinter.TOP or self.compound == "top": self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(self.border_width * self.scaling, 2)) + padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(self.apply_widget_scaling(self.border_width), 2)) self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(2, self.border_width * self.scaling)) + padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(2, self.apply_widget_scaling(self.border_width))) elif self.compound == tkinter.RIGHT or self.compound == "right": self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self.corner_radius * self.scaling, self.border_width * self.scaling)), pady=self.border_width * self.scaling) + padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), pady=self.apply_widget_scaling(self.border_width)) self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self.corner_radius * self.scaling, self.border_width * self.scaling), 2), pady=self.border_width * self.scaling) + padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), pady=self.apply_widget_scaling(self.border_width)) elif self.compound == tkinter.BOTTOM or self.compound == "bottom": self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(2, self.border_width * self.scaling)) + padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(2, self.apply_widget_scaling(self.border_width))) self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(self.border_width * self.scaling, 2)) + padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(self.apply_widget_scaling(self.border_width), 2)) def configure(self, *args, **kwargs): require_redraw = False # some attribute changes require a call of self.draw() at the end @@ -271,17 +285,18 @@ class CTkButton(CTkBaseClass): self.draw() def set_cursor(self): - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and self.function is not None and CTkSettings.hand_cursor_enabled: - self.configure(cursor="arrow") - elif sys.platform.startswith("win") and self.function is not None and CTkSettings.hand_cursor_enabled: - self.configure(cursor="arrow") + if CTkSettings.cursor_manipulation_enabled: + if self.state == tkinter.DISABLED: + if sys.platform == "darwin" and self.function is not None and CTkSettings.cursor_manipulation_enabled: + self.configure(cursor="arrow") + elif sys.platform.startswith("win") and self.function is not None and CTkSettings.cursor_manipulation_enabled: + self.configure(cursor="arrow") - elif self.state == tkinter.NORMAL: - if sys.platform == "darwin" and self.function is not None and CTkSettings.hand_cursor_enabled: - self.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and self.function is not None and CTkSettings.hand_cursor_enabled: - self.configure(cursor="hand2") + elif self.state == tkinter.NORMAL: + if sys.platform == "darwin" and self.function is not None and CTkSettings.cursor_manipulation_enabled: + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and self.function is not None and CTkSettings.cursor_manipulation_enabled: + self.configure(cursor="hand2") def set_text(self, text): self.text = text diff --git a/customtkinter/widgets/ctk_checkbox.py b/customtkinter/widgets/ctk_checkbox.py index 94532cf..6e4b5ec 100644 --- a/customtkinter/widgets/ctk_checkbox.py +++ b/customtkinter/widgets/ctk_checkbox.py @@ -68,20 +68,20 @@ class CTkCheckBox(CTkBaseClass): # configure grid system (1x3) self.grid_columnconfigure(0, weight=0) - self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling) + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) self.grid_columnconfigure(2, weight=1) self.grid_rowconfigure(0, weight=1) self.bg_canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1) self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -103,6 +103,16 @@ class CTkCheckBox(CTkBaseClass): self.set_cursor() self.draw() # initial draw + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) + self.text_label.configure(font=self.apply_font_scaling(self.text_font)) + + self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def destroy(self): if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) @@ -110,15 +120,15 @@ class CTkCheckBox(CTkBaseClass): super().destroy() def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling) + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width)) if self.check_state is True: - self.draw_engine.draw_checkmark(self.width * self.scaling, - self.height * self.scaling, - self.height * 0.58 * self.scaling) + self.draw_engine.draw_checkmark(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.current_height * 0.58)) else: self.canvas.delete("checkmark") @@ -146,7 +156,6 @@ class CTkCheckBox(CTkBaseClass): fill=CTkThemeManager.single_color(self.border_color, self.appearance_mode)) if self.text_label is None: - print("create label") self.text_label = tkinter.Label(master=self, bd=0, text=self.text, @@ -231,17 +240,18 @@ class CTkCheckBox(CTkBaseClass): self.draw() def set_cursor(self): - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="arrow") - elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="arrow") + if CTkSettings.cursor_manipulation_enabled: + if self.state == tkinter.DISABLED: + if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") + elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") - elif self.state == tkinter.NORMAL: - if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="hand2") + elif self.state == tkinter.NORMAL: + if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="hand2") def set_text(self, text): self.text = text diff --git a/customtkinter/widgets/ctk_entry.py b/customtkinter/widgets/ctk_entry.py index a17ea5c..0382b73 100644 --- a/customtkinter/widgets/ctk_entry.py +++ b/customtkinter/widgets/ctk_entry.py @@ -46,8 +46,8 @@ class CTkEntry(CTkBaseClass): self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.canvas.grid(column=0, row=0, sticky="we") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -58,7 +58,7 @@ class CTkEntry(CTkBaseClass): font=self.apply_font_scaling(self.text_font), **kwargs) self.entry.grid(column=0, row=0, sticky="we", - padx=self.corner_radius * self.scaling if self.corner_radius >= 6 * self.scaling else 6 * self.scaling) + padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6)) super().bind('', self.update_dimensions_event) self.entry.bind('', self.set_placeholder) @@ -67,6 +67,16 @@ class CTkEntry(CTkBaseClass): self.draw() self.set_placeholder() + def set_scaling(self, *args, **kwargs): + super().set_scaling( *args, **kwargs) + + self.entry.configure(font=self.apply_font_scaling(self.text_font)) + self.entry.grid(column=0, row=0, sticky="we", + padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6)) + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def configure_basic_grid(self): self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -91,10 +101,10 @@ class CTkEntry(CTkBaseClass): def draw(self, no_color_updates=False): self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling) + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width)) if CTkThemeManager.single_color(self.fg_color, self.appearance_mode) is not None: self.canvas.itemconfig("inner_parts", @@ -147,12 +157,12 @@ class CTkEntry(CTkBaseClass): if "corner_radius" in kwargs: self.corner_radius = kwargs["corner_radius"] - 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 * 2 > self.current_height: + self.corner_radius = self.current_height / 2 + elif self.corner_radius * 2 > self.current_width: + self.corner_radius = self.current_width / 2 - self.entry.grid(column=0, row=0, sticky="we", padx=self.corner_radius if self.corner_radius >= 6 else 6) + self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6)) del kwargs["corner_radius"] require_redraw = True diff --git a/customtkinter/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py index dcb27f6..87b044f 100644 --- a/customtkinter/widgets/ctk_frame.py +++ b/customtkinter/widgets/ctk_frame.py @@ -41,8 +41,8 @@ class CTkFrame(CTkBaseClass): self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.canvas.place(x=0, y=0, relwidth=1, relheight=1) self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -62,12 +62,18 @@ class CTkFrame(CTkBaseClass): except ValueError: return child_widgets + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling) + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width)) if no_color_updates is False or requires_recoloring: self.canvas.itemconfig("inner_parts", diff --git a/customtkinter/widgets/ctk_label.py b/customtkinter/widgets/ctk_label.py index e63a2ad..883633d 100644 --- a/customtkinter/widgets/ctk_label.py +++ b/customtkinter/widgets/ctk_label.py @@ -45,8 +45,8 @@ class CTkLabel(CTkBaseClass): self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) self.canvas.grid(row=0, column=0, sticky="nswe") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -56,15 +56,22 @@ class CTkLabel(CTkBaseClass): text=self.text, font=self.apply_font_scaling(self.text_font), **kwargs) - self.text_label.grid(row=0, column=0, padx=self.corner_radius) + self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius)) self.bind('', self.update_dimensions_event) self.draw() + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.text_label.configure(font=self.apply_font_scaling(self.text_font)) + self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius)) + def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), 0) self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py index 62b0125..28fd039 100644 --- a/customtkinter/widgets/ctk_progressbar.py +++ b/customtkinter/widgets/ctk_progressbar.py @@ -40,14 +40,14 @@ class CTkProgressBar(CTkBaseClass): self.border_width = CTkThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width self.value = 0.5 - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(1, weight=1) + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) - self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1) + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) + self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) # Each time an item is resized due to pack position mode, the binding Configure is called on the widget @@ -61,6 +61,12 @@ class CTkProgressBar(CTkBaseClass): self.set(self.variable.get(), from_variable_callback=True) self.variable_callback_blocked = False + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def destroy(self): if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) @@ -68,12 +74,10 @@ class CTkProgressBar(CTkBaseClass): super().destroy() def draw(self, no_color_updates=False): - print("progress", self.scaling) - - requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling, + requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), self.value, "w") if no_color_updates is False or requires_recoloring: diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py index f51ceed..d0ccd76 100644 --- a/customtkinter/widgets/ctk_radiobutton.py +++ b/customtkinter/widgets/ctk_radiobutton.py @@ -65,19 +65,19 @@ class CTkRadioButton(CTkBaseClass): # configure grid system (3x1) self.grid_columnconfigure(0, weight=0) - self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling) + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) self.grid_columnconfigure(2, weight=1) self.bg_canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1) self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -96,6 +96,16 @@ class CTkRadioButton(CTkBaseClass): else: self.deselect(from_variable_callback=True) + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) + self.text_label.configure(font=self.apply_font_scaling(self.text_font)) + + self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def destroy(self): if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) @@ -103,10 +113,10 @@ class CTkRadioButton(CTkBaseClass): super().destroy() def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling) + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width)) self.bg_canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) @@ -215,17 +225,18 @@ class CTkRadioButton(CTkBaseClass): self.draw() def set_cursor(self): - if self.state == tkinter.DISABLED: - if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="arrow") - elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="arrow") + if CTkSettings.cursor_manipulation_enabled: + if self.state == tkinter.DISABLED: + if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") + elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="arrow") - elif self.state == tkinter.NORMAL: - if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="hand2") + elif self.state == tkinter.NORMAL: + if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="hand2") def set_text(self, text): self.text = text diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py index fcb9538..90a64f9 100644 --- a/customtkinter/widgets/ctk_slider.py +++ b/customtkinter/widgets/ctk_slider.py @@ -62,11 +62,14 @@ class CTkSlider(CTkBaseClass): self.variable_callback_blocked = False self.variable_callback_name = None + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) - self.canvas.grid(column=0, row=0, sticky="nswe") + width=self.apply_widget_scaling(self.desired_width), + height=self.apply_widget_scaling(self.desired_height)) + self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) self.canvas.bind("", self.on_enter) @@ -86,6 +89,12 @@ class CTkSlider(CTkBaseClass): self.set(self.variable.get(), from_variable_callback=True) self.variable_callback_blocked = False + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def destroy(self): # remove variable_callback from variable callbacks if variable exists if self.variable is not None: @@ -93,23 +102,20 @@ class CTkSlider(CTkBaseClass): super().destroy() - def configure_basic_grid(self): - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - def set_cursor(self): - if sys.platform == "darwin": - self.configure(cursor="pointinghand") - elif sys.platform.startswith("win"): - self.configure(cursor="hand2") + if CTkSettings.cursor_manipulation_enabled: + if sys.platform == "darwin": + self.configure(cursor="pointinghand") + elif sys.platform.startswith("win"): + self.configure(cursor="hand2") def draw(self, no_color_updates=False): - requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling, - self.button_length * self.scaling, - self.button_corner_radius * self.scaling, + requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), + self.apply_widget_scaling(self.button_length), + self.apply_widget_scaling(self.button_corner_radius), self.value, "w") if no_color_updates is False or requires_recoloring: @@ -136,7 +142,7 @@ class CTkSlider(CTkBaseClass): outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode)) def clicked(self, event=None): - self.value = (event.x / self.width) / self.scaling + self.value = (event.x / self.current_width) / self.widget_scaling if self.value > 1: self.value = 1 @@ -159,12 +165,12 @@ class CTkSlider(CTkBaseClass): def on_enter(self, event=0): self.hover_state = True self.canvas.itemconfig("slider_parts", fill=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode), - outline=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode)) + outline=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode)) def on_leave(self, event=0): self.hover_state = False self.canvas.itemconfig("slider_parts", fill=CTkThemeManager.single_color(self.button_color, self.appearance_mode), - outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode)) + outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode)) def round_to_step_size(self, value): if self.number_of_steps is not None: diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py index 6416e57..d08671d 100644 --- a/customtkinter/widgets/ctk_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -70,19 +70,19 @@ class CTkSwitch(CTkBaseClass): # configure grid system (3x1) self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling) + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) self.grid_columnconfigure(2, weight=0) self.bg_canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe") self.canvas = CTkCanvas(master=self, highlightthickness=0, - width=self.width * self.scaling, - height=self.height * self.scaling) + width=self.apply_widget_scaling(self.current_width), + height=self.apply_widget_scaling(self.current_height)) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe") self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) @@ -100,6 +100,16 @@ class CTkSwitch(CTkBaseClass): elif self.variable.get() == self.offvalue: self.deselect(from_variable_callback=True) + def set_scaling(self, *args, **kwargs): + super().set_scaling(*args, **kwargs) + + self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6)) + self.text_label.configure(font=self.apply_font_scaling(self.text_font)) + + self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + self.draw() + def destroy(self): # remove variable_callback from variable callbacks if variable exists if self.variable is not None: @@ -108,28 +118,29 @@ class CTkSwitch(CTkBaseClass): super().destroy() def set_cursor(self): - if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="pointinghand") - elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled: - self.canvas.configure(cursor="hand2") + if CTkSettings.cursor_manipulation_enabled: + if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="pointinghand") + elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled: + self.canvas.configure(cursor="hand2") def draw(self, no_color_updates=False): if self.check_state is True: - requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling, - self.button_length * self.scaling, - self.corner_radius * self.scaling - , 1, "w") + requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), + self.apply_widget_scaling(self.button_length), + self.apply_widget_scaling(self.corner_radius), + 1, "w") else: - requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling, - self.height * self.scaling, - self.corner_radius * self.scaling, - self.border_width * self.scaling, - self.button_length * self.scaling, - self.corner_radius * self.scaling, + requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width), + self.apply_widget_scaling(self.current_height), + self.apply_widget_scaling(self.corner_radius), + self.apply_widget_scaling(self.border_width), + self.apply_widget_scaling(self.button_length), + self.apply_widget_scaling(self.corner_radius), 0, "w") if no_color_updates is False or requires_recoloring: diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/widget_base_class.py index 4a62038..2f0bd0a 100644 --- a/customtkinter/widgets/widget_base_class.py +++ b/customtkinter/widgets/widget_base_class.py @@ -2,10 +2,10 @@ import tkinter import tkinter.ttk as ttk import copy import re -import math +from typing import Callable, Union, TypedDict -from .ctk_tk import CTk -from .ctk_toplevel import CTkToplevel +from ..windows.ctk_tk import CTk +from ..windows.ctk_toplevel import CTkToplevel from ..appearance_mode_tracker import AppearanceModeTracker from ..scaling_tracker import ScalingTracker from ..theme_manager import CTkThemeManager @@ -16,12 +16,23 @@ class CTkBaseClass(tkinter.Frame): super().__init__(*args, width=width, height=height, **kwargs) # set desired size of underlying tkinter.Frame self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color - self.width = width # width and height in pixel, represent current size of the widget (not the desired size by init) - self.height = height # width and height are independent of the scale + + self.current_width = width # current_width and current_height in pixel, represent current size of the widget (not the desired size by init) + self.current_height = height # current_width and current_height are independent of the scale + self.desired_width = width + self.desired_height = height # add set_scaling method to callback list of ScalingTracker for automatic scaling changes ScalingTracker.add_widget(self.set_scaling, self) - self.scaling = ScalingTracker.get_widget_scaling(self) + self.widget_scaling = ScalingTracker.get_widget_scaling(self) + self.spacing_scaling = ScalingTracker.get_spacing_scaling(self) + + # save latest geometry function and kwargs + class GeometryCallDict(TypedDict): + function: Callable + kwargs: dict + + self.last_geometry_manager_call: Union[GeometryCallDict, None] = None # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes AppearanceModeTracker.add(self.set_appearance_mode, self) @@ -54,6 +65,39 @@ class CTkBaseClass(tkinter.Frame): AppearanceModeTracker.remove(self.set_appearance_mode) super().destroy() + def place(self, **kwargs): + self.last_geometry_manager_call = {"function": super().place, "kwargs": kwargs} + super().place(**self.apply_argument_scaling(kwargs)) + + def pack(self, **kwargs): + self.last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs} + super().pack(**self.apply_argument_scaling(kwargs)) + + def grid(self, **kwargs): + self.last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs} + super().grid(**self.apply_argument_scaling(kwargs)) + + def apply_argument_scaling(self, kwargs: dict) -> dict: + scaled_kwargs = copy.copy(kwargs) + + if "pady" in scaled_kwargs: + if isinstance(scaled_kwargs["pady"], (int, float, str)): + scaled_kwargs["pady"] = self.apply_spacing_scaling(scaled_kwargs["pady"]) + elif isinstance(scaled_kwargs["pady"], tuple): + scaled_kwargs["pady"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["pady"]]) + if "padx" in kwargs: + if isinstance(scaled_kwargs["padx"], (int, float, str)): + scaled_kwargs["padx"] = self.apply_spacing_scaling(scaled_kwargs["padx"]) + elif isinstance(scaled_kwargs["padx"], tuple): + scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]]) + + if "x" in scaled_kwargs: + scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"]) + if "y" in scaled_kwargs: + scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"]) + + return scaled_kwargs + def config(self, *args, **kwargs): self.configure(*args, **kwargs) @@ -77,9 +121,9 @@ class CTkBaseClass(tkinter.Frame): def update_dimensions_event(self, event): # only redraw if dimensions changed (for performance) - if self.width != math.floor(event.width * self.scaling) or self.height != math.floor(event.height * self.scaling): - self.width = event.width / self.scaling # adjust current size according to new size given by event - self.height = event.height / self.scaling # width and height are independent of the scale + if self.current_width != round(event.width / self.widget_scaling) or self.current_height != round(event.height / self.widget_scaling): + self.current_width = round(event.width / self.widget_scaling) # adjust current size according to new size given by event + self.current_height = round(event.height / self.widget_scaling) # current_width and current_height are independent of the scale self.draw(no_color_updates=True) # faster drawing without color changes @@ -115,28 +159,44 @@ class CTkBaseClass(tkinter.Frame): self.draw() - def set_scaling(self, new_scaling): - self.scaling = new_scaling + def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): + self.widget_scaling = new_widget_scaling + self.spacing_scaling = new_spacing_scaling - super().configure(width=self.width * self.scaling, height=self.height * self.scaling) + super().configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height)) + + if self.last_geometry_manager_call is not None: + self.last_geometry_manager_call["function"](**self.apply_argument_scaling(self.last_geometry_manager_call["kwargs"])) + + def apply_widget_scaling(self, value): + if isinstance(value, (int, float)): + return value * self.widget_scaling + else: + return value + + def apply_spacing_scaling(self, value): + if isinstance(value, (int, float)): + return value * self.spacing_scaling + else: + return value def apply_font_scaling(self, font): if type(font) == tuple or type(font) == list: font_list = list(font) for i in range(len(font_list)): if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0: - font_list[i] = int(font_list[i] * self.scaling) + font_list[i] = int(font_list[i] * self.widget_scaling) return tuple(font_list) elif type(font) == str: for negative_number in re.findall(r" -\d* ", font): - font = font.replace(negative_number, f" {int(int(negative_number) * self.scaling)} ") + font = font.replace(negative_number, f" {int(int(negative_number) * self.widget_scaling)} ") return font elif isinstance(font, tkinter.font.Font): new_font_object = copy.copy(font) if font.cget("size") < 0: - new_font_object.config(size=int(font.cget("size") * self.scaling)) + new_font_object.config(size=int(font.cget("size") * self.widget_scaling)) return new_font_object else: diff --git a/customtkinter/windows/__init__.py b/customtkinter/windows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customtkinter/widgets/ctk_input_dialog.py b/customtkinter/windows/ctk_input_dialog.py similarity index 93% rename from customtkinter/widgets/ctk_input_dialog.py rename to customtkinter/windows/ctk_input_dialog.py index 57f3153..e17b27d 100644 --- a/customtkinter/widgets/ctk_input_dialog.py +++ b/customtkinter/windows/ctk_input_dialog.py @@ -1,11 +1,11 @@ import tkinter import time -from .ctk_label import CTkLabel -from .ctk_entry import CTkEntry -from .ctk_frame import CTkFrame -from .ctk_toplevel import CTkToplevel -from .ctk_button import CTkButton +from ..widgets.ctk_label import CTkLabel +from ..widgets.ctk_entry import CTkEntry +from ..widgets.ctk_frame import CTkFrame +from ..windows.ctk_toplevel import CTkToplevel +from ..widgets.ctk_button import CTkButton from ..appearance_mode_tracker import AppearanceModeTracker from ..theme_manager import CTkThemeManager @@ -33,7 +33,7 @@ class CTkInputDialog: self.border_color = CTkThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color self.top = CTkToplevel() - self.top.geometry(f"280x{self.height}") + self.top.geometry(f"{280}x{self.height}") self.top.resizable(False, False) self.top.title(title) self.top.lift() diff --git a/customtkinter/widgets/ctk_tk.py b/customtkinter/windows/ctk_tk.py similarity index 68% rename from customtkinter/widgets/ctk_tk.py rename to customtkinter/windows/ctk_tk.py index 8be48a7..e4e5582 100644 --- a/customtkinter/widgets/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -4,9 +4,12 @@ import sys import os import platform import ctypes +import re from ..appearance_mode_tracker import AppearanceModeTracker from ..theme_manager import CTkThemeManager +from ..scaling_tracker import ScalingTracker +from ..ctk_settings import CTkSettings class CTk(tkinter.Tk): @@ -15,9 +18,20 @@ class CTk(tkinter.Tk): **kwargs): self.enable_macos_dark_title_bar() + super().__init__(*args, **kwargs) + + # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes + AppearanceModeTracker.add(self.set_appearance_mode, self) self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + # add set_scaling method to callback list of ScalingTracker for automatic scaling changes + ScalingTracker.add_widget(self.set_scaling, self) + self.window_scaling = ScalingTracker.get_window_scaling(self) + + self.current_width = 600 # initial window size, always without scaling + self.current_height = 500 + self.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color if "bg" in kwargs: @@ -27,10 +41,9 @@ class CTk(tkinter.Tk): self.fg_color = kwargs["background"] del kwargs["background"] - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.set_appearance_mode, self) super().configure(bg=CTkThemeManager.single_color(self.fg_color, self.appearance_mode)) super().title("CTk") + self.geometry(f"{self.current_width}x{self.current_height}") self.window_exists = False # indicates if the window is already shown through .update or .mainloop @@ -40,6 +53,22 @@ class CTk(tkinter.Tk): else: self.windows_set_titlebar_color("light") + self.bind('', self.update_dimensions_event) + + def update_dimensions_event(self, event=None): + detected_width = self.winfo_width() # detect current window size + detected_height = self.winfo_height() + + if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): + self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event + self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale + + def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): + self.window_scaling = new_window_scaling + + # set new window size by applying scaling to the current window size + self.geometry(f"{self.current_width}x{self.current_height}") + def destroy(self): AppearanceModeTracker.remove(self.set_appearance_mode) self.disable_macos_dark_title_bar() @@ -52,7 +81,7 @@ class CTk(tkinter.Tk): super().update() def mainloop(self, *args, **kwargs): - if self.window_exists is False: + if not self.window_exists: self.deiconify() self.window_exists = True super().mainloop(*args, **kwargs) @@ -66,6 +95,33 @@ class CTk(tkinter.Tk): else: self.windows_set_titlebar_color("light") + def geometry(self, geometry_string): + super().geometry(self.apply_geometry_scaling(geometry_string)) + + # update width and height attributes + numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers + self.current_width, self.current_height = numbers[0], numbers[1] + + def apply_geometry_scaling(self, geometry_string): + numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers + + if len(numbers) == 2: + return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\ + f"{self.apply_window_scaling(numbers[1]):.0f}" + elif len(numbers) == 4: + return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\ + f"{self.apply_window_scaling(numbers[1]):.0f}+" +\ + f"{self.apply_window_scaling(numbers[2]):.0f}+" +\ + f"{self.apply_window_scaling(numbers[3]):.0f}" + else: + return geometry_string + + def apply_window_scaling(self, value): + if isinstance(value, (int, float)): + return value * self.window_scaling + else: + return value + def config(self, *args, **kwargs): self.configure(*args, **kwargs) @@ -97,30 +153,25 @@ class CTk(tkinter.Tk): args[0]["background"] = CTkThemeManager.single_color(self.fg_color, self.appearance_mode) if bg_changed: - from .ctk_slider import CTkSlider - from .ctk_progressbar import CTkProgressBar - from .ctk_label import CTkLabel - from .ctk_frame import CTkFrame - from .ctk_entry import CTkEntry - from customtkinter.widgets.ctk_checkbox import CTkCheckBox - from customtkinter.widgets.ctk_button import CTkButton + from ..widgets.widget_base_class import CTkBaseClass for child in self.winfo_children(): - if isinstance(child, (CTkFrame, CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar)): + if isinstance(child, CTkBaseClass): child.configure(bg_color=self.fg_color) super().configure(*args, **kwargs) @staticmethod def enable_macos_dark_title_bar(): - if sys.platform == "darwin": # macOS + if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS if Version(platform.python_version()) < Version("3.10"): if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") + # This command allows dark-mode for all programs @staticmethod def disable_macos_dark_title_bar(): - if sys.platform == "darwin": # macOS + if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS if Version(platform.python_version()) < Version("3.10"): if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 os.system("defaults delete -g NSRequiresAquaSystemAppearance") @@ -137,7 +188,7 @@ class CTk(tkinter.Tk): https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute """ - if sys.platform.startswith("win"): + if sys.platform.startswith("win") and not CTkSettings.deactivate_windows_window_header_manipulation: super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible if not self.window_exists: diff --git a/customtkinter/widgets/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py similarity index 70% rename from customtkinter/widgets/ctk_toplevel.py rename to customtkinter/windows/ctk_toplevel.py index 7eeeeba..3a7dada 100644 --- a/customtkinter/widgets/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -4,9 +4,12 @@ import sys import os import platform import ctypes +import re from ..appearance_mode_tracker import AppearanceModeTracker from ..theme_manager import CTkThemeManager +from ..ctk_settings import CTkSettings +from ..scaling_tracker import ScalingTracker class CTkToplevel(tkinter.Toplevel): @@ -18,6 +21,13 @@ class CTkToplevel(tkinter.Toplevel): super().__init__(*args, **kwargs) self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + # add set_scaling method to callback list of ScalingTracker for automatic scaling changes + ScalingTracker.add_widget(self.set_scaling, self) + self.window_scaling = ScalingTracker.get_window_scaling(self) + + self.current_width = 600 # initial window size, always without scaling + self.current_height = 500 + self.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color if "bg" in kwargs: @@ -38,6 +48,47 @@ class CTkToplevel(tkinter.Toplevel): else: self.windows_set_titlebar_color("light") + def update_dimensions_event(self, event=None): + detected_width = self.winfo_width() # detect current window size + detected_height = self.winfo_height() + + if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): + self.current_width = round(detected_width / self.window_scaling) # adjust current size according to new size given by event + self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale + + def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): + self.window_scaling = new_window_scaling + + # set new window size by applying scaling to the current window size + self.geometry(f"{self.current_width}x{self.current_height}") + + def apply_geometry_scaling(self, geometry_string): + numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers + + if len(numbers) == 2: + return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\ + f"{self.apply_window_scaling(numbers[1]):.0f}" + elif len(numbers) == 4: + return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\ + f"{self.apply_window_scaling(numbers[1]):.0f}+" +\ + f"{self.apply_window_scaling(numbers[2]):.0f}+" +\ + f"{self.apply_window_scaling(numbers[3]):.0f}" + else: + return geometry_string + + def apply_window_scaling(self, value): + if isinstance(value, (int, float)): + return value * self.window_scaling + else: + return value + + def geometry(self, geometry_string): + super().geometry(self.apply_geometry_scaling(geometry_string)) + + # update width and height attributes + numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers + self.current_width, self.current_height = numbers[0], numbers[1] + def destroy(self): AppearanceModeTracker.remove(self.set_appearance_mode) self.disable_macos_dark_title_bar() @@ -99,14 +150,14 @@ class CTkToplevel(tkinter.Toplevel): @staticmethod def enable_macos_dark_title_bar(): - if sys.platform == "darwin": # macOS + if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS if Version(platform.python_version()) < Version("3.10"): if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No") @staticmethod def disable_macos_dark_title_bar(): - if sys.platform == "darwin": # macOS + if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS if Version(platform.python_version()) < Version("3.10"): if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9 os.system("defaults delete -g NSRequiresAquaSystemAppearance") @@ -123,7 +174,7 @@ class CTkToplevel(tkinter.Toplevel): https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute """ - if sys.platform.startswith("win"): + if sys.platform.startswith("win") and not CTkSettings.deactivate_windows_window_header_manipulation: super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible super().update() diff --git a/examples/complex_example.py b/examples/complex_example.py index 54def95..18e686f 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -3,6 +3,7 @@ import tkinter.messagebox import customtkinter import sys +customtkinter.ScalingTracker.set_user_scaling(0.5) customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" diff --git a/examples/simple_example.py b/examples/simple_example.py index 41cb1e8..7c9936d 100644 --- a/examples/simple_example.py +++ b/examples/simple_example.py @@ -4,8 +4,6 @@ import customtkinter # <- import the CustomTkinter module customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light" customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" -customtkinter.ScalingTracker.set_user_scaling(2.5) - root_tk = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window) root_tk.geometry("400x480") root_tk.title("CustomTkinter Test") @@ -16,6 +14,10 @@ def button_function(): def slider_function(value): + customtkinter.ScalingTracker.set_widget_scaling(value * 2) + customtkinter.ScalingTracker.set_spacing_scaling(value * 2) + customtkinter.ScalingTracker.set_window_scaling(value * 2) + progressbar_1.set(value) diff --git a/test/test_button_antialiasing.py b/test/test_button_antialiasing.py index a18c8d9..4bcc5cd 100644 --- a/test/test_button_antialiasing.py +++ b/test/test_button_antialiasing.py @@ -29,7 +29,7 @@ f4.grid(row=0, column=3, rowspan=1, columnspan=1, sticky="nsew") f4.grid_columnconfigure(0, weight=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, current_height=34, border_width=2, text=f"{i} {i-2}", # border_color="white", fg_color=None, text_color="white") b = tkinter.Button(f1, text=f"{i} {i-2}", width=20) b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")