changeed driectory structure, moved scaling and appearance mode functionality to super classes

This commit is contained in:
Tom Schimansky 2022-10-29 13:11:55 +02:00
parent e5484cb6cd
commit c2a5b4881e
42 changed files with 280 additions and 294 deletions

View File

@ -10,6 +10,8 @@ ToDo:
- image tuple for light/dark mode - image tuple for light/dark mode
- change font attribute in wiki - change font attribute in wiki
- add new button attributes to wiki - add new button attributes to wiki
- cursor configuring
- overwrite winfo methods
## Unreleased - 2022-10-2 ## Unreleased - 2022-10-2
### Added ### Added

View File

@ -4,11 +4,11 @@ import os
import sys import sys
# import manager classes # import manager classes
from .appearance_mode_tracker import AppearanceModeTracker from .windows.widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from .widgets.font.font_manager import FontManager from .windows.widgets.font.font_manager import FontManager
from .scaling_tracker import ScalingTracker from .windows.widgets.scaling.scaling_tracker import ScalingTracker
from .theme_manager import ThemeManager from .windows.widgets.theme.theme_manager import ThemeManager
from .widgets.core_rendering.draw_engine import DrawEngine from .windows.widgets.core_rendering.draw_engine import DrawEngine
AppearanceModeTracker.init_appearance_mode() AppearanceModeTracker.init_appearance_mode()
@ -46,23 +46,21 @@ if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Cust
DrawEngine.preferred_drawing_method = "circle_shapes" DrawEngine.preferred_drawing_method = "circle_shapes"
# import widgets # import widgets
from customtkinter.widgets.core_widget_classes.widget_base_class import CTkBaseClass from .windows.widgets.ctk_button import CTkButton
from .widgets.ctk_button import CTkButton from .windows.widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_checkbox import CTkCheckBox from .windows.widgets.ctk_combobox import CTkComboBox
from .widgets.ctk_entry import CTkEntry from .windows.widgets.ctk_entry import CTkEntry
from .widgets.ctk_slider import CTkSlider from .windows.widgets.ctk_frame import CTkFrame
from .widgets.ctk_frame import CTkFrame from .windows.widgets.ctk_label import CTkLabel
from .widgets.ctk_progressbar import CTkProgressBar from .windows.widgets.ctk_optionmenu import CTkOptionMenu
from .widgets.ctk_label import CTkLabel from .windows.widgets.ctk_progressbar import CTkProgressBar
from .widgets.ctk_radiobutton import CTkRadioButton from .windows.widgets.ctk_radiobutton import CTkRadioButton
from .widgets.core_rendering.ctk_canvas import CTkCanvas from .windows.widgets.ctk_scrollbar import CTkScrollbar
from .widgets.ctk_switch import CTkSwitch from .windows.widgets.ctk_segmented_button import CTkSegmentedButton
from .widgets.ctk_optionmenu import CTkOptionMenu from .windows.widgets.ctk_slider import CTkSlider
from .widgets.ctk_combobox import CTkComboBox from .windows.widgets.ctk_switch import CTkSwitch
from .widgets.ctk_scrollbar import CTkScrollbar from .windows.widgets.ctk_tabview import CTkTabview
from .widgets.ctk_textbox import CTkTextbox from .windows.widgets.ctk_textbox import CTkTextbox
from .widgets.ctk_tabview import CTkTabview
from .widgets.ctk_segmented_button import CTkSegmentedButton
# import windows # import windows
from .windows.ctk_tk import CTk from .windows.ctk_tk import CTk
@ -70,7 +68,7 @@ from .windows.ctk_toplevel import CTkToplevel
from .windows.ctk_input_dialog import CTkInputDialog from .windows.ctk_input_dialog import CTkInputDialog
# font classes # font classes
from .widgets.font.ctk_font import CTkFont from .windows.widgets.font.ctk_font import CTkFont
def set_appearance_mode(mode_string: str): def set_appearance_mode(mode_string: str):

View File

@ -1,3 +0,0 @@
from customtkinter.widgets.core_rendering.ctk_canvas import CTkCanvas
CTkCanvas.init_font_character_mapping()

View File

@ -1,11 +1,11 @@
from typing import Union, Tuple from typing import Union, Tuple
from ..widgets.ctk_label import CTkLabel from .widgets.ctk_label import CTkLabel
from ..widgets.ctk_entry import CTkEntry from .widgets.ctk_entry import CTkEntry
from ..windows.ctk_toplevel import CTkToplevel from .ctk_toplevel import CTkToplevel
from ..widgets.ctk_button import CTkButton from .widgets.ctk_button import CTkButton
from ..appearance_mode_tracker import AppearanceModeTracker from .widgets.appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import ThemeManager from .widgets.theme.theme_manager import ThemeManager
class CTkInputDialog: class CTkInputDialog:

View File

@ -4,17 +4,16 @@ import sys
import os import os
import platform import platform
import ctypes import ctypes
import re from typing import Union, Tuple
from typing import Union, Tuple, List
from ..appearance_mode_tracker import AppearanceModeTracker from .widgets.theme.theme_manager import ThemeManager
from ..theme_manager import ThemeManager from .widgets.scaling.scaling_base_class import CTkScalingBaseClass
from ..scaling_tracker import ScalingTracker from .widgets.appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
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 CTk(tkinter.Tk): class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" """
Main app window with dark titlebar on Windows and macOS. Main app window with dark titlebar on Windows and macOS.
For detailed information check out the documentation. For detailed information check out the documentation.
@ -33,21 +32,15 @@ class CTk(tkinter.Tk):
fg_color: Union[str, Tuple[str, str]] = "default_theme", fg_color: Union[str, Tuple[str, str]] = "default_theme",
**kwargs): **kwargs):
ScalingTracker.activate_high_dpi_awareness() # make process DPI aware
self._enable_macos_dark_title_bar() self._enable_macos_dark_title_bar()
super().__init__(**pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments)) # call init methods of super classes
tkinter.Tk.__init__(self, **pop_from_dict_by_set(kwargs, self._valid_tk_constructor_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True) check_kwargs_empty(kwargs, raise_error=True)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes self._current_width = 600 # initial window size, independent of scaling
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self._current_width = 600 # initial window size, always without scaling
self._current_height = 500 self._current_height = 500
self._min_width: int = 0 self._min_width: int = 0
self._min_height: int = 0 self._min_height: int = 0
@ -57,8 +50,11 @@ class CTk(tkinter.Tk):
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
# set bg of tkinter.Tk
super().configure(bg=self._apply_appearance_mode(self._fg_color)) super().configure(bg=self._apply_appearance_mode(self._fg_color))
super().title("CTk")
# set title and initial geometry
self.title("CTk")
self.geometry(f"{self._current_width}x{self._current_height}") self.geometry(f"{self._current_width}x{self._current_height}")
self._state_before_windows_set_titlebar_color = None self._state_before_windows_set_titlebar_color = None
@ -77,6 +73,14 @@ class CTk(tkinter.Tk):
self._block_update_dimensions_event = False self._block_update_dimensions_event = False
def destroy(self):
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Tk.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event): def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin": if sys.platform == "darwin":
@ -123,12 +127,6 @@ class CTk(tkinter.Tk):
if self._max_width is not None or self._max_height is not None: if self._max_width is not None or self._max_height is not None:
super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height)) super().maxsize(self._apply_window_scaling(self._max_width), self._apply_window_scaling(self._max_height))
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self): def withdraw(self):
if self._window_exists is False: if self._window_exists is False:
self._withdraw_called_before_window_exists = True self._withdraw_called_before_window_exists = True
@ -201,59 +199,6 @@ class CTk(tkinter.Tk):
else: else:
return self._reverse_geometry_scaling(super().geometry()) return self._reverse_geometry_scaling(super().geometry())
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
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)}"
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 _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
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)}"
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):
if isinstance(value, (int, float)):
return int(value * self._window_scaling)
else:
return value
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 type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def configure(self, **kwargs): def configure(self, **kwargs):
if "fg_color" in kwargs: if "fg_color" in kwargs:
self._fg_color = kwargs.pop("fg_color") self._fg_color = kwargs.pop("fg_color")

View File

@ -4,17 +4,16 @@ import sys
import os import os
import platform import platform
import ctypes import ctypes
import re from typing import Union, Tuple
from typing import Union, Tuple, List
from ..appearance_mode_tracker import AppearanceModeTracker from .widgets.theme.theme_manager import ThemeManager
from ..theme_manager import ThemeManager from .widgets.scaling.scaling_base_class import CTkScalingBaseClass
from ..scaling_tracker import ScalingTracker from .widgets.appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
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 CTkToplevel(tkinter.Toplevel): class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" """
Toplevel window with dark titlebar on Windows and macOS. Toplevel window with dark titlebar on Windows and macOS.
For detailed information check out the documentation. For detailed information check out the documentation.
@ -33,17 +32,12 @@ class CTkToplevel(tkinter.Toplevel):
self._enable_macos_dark_title_bar() self._enable_macos_dark_title_bar()
# call init methods of super classees
super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments)) super().__init__(*args, **pop_from_dict_by_set(kwargs, self._valid_tk_toplevel_arguments))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="window")
check_kwargs_empty(kwargs, raise_error=True) check_kwargs_empty(kwargs, raise_error=True)
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self._set_scaling, self)
self._window_scaling = ScalingTracker.get_window_scaling(self)
self._current_width = 200 # initial window size, always without scaling self._current_width = 200 # initial window size, always without scaling
self._current_height = 200 self._current_height = 200
self._min_width: int = 0 self._min_width: int = 0
@ -54,7 +48,10 @@ class CTkToplevel(tkinter.Toplevel):
self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color self._fg_color = ThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
# set bg color of tkinter.Toplevel
super().configure(bg=self._apply_appearance_mode(self._fg_color)) super().configure(bg=self._apply_appearance_mode(self._fg_color))
# set title of tkinter.Toplevel
super().title("CTkToplevel") super().title("CTkToplevel")
self._state_before_windows_set_titlebar_color = None self._state_before_windows_set_titlebar_color = None
@ -71,6 +68,14 @@ class CTkToplevel(tkinter.Toplevel):
self.bind('<Configure>', self._update_dimensions_event) self.bind('<Configure>', self._update_dimensions_event)
self.bind('<FocusIn>', self._focus_in_event) self.bind('<FocusIn>', self._focus_in_event)
def destroy(self):
self._disable_macos_dark_title_bar()
# call destroy methods of super classes
tkinter.Toplevel.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _focus_in_event(self, event): def _focus_in_event(self, event):
# sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again # sometimes window looses jumps back on macOS if window is selected from Mission Control, so has to be lifted again
if sys.platform == "darwin": if sys.platform == "darwin":
@ -114,65 +119,6 @@ class CTkToplevel(tkinter.Toplevel):
else: else:
return self._reverse_geometry_scaling(super().geometry()) return self._reverse_geometry_scaling(super().geometry())
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
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)}"
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 _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
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)}"
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):
if isinstance(value, (int, float)):
return int(value * self._window_scaling)
else:
return value
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 type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def destroy(self):
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_window(self._set_scaling, self)
self._disable_macos_dark_title_bar()
super().destroy()
def withdraw(self): def withdraw(self):
if self._windows_set_titlebar_color_called: if self._windows_set_titlebar_color_called:
self._withdraw_called_after_windows_set_titlebar_color = True self._withdraw_called_after_windows_set_titlebar_color = True

View File

@ -0,0 +1,3 @@
from customtkinter.windows.widgets.core_rendering.ctk_canvas import CTkCanvas
CTkCanvas.init_font_character_mapping()

View File

@ -1,8 +1,16 @@
from typing import Union, Tuple, List from typing import Union, Tuple, List
from abc import ABC, abstractmethod
from .appearance_mode_tracker import AppearanceModeTracker
class CTkAppearanceModeBaseClass: class CTkAppearanceModeBaseClass(ABC):
def __init__(self): def __init__(self):
AppearanceModeTracker.add(self._set_appearance_mode, self)
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: 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 """ color can be either a single hex color string or a color name or it can be a
@ -13,3 +21,7 @@ class CTkAppearanceModeBaseClass:
return color[self._appearance_mode] return color[self._appearance_mode]
else: else:
return color return color
@abstractmethod
def _set_appearance_mode(self, mode_string: str):
return

View File

@ -5,7 +5,7 @@ import tkinter
from typing import Union, TYPE_CHECKING from typing import Union, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from customtkinter.widgets.core_rendering.ctk_canvas import CTkCanvas from customtkinter.windows.widgets.core_rendering.ctk_canvas import CTkCanvas
class DrawEngine: class DrawEngine:

View File

@ -2,13 +2,14 @@ import tkinter
import sys import sys
from typing import Union, Tuple, Callable, List from typing import Union, Tuple, Callable, List
from ...theme_manager import ThemeManager from ..theme.theme_manager import ThemeManager
from ...appearance_mode_tracker import AppearanceModeTracker from ..scaling.scaling_tracker import ScalingTracker
from ...scaling_tracker import ScalingTracker
from ..font.ctk_font import CTkFont from ..font.ctk_font import CTkFont
from ..appearance_mode.appearance_mode_tracker import AppearanceModeTracker
from ..appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
class DropdownMenu(tkinter.Menu): class DropdownMenu(tkinter.Menu, CTkAppearanceModeBaseClass):
def __init__(self, *args, def __init__(self, *args,
min_character_width: int = 18, min_character_width: int = 18,
@ -21,7 +22,9 @@ class DropdownMenu(tkinter.Menu):
values: List[str] = None, values: List[str] = None,
**kwargs): **kwargs):
super().__init__(*args, **kwargs) # call init methods of super classes
tkinter.Menu.__init__(self, *args, **kwargs)
CTkAppearanceModeBaseClass.__init__(self)
ScalingTracker.add_widget(self._set_scaling, self) ScalingTracker.add_widget(self._set_scaling, self)
self._widget_scaling = ScalingTracker.get_widget_scaling(self) self._widget_scaling = ScalingTracker.get_widget_scaling(self)
@ -46,14 +49,17 @@ class DropdownMenu(tkinter.Menu):
self._add_menu_commands() self._add_menu_commands()
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling """
super().configure(font=self._apply_font_scaling(self._font))
def destroy(self): def destroy(self):
if isinstance(self._font, CTkFont): if isinstance(self._font, CTkFont):
self._font.remove_size_configure_callback(self._update_font) self._font.remove_size_configure_callback(self._update_font)
super().destroy()
# call destroy methods of super classes
tkinter.Menu.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
def _update_font(self):
""" pass font to tkinter widgets with applied font scaling """
super().configure(font=self._apply_font_scaling(self._font))
def _configure_menu_for_platforms(self): def _configure_menu_for_platforms(self):
""" apply platform specific appearance attributes, configure all colors """ """ apply platform specific appearance attributes, configure all colors """

View File

@ -1,26 +1,24 @@
import sys import sys
import tkinter import tkinter
import tkinter.ttk as ttk import tkinter.ttk as ttk
import copy from typing import Union, Callable, Tuple
from typing import Union, Callable, Tuple, List
from abc import ABC, abstractmethod
try: try:
from typing import TypedDict from typing import TypedDict
except ImportError: except ImportError:
from typing_extensions import TypedDict from typing_extensions import TypedDict
from ...windows.ctk_tk import CTk from ...ctk_tk import CTk
from ...windows.ctk_toplevel import CTkToplevel from ...ctk_toplevel import CTkToplevel
from ...appearance_mode_tracker import AppearanceModeTracker from ..theme.theme_manager import ThemeManager
from ...scaling_tracker import ScalingTracker
from ...theme_manager import ThemeManager
from ..font.ctk_font import CTkFont from ..font.ctk_font import CTkFont
from ..appearance_mode.appearance_mode_base_class import CTkAppearanceModeBaseClass
from ..scaling.scaling_base_class import CTkScalingBaseClass
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, ABC): class CTkBaseClass(tkinter.Frame, CTkAppearanceModeBaseClass, CTkScalingBaseClass):
""" Base class of every CTk widget, handles the dimensions, _bg_color, """ Base class of every CTk widget, handles the dimensions, _bg_color,
appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """ appearance_mode changes, scaling, bg changes of master if master is not a CTk widget """
@ -37,21 +35,21 @@ class CTkBaseClass(tkinter.Frame, ABC):
bg_color: Union[str, Tuple[str, str], None] = None, bg_color: Union[str, Tuple[str, str], None] = None,
**kwargs): **kwargs):
super().__init__(master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes)) # call init methods of super classes
tkinter.Frame.__init__(self, master=master, width=width, height=height, **pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))
CTkAppearanceModeBaseClass.__init__(self)
CTkScalingBaseClass.__init__(self, scaling_type="widget")
# check if kwargs is empty, if not raise error for unsupported arguments # check if kwargs is empty, if not raise error for unsupported arguments
check_kwargs_empty(kwargs, raise_error=True) check_kwargs_empty(kwargs, raise_error=True)
# dimensions # dimensions independent of scaling
self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget self._current_width = width # _current_width and _current_height in pixel, represent current size of the widget
self._current_height = height # _current_width and _current_height are independent of the scale self._current_height = height # _current_width and _current_height are independent of the scale
self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height self._desired_width = width # _desired_width and _desired_height, represent desired size set by width and height
self._desired_height = height self._desired_height = height
# scaling # set width and height of tkinter.Frame
ScalingTracker.add_widget(self._set_scaling, self) # add callback for automatic scaling changes
self._widget_scaling = ScalingTracker.get_widget_scaling(self)
super().configure(width=self._apply_widget_scaling(self._desired_width), super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height)) height=self._apply_widget_scaling(self._desired_height))
@ -59,17 +57,15 @@ class CTkBaseClass(tkinter.Frame, ABC):
class GeometryCallDict(TypedDict): class GeometryCallDict(TypedDict):
function: Callable function: Callable
kwargs: dict kwargs: dict
self._last_geometry_manager_call: Union[GeometryCallDict, None] = None self._last_geometry_manager_call: Union[GeometryCallDict, None] = None
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self._set_appearance_mode, self)
self._appearance_mode = AppearanceModeTracker.get_mode() # 0: "Light" 1: "Dark"
# background color # background color
self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color is None else bg_color self._bg_color: Union[str, Tuple[str, str]] = self._detect_color_of_master() if bg_color is None else bg_color
# set bg color of tkinter.Frame
super().configure(bg=self._apply_appearance_mode(self._bg_color)) super().configure(bg=self._apply_appearance_mode(self._bg_color))
# add configure callback to tkinter.Frame
super().bind('<Configure>', self._update_dimensions_event) super().bind('<Configure>', self._update_dimensions_event)
# overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well # overwrite configure methods of master when master is tkinter widget, so that bg changes get applied on child CTk widget as well
@ -93,7 +89,14 @@ class CTkBaseClass(tkinter.Frame, ABC):
self.master.config = new_configure self.master.config = new_configure
self.master.configure = new_configure self.master.configure = new_configure
@abstractmethod def destroy(self):
""" Destroy this and all descendants widgets. """
# call destroy methods of super classes
tkinter.Frame.destroy(self)
CTkAppearanceModeBaseClass.destroy(self)
CTkScalingBaseClass.destroy(self)
def _draw(self, no_color_updates: bool = False): def _draw(self, no_color_updates: bool = False):
return return
@ -221,66 +224,6 @@ class CTkBaseClass(tkinter.Frame, ABC):
super().configure(width=self._apply_widget_scaling(self._desired_width), super().configure(width=self._apply_widget_scaling(self._desired_width),
height=self._apply_widget_scaling(self._desired_height)) height=self._apply_widget_scaling(self._desired_height))
def _apply_widget_scaling(self, value: Union[int, float, str]) -> Union[float, str]:
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
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 """
if type(font) == tuple:
if len(font) == 1:
return font
elif len(font) == 2:
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]
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)
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:
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
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)):
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"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
return scaled_kwargs
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 type(color) == tuple or type(color) == list:
return color[self._appearance_mode]
else:
return color
def destroy(self):
""" Destroy this and all descendants widgets. """
AppearanceModeTracker.remove(self._set_appearance_mode)
ScalingTracker.remove_widget(self._set_scaling, self)
super().destroy()
def place(self, **kwargs): def place(self, **kwargs):
""" """
Place a widget in the parent widget. Use as options: Place a widget in the parent widget. Use as options:

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -4,7 +4,7 @@ from typing import Union, Tuple, Callable, List
from .core_widget_classes.dropdown_menu import DropdownMenu from .core_widget_classes.dropdown_menu import DropdownMenu
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ import tkinter
from typing import Union, Tuple from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -1,7 +1,7 @@
from typing import Union, Tuple, List from typing import Union, Tuple, List
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -2,7 +2,7 @@ import tkinter
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .core_widget_classes.dropdown_menu import DropdownMenu from .core_widget_classes.dropdown_menu import DropdownMenu

View File

@ -3,7 +3,7 @@ import math
from typing import Union, Tuple from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -1,7 +1,7 @@
import tkinter import tkinter
from typing import Union, Tuple, List, Dict, Callable from typing import Union, Tuple, List, Dict, Callable
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .ctk_button import CTkButton from .ctk_button import CTkButton
from .ctk_frame import CTkFrame from .ctk_frame import CTkFrame
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass

View File

@ -3,7 +3,7 @@ import sys
from typing import Union, Tuple, Callable from typing import Union, Tuple, Callable
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -1,6 +1,6 @@
from typing import Union, Tuple, Dict, List, Callable from typing import Union, Tuple, Dict, List, Callable
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .ctk_frame import CTkFrame from .ctk_frame import CTkFrame
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .ctk_segmented_button import CTkSegmentedButton from .ctk_segmented_button import CTkSegmentedButton

View File

@ -3,7 +3,7 @@ from typing import Union, Tuple
from .core_rendering.ctk_canvas import CTkCanvas from .core_rendering.ctk_canvas import CTkCanvas
from .ctk_scrollbar import CTkScrollbar from .ctk_scrollbar import CTkScrollbar
from ..theme_manager import ThemeManager from .theme.theme_manager import ThemeManager
from .core_rendering.draw_engine import DrawEngine from .core_rendering.draw_engine import DrawEngine
from .core_widget_classes.widget_base_class import CTkBaseClass from .core_widget_classes.widget_base_class import CTkBaseClass
from .font.ctk_font import CTkFont from .font.ctk_font import CTkFont

View File

@ -2,7 +2,7 @@ from tkinter.font import Font
import copy import copy
from typing import List, Callable, Tuple from typing import List, Callable, Tuple
from customtkinter.theme_manager import ThemeManager from ..theme.theme_manager import ThemeManager
class CTkFont(Font): class CTkFont(Font):

View File

@ -0,0 +1,133 @@
from typing import Union, Tuple
from abc import ABC, abstractmethod
import copy
import re
try:
from typing import Literal
except ImportError:
from typing_extensions import Literal
from .scaling_tracker import ScalingTracker
from ..font.ctk_font import CTkFont
class CTkScalingBaseClass(ABC):
def __init__(self, scaling_type: Literal["widget", "window"] = "widget"):
self._scaling_type = scaling_type
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":
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)
def destroy(self):
if self._scaling_type == "widget":
ScalingTracker.remove_widget(self._set_scaling, self)
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"
if isinstance(value, (int, float)):
return value * self._widget_scaling
else:
return value
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"
if type(font) == tuple:
if len(font) == 1:
return font
elif len(font) == 2:
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]
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)
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"
scaled_kwargs = copy.copy(kwargs)
if "pady" in scaled_kwargs:
if isinstance(scaled_kwargs["pady"], (int, float, str)):
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)):
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"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self._apply_widget_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self._apply_widget_scaling(scaled_kwargs["y"])
return scaled_kwargs
@staticmethod
def _parse_geometry_string(geometry_string: str) -> tuple:
# index: 1 2 3 4 5 6
# regex group structure: ('<width>x<height>', '<width>', '<height>', '+-<x>+-<y>', '-<x>', '-<y>')
result = re.search(r"((\d+)x(\d+)){0,1}(\+{0,1}([+-]{0,1}\d+)\+{0,1}([+-]{0,1}\d+)){0,1}", geometry_string)
width = int(result.group(2)) if result.group(2) is not None else None
height = int(result.group(3)) if result.group(3) is not None else None
x = int(result.group(5)) if result.group(5) is not None else None
y = int(result.group(6)) if result.group(6) is not None else None
return width, height, x, y
def _apply_geometry_scaling(self, geometry_string: str) -> str:
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)}"
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 _reverse_geometry_scaling(self, scaled_geometry_string: str) -> str:
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)}"
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

View File

@ -13,7 +13,7 @@ class ThemeManager:
script_directory = os.path.dirname(os.path.abspath(__file__)) script_directory = os.path.dirname(os.path.abspath(__file__))
if theme_name_or_path in cls.built_in_themes: if theme_name_or_path in cls.built_in_themes:
with open(os.path.join(script_directory, "assets", "themes", f"{theme_name_or_path}.json"), "r") as f: with open(os.path.join(script_directory, "../../../assets", "themes", f"{theme_name_or_path}.json"), "r") as f:
cls.theme = json.load(f) cls.theme = json.load(f)
else: else:
with open(theme_name_or_path, "r") as f: with open(theme_name_or_path, "r") as f:

View File

@ -1 +1,2 @@
darkdetect~=0.3.1 darkdetect~=0.3.1
typing-extensions~=4.4.0