architecture fixes

This commit is contained in:
Tom Schimansky
2022-11-01 00:37:30 +01:00
parent 302313916a
commit 7374e7a3bc
27 changed files with 274 additions and 263 deletions

View File

@@ -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('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', 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))

View File

@@ -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('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', 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))

View File

@@ -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

View File

@@ -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):

View File

@@ -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))

View File

@@ -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:

View File

@@ -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),

View File

@@ -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),

View File

@@ -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),

View File

@@ -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

View File

@@ -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),

View File

@@ -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),

View File

@@ -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":

View File

@@ -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),

View File

@@ -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)))

View File

@@ -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":

View File

@@ -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),

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 <x> and <y> 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 <width> and <height> 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 <x> and <y> 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 <width> and <height> 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}"