From 08a0835fd0c0a458746da9ac23c297aac592c848 Mon Sep 17 00:00:00 2001 From: Tom Schimansky Date: Sat, 29 Oct 2022 00:42:33 +0200 Subject: [PATCH] added new button grid system, fixed CTkSwitch grid positioning, moved ctk_canvas.py and draw_engine.py to widgets/core_rendering, created CTkImage --- CHANGELOG.md | 1 + customtkinter/__init__.py | 12 +- customtkinter/theme_manager.py | 6 - .../widgets/core_widget_classes/__init__.py | 0 .../dropdown_menu.py | 8 +- .../widget_base_class.py | 16 +- customtkinter/widgets/ctk_button.py | 151 ++++++++++++------ customtkinter/widgets/ctk_checkbox.py | 4 +- customtkinter/widgets/ctk_combobox.py | 6 +- customtkinter/widgets/ctk_entry.py | 4 +- customtkinter/widgets/ctk_frame.py | 2 +- customtkinter/widgets/ctk_label.py | 4 +- customtkinter/widgets/ctk_optionmenu.py | 6 +- customtkinter/widgets/ctk_progressbar.py | 2 +- customtkinter/widgets/ctk_radiobutton.py | 4 +- customtkinter/widgets/ctk_scrollbar.py | 2 +- customtkinter/widgets/ctk_segmented_button.py | 3 +- customtkinter/widgets/ctk_slider.py | 2 +- customtkinter/widgets/ctk_switch.py | 4 +- customtkinter/widgets/ctk_tabview.py | 2 +- customtkinter/widgets/ctk_textbox.py | 4 +- customtkinter/widgets/font/__init__.py | 0 .../{utility => widgets/font}/ctk_font.py | 2 +- .../{ => widgets/font}/font_manager.py | 2 +- customtkinter/widgets/image/__init__.py | 0 customtkinter/widgets/image/ctk_image.py | 116 ++++++++++++++ examples/example_button_images.py | 72 +-------- 27 files changed, 261 insertions(+), 174 deletions(-) create mode 100644 customtkinter/widgets/core_widget_classes/__init__.py rename customtkinter/widgets/{ => core_widget_classes}/dropdown_menu.py (98%) rename customtkinter/widgets/{ => core_widget_classes}/widget_base_class.py (97%) create mode 100644 customtkinter/widgets/font/__init__.py rename customtkinter/{utility => widgets/font}/ctk_font.py (98%) rename customtkinter/{ => widgets/font}/font_manager.py (98%) create mode 100644 customtkinter/widgets/image/__init__.py create mode 100644 customtkinter/widgets/image/ctk_image.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9eb3b..1853ac1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ToDo: - auto-scaling of images - image tuple for light/dark mode - change font attribute in wiki + - add new button attributes to wiki ## Unreleased - 2022-10-2 ### Added diff --git a/customtkinter/__init__.py b/customtkinter/__init__.py index 54785c0..49d01f8 100644 --- a/customtkinter/__init__.py +++ b/customtkinter/__init__.py @@ -5,9 +5,9 @@ import sys # import manager classes from .appearance_mode_tracker import AppearanceModeTracker -from .theme_manager import ThemeManager +from .widgets.font.font_manager import FontManager from .scaling_tracker import ScalingTracker -from .font_manager import FontManager +from .theme_manager import ThemeManager from .widgets.core_rendering.draw_engine import DrawEngine AppearanceModeTracker.init_appearance_mode() @@ -46,7 +46,7 @@ if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Cust DrawEngine.preferred_drawing_method = "circle_shapes" # import widgets -from .widgets.widget_base_class import CTkBaseClass +from customtkinter.widgets.core_widget_classes.widget_base_class import CTkBaseClass from .widgets.ctk_button import CTkButton from .widgets.ctk_checkbox import CTkCheckBox from .widgets.ctk_entry import CTkEntry @@ -64,15 +64,13 @@ from .widgets.ctk_textbox import CTkTextbox from .widgets.ctk_tabview import CTkTabview from .widgets.ctk_segmented_button import CTkSegmentedButton -from .widgets.ctk_button_new_grid import CTkButtonNewGrid - # import windows from .windows.ctk_tk import CTk from .windows.ctk_toplevel import CTkToplevel from .windows.ctk_input_dialog import CTkInputDialog -# util classes -from .utility.ctk_font import CTkFont +# font classes +from .widgets.font.ctk_font import CTkFont def set_appearance_mode(mode_string: str): diff --git a/customtkinter/theme_manager.py b/customtkinter/theme_manager.py index 7c70c8a..ff3a52a 100644 --- a/customtkinter/theme_manager.py +++ b/customtkinter/theme_manager.py @@ -84,10 +84,4 @@ class ThemeManager: min(255, rgb_color[2] * factor)) return ThemeManager.rgb2hex(dark_rgb_color) except Exception as err: - # sys.stderr.write("ERROR (CTkColorManager): failed to darken the following color: " + str(hex_color) + " " + str(err)) return hex_color - - @classmethod - def set_main_color(cls, main_color, main_color_hover): - cls.MAIN_COLOR = main_color - cls.MAIN_HOVER_COLOR = main_color_hover diff --git a/customtkinter/widgets/core_widget_classes/__init__.py b/customtkinter/widgets/core_widget_classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customtkinter/widgets/dropdown_menu.py b/customtkinter/widgets/core_widget_classes/dropdown_menu.py similarity index 98% rename from customtkinter/widgets/dropdown_menu.py rename to customtkinter/widgets/core_widget_classes/dropdown_menu.py index 92ff48b..b5cfba9 100644 --- a/customtkinter/widgets/dropdown_menu.py +++ b/customtkinter/widgets/core_widget_classes/dropdown_menu.py @@ -2,10 +2,10 @@ import tkinter import sys from typing import Union, Tuple, Callable, List -from ..theme_manager import ThemeManager -from ..appearance_mode_tracker import AppearanceModeTracker -from ..scaling_tracker import ScalingTracker -from ..utility.ctk_font import CTkFont +from ...theme_manager import ThemeManager +from ...appearance_mode_tracker import AppearanceModeTracker +from ...scaling_tracker import ScalingTracker +from ..font.ctk_font import CTkFont class DropdownMenu(tkinter.Menu): diff --git a/customtkinter/widgets/widget_base_class.py b/customtkinter/widgets/core_widget_classes/widget_base_class.py similarity index 97% rename from customtkinter/widgets/widget_base_class.py rename to customtkinter/widgets/core_widget_classes/widget_base_class.py index c27f5ec..6b96d0c 100644 --- a/customtkinter/widgets/widget_base_class.py +++ b/customtkinter/widgets/core_widget_classes/widget_base_class.py @@ -9,15 +9,15 @@ try: except ImportError: from typing_extensions import TypedDict -from ..windows.ctk_tk import CTk -from ..windows.ctk_toplevel import CTkToplevel -from ..appearance_mode_tracker import AppearanceModeTracker -from ..scaling_tracker import ScalingTracker -from ..theme_manager import ThemeManager +from ...windows.ctk_tk import CTk +from ...windows.ctk_toplevel import CTkToplevel +from ...appearance_mode_tracker import AppearanceModeTracker +from ...scaling_tracker import ScalingTracker +from ...theme_manager import ThemeManager +from ..font.ctk_font import CTkFont +from ..image.ctk_image import CTkImage -from ..utility.ctk_font import CTkFont - -from ..utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty +from ...utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty class CTkBaseClass(tkinter.Frame): diff --git a/customtkinter/widgets/ctk_button.py b/customtkinter/widgets/ctk_button.py index e56ebbc..3a10c54 100644 --- a/customtkinter/widgets/ctk_button.py +++ b/customtkinter/widgets/ctk_button.py @@ -5,8 +5,9 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont +from .image.ctk_image import CTkImage class CTkButton(CTkBaseClass): @@ -15,12 +16,15 @@ class CTkButton(CTkBaseClass): For detailed information check out the documentation. """ + _image_label_spacing = 6 + def __init__(self, master: any = None, width: int = 140, height: int = 28, corner_radius: Union[int, str] = "default_theme", border_width: Union[int, str] = "default_theme", + border_spacing: int = 2, bg_color: Union[str, Tuple[str, str], None] = None, fg_color: Union[str, Tuple[str, str], None] = "default_theme", @@ -36,11 +40,12 @@ class CTkButton(CTkBaseClass): text: str = "CTkButton", font: Union[tuple, CTkFont] = "default_theme", textvariable: tkinter.Variable = None, - image: tkinter.PhotoImage = None, + image: Union[tkinter.PhotoImage, CTkImage] = None, state: str = "normal", hover: bool = True, command: Callable = None, compound: str = "left", + anchor: str = "center", **kwargs): # transfer basic functionality (_bg_color, size, _appearance_mode, scaling) to CTkBaseClass @@ -59,8 +64,10 @@ class CTkButton(CTkBaseClass): self._border_width = ThemeManager.theme["shape"]["button_border_width"] if border_width == "default_theme" else border_width self._round_width_to_even_numbers = round_width_to_even_numbers # rendering options for DrawEngine self._round_height_to_even_numbers = round_height_to_even_numbers # rendering options for DrawEngine - self._corner_radius = min(self._corner_radius, round(self._current_height/2)) + self._compound = compound + self._anchor = anchor + self._border_spacing = border_spacing # text, image self._image = image @@ -78,21 +85,14 @@ class CTkButton(CTkBaseClass): self._textvariable = textvariable self._state = state self._hover = hover - self._compound = compound self._click_animation_running: bool = False - # configure grid system (2x2) - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(1, weight=1) - self.grid_columnconfigure(1, weight=1) - # canvas self._canvas = CTkCanvas(master=self, highlightthickness=0, width=self._apply_widget_scaling(self._desired_width), height=self._apply_widget_scaling(self._desired_height)) - self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") + self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew") self._draw_engine = DrawEngine(self._canvas) self._draw_engine.set_round_to_even_numbers(self._round_width_to_even_numbers, self._round_height_to_even_numbers) # rendering options @@ -109,6 +109,8 @@ class CTkButton(CTkBaseClass): def _set_scaling(self, *args, **kwargs): super()._set_scaling(*args, **kwargs) + self._create_grid() + if self._text_label is not None: self._text_label.configure(font=self._apply_font_scaling(self._font)) @@ -131,7 +133,7 @@ class CTkButton(CTkBaseClass): # Workaround to force grid to be resized when text changes size. # Otherwise grid will lag and only resizes if other mouse action occurs. self._canvas.grid_forget() - self._canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew") + self._canvas.grid(row=0, column=0, rowspan=5, columnspan=5, sticky="nsew") def destroy(self): if isinstance(self._font, CTkFont): @@ -184,6 +186,7 @@ class CTkButton(CTkBaseClass): pady=0, borderwidth=1, textvariable=self._textvariable) + self._create_grid() self._text_label.bind("", self._on_enter) self._text_label.bind("", self._on_leave) @@ -209,12 +212,14 @@ class CTkButton(CTkBaseClass): if self._text_label is not None: self._text_label.destroy() self._text_label = None + self._create_grid() # create image label if image given if self._image is not None: if self._image_label is None: self._image_label = tkinter.Label(master=self) + self._create_grid() self._image_label.bind("", self._on_enter) self._image_label.bind("", self._on_leave) @@ -235,56 +240,88 @@ class CTkButton(CTkBaseClass): if self._image_label is not None: self._image_label.destroy() self._image_label = None + self._create_grid() - # create grid layout with just an image given - if self._image_label is not None and self._text_label is None: - self._image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even + def _create_grid(self): + """ configure grid system (5x5) """ - # create grid layout with just text given - if self._image_label is None and self._text_label is not None: - self._text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", - padx=self._apply_widget_scaling(self._corner_radius), - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) # bottom pady with +1 for rounding to even + # Outer rows and columns have weight of 1000 to overpower the rows and columns of the label and image with weight 1. + # Rows and columns of image and label need weight of 1 to collapse in case of missing space on the button, + # so image and label need sticky option to stick together in the center, and therefore outer rows and columns + # need weight of 100 in case of other anchor than center. + n_padding_weight, s_padding_weight, e_padding_weight, w_padding_weight = 1000, 1000, 1000, 1000 + if self._anchor != "center": + if "n" in self._anchor: + n_padding_weight, s_padding_weight = 0, 1000 + if "s" in self._anchor: + n_padding_weight, s_padding_weight = 1000, 0 + if "e" in self._anchor: + e_padding_weight, w_padding_weight = 1000, 0 + if "w" in self._anchor: + e_padding_weight, w_padding_weight = 0, 1000 - # create grid layout of image and text label in 2x2 grid system with given compound - if self._image_label is not None and self._text_label is not None: - if self._compound == tkinter.LEFT or self._compound == "left": - self._image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2), - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) - self._text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))), - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) - elif self._compound == tkinter.TOP or self._compound == "top": - self._image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), - pady=(self._apply_widget_scaling(self._border_width), 2)) - self._text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), - pady=(2, self._apply_widget_scaling(self._border_width))) - elif self._compound == tkinter.RIGHT or self._compound == "right": - self._image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1, - padx=(2, max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width))), - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) - self._text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1, - padx=(max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), 2), - pady=(self._apply_widget_scaling(self._border_width), self._apply_widget_scaling(self._border_width) + 1)) - elif self._compound == tkinter.BOTTOM or self._compound == "bottom": - self._image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1, - padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), - pady=(2, self._apply_widget_scaling(self._border_width))) - self._text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1, - padx=max(self._apply_widget_scaling(self._corner_radius), self._apply_widget_scaling(self._border_width)), - pady=(self._apply_widget_scaling(self._border_width), 2)) + scaled_minsize_rows = self._apply_widget_scaling(max(self._border_width + 1, self._border_spacing)) + scaled_minsize_columns = self._apply_widget_scaling(max(self._corner_radius, self._border_width + 1, self._border_spacing)) + + self.grid_rowconfigure(0, weight=n_padding_weight, minsize=scaled_minsize_rows) + self.grid_rowconfigure(4, weight=s_padding_weight, minsize=scaled_minsize_rows) + self.grid_columnconfigure(0, weight=e_padding_weight, minsize=scaled_minsize_columns) + self.grid_columnconfigure(4, weight=w_padding_weight, minsize=scaled_minsize_columns) + + if self._compound in ("right", "left"): + self.grid_rowconfigure(2, weight=1) + if self._image_label is not None and self._text_label is not None: + self.grid_columnconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing)) + else: + self.grid_columnconfigure(2, weight=0) + + self.grid_rowconfigure((1, 3), weight=0) + self.grid_columnconfigure((1, 3), weight=1) + else: + self.grid_columnconfigure(2, weight=1) + if self._image_label is not None and self._text_label is not None: + self.grid_rowconfigure(2, weight=0, minsize=self._apply_widget_scaling(self._image_label_spacing)) + else: + self.grid_rowconfigure(2, weight=0) + + self.grid_columnconfigure((1, 3), weight=0) + self.grid_rowconfigure((1, 3), weight=1) + + if self._compound == "right": + if self._image_label is not None: + self._image_label.grid(row=2, column=3, sticky="w") + if self._text_label is not None: + self._text_label.grid(row=2, column=1, sticky="e") + elif self._compound == "left": + if self._image_label is not None: + self._image_label.grid(row=2, column=1, sticky="e") + if self._text_label is not None: + self._text_label.grid(row=2, column=3, sticky="w") + elif self._compound == "top": + if self._image_label is not None: + self._image_label.grid(row=1, column=2, sticky="s") + if self._text_label is not None: + self._text_label.grid(row=3, column=2, sticky="n") + elif self._compound == "bottom": + if self._image_label is not None: + self._image_label.grid(row=3, column=2, sticky="n") + if self._text_label is not None: + self._text_label.grid(row=1, column=2, sticky="s") def configure(self, require_redraw=False, **kwargs): if "corner_radius" in kwargs: self._corner_radius = kwargs.pop("corner_radius") + self._create_grid() require_redraw = True if "border_width" in kwargs: self._border_width = kwargs.pop("border_width") + self._create_grid() + require_redraw = True + + if "border_spacing" in kwargs: + self._border_spacing = kwargs.pop("border_spacing") + self._create_grid() require_redraw = True if "fg_color" in kwargs: @@ -317,6 +354,7 @@ 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): @@ -334,6 +372,7 @@ class CTkButton(CTkBaseClass): if "image" in kwargs: self._image = kwargs.pop("image") + self._create_grid() require_redraw = True if "state" in kwargs: @@ -351,6 +390,10 @@ class CTkButton(CTkBaseClass): self._compound = kwargs.pop("compound") require_redraw = True + if "anchor" in kwargs: + self._anchor = kwargs.pop("anchor") + require_redraw = True + super().configure(require_redraw=require_redraw, **kwargs) def cget(self, attribute_name: str) -> any: @@ -358,6 +401,8 @@ class CTkButton(CTkBaseClass): return self._corner_radius elif attribute_name == "border_width": return self._border_width + elif attribute_name == "border_spacing": + return self._border_spacing elif attribute_name == "fg_color": return self._fg_color @@ -388,6 +433,8 @@ class CTkButton(CTkBaseClass): return self._command elif attribute_name == "compound": return self._compound + elif attribute_name == "anchor": + return self._anchor else: return super().cget(attribute_name) diff --git a/customtkinter/widgets/ctk_checkbox.py b/customtkinter/widgets/ctk_checkbox.py index d4104db..9d5ab93 100644 --- a/customtkinter/widgets/ctk_checkbox.py +++ b/customtkinter/widgets/ctk_checkbox.py @@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont class CTkCheckBox(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_combobox.py b/customtkinter/widgets/ctk_combobox.py index 9bf1a09..76dcb7b 100644 --- a/customtkinter/widgets/ctk_combobox.py +++ b/customtkinter/widgets/ctk_combobox.py @@ -2,12 +2,12 @@ import tkinter import sys from typing import Union, Tuple, Callable, List -from .dropdown_menu import DropdownMenu +from .core_widget_classes.dropdown_menu import DropdownMenu from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont class CTkComboBox(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_entry.py b/customtkinter/widgets/ctk_entry.py index f33baac..1709346 100644 --- a/customtkinter/widgets/ctk_entry.py +++ b/customtkinter/widgets/ctk_entry.py @@ -4,8 +4,8 @@ from typing import Union, Tuple from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty diff --git a/customtkinter/widgets/ctk_frame.py b/customtkinter/widgets/ctk_frame.py index 98f47c0..537855d 100644 --- a/customtkinter/widgets/ctk_frame.py +++ b/customtkinter/widgets/ctk_frame.py @@ -3,7 +3,7 @@ from typing import Union, Tuple, List from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass +from .core_widget_classes.widget_base_class import CTkBaseClass class CTkFrame(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_label.py b/customtkinter/widgets/ctk_label.py index 27e41ac..02b23ed 100644 --- a/customtkinter/widgets/ctk_label.py +++ b/customtkinter/widgets/ctk_label.py @@ -4,8 +4,8 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty diff --git a/customtkinter/widgets/ctk_optionmenu.py b/customtkinter/widgets/ctk_optionmenu.py index 26e0bc8..d9ff3f4 100644 --- a/customtkinter/widgets/ctk_optionmenu.py +++ b/customtkinter/widgets/ctk_optionmenu.py @@ -5,9 +5,9 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from .dropdown_menu import DropdownMenu -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .core_widget_classes.dropdown_menu import DropdownMenu +from .font.ctk_font import CTkFont class CTkOptionMenu(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_progressbar.py b/customtkinter/widgets/ctk_progressbar.py index 899060a..5ffe585 100644 --- a/customtkinter/widgets/ctk_progressbar.py +++ b/customtkinter/widgets/ctk_progressbar.py @@ -5,7 +5,7 @@ from typing import Union, Tuple from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass +from .core_widget_classes.widget_base_class import CTkBaseClass class CTkProgressBar(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_radiobutton.py b/customtkinter/widgets/ctk_radiobutton.py index d32f95c..fe23d08 100644 --- a/customtkinter/widgets/ctk_radiobutton.py +++ b/customtkinter/widgets/ctk_radiobutton.py @@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont class CTkRadioButton(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_scrollbar.py b/customtkinter/widgets/ctk_scrollbar.py index dbe17ea..83429e8 100644 --- a/customtkinter/widgets/ctk_scrollbar.py +++ b/customtkinter/widgets/ctk_scrollbar.py @@ -4,7 +4,7 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass +from .core_widget_classes.widget_base_class import CTkBaseClass class CTkScrollbar(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_segmented_button.py b/customtkinter/widgets/ctk_segmented_button.py index b208bec..5565d99 100644 --- a/customtkinter/widgets/ctk_segmented_button.py +++ b/customtkinter/widgets/ctk_segmented_button.py @@ -4,6 +4,7 @@ from typing import Union, Tuple, List, Dict, Callable from ..theme_manager import ThemeManager from .ctk_button import CTkButton from .ctk_frame import CTkFrame +from .font.ctk_font import CTkFont class CTkSegmentedButton(CTkFrame): @@ -29,7 +30,7 @@ class CTkSegmentedButton(CTkFrame): text_color_disabled: Union[str, Tuple[str, str]] = "default_theme", background_corner_colors: Tuple[Union[str, Tuple[str, str]]] = None, - font: any = "default_theme", + font: Union[tuple, CTkFont] = "default_theme", values: list = None, variable: tkinter.Variable = None, dynamic_resizing: bool = True, diff --git a/customtkinter/widgets/ctk_slider.py b/customtkinter/widgets/ctk_slider.py index bf5b28e..8f7a657 100644 --- a/customtkinter/widgets/ctk_slider.py +++ b/customtkinter/widgets/ctk_slider.py @@ -5,7 +5,7 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass +from .core_widget_classes.widget_base_class import CTkBaseClass class CTkSlider(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_switch.py b/customtkinter/widgets/ctk_switch.py index e6a1a65..d9a3303 100644 --- a/customtkinter/widgets/ctk_switch.py +++ b/customtkinter/widgets/ctk_switch.py @@ -5,8 +5,8 @@ from typing import Union, Tuple, Callable from .core_rendering.ctk_canvas import CTkCanvas from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont class CTkSwitch(CTkBaseClass): diff --git a/customtkinter/widgets/ctk_tabview.py b/customtkinter/widgets/ctk_tabview.py index c1490fd..91f49dd 100644 --- a/customtkinter/widgets/ctk_tabview.py +++ b/customtkinter/widgets/ctk_tabview.py @@ -2,7 +2,7 @@ from typing import Union, Tuple, Dict, List, Callable from ..theme_manager import ThemeManager from .ctk_frame import CTkFrame -from .widget_base_class import CTkBaseClass +from .core_widget_classes.widget_base_class import CTkBaseClass from .ctk_segmented_button import CTkSegmentedButton from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.draw_engine import DrawEngine diff --git a/customtkinter/widgets/ctk_textbox.py b/customtkinter/widgets/ctk_textbox.py index 8d9ad1c..da2b6f4 100644 --- a/customtkinter/widgets/ctk_textbox.py +++ b/customtkinter/widgets/ctk_textbox.py @@ -5,8 +5,8 @@ from .core_rendering.ctk_canvas import CTkCanvas from .ctk_scrollbar import CTkScrollbar from ..theme_manager import ThemeManager from .core_rendering.draw_engine import DrawEngine -from .widget_base_class import CTkBaseClass -from ..utility.ctk_font import CTkFont +from .core_widget_classes.widget_base_class import CTkBaseClass +from .font.ctk_font import CTkFont from customtkinter.utility.utility_functions import pop_from_dict_by_set, check_kwargs_empty diff --git a/customtkinter/widgets/font/__init__.py b/customtkinter/widgets/font/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customtkinter/utility/ctk_font.py b/customtkinter/widgets/font/ctk_font.py similarity index 98% rename from customtkinter/utility/ctk_font.py rename to customtkinter/widgets/font/ctk_font.py index 26cedda..83f8d69 100644 --- a/customtkinter/utility/ctk_font.py +++ b/customtkinter/widgets/font/ctk_font.py @@ -2,7 +2,7 @@ from tkinter.font import Font import copy from typing import List, Callable, Tuple -from ..theme_manager import ThemeManager +from customtkinter.theme_manager import ThemeManager class CTkFont(Font): diff --git a/customtkinter/font_manager.py b/customtkinter/widgets/font/font_manager.py similarity index 98% rename from customtkinter/font_manager.py rename to customtkinter/widgets/font/font_manager.py index 91dfd04..b3ef369 100644 --- a/customtkinter/font_manager.py +++ b/customtkinter/widgets/font/font_manager.py @@ -44,7 +44,7 @@ class FontManager: flags = (FR_PRIVATE if private else 0) | (FR_NOT_ENUM if not enumerable else 0) num_fonts_added = add_font_resource_ex(byref(path_buffer), flags, 0) - return bool(num_fonts_added) + return bool(min(num_fonts_added, 1)) @classmethod def load_font(cls, font_path: str) -> bool: diff --git a/customtkinter/widgets/image/__init__.py b/customtkinter/widgets/image/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customtkinter/widgets/image/ctk_image.py b/customtkinter/widgets/image/ctk_image.py new file mode 100644 index 0000000..d8709cf --- /dev/null +++ b/customtkinter/widgets/image/ctk_image.py @@ -0,0 +1,116 @@ +from typing import Tuple, Dict, Callable, List +try: + from PIL import Image, ImageTk +except ImportError: + pass + + +class CTkImage: + """ + Class to store one or two PIl.Image.Image objects and display size independent of scaling: + + light_image: PIL.Image.Image for light mode + dark_image: PIL.Image.Image for dark mode + size: tuple (, ) with display size for both images + + One of the two images can be None and will be replaced by the other image. + """ + + _checked_PIL_import = False + + def __init__(self, light_image: Image.Image = None, dark_image: Image.Image = None, size: Tuple[int, int] = None): + if not self._checked_PIL_import: + self._check_pil_import() + + self._light_image = light_image + self._dark_image = dark_image + self._check_images() + self._size = size + + self._configure_callback_list: List[Callable] = [] + self._scaled_light_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {} + self._scaled_dark_photo_images: Dict[Tuple[int, int], ImageTk.PhotoImage] = {} + + @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.") + + def add_configure_callback(self, callback: Callable): + """ add function, that gets called when image got configured """ + self._configure_callback_list.append(callback) + + def remove_configure_callback(self, callback: Callable): + """ remove function, that gets called when image got configured """ + self._configure_callback_list.remove(callback) + + def configure(self, **kwargs): + if "light_image" in kwargs: + self._light_image = kwargs.pop("light_image") + self._scaled_light_photo_images = {} + self._check_images() + if "dark_image" in kwargs: + self._dark_image = kwargs.pop("dark_image") + self._scaled_dark_photo_images = {} + self._check_images() + if "size" in kwargs: + self._size = kwargs.pop("size") + + # call all functions registered with add_configure_callback() + for callback in self._configure_callback_list: + callback() + + def cget(self, attribute_name: str) -> any: + if attribute_name == "light_image": + return self._light_image + if attribute_name == "dark_image": + return self._dark_image + if attribute_name == "size": + return self._size + + def _check_images(self): + # check types + if self._light_image is not None and not isinstance(self._light_image, Image.Image): + raise ValueError(f"CTkImage: light_image must be instance if PIL.Image.Image, not {type(self._light_image)}") + if self._dark_image is not None and not isinstance(self._dark_image, Image.Image): + raise ValueError(f"CTkImage: dark_image must be instance if PIL.Image.Image, not {type(self._dark_image)}") + + # check values + if self._light_image is None and self._dark_image is None: + raise ValueError("CTkImage: No image given, light_image is None and dark_image is None.") + + # check sizes + if self._light_image is not None and self._dark_image is not None and self._light_image.size != self._dark_image.size: + 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) + + def _get_scaled_light_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage: + if scaled_size in self._scaled_light_photo_images: + return self._scaled_light_photo_images[scaled_size] + else: + self._scaled_light_photo_images[scaled_size] = ImageTk.PhotoImage(self._light_image.resize(scaled_size)) + return self._scaled_light_photo_images[scaled_size] + + def _get_scaled_dark_photo_image(self, scaled_size: Tuple[int, int]) -> ImageTk.PhotoImage: + if scaled_size in self._scaled_dark_photo_images: + return self._scaled_dark_photo_images[scaled_size] + else: + 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: + scaled_size = self._get_scaled_size(widget_scaling) + + if appearance_mode == 0 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: + return self._get_scaled_dark_photo_image(scaled_size) + + elif appearance_mode == 1 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: + return self._get_scaled_light_photo_image(scaled_size) + + diff --git a/examples/example_button_images.py b/examples/example_button_images.py index 23c9374..caec9b7 100644 --- a/examples/example_button_images.py +++ b/examples/example_button_images.py @@ -33,77 +33,7 @@ class App(customtkinter.CTk): self.chat_image = self.load_image("/test_images/chat.png", 20) self.home_image = self.load_image("/test_images/home.png", 20) - def _pyimagingtkcall(command, photo, id): - tk = photo.tk - try: - tk.call(command, photo, id) - except tkinter.TclError: - print("_pyimagingtkcall error") - - class PhotoImage: - def __init__(self, image=None, size=None, **kw): - if hasattr(image, "mode") and hasattr(image, "size"): - # got an image instead of a mode - mode = image.mode - if mode == "P": - # palette mapped data - image.apply_transparency() - image.load() - try: - mode = image.palette.mode - except AttributeError: - mode = "RGB" # default - size = image.size - kw["width"], kw["height"] = size - else: - mode = image - image = None - - if mode not in ["1", "L", "RGB", "RGBA"]: - mode = Image.getmodebase(mode) - - self.__mode = mode - self.__size = size - self.__photo = tkinter.PhotoImage(**kw) - self.tk = self.__photo.tk - if image: - self.paste(image) - - def __del__(self): - name = self.__photo.name - self.__photo.name = None - try: - self.__photo.tk.call("image", "delete", name) - except Exception: - pass # ignore internal errors - - def __str__(self): - return str(self.__photo) - - def width(self): - return self.__size[0] - - def height(self): - return self.__size[1] - - def paste(self, im, box=None): - if box is not None: - deprecate("The box parameter", 10, None) - - # convert to blittable - im.load() - image = im.im - if image.isblock() and im.mode == self.__mode: - block = image - else: - block = image.new_block(self.__mode, im.size) - image.convert2(block, image) # convert directly between buffers - _pyimagingtkcall("PyImagingPhoto", self.__photo, block.id) - - pil_img = Image.open(PATH + "/test_images/add-folder.png").resize((10, 10)) - image = PhotoImage(pil_img) - - self.button_1 = customtkinter.CTkButton(master=self.frame_1, image=image, text="Add Folder", height=32, + 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")