diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index bbc63c9..a4ce5db 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -1,18 +1,18 @@ __version__ = "3.12" -from .widgets.customtkinter_input_dialog import CTkInputDialog -from .widgets.customtkinter_button import CTkButton -from .widgets.customtkinter_slider import CTkSlider -from .widgets.customtkinter_frame import CTkFrame -from .widgets.customtkinter_progressbar import CTkProgressBar -from .widgets.customtkinter_label import CTkLabel -from .widgets.customtkinter_entry import CTkEntry -from .widgets.customtkinter_checkbox import CTkCheckBox -from .widgets.customtkinter_radiobutton import CTkRadioButton -from .widgets.customtkinter_tk import CTk -from .widgets.customtkinter_canvas import CTkCanvas -from .widgets.customtkinter_switch import CTkSwitch -from .widgets.customtkinter_toplevel import CTkToplevel +from .widgets.ctk_input_dialog import CTkInputDialog +from .widgets.ctk_button import CTkButton +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 .customtkinter_settings import CTkSettings from .appearance_mode_tracker import AppearanceModeTracker diff --git a/customtkinter/customtkinter_draw_engine.py b/customtkinter/customtkinter_draw_engine.py index bf74387..3a4a682 100644 --- a/customtkinter/customtkinter_draw_engine.py +++ b/customtkinter/customtkinter_draw_engine.py @@ -2,7 +2,7 @@ import sys import tkinter from typing import Union -from .widgets.customtkinter_canvas import CTkCanvas +from .widgets.ctk_canvas import CTkCanvas class CTkDrawEngine: diff --git a/customtkinter/widgets/customtkinter_button.py b/customtkinter/widgets/ctk_button.py similarity index 80% rename from customtkinter/widgets/customtkinter_button.py rename to customtkinter/widgets/ctk_button.py index e8f73c9..46a620a 100644 --- a/customtkinter/widgets/customtkinter_button.py +++ b/customtkinter/widgets/ctk_button.py @@ -1,17 +1,14 @@ import tkinter -import tkinter.ttk as ttk import sys -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from customtkinter.appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkButton(tkinter.Frame): +class CTkButton(CTkBaseClass): """ tkinter custom button with border, rounded corners and hover effect """ def __init__(self, *args, @@ -34,45 +31,18 @@ class CTkButton(tkinter.Frame): compound=tkinter.LEFT, state=tkinter.NORMAL, **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 - - # 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" + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) self.configure_basic_grid() # color variables - 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"]["button_border"] if border_color == "default_theme" else border_color - # shape and size - self.width = width - self.height = height - self.configure(width=self.width, height=self.height) + # shape self.corner_radius = CTkThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = CTkThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width @@ -98,7 +68,6 @@ class CTkButton(tkinter.Frame): width=self.width, height=self.height) self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) # event bindings @@ -113,45 +82,13 @@ class CTkButton(tkinter.Frame): self.set_cursor() self.draw() # initial draw - def destroy(self): - AppearanceModeTracker.remove(self.set_appearance_mode) - super().destroy() - def configure_basic_grid(self): - # Configuration of a basic grid (2x2) in which all elements of CTkButtons are centered on one row and one column + # Configuration of a grid system (2x2) in which all parts of CTkButton are centered on one row and one column self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(1, weight=1) self.grid_columnconfigure(1, weight=1) - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - self.width = event.width - self.height = event.height - - # self.canvas.config(width=self.width, height=self.height) - self.draw(no_color_updates=True) # fast drawing without color changes - - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - def draw(self, no_color_updates=False): requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width, self.height, self.corner_radius, self.border_width) @@ -256,9 +193,6 @@ class CTkButton(tkinter.Frame): self.image_label.grid(row=1, column=0, padx=max(self.corner_radius, self.border_width), sticky="n", columnspan=2, rowspan=1, pady=(2, self.border_width)) self.text_label.grid(row=0, column=0, padx=max(self.corner_radius, self.border_width), sticky="s", columnspan=2, rowspan=1, pady=(self.border_width, 2)) - 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() at the end @@ -401,16 +335,3 @@ class CTkButton(tkinter.Frame): self.after(100, self.click_animation) self.function() - - 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/widgets/customtkinter_canvas.py b/customtkinter/widgets/ctk_canvas.py similarity index 100% rename from customtkinter/widgets/customtkinter_canvas.py rename to customtkinter/widgets/ctk_canvas.py diff --git a/customtkinter/widgets/customtkinter_checkbox.py b/customtkinter/widgets/ctk_checkbox.py similarity index 80% rename from customtkinter/widgets/customtkinter_checkbox.py rename to customtkinter/widgets/ctk_checkbox.py index b59d593..9d78b64 100644 --- a/customtkinter/widgets/customtkinter_checkbox.py +++ b/customtkinter/widgets/ctk_checkbox.py @@ -1,17 +1,14 @@ import tkinter -import tkinter.ttk as ttk import sys -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkCheckBox(tkinter.Frame): +class CTkCheckBox(CTkBaseClass): """ tkinter custom checkbox with border, rounded corners and hover effect """ def __init__(self, *args, @@ -36,59 +33,28 @@ class CTkCheckBox(tkinter.Frame): 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 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - 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 - - # 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" - - self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color + # 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.checkmark_color = CTkThemeManager.theme["color"]["checkmark"] if checkmark_color == "default_theme" else checkmark_color - self.width = width - self.height = height + # shape self.corner_radius = CTkThemeManager.theme["shape"]["checkbox_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = CTkThemeManager.theme["shape"]["checkbox_border_width"] if border_width == "default_theme" else border_width - 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 - + # text self.text = text + self.text_label = None self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color self.text_color_disabled = CTkThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled self.text_font = (CTkThemeManager.theme["text"]["font"], CTkThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font + # callback and hover functionality self.function = command self.state = state self.hover = hover @@ -100,7 +66,7 @@ class CTkCheckBox(tkinter.Frame): self.textvariable = textvariable self.variable_callback_name = None - # configure grid system + # configure grid system (1x3) self.grid_columnconfigure(0, weight=0) self.grid_columnconfigure(1, weight=0, minsize=6) self.grid_columnconfigure(2, weight=1) @@ -110,7 +76,6 @@ class CTkCheckBox(tkinter.Frame): width=self.width, height=self.height) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1) - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) if self.hover is True: @@ -120,11 +85,7 @@ class CTkCheckBox(tkinter.Frame): self.canvas.bind("", self.toggle) self.canvas.bind("", self.toggle) - self.text_label = None - - self.set_cursor() - self.draw() # initial draw - + # set select state according to variable if self.variable is not None: self.variable_callback_name = self.variable.trace_add("write", self.variable_callback) if self.variable.get() == self.onvalue: @@ -132,34 +93,16 @@ class CTkCheckBox(tkinter.Frame): elif self.variable.get() == self.offvalue: self.deselect(from_variable_callback=True) - def destroy(self): - AppearanceModeTracker.remove(self.set_appearance_mode) + self.set_cursor() + self.draw() # initial draw + def destroy(self): if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) super().destroy() - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def draw(self): + def draw(self, no_color_updates=False): requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width, self.height, self.corner_radius, self.border_width) if self.check_state is True: @@ -207,9 +150,6 @@ class CTkCheckBox(tkinter.Frame): 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() @@ -378,16 +318,3 @@ class CTkCheckBox(tkinter.Frame): def get(self): return self.onvalue if self.check_state is True else self.offvalue - - 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/widgets/customtkinter_entry.py b/customtkinter/widgets/ctk_entry.py similarity index 66% rename from customtkinter/widgets/customtkinter_entry.py rename to customtkinter/widgets/ctk_entry.py index c5aee99..a473b83 100644 --- a/customtkinter/widgets/customtkinter_entry.py +++ b/customtkinter/widgets/ctk_entry.py @@ -1,18 +1,14 @@ import tkinter -import tkinter.ttk as ttk -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkEntry(tkinter.Frame): +class CTkEntry(CTkBaseClass): def __init__(self, *args, - master=None, bg_color=None, fg_color="default_theme", text_color="default_theme", @@ -25,39 +21,16 @@ class CTkEntry(tkinter.Frame): width=120, height=30, **kwargs): - if master is None: - super().__init__(*args) + + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + if "master" in kwargs: + super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs["master"]) + del kwargs["master"] else: - super().__init__(*args, master=master) - - # 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_appearance_mode, self) - self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + super().__init__(*args, bg_color=bg_color, width=width, height=height) self.configure_basic_grid() - self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color self.fg_color = CTkThemeManager.theme["color"]["entry"] if fg_color == "default_theme" else fg_color self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color self.placeholder_text_color = CTkThemeManager.theme["color"]["entry_placeholder_text"] if placeholder_text_color == "default_theme" else placeholder_text_color @@ -68,23 +41,15 @@ class CTkEntry(tkinter.Frame): self.placeholder_text_active = False self.pre_placeholder_arguments = {} # some set arguments of the entry will be changed for placeholder and then set back - self.width = width - self.height = height self.corner_radius = CTkThemeManager.theme["shape"]["button_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = CTkThemeManager.theme["shape"]["entry_border_width"] if border_width == "default_theme" else border_width - 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 - - super().configure(width=self.width, height=self.height) - self.canvas = CTkCanvas(master=self, highlightthickness=0, width=self.width, height=self.height) self.canvas.grid(column=0, row=0, sticky="we") + self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) self.entry = tkinter.Entry(master=self, bd=0, @@ -94,8 +59,6 @@ class CTkEntry(tkinter.Frame): **kwargs) self.entry.grid(column=0, row=0, sticky="we", padx=self.corner_radius if self.corner_radius >= 6 else 6) - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) - super().bind('', self.update_dimensions) self.entry.bind('', self.set_placeholder) self.entry.bind('', self.clear_placeholder) @@ -103,42 +66,10 @@ class CTkEntry(tkinter.Frame): self.draw() self.set_placeholder() - def destroy(self): - AppearanceModeTracker.remove(self.change_appearance_mode) - super().destroy() - def configure_basic_grid(self): self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - # print(event.x, event.width, self.width) - self.width = event.width - self.height = event.height - - self.draw() - def set_placeholder(self, event=None): if self.placeholder_text is not None: if not self.placeholder_text_active and self.entry.get() == "": @@ -156,7 +87,7 @@ class CTkEntry(tkinter.Frame): for argument, value in self.pre_placeholder_arguments.items(): self.entry[argument] = value - def draw(self): + 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.height, self.corner_radius, self.border_width) @@ -188,16 +119,16 @@ class CTkEntry(tkinter.Frame): def bind(self, *args, **kwargs): self.entry.bind(*args, **kwargs) - 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() at the end if "bg_color" in kwargs: - self.bg_color = kwargs["bg_color"] - del kwargs["bg_color"] + 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 "fg_color" in kwargs: self.fg_color = kwargs["fg_color"] @@ -232,7 +163,6 @@ class CTkEntry(tkinter.Frame): def delete(self, *args, **kwargs): self.entry.delete(*args, **kwargs) self.set_placeholder() - return def insert(self, *args, **kwargs): self.clear_placeholder() @@ -243,16 +173,3 @@ class CTkEntry(tkinter.Frame): return "" else: return self.entry.get() - - def change_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/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py new file mode 100644 index 0000000..529f0cf --- /dev/null +++ b/customtkinter/widgets/ctk_frame.py @@ -0,0 +1,111 @@ +import tkinter + +from .ctk_canvas import CTkCanvas +from ..customtkinter_theme_manager import CTkThemeManager +from ..customtkinter_settings import CTkSettings +from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass + + +class CTkFrame(CTkBaseClass): + def __init__(self, *args, + bg_color=None, + fg_color="default_theme", + border_color="default_theme", + border_width="default_theme", + corner_radius="default_theme", + width=200, + height=200, + **kwargs): + + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) + + # color + self.border_color = CTkThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color + + if fg_color == "default_theme": + if isinstance(self.master, CTkFrame): + if self.master.fg_color == CTkThemeManager.theme["color"]["frame_low"]: + self.fg_color = CTkThemeManager.theme["color"]["frame_high"] + else: + self.fg_color = CTkThemeManager.theme["color"]["frame_low"] + else: + self.fg_color = CTkThemeManager.theme["color"]["frame_low"] + else: + self.fg_color = fg_color + + # shape + self.corner_radius = CTkThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius + self.border_width = CTkThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width + + self.canvas = CTkCanvas(master=self, + highlightthickness=0, + width=self.width, + height=self.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) + + self.bind('', self.update_dimensions) + + self.draw() + + def winfo_children(self): + """ winfo_children of CTkFrame without self.canvas widget, + because it's not a child but part of the CTkFrame itself """ + + child_widgets = super().winfo_children() + try: + child_widgets.remove(self.canvas) + return child_widgets + except ValueError: + return child_widgets + + def draw(self, no_color_updates=False): + + requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width, self.height, self.corner_radius, self.border_width) + + if no_color_updates is False or requires_recoloring: + self.canvas.itemconfig("inner_parts", + fill=CTkThemeManager.single_color(self.fg_color, self.appearance_mode), + outline=CTkThemeManager.single_color(self.fg_color, self.appearance_mode)) + 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.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) + + self.canvas.tag_lower("inner_parts") + self.canvas.tag_lower("border_parts") + + def configure(self, *args, **kwargs): + require_redraw = False # some attribute changes require a call of self.draw() at the end + + if "fg_color" in kwargs: + self.fg_color = kwargs["fg_color"] + require_redraw = True + del kwargs["fg_color"] + + # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color + for child in self.winfo_children(): + if isinstance(child, CTkBaseClass): + child.configure(bg_color=self.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 "corner_radius" in kwargs: + self.corner_radius = kwargs["corner_radius"] + require_redraw = True + del kwargs["corner_radius"] + + super().configure(*args, **kwargs) + + if require_redraw: + self.draw() diff --git a/customtkinter/widgets/customtkinter_input_dialog.py b/customtkinter/widgets/ctk_input_dialog.py similarity index 94% rename from customtkinter/widgets/customtkinter_input_dialog.py rename to customtkinter/widgets/ctk_input_dialog.py index 6172f33..8a438bd 100644 --- a/customtkinter/widgets/customtkinter_input_dialog.py +++ b/customtkinter/widgets/ctk_input_dialog.py @@ -1,11 +1,11 @@ import tkinter import time -from .customtkinter_label import CTkLabel -from .customtkinter_entry import CTkEntry -from .customtkinter_frame import CTkFrame -from .customtkinter_toplevel import CTkToplevel -from .customtkinter_button import CTkButton +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 ..appearance_mode_tracker import AppearanceModeTracker from ..customtkinter_theme_manager import CTkThemeManager diff --git a/customtkinter/widgets/customtkinter_label.py b/customtkinter/widgets/ctk_label.py similarity index 53% rename from customtkinter/widgets/customtkinter_label.py rename to customtkinter/widgets/ctk_label.py index 02e3c68..4ed6925 100644 --- a/customtkinter/widgets/customtkinter_label.py +++ b/customtkinter/widgets/ctk_label.py @@ -1,18 +1,14 @@ import tkinter -import tkinter.ttk as ttk -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkLabel(tkinter.Frame): +class CTkLabel(CTkBaseClass): def __init__(self, *args, - master=None, bg_color=None, fg_color="default_theme", text_color="default_theme", @@ -22,54 +18,28 @@ class CTkLabel(tkinter.Frame): text="CTkLabel", text_font="default_theme", **kwargs): - if master is None: - super().__init__(*args) + + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + if "master" in kwargs: + super().__init__(*args, bg_color=bg_color, width=width, height=height, master=kwargs["master"]) + del kwargs["master"] else: - super().__init__(*args, master=master) + super().__init__(*args, bg_color=bg_color, width=width, height=height) - # 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_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 + # color self.fg_color = CTkThemeManager.theme["color"]["label"] if fg_color == "default_theme" else fg_color if self.fg_color is None: self.fg_color = self.bg_color self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color - self.width = width - self.height = height + # shape self.corner_radius = CTkThemeManager.theme["shape"]["label_corner_radius"] if corner_radius == "default_theme" else 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 - + # text self.text = text self.text_font = (CTkThemeManager.theme["text"]["font"], CTkThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font + # configure grid system (1x1) self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) @@ -78,6 +48,7 @@ class CTkLabel(tkinter.Frame): width=self.width, height=self.height) self.canvas.grid(row=0, column=0, sticky="nswe") + self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) self.text_label = tkinter.Label(master=self, highlightthickness=0, @@ -87,46 +58,10 @@ class CTkLabel(tkinter.Frame): **kwargs) self.text_label.grid(row=0, column=0, padx=self.corner_radius) - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) - - super().configure(width=self.width, height=self.height) - self.bind('', self.update_dimensions) self.draw() - def destroy(self): - AppearanceModeTracker.remove(self.change_appearance_mode) - super().destroy() - - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - self.width = event.width - self.height = event.height - - # self.canvas.config(width=self.width, height=self.height) - self.draw() - - def draw(self): + def draw(self, no_color_updates=False): requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width, self.height, self.corner_radius, 0) self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) @@ -146,9 +81,6 @@ class CTkLabel(tkinter.Frame): self.text_label.configure(fg=CTkThemeManager.single_color(self.text_color, self.appearance_mode), bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) - 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() at the end @@ -182,16 +114,3 @@ class CTkLabel(tkinter.Frame): def set_text(self, text): self.text = text self.text_label.configure(text=self.text, width=len(self.text)) - - def change_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/widgets/customtkinter_progressbar.py b/customtkinter/widgets/ctk_progressbar.py similarity index 60% rename from customtkinter/widgets/customtkinter_progressbar.py rename to customtkinter/widgets/ctk_progressbar.py index 572f6f3..aa536c0 100644 --- a/customtkinter/widgets/customtkinter_progressbar.py +++ b/customtkinter/widgets/ctk_progressbar.py @@ -1,17 +1,13 @@ -import sys import tkinter -import tkinter.ttk as ttk -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_draw_engine import CTkDrawEngine from ..customtkinter_settings import CTkSettings +from .widget_base_class import CTkBaseClass -class CTkProgressBar(tkinter.Frame): +class CTkProgressBar(CTkBaseClass): """ tkinter custom progressbar, always horizontal, values are from 0 to 1 """ def __init__(self, *args, @@ -25,56 +21,30 @@ class CTkProgressBar(tkinter.Frame): height=8, border_width="default_theme", **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 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_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 + # color self.border_color = CTkThemeManager.theme["color"]["progressbar_border"] if border_color == "default_theme" else border_color self.fg_color = CTkThemeManager.theme["color"]["progressbar"] if fg_color == "default_theme" else fg_color self.progress_color = CTkThemeManager.theme["color"]["progressbar_progress"] if progress_color == "default_theme" else progress_color + # control variable self.variable = variable self.variable_callback_blocked = False self.variable_callback_name = None - self.width = width - self.height = height + # shape self.corner_radius = CTkThemeManager.theme["shape"]["progressbar_corner_radius"] if corner_radius == "default_theme" else corner_radius self.border_width = CTkThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width self.value = 0.5 - self.configure(width=self.width, height=self.height) - self.canvas = CTkCanvas(master=self, highlightthickness=0, width=self.width, height=self.height) self.canvas.place(x=0, y=0, relwidth=1, relheight=1) - 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 @@ -89,53 +59,11 @@ class CTkProgressBar(tkinter.Frame): self.variable_callback_blocked = False def destroy(self): - AppearanceModeTracker.remove(self.change_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): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - @staticmethod - def calc_optimal_height(user_height): - if sys.platform == "darwin": - return user_height # on macOS just use given value (canvas has Antialiasing) - else: - # make sure the value is always with uneven for better rendering of the ovals - if user_height == 0: - return 0 - elif user_height % 2 == 0: - return user_height + 1 - else: - return user_height - - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - self.width = event.width - self.height = event.height - - self.draw() - def draw(self, no_color_updates=False): requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.width, self.height, self.corner_radius, self.border_width, self.value, "w") @@ -156,9 +84,12 @@ class CTkProgressBar(tkinter.Frame): require_redraw = False # some attribute changes require a call of self.draw() at the end if "bg_color" in kwargs: - self.bg_color = kwargs["bg_color"] - del kwargs["bg_color"] + 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 "fg_color" in kwargs: self.fg_color = kwargs["fg_color"] @@ -217,16 +148,3 @@ class CTkProgressBar(tkinter.Frame): self.variable_callback_blocked = True self.variable.set(round(self.value) if isinstance(self.variable, tkinter.IntVar) else self.value) self.variable_callback_blocked = False - - def change_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): - self.bg_color = self.master.fg_color - else: - self.bg_color = self.master.cget("bg") - - self.draw() diff --git a/customtkinter/widgets/customtkinter_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py similarity index 76% rename from customtkinter/widgets/customtkinter_radiobutton.py rename to customtkinter/widgets/ctk_radiobutton.py index b3943c9..1b5ef94 100644 --- a/customtkinter/widgets/customtkinter_radiobutton.py +++ b/customtkinter/widgets/ctk_radiobutton.py @@ -1,17 +1,14 @@ import tkinter -import tkinter.ttk as ttk import sys -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkRadioButton(tkinter.Frame): +class CTkRadioButton(CTkBaseClass): def __init__(self, *args, bg_color=None, fg_color="default_theme", @@ -33,60 +30,29 @@ class CTkRadioButton(tkinter.Frame): 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 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - 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 - - # 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" - - self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color + # 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 + # shape 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 - + # text self.text = text + self.text_label = None self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color self.text_color_disabled = CTkThemeManager.theme["color"]["text_disabled"] if text_color_disabled == "default_theme" else text_color_disabled self.text_font = (CTkThemeManager.theme["text"]["font"], CTkThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font + # callback and control variables self.function = command self.state = state self.hover = hover @@ -97,7 +63,7 @@ class CTkRadioButton(tkinter.Frame): self.textvariable = textvariable self.variable_callback_name = None - # configure grid system + # configure grid system (3x1) self.grid_columnconfigure(0, weight=0) self.grid_columnconfigure(1, weight=0, minsize=6) self.grid_columnconfigure(2, weight=1) @@ -107,7 +73,6 @@ class CTkRadioButton(tkinter.Frame): width=self.width, height=self.height) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1) - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) self.canvas.bind("", self.on_enter) @@ -115,8 +80,6 @@ class CTkRadioButton(tkinter.Frame): self.canvas.bind("", self.invoke) self.canvas.bind("", self.invoke) - self.text_label = None - self.set_cursor() self.draw() # initial draw @@ -128,33 +91,12 @@ class CTkRadioButton(tkinter.Frame): 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): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def draw(self): + def draw(self, no_color_updates=False): 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)) @@ -191,9 +133,6 @@ class CTkRadioButton(tkinter.Frame): 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() @@ -338,16 +277,3 @@ class CTkRadioButton(tkinter.Frame): 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/widgets/customtkinter_slider.py b/customtkinter/widgets/ctk_slider.py similarity index 73% rename from customtkinter/widgets/customtkinter_slider.py rename to customtkinter/widgets/ctk_slider.py index 4e57759..7ef2c11 100644 --- a/customtkinter/widgets/customtkinter_slider.py +++ b/customtkinter/widgets/ctk_slider.py @@ -1,17 +1,14 @@ import tkinter -import tkinter.ttk as ttk import sys -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkSlider(tkinter.Frame): +class CTkSlider(CTkBaseClass): """ tkinter custom slider, always horizontal """ def __init__(self, *args, @@ -33,44 +30,18 @@ class CTkSlider(tkinter.Frame): command=None, variable=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 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_appearance_mode, self) - self.appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" - - self.configure_basic_grid() - - self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color + # color self.border_color = border_color self.fg_color = CTkThemeManager.theme["color"]["slider"] if fg_color == "default_theme" else fg_color self.progress_color = CTkThemeManager.theme["color"]["slider_progress"] if progress_color == "default_theme" else progress_color self.button_color = CTkThemeManager.theme["color"]["slider_button"] if button_color == "default_theme" else button_color self.button_hover_color = CTkThemeManager.theme["color"]["slider_button_hover"] if button_hover_color == "default_theme" else button_hover_color - self.width = width - self.height = height + # shape self.corner_radius = CTkThemeManager.theme["shape"]["slider_corner_radius"] if corner_radius == "default_theme" else corner_radius self.button_corner_radius = CTkThemeManager.theme["shape"]["slider_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius self.border_width = CTkThemeManager.theme["shape"]["slider_border_width"] if border_width == "default_theme" else border_width @@ -85,23 +56,17 @@ class CTkSlider(tkinter.Frame): if self.corner_radius < self.button_corner_radius: self.corner_radius = self.button_corner_radius + # callback and control variables self.callback_function = command self.variable: tkinter.Variable = variable self.variable_callback_blocked = False self.variable_callback_name = None - self.configure(width=self.width, height=self.height) - if sys.platform == "darwin": - self.configure(cursor="pointinghand") - elif sys.platform.startswith("win"): - self.configure(cursor="hand2") - self.canvas = CTkCanvas(master=self, highlightthickness=0, width=self.width, height=self.height) self.canvas.grid(column=0, row=0, sticky="nswe") - self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method) self.canvas.bind("", self.on_enter) @@ -112,6 +77,7 @@ class CTkSlider(tkinter.Frame): # Each time an item is resized due to pack position mode, the binding Configure is called on the widget self.bind('', self.update_dimensions) + self.set_cursor() self.draw() # initial draw if self.variable is not None: @@ -121,9 +87,6 @@ class CTkSlider(tkinter.Frame): self.variable_callback_blocked = False def destroy(self): - # remove change_appearance_mode function from callback list of AppearanceModeTracker - AppearanceModeTracker.remove(self.change_appearance_mode) - # remove variable_callback from variable callbacks if variable exists if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) @@ -134,52 +97,17 @@ class CTkSlider(tkinter.Frame): self.grid_rowconfigure(0, weight=1) self.grid_columnconfigure(0, weight=1) - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - @staticmethod - def calc_optimal_height(user_height): + def set_cursor(self): if sys.platform == "darwin": - return user_height # on macOS just use given value (canvas has Antialiasing) - else: - # make sure the value is always with uneven for better rendering of the ovals - if user_height == 0: - return 0 - elif user_height % 2 == 0: - return user_height + 1 - else: - return user_height - - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - self.width = event.width - self.height = event.height - - self.draw() - - def draw(self, color_updates=True): + 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.height, self.corner_radius, self.border_width, self.button_length, self.button_corner_radius, self.value, "w") - if color_updates or requires_recoloring: + if no_color_updates is False or requires_recoloring: self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) if self.border_color is None: @@ -213,7 +141,7 @@ class CTkSlider(tkinter.Frame): self.output_value = self.round_to_step_size(self.from_ + (self.value * (self.to - self.from_))) self.value = (self.output_value - self.from_) / (self.to - self.from_) - self.draw(color_updates=False) + self.draw(no_color_updates=False) if self.callback_function is not None: self.callback_function(self.output_value) @@ -259,7 +187,7 @@ class CTkSlider(tkinter.Frame): self.output_value = self.round_to_step_size(output_value) self.value = (self.output_value - self.from_) / (self.to - self.from_) - self.draw(color_updates=False) + self.draw(no_color_updates=False) if self.callback_function is not None: self.callback_function(self.output_value) @@ -273,9 +201,6 @@ class CTkSlider(tkinter.Frame): if not self.variable_callback_blocked: self.set(self.variable.get(), from_variable_callback=True) - 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() at the end @@ -354,16 +279,3 @@ class CTkSlider(tkinter.Frame): if require_redraw: self.draw() - - def change_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): - self.bg_color = self.master.fg_color - else: - self.bg_color = self.master.cget("bg") - - self.draw() diff --git a/customtkinter/widgets/customtkinter_switch.py b/customtkinter/widgets/ctk_switch.py similarity index 77% rename from customtkinter/widgets/customtkinter_switch.py rename to customtkinter/widgets/ctk_switch.py index b6d9c66..467596b 100644 --- a/customtkinter/widgets/customtkinter_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -1,17 +1,14 @@ import tkinter -import tkinter.ttk as ttk import sys -from .customtkinter_tk import CTk -from .customtkinter_frame import CTkFrame -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker +from .ctk_canvas import CTkCanvas from ..customtkinter_theme_manager import CTkThemeManager from ..customtkinter_settings import CTkSettings from ..customtkinter_draw_engine import CTkDrawEngine +from .widget_base_class import CTkBaseClass -class CTkSwitch(tkinter.Frame): +class CTkSwitch(CTkBaseClass): def __init__(self, *args, text="CTkSwitch", text_font="default_theme", @@ -34,34 +31,11 @@ class CTkSwitch(tkinter.Frame): 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 + # transfer basic functionality (bg_color, size, appearance_mode, scaling) to CTkBaseClass + super().__init__(*args, bg_color=bg_color, width=width, height=height, **kwargs) - 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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_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 + # color self.border_color = border_color self.fg_color = CTkThemeManager.theme["color"]["switch"] if fg_color == "default_theme" else fg_color self.progress_color = CTkThemeManager.theme["color"]["switch_progress"] if progress_color == "default_theme" else progress_color @@ -69,10 +43,12 @@ class CTkSwitch(tkinter.Frame): self.button_hover_color = CTkThemeManager.theme["color"]["switch_button_hover"] if button_hover_color == "default_theme" else button_hover_color self.text_color = CTkThemeManager.theme["color"]["text"] if text_color == "default_theme" else text_color + # text self.text = text + self.text_label = None self.text_font = (CTkThemeManager.theme["text"]["font"], CTkThemeManager.theme["text"]["size"]) if text_font == "default_theme" else text_font - self.width = width - self.height = height + + # shape self.corner_radius = CTkThemeManager.theme["shape"]["switch_corner_radius"] if corner_radius == "default_theme" else corner_radius # self.button_corner_radius = CTkThemeManager.theme["shape"]["switch_button_corner_radius"] if button_corner_radius == "default_theme" else button_corner_radius self.border_width = CTkThemeManager.theme["shape"]["switch_border_width"] if border_width == "default_theme" else border_width @@ -82,16 +58,17 @@ class CTkSwitch(tkinter.Frame): self.onvalue = onvalue self.offvalue = offvalue - #if self.corner_radius < self.button_corner_radius: - # self.corner_radius = self.button_corner_radius + # if self.corner_radius < self.button_corner_radius: + # self.corner_radius = self.button_corner_radius + # callback and control variables self.callback_function = command self.variable: tkinter.Variable = variable self.variable_callback_blocked = False self.variable_callback_name = None self.textvariable = textvariable - # configure grid system + # configure grid system (3x1) self.grid_columnconfigure(0, weight=1) self.grid_columnconfigure(1, weight=0, minsize=6) self.grid_columnconfigure(2, weight=0) @@ -101,20 +78,13 @@ class CTkSwitch(tkinter.Frame): width=self.width, height=self.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) - 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") - self.canvas.bind("", self.on_enter) self.canvas.bind("", self.on_leave) self.canvas.bind("", self.toggle) - self.text_label = None - + self.set_cursor() self.draw() # initial draw if self.variable is not None: @@ -125,35 +95,19 @@ class CTkSwitch(tkinter.Frame): self.deselect(from_variable_callback=True) def destroy(self): - # remove change_appearance_mode function from callback list of AppearanceModeTracker - AppearanceModeTracker.remove(self.change_appearance_mode) - # remove variable_callback from variable callbacks if variable exists if self.variable is not None: self.variable.trace_remove("write", self.variable_callback_name) super().destroy() - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ + 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 isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def draw(self, color_updates=True): + 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.height, self.corner_radius, self.border_width, @@ -162,7 +116,7 @@ class CTkSwitch(tkinter.Frame): requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width, self.height, self.corner_radius, self.border_width, self.button_length, self.corner_radius, 0, "w") - if color_updates or requires_recoloring: + if no_color_updates is False or requires_recoloring: self.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) @@ -186,8 +140,6 @@ class CTkSwitch(tkinter.Frame): 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)) - # self.canvas.configure(bg="red") - if self.text_label is None: self.text_label = tkinter.Label(master=self, bd=0, @@ -208,8 +160,6 @@ class CTkSwitch(tkinter.Frame): self.text = text if self.text_label is not None: self.text_label.configure(text=self.text) - else: - sys.stderr.write("ERROR (CTkSwitch): Cant change text because checkbox has no text.") def toggle(self, event=None): if self.check_state is True: @@ -217,7 +167,7 @@ class CTkSwitch(tkinter.Frame): else: self.check_state = True - self.draw(color_updates=False) + self.draw(no_color_updates=True) if self.callback_function is not None: self.callback_function() @@ -230,7 +180,7 @@ class CTkSwitch(tkinter.Frame): def select(self, from_variable_callback=False): self.check_state = True - self.draw(color_updates=False) + self.draw(no_color_updates=True) if self.callback_function is not None: self.callback_function() @@ -243,7 +193,7 @@ class CTkSwitch(tkinter.Frame): def deselect(self, from_variable_callback=False): self.check_state = False - self.draw(color_updates=False) + self.draw(no_color_updates=True) if self.callback_function is not None: self.callback_function() @@ -273,9 +223,6 @@ class CTkSwitch(tkinter.Frame): elif self.variable.get() == self.offvalue: self.deselect(from_variable_callback=True) - 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() at the end @@ -349,16 +296,3 @@ class CTkSwitch(tkinter.Frame): if require_redraw: self.draw() - - def change_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): - self.bg_color = self.master.fg_color - else: - self.bg_color = self.master.cget("bg") - - self.draw() diff --git a/customtkinter/widgets/customtkinter_tk.py b/customtkinter/widgets/ctk_tk.py similarity index 94% rename from customtkinter/widgets/customtkinter_tk.py rename to customtkinter/widgets/ctk_tk.py index 9a9d1b1..d0956ff 100644 --- a/customtkinter/widgets/customtkinter_tk.py +++ b/customtkinter/widgets/ctk_tk.py @@ -97,13 +97,13 @@ class CTk(tkinter.Tk): args[0]["background"] = CTkThemeManager.single_color(self.fg_color, self.appearance_mode) if bg_changed: - from .customtkinter_slider import CTkSlider - from .customtkinter_progressbar import CTkProgressBar - from .customtkinter_label import CTkLabel - from .customtkinter_frame import CTkFrame - from .customtkinter_entry import CTkEntry - from customtkinter.widgets.customtkinter_checkbox import CTkCheckBox - from customtkinter.widgets.customtkinter_button import CTkButton + 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 for child in self.winfo_children(): if isinstance(child, (CTkFrame, CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar)): diff --git a/customtkinter/widgets/customtkinter_toplevel.py b/customtkinter/widgets/ctk_toplevel.py similarity index 93% rename from customtkinter/widgets/customtkinter_toplevel.py rename to customtkinter/widgets/ctk_toplevel.py index 6ff801f..3613dcd 100644 --- a/customtkinter/widgets/customtkinter_toplevel.py +++ b/customtkinter/widgets/ctk_toplevel.py @@ -83,13 +83,13 @@ class CTkToplevel(tkinter.Toplevel): args[0]["background"] = CTkThemeManager.single_color(self.fg_color, self.appearance_mode) if bg_changed: - from .customtkinter_slider import CTkSlider - from .customtkinter_progressbar import CTkProgressBar - from .customtkinter_label import CTkLabel - from .customtkinter_frame import CTkFrame - from .customtkinter_entry import CTkEntry - from customtkinter.widgets.customtkinter_checkbox import CTkCheckBox - from customtkinter.widgets.customtkinter_button import CTkButton + 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 for child in self.winfo_children(): if isinstance(child, (CTkFrame, CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar)): diff --git a/customtkinter/widgets/customtkinter_frame.py b/customtkinter/widgets/customtkinter_frame.py deleted file mode 100644 index 11fda7b..0000000 --- a/customtkinter/widgets/customtkinter_frame.py +++ /dev/null @@ -1,203 +0,0 @@ -import tkinter -import tkinter.ttk as ttk - -from .customtkinter_tk import CTk -from .customtkinter_canvas import CTkCanvas -from ..appearance_mode_tracker import AppearanceModeTracker -from ..customtkinter_theme_manager import CTkThemeManager -from ..customtkinter_settings import CTkSettings -from ..customtkinter_draw_engine import CTkDrawEngine - - -class CTkFrame(tkinter.Frame): - def __init__(self, *args, - bg_color=None, - fg_color="default_theme", - border_color="default_theme", - border_width="default_theme", - corner_radius="default_theme", - width=200, - height=200, - **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 - - # add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes - AppearanceModeTracker.add(self.change_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.border_color = CTkThemeManager.theme["color"]["frame_border"] if border_color == "default_theme" else border_color - - if fg_color == "default_theme": - if isinstance(self.master, CTkFrame): - if self.master.fg_color == CTkThemeManager.theme["color"]["frame_low"]: - self.fg_color = CTkThemeManager.theme["color"]["frame_high"] - else: - self.fg_color = CTkThemeManager.theme["color"]["frame_low"] - else: - self.fg_color = CTkThemeManager.theme["color"]["frame_low"] - else: - self.fg_color = fg_color - - self.width = width - self.height = height - self.configure(width=self.width, height=self.height) - - self.corner_radius = CTkThemeManager.theme["shape"]["frame_corner_radius"] if corner_radius == "default_theme" else corner_radius - self.border_width = CTkThemeManager.theme["shape"]["frame_border_width"] if border_width == "default_theme" else border_width - - 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.canvas = CTkCanvas(master=self, - highlightthickness=0, - width=self.width, - height=self.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) - - self.bind('', self.update_dimensions) - - self.draw() - - def destroy(self): - AppearanceModeTracker.remove(self.change_appearance_mode) - super().destroy() - - def winfo_children(self): - """ winfo_children of CTkFrame without self.canvas widget, - because it's not a child but part of the CTkFrame itself """ - - child_widgets = super().winfo_children() - try: - child_widgets.remove(self.canvas) - return child_widgets - except ValueError: - return child_widgets - - def detect_color_of_master(self): - """ detect color of self.master widget to set correct bg_color """ - - if isinstance(self.master, CTkFrame): # master is CTkFrame - return self.master.fg_color - - elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget - try: - ttk_style = ttk.Style() - return ttk_style.lookup(self.master.winfo_class(), 'background') - except Exception: - return "#FFFFFF", "#000000" - - else: # master is normal tkinter widget - try: - return self.master.cget("bg") # try to get bg color by .cget() method - except Exception: - return "#FFFFFF", "#000000" - - def update_dimensions(self, event): - # only redraw if dimensions changed (for performance) - if self.width != event.width or self.height != event.height: - self.width = event.width - self.height = event.height - - self.draw() - - 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.itemconfig("inner_parts", - fill=CTkThemeManager.single_color(self.fg_color, self.appearance_mode), - outline=CTkThemeManager.single_color(self.fg_color, self.appearance_mode)) - 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.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode)) - - self.canvas.tag_lower("inner_parts") - self.canvas.tag_lower("border_parts") - - 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() at the end - - if "fg_color" in kwargs: - self.fg_color = kwargs["fg_color"] - require_redraw = True - del kwargs["fg_color"] - - # check if CTk widgets are children of the frame and change their bg_color to new frame fg_color - from .customtkinter_slider import CTkSlider - from .customtkinter_progressbar import CTkProgressBar - from .customtkinter_label import CTkLabel - from .customtkinter_entry import CTkEntry - from customtkinter.widgets.customtkinter_checkbox import CTkCheckBox - from customtkinter.widgets.customtkinter_button import CTkButton - - for child in self.winfo_children(): - if isinstance(child, (CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar, CTkFrame)): - child.configure(bg_color=self.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 "corner_radius" in kwargs: - self.corner_radius = kwargs["corner_radius"] - require_redraw = True - del kwargs["corner_radius"] - - super().configure(*args, **kwargs) - - if require_redraw: - self.draw() - - def change_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): - self.bg_color = self.master.fg_color - else: - self.bg_color = self.master.cget("bg") - - self.draw() diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/widget_base_class.py new file mode 100644 index 0000000..796c2cb --- /dev/null +++ b/customtkinter/widgets/widget_base_class.py @@ -0,0 +1,111 @@ +import tkinter +import tkinter.ttk as ttk + +from .ctk_tk import CTk +from .ctk_toplevel import CTkToplevel +from ..appearance_mode_tracker import AppearanceModeTracker + + +class CTkBaseClass(tkinter.Frame): + def __init__(self, *args, bg_color=None, width, height, **kwargs): + 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 + + # 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" + + # 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.Toplevel, tkinter.Frame)) and not isinstance(self.master, (CTkBaseClass, CTk, CTkToplevel)): + 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 + + def destroy(self): + AppearanceModeTracker.remove(self.set_appearance_mode) + super().destroy() + + def config(self, *args, **kwargs): + self.configure(*args, **kwargs) + + def configure(self, *args, **kwargs): + """ basic configure with bg_color support, to be overridden """ + + require_redraw = False + + 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"] + + super().configure(*args, **kwargs) + + if require_redraw: + self.draw() + + def update_dimensions(self, event): + # only redraw if dimensions changed (for performance) + if self.width != event.width or self.height != event.height: + self.width = event.width # adjust current size according to new size given by event + self.height = event.height + + self.draw(no_color_updates=True) # faster drawing without color changes + + def detect_color_of_master(self): + """ detect color of self.master widget to set correct bg_color """ + + if isinstance(self.master, CTkBaseClass) and hasattr(self.master, "fg_color"): # master is CTkFrame + return self.master.fg_color + + elif isinstance(self.master, (ttk.Frame, ttk.LabelFrame, ttk.Notebook)): # master is ttk widget + try: + ttk_style = ttk.Style() + return ttk_style.lookup(self.master.winfo_class(), 'background') + except Exception: + return "#FFFFFF", "#000000" + + else: # master is normal tkinter widget + try: + return self.master.cget("bg") # try to get bg color by .cget() method + except Exception: + return "#FFFFFF", "#000000" + + 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, (CTkBaseClass, CTk)) and hasattr(self.master, "fg_color"): + self.bg_color = self.master.fg_color + else: + self.bg_color = self.master.cget("bg") + + self.draw() + + def draw(self, no_color_updates=False): + """ abstract of draw method to be overridden """ + pass + + diff --git a/test/test_string_dialog.py b/test/test_string_dialog.py index 2444781..7ea7ff6 100644 --- a/test/test_string_dialog.py +++ b/test/test_string_dialog.py @@ -10,9 +10,9 @@ app.title("CTkDialog Test") def change_mode(): - if customtkinter.get_appearance_mode().lower() == "dark": + if c1.get() == 0: customtkinter.set_appearance_mode("light") - elif customtkinter.get_appearance_mode().lower() == "light": + else: customtkinter.set_appearance_mode("dark")