From 7374e7a3bc7c45e1205365850d63982ab2d73d14 Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Tue, 1 Nov 2022 00:37:30 +0100 Subject: [PATCH] architecture fixes --- customtkinter/__init__.py | 3 + customtkinter/windows/ctk_tk.py | 34 ++---- customtkinter/windows/ctk_toplevel.py | 28 ++--- .../appearance_mode_base_class.py | 45 +++++--- .../core_widget_classes/dropdown_menu.py | 8 +- .../core_widget_classes/widget_base_class.py | 19 ++- customtkinter/windows/widgets/ctk_button.py | 19 ++- customtkinter/windows/widgets/ctk_checkbox.py | 4 +- customtkinter/windows/widgets/ctk_combobox.py | 4 +- customtkinter/windows/widgets/ctk_entry.py | 2 + customtkinter/windows/widgets/ctk_frame.py | 4 +- customtkinter/windows/widgets/ctk_label.py | 4 +- .../windows/widgets/ctk_optionmenu.py | 4 +- .../windows/widgets/ctk_progressbar.py | 4 +- .../windows/widgets/ctk_radiobutton.py | 4 +- .../windows/widgets/ctk_scrollbar.py | 8 +- customtkinter/windows/widgets/ctk_slider.py | 4 +- customtkinter/windows/widgets/ctk_switch.py | 3 +- customtkinter/windows/widgets/ctk_tabview.py | 2 + customtkinter/windows/widgets/ctk_textbox.py | 3 +- .../windows/widgets/image/ctk_image.py | 26 +++-- .../widgets/scaling/scaling_base_class.py | 108 +++++++++++------- examples/complex_example.py | 106 ++++++++--------- examples/example_button_images.py | 75 ++---------- examples/test_images/add-folder.png | Bin 4901 -> 0 bytes .../test_segmented_button.py | 14 +-- test/manual_integration_tests/test_tabview.py | 2 +- 27 files changed, 274 insertions(+), 263 deletions(-) delete mode 100644 examples/test_images/add-folder.png diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index c3cca8f..4ac2e04 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -70,6 +70,9 @@ from .windows.ctk_input_dialog import CTkInputDialog # font classes from .windows.widgets.font.ctk_font import CTkFont +# image classes +from .windows.widgets.image.ctk_image import CTkImage + def set_appearance_mode(mode_string: str): """ possible values: light, dark, system """ diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index c7e5251..e74730b 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -63,10 +63,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): self._iconify_called_before_window_exists = False # indicates if iconify() was called before window is first shown through update() or mainloop() if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self._windows_set_titlebar_color("dark") - else: - self._windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) @@ -97,12 +94,12 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): # detected_width = event.width # detected_height = event.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 + if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): + self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event + self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale def _set_scaling(self, new_widget_scaling, new_window_scaling): - self._window_scaling = new_window_scaling + super()._set_scaling(new_widget_scaling, new_window_scaling) # block update_dimensions_event to prevent current_width and current_height to get updated self._block_update_dimensions_event = True @@ -164,12 +161,9 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): self._last_resizable_args = ([], {"width": width, "height": height}) if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self._windows_set_titlebar_color("dark") - else: - self._windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) - def minsize(self, width=None, height=None): + def minsize(self, width: int = None, height: int = None): self._min_width = width self._min_height = height if self._current_width < width: @@ -178,7 +172,7 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): self._current_height = height super().minsize(self._apply_window_scaling(self._min_width), self._apply_window_scaling(self._min_height)) - def maxsize(self, width=None, height=None): + def maxsize(self, width: int = None, height: int = None): self._max_width = width self._max_height = height if self._current_width > width: @@ -297,16 +291,10 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): else: pass # wait for update or mainloop to be called - 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 + def _set_appearance_mode(self, mode_string: str): + super()._set_appearance_mode(mode_string) if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self._windows_set_titlebar_color("dark") - else: - self._windows_set_titlebar_color("light") + self._windows_set_titlebar_color(mode_string) super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 6c25077..d26a6ac 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -60,10 +60,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self._iconify_called_after_windows_set_titlebar_color = False # indicates if iconify() was called after windows_set_titlebar_color if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self._windows_set_titlebar_color("dark") - else: - self._windows_set_titlebar_color("light") + self._windows_set_titlebar_color(self._get_appearance_mode()) self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) @@ -85,12 +82,12 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl 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 + if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): + self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event + self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale def _set_scaling(self, new_widget_scaling, new_window_scaling): - self._window_scaling = new_window_scaling + super()._set_scaling(new_widget_scaling, new_window_scaling) # force new dimensions on window by using min, max, and geometry super().minsize(self._apply_window_scaling(self._current_width), self._apply_window_scaling(self._current_height)) @@ -134,10 +131,7 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self._last_resizable_args = ([], {"width": width, "height": height}) if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self.after(10, lambda: self._windows_set_titlebar_color("dark")) - else: - self.after(10, lambda: self._windows_set_titlebar_color("light")) + self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode())) def minsize(self, width=None, height=None): self._min_width = width @@ -259,15 +253,9 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self._iconify_called_after_windows_set_titlebar_color = 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 + super()._set_appearance_mode(mode_string) if sys.platform.startswith("win"): - if self._appearance_mode == 1: - self._windows_set_titlebar_color("dark") - else: - self._windows_set_titlebar_color("light") + self._windows_set_titlebar_color(mode_string) super().configure(bg=self._apply_appearance_mode(self._fg_color)) diff --git a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py index 8db5978..910d2bf 100644 --- a/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py +++ b/customtkinter/windows/widgets/appearance_mode/appearance_mode_base_class.py @@ -1,27 +1,46 @@ from typing import Union, Tuple, List -from abc import ABC, abstractmethod from .appearance_mode_tracker import AppearanceModeTracker -class CTkAppearanceModeBaseClass(ABC): +class CTkAppearanceModeBaseClass: + """ + Super-class that manages the appearance mode. Methods: + + - destroy() must be called when sub-class is destroyed + - _set_appearance_mode() abstractmethod, gets called when appearance mode changes, must be overridden + - _apply_appearance_mode() + + """ def __init__(self): AppearanceModeTracker.add(self._set_appearance_mode, self) - self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" + self.__appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark" def destroy(self): AppearanceModeTracker.remove(self._set_appearance_mode) - def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str: - """ color can be either a single hex color string or a color name or it can be a - tuple color with (light_color, dark_color). The functions returns - always a single color string """ + def _set_appearance_mode(self, mode_string: str): + """ can be overridden but super method must be called at the beginning """ + if mode_string.lower() == "dark": + self.__appearance_mode = 1 + elif mode_string.lower() == "light": + self.__appearance_mode = 0 - if type(color) == tuple or type(color) == list: - return color[self._appearance_mode] + def _get_appearance_mode(self) -> str: + """ get appearance mode as a string, 'light' or 'dark' """ + if self.__appearance_mode == 0: + return "light" + else: + return "dark" + + def _apply_appearance_mode(self, color: Union[str, Tuple[str, str], List[str]]) -> str: + """ + color can be either a single hex color string or a color name or it can be a + tuple color with (light_color, dark_color). The functions returns + always a single color string + """ + + if isinstance(color, (tuple, list)): + return color[self.__appearance_mode] else: return color - - @abstractmethod - def _set_appearance_mode(self, mode_string: str): - return diff --git a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py index 6a47bb0..846400e 100644 --- a/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py +++ b/customtkinter/windows/widgets/core_widget_classes/dropdown_menu.py @@ -83,12 +83,12 @@ class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass): else: super().configure(tearoff=False, relief="flat", - activebackground=ThemeManager._apply_appearance_mode(self._hover_color, self._appearance_mode), + activebackground=self._apply_appearance_mode(self._hover_color), borderwidth=0, activeborderwidth=0, - bg=ThemeManager._apply_appearance_mode(self._fg_color, self._appearance_mode), - fg=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode), - activeforeground=ThemeManager._apply_appearance_mode(self._text_color, self._appearance_mode), + bg=self._apply_appearance_mode(self._fg_color), + fg=self._apply_appearance_mode(self._text_color), + activeforeground=self._apply_appearance_mode(self._text_color), font=self._apply_font_scaling(self._font)) def _add_menu_commands(self): diff --git a/customtkinter/windows/widgets/core_widget_classes/widget_base_class.py b/customtkinter/windows/widgets/core_widget_classes/widget_base_class.py index 82eef05..fcd6f68 100644 --- a/customtkinter/windows/widgets/core_widget_classes/widget_base_class.py +++ b/customtkinter/windows/widgets/core_widget_classes/widget_base_class.py @@ -98,7 +98,9 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas CTkScalingBaseClass.destroy(self) def _draw(self, no_color_updates: bool = False): - return + """ can be overridden but super method must be called """ + if no_color_updates is False: + super().configure(bg=self._apply_appearance_mode(self._bg_color)) def config(self, *args, **kwargs): raise AttributeError("'config' is not implemented for CTk widgets. For consistency, always use 'configure' instead.") @@ -164,9 +166,9 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas def _update_dimensions_event(self, event): # only redraw if dimensions changed (for performance), independent of scaling - if round(self._current_width) != round(event.width / self._widget_scaling) or round(self._current_height) != round(event.height / self._widget_scaling): - self._current_width = (event.width / self._widget_scaling) # adjust current size according to new size given by event - self._current_height = (event.height / self._widget_scaling) # _current_width and _current_height are independent of the scale + if round(self._current_width) != round(self._reverse_widget_scaling(event.width)) or round(self._current_height) != round(self._reverse_widget_scaling(event.height)): + self._current_width = self._reverse_widget_scaling(event.width) # adjust current size according to new size given by event + self._current_height = self._reverse_widget_scaling(event.height) # _current_width and _current_height are independent of the scale self._draw(no_color_updates=True) # faster drawing without color changes @@ -198,16 +200,11 @@ class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClas 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 - - super().configure(bg=self._apply_appearance_mode(self._bg_color)) + super()._set_appearance_mode(mode_string) self._draw() def _set_scaling(self, new_widget_scaling, new_window_scaling): - self._widget_scaling = new_widget_scaling + super()._set_scaling(new_widget_scaling, new_window_scaling) super().configure(width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) diff --git a/customtkinter/windows/widgets/ctk_button.py b/customtkinter/windows/widgets/ctk_button.py index f2c83c2..b606ab1 100644 --- a/customtkinter/windows/widgets/ctk_button.py +++ b/customtkinter/windows/widgets/ctk_button.py @@ -48,7 +48,7 @@ class CTkButton(CTkBaseClass): anchor: str = "center", **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color @@ -74,6 +74,8 @@ class CTkButton(CTkBaseClass): self._image_label: Union[tkinter.Label, None] = None self._text = text self._text_label: Union[tkinter.Label, None] = None + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) # font self._font = CTkFont() if font == "default_theme" else self._check_font_type(font) @@ -135,12 +137,19 @@ class CTkButton(CTkBaseClass): self._canvas.grid_forget() self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew") + def _update_image(self): + if self._image_label is not None: + self._image_label.configure(image=self._image.create_scaled_photo_image(self._get_widget_scaling(), + self._get_appearance_mode())) + def destroy(self): if isinstance(self._font, CTkFont): self._font.remove_size_configure_callback(self._update_font) super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + if self._background_corner_colors is not None: self._draw_engine.draw_background_corners(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height)) @@ -233,7 +242,7 @@ class CTkButton(CTkBaseClass): else: self._image_label.configure(bg=self._apply_appearance_mode(self._fg_color)) - self._image_label.configure(image=self._image) # set image + self._update_image() # set image else: # delete text_label if no text given @@ -354,7 +363,6 @@ class CTkButton(CTkBaseClass): require_redraw = True # text_label will be created in .draw() else: self._text_label.configure(text=self._text) - self._create_grid() if "font" in kwargs: if isinstance(self._font, CTkFont): @@ -371,8 +379,11 @@ class CTkButton(CTkBaseClass): self._text_label.configure(textvariable=self._textvariable) if "image" in kwargs: + if isinstance(self._image, CTkImage): + self._image.remove_configure_callback(self._update_image) self._image = kwargs.pop("image") - self._create_grid() + if isinstance(self._image, CTkImage): + self._image.add_configure_callback(self._update_image) require_redraw = True if "state" in kwargs: diff --git a/customtkinter/windows/widgets/ctk_checkbox.py b/customtkinter/windows/widgets/ctk_checkbox.py index ed2d459..238de15 100644 --- a/customtkinter/windows/widgets/ctk_checkbox.py +++ b/customtkinter/windows/widgets/ctk_checkbox.py @@ -43,7 +43,7 @@ class CTkCheckBox(CTkBaseClass): variable: tkinter.Variable = None, **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # dimensions @@ -169,6 +169,8 @@ class CTkCheckBox(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + requires_recoloring_1 = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._checkbox_width), self._apply_widget_scaling(self._checkbox_height), self._apply_widget_scaling(self._corner_radius), diff --git a/customtkinter/windows/widgets/ctk_combobox.py b/customtkinter/windows/widgets/ctk_combobox.py index 6889664..86526b9 100644 --- a/customtkinter/windows/widgets/ctk_combobox.py +++ b/customtkinter/windows/widgets/ctk_combobox.py @@ -44,7 +44,7 @@ class CTkComboBox(CTkBaseClass): justify: str = "left", **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color variables @@ -167,6 +167,8 @@ class CTkComboBox(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + left_section_width = self._current_width - self._current_height requires_recoloring = self.draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height), diff --git a/customtkinter/windows/widgets/ctk_entry.py b/customtkinter/windows/widgets/ctk_entry.py index bb4fc3d..a04340c 100644 --- a/customtkinter/windows/widgets/ctk_entry.py +++ b/customtkinter/windows/widgets/ctk_entry.py @@ -148,6 +148,8 @@ class CTkEntry(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + self._canvas.configure(bg=self._apply_appearance_mode(self._bg_color)) requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._current_width), diff --git a/customtkinter/windows/widgets/ctk_frame.py b/customtkinter/windows/widgets/ctk_frame.py index 93048ee..18793eb 100644 --- a/customtkinter/windows/widgets/ctk_frame.py +++ b/customtkinter/windows/widgets/ctk_frame.py @@ -29,7 +29,7 @@ class CTkFrame(CTkBaseClass): overwrite_preferred_drawing_method: str = None, **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color @@ -91,6 +91,8 @@ class CTkFrame(CTkBaseClass): self._draw() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + if not self._canvas.winfo_exists(): return diff --git a/customtkinter/windows/widgets/ctk_label.py b/customtkinter/windows/widgets/ctk_label.py index d0c71a1..c4e92bd 100644 --- a/customtkinter/windows/widgets/ctk_label.py +++ b/customtkinter/windows/widgets/ctk_label.py @@ -35,7 +35,7 @@ class CTkLabel(CTkBaseClass): anchor: str = "center", # label anchor: center, n, e, s, w **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height) # color @@ -117,6 +117,8 @@ class CTkLabel(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + 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), diff --git a/customtkinter/windows/widgets/ctk_optionmenu.py b/customtkinter/windows/widgets/ctk_optionmenu.py index 00541c1..8002ca5 100644 --- a/customtkinter/windows/widgets/ctk_optionmenu.py +++ b/customtkinter/windows/widgets/ctk_optionmenu.py @@ -43,7 +43,7 @@ class CTkOptionMenu(CTkBaseClass): anchor: str = "w", **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color variables @@ -180,6 +180,8 @@ class CTkOptionMenu(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + left_section_width = self._current_width - self._current_height requires_recoloring = self._draw_engine.draw_rounded_rect_with_border_vertical_split(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height), diff --git a/customtkinter/windows/widgets/ctk_progressbar.py b/customtkinter/windows/widgets/ctk_progressbar.py index 88ab230..a8c4815 100644 --- a/customtkinter/windows/widgets/ctk_progressbar.py +++ b/customtkinter/windows/widgets/ctk_progressbar.py @@ -46,7 +46,7 @@ class CTkProgressBar(CTkBaseClass): else: height = 8 - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color @@ -110,6 +110,8 @@ class CTkProgressBar(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + if self._orientation.lower() == "horizontal": orientation = "w" elif self._orientation.lower() == "vertical": diff --git a/customtkinter/windows/widgets/ctk_radiobutton.py b/customtkinter/windows/widgets/ctk_radiobutton.py index 4946625..25bcd19 100644 --- a/customtkinter/windows/widgets/ctk_radiobutton.py +++ b/customtkinter/windows/widgets/ctk_radiobutton.py @@ -42,7 +42,7 @@ class CTkRadioButton(CTkBaseClass): command: Callable = None, **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # dimensions @@ -163,6 +163,8 @@ class CTkRadioButton(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + requires_recoloring = self._draw_engine.draw_rounded_rect_with_border(self._apply_widget_scaling(self._radiobutton_width), self._apply_widget_scaling(self._radiobutton_height), self._apply_widget_scaling(self._corner_radius), diff --git a/customtkinter/windows/widgets/ctk_scrollbar.py b/customtkinter/windows/widgets/ctk_scrollbar.py index 2e76e06..745b36d 100644 --- a/customtkinter/windows/widgets/ctk_scrollbar.py +++ b/customtkinter/windows/widgets/ctk_scrollbar.py @@ -44,7 +44,7 @@ class CTkScrollbar(CTkBaseClass): else: height = 200 - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color @@ -118,6 +118,8 @@ class CTkScrollbar(CTkBaseClass): return self._start_value, self._end_value def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + corrected_start_value, corrected_end_value = self._get_scrollbar_values_for_minimum_pixel_size() requires_recoloring = self._draw_engine.draw_rounded_scrollbar(self._apply_widget_scaling(self._current_width), self._apply_widget_scaling(self._current_height), @@ -219,9 +221,9 @@ class CTkScrollbar(CTkBaseClass): def _clicked(self, event): if self._orientation == "vertical": - value = ((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing)) / self._widget_scaling + value = self._reverse_widget_scaling(((event.y - self._border_spacing) / (self._current_height - 2 * self._border_spacing))) else: - value = ((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing)) / self._widget_scaling + value = self._reverse_widget_scaling(((event.x - self._border_spacing) / (self._current_width - 2 * self._border_spacing))) current_scrollbar_length = self._end_value - self._start_value value = max(current_scrollbar_length / 2, min(value, 1 - (current_scrollbar_length / 2))) diff --git a/customtkinter/windows/widgets/ctk_slider.py b/customtkinter/windows/widgets/ctk_slider.py index c8011ac..cf6ceeb 100644 --- a/customtkinter/windows/widgets/ctk_slider.py +++ b/customtkinter/windows/widgets/ctk_slider.py @@ -52,7 +52,7 @@ class CTkSlider(CTkBaseClass): else: height = 16 - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # color @@ -145,6 +145,8 @@ class CTkSlider(CTkBaseClass): self.configure(cursor="arrow") def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) + if self._orientation.lower() == "horizontal": orientation = "w" elif self._orientation.lower() == "vertical": diff --git a/customtkinter/windows/widgets/ctk_switch.py b/customtkinter/windows/widgets/ctk_switch.py index 726033d..d876c7b 100644 --- a/customtkinter/windows/widgets/ctk_switch.py +++ b/customtkinter/windows/widgets/ctk_switch.py @@ -45,7 +45,7 @@ class CTkSwitch(CTkBaseClass): state: str = tkinter.NORMAL, **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height, **kwargs) # dimensions @@ -193,6 +193,7 @@ class CTkSwitch(CTkBaseClass): self._text_label.configure(cursor="hand2") def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) if self._check_state is True: requires_recoloring = self._draw_engine.draw_rounded_slider_with_border_and_button(self._apply_widget_scaling(self._switch_width), diff --git a/customtkinter/windows/widgets/ctk_tabview.py b/customtkinter/windows/widgets/ctk_tabview.py index d3065cb..31a8fbd 100644 --- a/customtkinter/windows/widgets/ctk_tabview.py +++ b/customtkinter/windows/widgets/ctk_tabview.py @@ -188,6 +188,8 @@ class CTkTabview(CTkBaseClass): return new_tab def _draw(self, no_color_updates: bool = False): + super()._draw(no_color_updates) + if not self._canvas.winfo_exists(): return diff --git a/customtkinter/windows/widgets/ctk_textbox.py b/customtkinter/windows/widgets/ctk_textbox.py index a914c93..cb302fe 100644 --- a/customtkinter/windows/widgets/ctk_textbox.py +++ b/customtkinter/windows/widgets/ctk_textbox.py @@ -51,7 +51,7 @@ class CTkTextbox(CTkBaseClass): activate_scrollbars: bool = True, **kwargs): - # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass + # transfer basic functionality (_bg_color, size, __appearance_mode, scaling) to CTkBaseClass super().__init__(master=master, bg_color=bg_color, width=width, height=height) # color @@ -210,6 +210,7 @@ class CTkTextbox(CTkBaseClass): super().destroy() def _draw(self, no_color_updates=False): + super()._draw(no_color_updates) if not self._canvas.winfo_exists(): return diff --git a/customtkinter/windows/widgets/image/ctk_image.py b/customtkinter/windows/widgets/image/ctk_image.py index d8709cf..82bdc51 100644 --- a/customtkinter/windows/widgets/image/ctk_image.py +++ b/customtkinter/windows/widgets/image/ctk_image.py @@ -18,11 +18,16 @@ class CTkImage: _checked_PIL_import = False - def __init__(self, light_image: Image.Image = None, dark_image: Image.Image = None, size: Tuple[int, int] = None): + def __init__(self, + light_image: Image.Image = None, + dark_image: Image.Image = None, + size: Tuple[int, int] = (20, 20)): + if not self._checked_PIL_import: self._check_pil_import() self._light_image = light_image + print(self._light_image) self._dark_image = dark_image self._check_images() self._size = size @@ -33,8 +38,10 @@ class CTkImage: @classmethod def _check_pil_import(cls): - if "Image" not in dir() or "ImageTk" not in dir(): - raise ImportError("CTkImage: Couldn't import PIL.Image or PIL.ImageTk. PIL must be installed.") + try: + _, _ = Image, ImageTk + except NameError: + raise ImportError("PIL.Image and PIL.ImageTk couldn't be imported") def add_configure_callback(self, callback: Callable): """ add function, that gets called when image got configured """ @@ -84,7 +91,7 @@ class CTkImage: raise ValueError(f"CTkImage: light_image size {self._light_image.size} must be the same as dark_image size {self._dark_image.size}.") def _get_scaled_size(self, widget_scaling: float) -> Tuple[int, int]: - return round(self._size[0] * widget_scaling), round(self._size[0] * widget_scaling) + return round(self._size[0] * widget_scaling), round(self._size[1] * widget_scaling) def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage: if scaled_size in self._scaled_light_photo_images: @@ -100,17 +107,18 @@ class CTkImage: self._scaled_dark_photo_images[scaled_size] = ImageTk.PhotoImage(self._dark_image.resize(scaled_size)) return self._scaled_dark_photo_images[scaled_size] - def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: int) -> ImageTk.PhotoImage: + def create_scaled_photo_image(self, widget_scaling: float, appearance_mode: str) -> ImageTk.PhotoImage: scaled_size = self._get_scaled_size(widget_scaling) + print(scaled_size) - if appearance_mode == 0 and self._light_image is not None: + if appearance_mode == "light" and self._light_image is not None: return self._get_scaled_light_photo_image(scaled_size) - elif appearance_mode == 0 and self._light_image is None: + elif appearance_mode == "light" and self._light_image is None: return self._get_scaled_dark_photo_image(scaled_size) - elif appearance_mode == 1 and self._dark_image is not None: + elif appearance_mode == "dark" and self._dark_image is not None: return self._get_scaled_dark_photo_image(scaled_size) - elif appearance_mode == 1 and self._dark_image is None: + elif appearance_mode == "dark" and self._dark_image is None: return self._get_scaled_light_photo_image(scaled_size) diff --git a/customtkinter/windows/widgets/scaling/scaling_base_class.py b/customtkinter/windows/widgets/scaling/scaling_base_class.py index a27a33c..009ec99 100644 --- a/customtkinter/windows/widgets/scaling/scaling_base_class.py +++ b/customtkinter/windows/widgets/scaling/scaling_base_class.py @@ -1,5 +1,5 @@ +import tkinter from typing import Union, Tuple -from abc import ABC, abstractmethod import copy import re try: @@ -9,69 +9,109 @@ except ImportError: from .scaling_tracker import ScalingTracker from ..font.ctk_font import CTkFont +from ..image.ctk_image import CTkImage -class CTkScalingBaseClass(ABC): +class CTkScalingBaseClass(): + """ + Super-class that manages the scaling values and callbacks. + Works for widgets and windows, type must be set in init method with + scaling_type attribute. Methods: + + - _set_scaling() abstractmethod, gets called when scaling changes, must be overridden + - destroy() must be called when sub-class is destroyed + - _apply_widget_scaling() + - _reverse_widget_scaling() + - _apply_window_scaling() + - _reverse_window_scaling() + - _apply_font_scaling() + - _apply_argument_scaling() + - _apply_geometry_scaling() + - _reverse_geometry_scaling() + - _parse_geometry_string() + + """ def __init__(self, scaling_type: Literal["widget", "window"] = "widget"): - self._scaling_type = scaling_type + self.__scaling_type = scaling_type - if self._scaling_type == "widget": + if self.__scaling_type == "widget": ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes - self._widget_scaling = ScalingTracker.get_widget_scaling(self) - elif self._scaling_type == "window": + self.__widget_scaling = ScalingTracker.get_widget_scaling(self) + elif self.__scaling_type == "window": ScalingTracker.activate_high_dpi_awareness() # make process DPI aware ScalingTracker.add_window(self._set_scaling, self) # add callback for automatic scaling changes - self._window_scaling = ScalingTracker.get_window_scaling(self) + self.__window_scaling = ScalingTracker.get_window_scaling(self) def destroy(self): - if self._scaling_type == "widget": + if self.__scaling_type == "widget": ScalingTracker.remove_widget(self._set_scaling, self) - elif self._scaling_type == "window": + elif self.__scaling_type == "window": ScalingTracker.remove_window(self._set_scaling, self) - def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]: - assert self._scaling_type == "widget" + def _set_scaling(self, new_widget_scaling, new_window_scaling): + """ can be overridden, but super method must be called at the beginning """ + self.__widget_scaling = new_widget_scaling + self.__window_scaling = new_window_scaling - if isinstance(value, (int, float)): - return value * self._widget_scaling - else: - return value + def _get_widget_scaling(self) -> float: + return self.__widget_scaling + + def _get_window_scaling(self) -> float: + return self.__window_scaling + + def _apply_widget_scaling(self, value: Union[int, float]) -> Union[float]: + assert self.__scaling_type == "widget" + return value * self.__widget_scaling + + def _reverse_widget_scaling(self, value: Union[int, float]) -> Union[float]: + assert self.__scaling_type == "widget" + return value / self.__widget_scaling + + def _apply_window_scaling(self, value: Union[int, float]) -> int: + assert self.__scaling_type == "window" + return int(value * self.__window_scaling) + + def _reverse_window_scaling(self, scaled_value: Union[int, float]) -> int: + assert self.__scaling_type == "window" + return int(scaled_value / self.__window_scaling) def _apply_font_scaling(self, font: Union[Tuple, CTkFont]) -> tuple: """ Takes CTkFont object and returns tuple font with scaled size, has to be called again for every change of font object """ - assert self._scaling_type == "widget" + assert self.__scaling_type == "widget" if type(font) == tuple: if len(font) == 1: return font elif len(font) == 2: - return font[0], -abs(round(font[1] * self._widget_scaling)) + return font[0], -abs(round(font[1] * self.__widget_scaling)) elif len(font) == 3: - return font[0], -abs(round(font[1] * self._widget_scaling)), font[2] + return font[0], -abs(round(font[1] * self.__widget_scaling)), font[2] else: raise ValueError(f"Can not scale font {font}. font needs to be tuple of len 1, 2 or 3") elif isinstance(font, CTkFont): - return font.create_scaled_tuple(self._widget_scaling) + return font.create_scaled_tuple(self.__widget_scaling) else: raise ValueError(f"Can not scale font '{font}' of type {type(font)}. font needs to be tuple or instance of CTkFont") def _apply_argument_scaling(self, kwargs: dict) -> dict: - assert self._scaling_type == "widget" + assert self.__scaling_type == "widget" scaled_kwargs = copy.copy(kwargs) + # scale padding values if "pady" in scaled_kwargs: - if isinstance(scaled_kwargs["pady"], (int, float, str)): + if isinstance(scaled_kwargs["pady"], (int, float)): scaled_kwargs["pady"] = self._apply_widget_scaling(scaled_kwargs["pady"]) elif isinstance(scaled_kwargs["pady"], tuple): scaled_kwargs["pady"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["pady"]]) if "padx" in kwargs: - if isinstance(scaled_kwargs["padx"], (int, float, str)): + if isinstance(scaled_kwargs["padx"], (int, float)): scaled_kwargs["padx"] = self._apply_widget_scaling(scaled_kwargs["padx"]) elif isinstance(scaled_kwargs["padx"], tuple): scaled_kwargs["padx"] = tuple([self._apply_widget_scaling(v) for v in scaled_kwargs["padx"]]) + # scaled x, y values for place geometry manager if "x" in scaled_kwargs: scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"]) if "y" in scaled_kwargs: @@ -93,41 +133,29 @@ class CTkScalingBaseClass(ABC): return width, height, x, y def _apply_geometry_scaling(self, geometry_string: str) -> str: - assert self._scaling_type == "window" + assert self.__scaling_type == "window" width, height, x, y = self._parse_geometry_string(geometry_string) if x is None and y is None: # no and in geometry_string - return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}" + return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}" elif width is None and height is None: # no and in geometry_string return f"+{x}+{y}" else: - return f"{round(width * self._window_scaling)}x{round(height * self._window_scaling)}+{x}+{y}" + return f"{round(width * self.__window_scaling)}x{round(height * self.__window_scaling)}+{x}+{y}" def _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str: - assert self._scaling_type == "window" + assert self.__scaling_type == "window" width, height, x, y = self._parse_geometry_string(scaled_geometry_string) if x is None and y is None: # no and in geometry_string - return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}" + return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}" elif width is None and height is None: # no and in geometry_string return f"+{x}+{y}" else: - return f"{round(width / self._window_scaling)}x{round(height / self._window_scaling)}+{x}+{y}" - - def _apply_window_scaling(self, value): - assert self._scaling_type == "window" - - if isinstance(value, (int, float)): - return int(value * self._window_scaling) - else: - return value - - @abstractmethod - def _set_scaling(self, new_widget_scaling, new_window_scaling): - return + return f"{round(width / self.__window_scaling)}x{round(height / self.__window_scaling)}+{x}+{y}" diff --git a/examples/complex_example.py b/examples/complex_example.py index 58e8f26..df9eb98 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -13,13 +13,13 @@ class App(customtkinter.CTk): self.title("CustomTkinter complex_example.py") self.geometry(f"{1100}x{580}") - self.minsize(800, 400) + #self.minsize(800, 400) #self.maxsize(1200, 700) self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed # configure grid layout (4x4) self.grid_columnconfigure(1, weight=1) - self.grid_columnconfigure((2, 3, 4), weight=0, minsize=200) + self.grid_columnconfigure((2, 3), weight=0) self.grid_rowconfigure((0, 1, 2), weight=1) # create sidebar frame with widgets @@ -47,63 +47,18 @@ class App(customtkinter.CTk): # create main entry and button self.entry = customtkinter.CTkEntry(self, placeholder_text="CTkEntry") - self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 10), pady=(10, 20), sticky="nsew") + self.entry.grid(row=3, column=1, columnspan=2, padx=(20, 0), pady=(20, 20), sticky="nsew") self.main_button_1 = customtkinter.CTkButton(master=self, fg_color=None, border_width=2) - self.main_button_1.grid(row=3, column=3, padx=(10, 20), pady=(10, 20), sticky="nsew") + self.main_button_1.grid(row=3, column=3, padx=(20, 20), pady=(20, 20), sticky="nsew") # create textbox - self.textbox = customtkinter.CTkTextbox(self) - self.textbox.grid(row=0, column=1, padx=(20, 10), pady=(20, 10), sticky="nsew") - - # create radiobutton frame - self.radiobutton_frame = customtkinter.CTkFrame(self) - self.radiobutton_frame.grid(row=0, column=3, padx=(10, 10), pady=(20, 10), sticky="nsew") - self.radio_var = tkinter.IntVar(value=0) - self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:") - self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="") - self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0) - self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n") - self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1) - self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n") - self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) - self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") - - # create checkbox and switch frame - self.checkbox_slider_frame = customtkinter.CTkFrame(self) - self.checkbox_slider_frame.grid(row=0, column=4, padx=(10, 20), pady=(20, 10), sticky="nsew") - self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") - self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) - self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") - self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) - self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") - self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) - self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") - - # create slider and progressbar frame - self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color=None) - self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 10), pady=(10, 10), sticky="nsew") - self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) - self.slider_progressbar_frame.grid_rowconfigure(4, weight=1) - - self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame) - self.seg_button_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - - self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) - self.progressbar_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) - self.progressbar_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4) - self.slider_1.grid(row=3, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") - self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orientation="vertical") - self.slider_2.grid(row=0, column=1, rowspan=5, padx=(10, 10), pady=(10, 10), sticky="ns") - self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical") - self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns") + self.textbox = customtkinter.CTkTextbox(self, width=250) + self.textbox.grid(row=0, column=1, padx=(20, 0), pady=(20, 0), sticky="nsew") # create tabview - self.tabview = customtkinter.CTkTabview(self) - self.tabview.grid(row=1, column=3, columnspan=2, padx=(10, 20), pady=(10, 10), sticky="nsew") + self.tabview = customtkinter.CTkTabview(self, width=250) + self.tabview.grid(row=0, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew") self.tabview.add("CTkTabview") self.tabview.add("Tab 2") self.tabview.add("Tab 3") @@ -121,6 +76,51 @@ class App(customtkinter.CTk): command=self.open_input_dialog) self.string_input_button.grid(row=2, column=0, padx=20, pady=(10, 10)) + # create radiobutton frame + self.radiobutton_frame = customtkinter.CTkFrame(self) + self.radiobutton_frame.grid(row=0, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") + self.radio_var = tkinter.IntVar(value=0) + self.label_radio_group = customtkinter.CTkLabel(master=self.radiobutton_frame, text="CTkRadioButton Group:") + self.label_radio_group.grid(row=0, column=2, columnspan=1, padx=10, pady=10, sticky="") + self.radio_button_1 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=0) + self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n") + self.radio_button_2 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=1) + self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n") + self.radio_button_3 = customtkinter.CTkRadioButton(master=self.radiobutton_frame, variable=self.radio_var, value=2) + self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n") + + # create checkbox and switch frame + self.checkbox_slider_frame = customtkinter.CTkFrame(self) + self.checkbox_slider_frame.grid(row=1, column=3, padx=(20, 20), pady=(20, 0), sticky="nsew") + self.checkbox_1 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_1.grid(row=1, column=0, pady=(20, 10), padx=20, sticky="n") + self.checkbox_2 = customtkinter.CTkCheckBox(master=self.checkbox_slider_frame) + self.checkbox_2.grid(row=2, column=0, pady=10, padx=20, sticky="n") + self.switch_1 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame, command=lambda: print("switch 1 toggle")) + self.switch_1.grid(row=3, column=0, pady=10, padx=20, sticky="n") + self.switch_2 = customtkinter.CTkSwitch(master=self.checkbox_slider_frame) + self.switch_2.grid(row=4, column=0, pady=(10, 20), padx=20, sticky="n") + + # create slider and progressbar frame + self.slider_progressbar_frame = customtkinter.CTkFrame(self, fg_color=None) + self.slider_progressbar_frame.grid(row=1, column=1, columnspan=2, padx=(20, 0), pady=(20, 0), sticky="nsew") + self.slider_progressbar_frame.grid_columnconfigure(0, weight=1) + self.slider_progressbar_frame.grid_rowconfigure(4, weight=1) + + self.seg_button_1 = customtkinter.CTkSegmentedButton(self.slider_progressbar_frame) + self.seg_button_1.grid(row=0, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + + self.progressbar_1 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) + self.progressbar_1.grid(row=1, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.progressbar_2 = customtkinter.CTkProgressBar(self.slider_progressbar_frame) + self.progressbar_2.grid(row=2, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_1 = customtkinter.CTkSlider(self.slider_progressbar_frame, from_=0, to=1, number_of_steps=4) + self.slider_1.grid(row=3, column=0, padx=(20, 10), pady=(10, 10), sticky="ew") + self.slider_2 = customtkinter.CTkSlider(self.slider_progressbar_frame, orientation="vertical") + self.slider_2.grid(row=0, column=1, rowspan=5, padx=(10, 10), pady=(10, 10), sticky="ns") + self.progressbar_3 = customtkinter.CTkProgressBar(self.slider_progressbar_frame, orientation="vertical") + self.progressbar_3.grid(row=0, column=2, rowspan=5, padx=(10, 20), pady=(10, 10), sticky="ns") + # set default values self.sidebar_button_3.configure(state="disabled", text="Disabled CTkButton") self.checkbox_2.configure(state="disabled") diff --git a/examples/example_button_images.py b/examples/example_button_images.py index caec9b7..ac6fb7a 100644 --- a/examples/example_button_images.py +++ b/examples/example_button_images.py @@ -1,77 +1,22 @@ -import PIL.ImageTk - import customtkinter -import tkinter from PIL import Image, ImageTk import os PATH = os.path.dirname(os.path.realpath(__file__)) -customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light" -customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" +app = customtkinter.CTk() -class App(customtkinter.CTk): - def __init__(self): - super().__init__() - self.geometry("450x260") - self.title("CustomTkinter example_button_images.py") +switch_1 = customtkinter.CTkSwitch(app, text="darkmode", command=lambda: customtkinter.set_appearance_mode("dark" if switch_1.get() == 1 else "light")) +switch_1.pack(padx=20, pady=20) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1, minsize=200) +image_1 = customtkinter.CTkImage(light_image=Image.open(PATH + "/test_images/add_folder_dark.png"), + dark_image=Image.open(PATH + "/test_images/add_folder_light.png"), + size=(30, 50)) +image_1.configure(dark_image=Image.open(PATH + "/test_images/add_folder_light.png")) - self.frame_1 = customtkinter.CTkFrame(master=self, width=250, height=240, corner_radius=15) - self.frame_1.grid(row=0, column=0, padx=20, pady=20, sticky="nsew") - self.frame_1.grid_columnconfigure(0, weight=1) - self.frame_1.grid_columnconfigure(1, weight=1) +button_1 = customtkinter.CTkButton(app, image=image_1) +button_1.pack(padx=20, pady=20) - self.settings_image = self.load_image("/test_images/settings.png", 20) - self.bell_image = self.load_image("/test_images/bell.png", 20) - self.add_folder_image = self.load_image("/test_images/add-folder.png", 20) - self.add_list_image = self.load_image("/test_images/add-folder.png", 20) - self.add_user_image = self.load_image("/test_images/add-user.png", 20) - self.chat_image = self.load_image("/test_images/chat.png", 20) - self.home_image = self.load_image("/test_images/home.png", 20) +app.mainloop() - self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=self.settings_image, text="Add Folder", height=32, - compound="right", command=self.button_function) - self.button_1.grid(row=1, column=0, columnspan=2, padx=20, pady=(20, 10), sticky="ew") - - self.button_2 = customtkinter.CTkButton(master=self.frame_1, image=self.add_list_image, text="Add Item", height=32, - compound="right", fg_color="#D35B58", hover_color="#C77C78", - command=self.button_function) - self.button_2.grid(row=2, column=0, columnspan=2, padx=20, pady=10, sticky="ew") - - self.button_3 = customtkinter.CTkButton(master=self.frame_1, image=self.chat_image, text="", width=40, height=40, - corner_radius=10, fg_color="gray40", hover_color="gray25", - command=self.button_function) - self.button_3.grid(row=3, column=0, columnspan=1, padx=20, pady=10, sticky="w") - - self.button_4 = customtkinter.CTkButton(master=self.frame_1, image=self.home_image, text="", width=40, height=40, - corner_radius=10, fg_color="gray40", hover_color="gray25", - command=self.button_function) - self.button_4.grid(row=3, column=1, columnspan=1, padx=20, pady=10, sticky="e") - - self.button_5 = customtkinter.CTkButton(master=self, image=self.add_user_image, text="Add User", width=130, height=60, border_width=2, - corner_radius=10, compound="bottom", border_color="#D35B58", fg_color=("gray84", "gray25"), - hover_color="#C77C78", command=self.button_function) - self.button_5.grid(row=0, column=1, padx=20, pady=20) - - self.scaling_button = customtkinter.CTkSegmentedButton(self, values=[0.8, 0.9, 1.0, 1.1, 1.2, 1.5], - command=lambda v: customtkinter.set_widget_scaling(v)) - self.scaling_button.grid(row=1, column=0, pady=(0, 20)) - self.mode_switch = customtkinter.CTkSwitch(self, text="darkmode", onvalue="dark", offvalue="light", - command=lambda: customtkinter.set_appearance_mode(self.mode_switch.get())) - self.mode_switch.grid(row=1, column=1, pady=(0, 20)) - - def load_image(self, path, image_size): - """ load rectangular image with path relative to PATH """ - return ImageTk.PhotoImage(Image.open(PATH + path).resize((image_size, image_size))) - - def button_function(self): - print("button pressed") - - -if __name__ == "__main__": - app = App() - app.mainloop() diff --git a/examples/test_images/add-folder.png b/examples/test_images/add-folder.png deleted file mode 100644 index feff512bf76d76489aa2fa75aa9948b0d723d9c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4901 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