restructured settings, small scaling fixes in CTk

This commit is contained in:
TomSchimansky 2022-05-22 00:02:45 +02:00
parent 4b48bf57b2
commit 35bdbed95c
21 changed files with 135 additions and 408 deletions

View File

@ -9,32 +9,30 @@ from .appearance_mode_tracker import AppearanceModeTracker
from .theme_manager import ThemeManager from .theme_manager import ThemeManager
from .scaling_tracker import ScalingTracker from .scaling_tracker import ScalingTracker
from .font_manager import FontManager from .font_manager import FontManager
from .draw_engine import DrawEngine
Settings.init_font_character_mapping()
Settings.init_drawing_method()
AppearanceModeTracker.init_appearance_mode() AppearanceModeTracker.init_appearance_mode()
ThemeManager.load_theme("blue") ThemeManager.load_theme("blue")
FontManager.init_font_manager() FontManager.init_font_manager()
# determine draw method based on current platform
if sys.platform == "darwin":
DrawEngine.preferred_drawing_method = "polygon_shapes"
else:
DrawEngine.preferred_drawing_method = "font_shapes"
# load Roboto fonts # load Roboto fonts
script_directory = os.path.dirname(os.path.abspath(__file__)) script_directory = os.path.dirname(os.path.abspath(__file__))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf")) FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Regular.ttf"))
FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf")) FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "Roboto", "Roboto-Medium.ttf"))
# load font necessary for rendering the widgets on Windows, Linux # load font necessary for rendering the widgets on Windows, Linux
if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf")) is True: if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf")) is False:
Settings.circle_font_is_ready = True if DrawEngine.preferred_drawing_method == "font_shapes":
else:
Settings.circle_font_is_ready = False
if Settings.preferred_drawing_method == "font_shapes":
sys.stderr.write("customtkinter.__init__ warning: " + sys.stderr.write("customtkinter.__init__ warning: " +
"Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" + "Preferred drawing method 'font_shapes' can not be used because the font file could not be loaded.\n" +
"Using 'circle_shapes' instead. The rendering quality will be bad!") "Using 'circle_shapes' instead. The rendering quality will be bad!")
Settings.preferred_drawing_method = "circle_shapes" DrawEngine.preferred_drawing_method = "circle_shapes"
# import widgets # import widgets
from .widgets.ctk_button import CTkButton from .widgets.ctk_button import CTkButton
@ -69,10 +67,10 @@ def set_default_color_theme(color_string):
ThemeManager.load_theme(color_string) ThemeManager.load_theme(color_string)
def deactivate_dpi_awareness(deactivate_awareness: bool):
Settings.deactivate_automatic_dpi_awareness = deactivate_awareness
def set_user_scaling(scaling_value: float): def set_user_scaling(scaling_value: float):
ScalingTracker.set_spacing_scaling(scaling_value) ScalingTracker.set_spacing_scaling(scaling_value)
ScalingTracker.set_widget_scaling(scaling_value) ScalingTracker.set_widget_scaling(scaling_value)
def deactivate_automatic_dpi_awareness():
ScalingTracker.deactivate_automatic_dpi_awareness = False

View File

@ -98,7 +98,7 @@ class AppearanceModeTracker:
# find an existing tkinter.Tk object for the next call of .after() # find an existing tkinter.Tk object for the next call of .after()
for root_tk in cls.root_tk_list: for root_tk in cls.root_tk_list:
try: try:
root_tk.after(200, cls.update) root_tk.after(500, cls.update)
return return
except Exception: except Exception:
continue continue

View File

@ -51,7 +51,7 @@
"radiobutton_border_width_unchecked": 3, "radiobutton_border_width_unchecked": 3,
"radiobutton_border_width_checked": 6, "radiobutton_border_width_checked": 6,
"entry_border_width": 2, "entry_border_width": 2,
"frame_corner_radius": 10, "frame_corner_radius": 8,
"frame_border_width": 0, "frame_border_width": 0,
"label_corner_radius": 8, "label_corner_radius": 8,
"progressbar_border_width": 0, "progressbar_border_width": 0,

View File

@ -1,9 +1,11 @@
from __future__ import annotations
import sys import sys
import math import math
import tkinter import tkinter
from typing import Union from typing import Union, TYPE_CHECKING
from .widgets.ctk_canvas import CTkCanvas if TYPE_CHECKING:
from .widgets.ctk_canvas import CTkCanvas
class DrawEngine: class DrawEngine:
@ -21,25 +23,26 @@ class DrawEngine:
""" """
def __init__(self, canvas: CTkCanvas, rendering_method: str): preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes'
def __init__(self, canvas: CTkCanvas):
self._canvas = canvas self._canvas = canvas
self._rendering_method = rendering_method # "polygon_shapes" (macOS), "font_shapes" (Windows, Linux), "circle_shapes" (backup without fonts)
self._existing_tags = set() self._existing_tags = set()
def _calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]: def _calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
# optimize for drawing with polygon shapes # optimize for drawing with polygon shapes
if self._rendering_method == "polygon_shapes": if self.preferred_drawing_method == "polygon_shapes":
if sys.platform == "darwin": if sys.platform == "darwin":
return user_corner_radius return user_corner_radius
else: else:
return round(user_corner_radius) return round(user_corner_radius)
# optimize forx drawing with antialiased font shapes # optimize forx drawing with antialiased font shapes
elif self._rendering_method == "font_shapes": elif self.preferred_drawing_method == "font_shapes":
return round(user_corner_radius) return round(user_corner_radius)
# optimize for drawing with circles and rects # optimize for drawing with circles and rects
elif self._rendering_method == "circle_shapes": elif self.preferred_drawing_method == "circle_shapes":
user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps user_corner_radius = 0.5 * round(user_corner_radius / 0.5) # round to 0.5 steps
# make sure the value is always with .5 at the end for smoother corners # make sure the value is always with .5 at the end for smoother corners
@ -71,11 +74,11 @@ class DrawEngine:
else: else:
inner_corner_radius = 0 inner_corner_radius = 0
if self._rendering_method == "polygon_shapes": if self.preferred_drawing_method == "polygon_shapes":
return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius) return self._draw_rounded_rect_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius)
elif self._rendering_method == "font_shapes": elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ()) return self._draw_rounded_rect_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, ())
elif self._rendering_method == "circle_shapes": elif self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius) return self._draw_rounded_rect_with_border_circle_shapes(width, height, corner_radius, border_width, inner_corner_radius)
def _draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool: def _draw_rounded_rect_with_border_polygon_shapes(self, width: int, height: int, corner_radius: int, border_width: int, inner_corner_radius: int) -> bool:
@ -365,10 +368,10 @@ class DrawEngine:
else: else:
inner_corner_radius = 0 inner_corner_radius = 0
if self._rendering_method == "polygon_shapes" or self._rendering_method == "circle_shapes": if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, return self._draw_rounded_progress_bar_with_border_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation) progress_value, orientation)
elif self._rendering_method == "font_shapes": elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, return self._draw_rounded_progress_bar_with_border_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
progress_value, orientation) progress_value, orientation)
@ -534,10 +537,10 @@ class DrawEngine:
else: else:
inner_corner_radius = 0 inner_corner_radius = 0
if self._rendering_method == "polygon_shapes" or self._rendering_method == "circle_shapes": if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
return self._draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius, return self._draw_rounded_slider_with_border_and_button_polygon_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation) button_length, button_corner_radius, slider_value, orientation)
elif self._rendering_method == "font_shapes": elif self.preferred_drawing_method == "font_shapes":
return self._draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius, return self._draw_rounded_slider_with_border_and_button_font_shapes(width, height, corner_radius, border_width, inner_corner_radius,
button_length, button_corner_radius, slider_value, orientation) button_length, button_corner_radius, slider_value, orientation)
@ -659,7 +662,7 @@ class DrawEngine:
size = round(size) size = round(size)
requires_recoloring = False requires_recoloring = False
if self._rendering_method == "polygon_shapes" or self._rendering_method == "circle_shapes": if self.preferred_drawing_method == "polygon_shapes" or self.preferred_drawing_method == "circle_shapes":
x, y, radius = width / 2, height / 2, size / 2.8 x, y, radius = width / 2, height / 2, size / 2.8
if not self._canvas.find_withtag("checkmark"): if not self._canvas.find_withtag("checkmark"):
self._canvas.create_line(0, 0, 0, 0, tags=("checkmark", "create_line"), width=round(height / 8), joinstyle=tkinter.MITER, capstyle=tkinter.ROUND) self._canvas.create_line(0, 0, 0, 0, tags=("checkmark", "create_line"), width=round(height / 8), joinstyle=tkinter.MITER, capstyle=tkinter.ROUND)
@ -670,7 +673,7 @@ class DrawEngine:
x + radius, y - radius, x + radius, y - radius,
x - radius / 4, y + radius * 0.8, x - radius / 4, y + radius * 0.8,
x - radius, y + radius / 6) x - radius, y + radius / 6)
elif self._rendering_method == "font_shapes": elif self.preferred_drawing_method == "font_shapes":
if not self._canvas.find_withtag("checkmark"): if not self._canvas.find_withtag("checkmark"):
self._canvas.create_text(0, 0, text="Z", font=("CustomTkinter_shapes_font", -size), tags=("checkmark", "create_text"), anchor=tkinter.CENTER) self._canvas.create_text(0, 0, text="Z", font=("CustomTkinter_shapes_font", -size), tags=("checkmark", "create_text"), anchor=tkinter.CENTER)
self._canvas.tag_raise("checkmark") self._canvas.tag_raise("checkmark")

View File

@ -1,11 +1,10 @@
import tkinter import tkinter
import sys import sys
from typing import Callable, Type from typing import Callable
from .settings import Settings
class ScalingTracker: class ScalingTracker:
deactivate_automatic_dpi_awareness = False
window_widgets_dict = {} # contains window objects as keys with list of widget callbacks as elements window_widgets_dict = {} # contains window objects as keys with list of widget callbacks as elements
window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors
@ -34,17 +33,17 @@ class ScalingTracker:
@classmethod @classmethod
def set_widget_scaling(cls, widget_scaling_factor: float): def set_widget_scaling(cls, widget_scaling_factor: float):
cls.widget_scaling = max(widget_scaling_factor, 0.4) cls.widget_scaling = max(widget_scaling_factor, 0.4)
cls.update_scaling_callbacks() cls.update_scaling_callbacks_all()
@classmethod @classmethod
def set_spacing_scaling(cls, spacing_scaling_factor: float): def set_spacing_scaling(cls, spacing_scaling_factor: float):
cls.spacing_scaling = max(spacing_scaling_factor, 0.4) cls.spacing_scaling = max(spacing_scaling_factor, 0.4)
cls.update_scaling_callbacks() cls.update_scaling_callbacks_all()
@classmethod @classmethod
def set_window_scaling(cls, window_scaling_factor: float): def set_window_scaling(cls, window_scaling_factor: float):
cls.window_scaling = max(window_scaling_factor, 0.4) cls.window_scaling = max(window_scaling_factor, 0.4)
cls.update_scaling_callbacks() cls.update_scaling_callbacks_all()
@classmethod @classmethod
def get_window_root_of_widget(cls, widget): def get_window_root_of_widget(cls, widget):
@ -57,15 +56,27 @@ class ScalingTracker:
return current_widget return current_widget
@classmethod @classmethod
def update_scaling_callbacks(cls): def update_scaling_callbacks_all(cls):
for window, callback_list in cls.window_widgets_dict.items(): for window, callback_list in cls.window_widgets_dict.items():
for callback in callback_list: for set_scaling_callback in callback_list:
if not Settings.deactivate_automatic_dpi_awareness: if not cls.deactivate_automatic_dpi_awareness:
callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling, set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
cls.window_dpi_scaling_dict[window] * cls.spacing_scaling, cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling) cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else: else:
callback(cls.widget_scaling, set_scaling_callback(cls.widget_scaling,
cls.spacing_scaling,
cls.window_scaling)
@classmethod
def update_scaling_callbacks_for_window(cls, window):
for set_scaling_callback in cls.window_widgets_dict[window]:
if not cls.deactivate_automatic_dpi_awareness:
set_scaling_callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
cls.window_dpi_scaling_dict[window] * cls.spacing_scaling,
cls.window_dpi_scaling_dict[window] * cls.window_scaling)
else:
set_scaling_callback(cls.widget_scaling,
cls.spacing_scaling, cls.spacing_scaling,
cls.window_scaling) cls.window_scaling)
@ -81,9 +92,9 @@ class ScalingTracker:
if window_root not in cls.window_dpi_scaling_dict: if window_root not in cls.window_dpi_scaling_dict:
cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root) cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root)
#if not cls.update_loop_running: if not cls.update_loop_running:
# window_root.after(100, cls.check_dpi_scaling) window_root.after(100, cls.check_dpi_scaling)
# cls.update_loop_running = True cls.update_loop_running = True
@classmethod @classmethod
def remove_widget(cls, widget_callback, widget): def remove_widget(cls, widget_callback, widget):
@ -115,7 +126,7 @@ class ScalingTracker:
""" make process DPI aware, customtkinter elemets will get scaled automatically, """ make process DPI aware, customtkinter elemets will get scaled automatically,
only gets activated when CTk object is created """ only gets activated when CTk object is created """
if not Settings.deactivate_automatic_dpi_awareness: if not cls.deactivate_automatic_dpi_awareness:
if sys.platform == "darwin": if sys.platform == "darwin":
pass # high DPI scaling works automatically on macOS pass # high DPI scaling works automatically on macOS
@ -151,10 +162,16 @@ class ScalingTracker:
# check for every window if scaling value changed # check for every window if scaling value changed
# (not implemented yet) # (not implemented yet)
for window in cls.window_widgets_dict:
current_dpi_scaling_value = cls.get_window_dpi_scaling(window)
if current_dpi_scaling_value != cls.window_dpi_scaling_dict[window]:
cls.window_dpi_scaling_dict[window] = current_dpi_scaling_value
cls.update_scaling_callbacks_for_window(window)
# find an existing tkinter object for the next call of .after() # find an existing tkinter object for the next call of .after()
for root_tk in cls.window_widgets_dict.keys(): for root_tk in cls.window_widgets_dict.keys():
try: try:
root_tk.after(500, cls.check_dpi_scaling) root_tk.after(200, cls.check_dpi_scaling)
return return
except Exception: except Exception:
continue continue

View File

@ -1,42 +1,5 @@
import sys
class Settings: class Settings:
circle_font_is_ready = False
preferred_drawing_method: str = None # 'polygon_shapes', 'font_shapes', 'circle_shapes'
radius_to_char_fine: dict = None # set in self.init_font_character_mapping()
cursor_manipulation_enabled = True cursor_manipulation_enabled = True
deactivate_macos_window_header_manipulation = False deactivate_macos_window_header_manipulation = False
deactivate_windows_window_header_manipulation = False deactivate_windows_window_header_manipulation = False
deactivate_automatic_dpi_awareness = False
@classmethod
def init_font_character_mapping(cls):
""" optimizations made for Windows 10, 11 only """
radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B', 10: 'B',
9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', 11: 'C', 10: 'C',
9: 'D', 8: 'D', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C', 11: 'D', 10: 'D',
9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R', 0: 'A'}
if sys.platform.startswith("win"):
if sys.getwindowsversion().build > 20000: # Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_11
else: # < Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_10
else: # macOS and Linux
cls.radius_to_char_fine = radius_to_char_fine_windows_10
@classmethod
def init_drawing_method(cls):
""" possible: 'polygon_shapes', 'font_shapes', 'circle_shapes' """
if sys.platform == "darwin":
cls.preferred_drawing_method = "polygon_shapes"
else:
cls.preferred_drawing_method = "font_shapes"

View File

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

View File

@ -69,7 +69,7 @@ class CTkButton(CTkBaseClass):
width=self.apply_widget_scaling(self.desired_width), width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)) 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=2, columnspan=2, sticky="nsew")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
# event bindings # event bindings
self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Enter>", self.on_enter)

View File

@ -1,23 +1,50 @@
import tkinter import tkinter
from ..settings import Settings import sys
from typing import Union
class CTkCanvas(tkinter.Canvas): class CTkCanvas(tkinter.Canvas):
radius_to_char_fine: dict = None # dict to map radius to font circle character
radius_to_char_fine = Settings.radius_to_char_fine # dict to map radius to font circle character
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.aa_circle_canvas_ids = set() self.aa_circle_canvas_ids = set()
def get_char_from_radius(self, radius): @classmethod
def init_font_character_mapping(cls):
""" optimizations made for Windows 10, 11 only """
radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B',
10: 'B',
9: 'C', 8: 'D', 7: 'C', 6: 'E', 5: 'F', 4: 'G', 3: 'H', 2: 'H', 1: 'H', 0: 'A'}
radius_to_char_fine_windows_10 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
11: 'C', 10: 'C',
9: 'D', 8: 'D', 7: 'D', 6: 'F', 5: 'D', 4: 'G', 3: 'G', 2: 'H', 1: 'H',
0: 'A'}
radius_to_char_fine_windows_11 = {19: 'A', 18: 'A', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'C', 12: 'C',
11: 'D', 10: 'D',
9: 'E', 8: 'F', 7: 'C', 6: 'I', 5: 'E', 4: 'G', 3: 'P', 2: 'R', 1: 'R',
0: 'A'}
if sys.platform.startswith("win"):
if sys.getwindowsversion().build > 20000: # Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_11
else: # < Windows 11
cls.radius_to_char_fine = radius_to_char_fine_windows_10
else: # macOS and Linux
cls.radius_to_char_fine = radius_to_char_fine_windows_10
def get_char_from_radius(self, radius: int) -> str:
if radius >= 20: if radius >= 20:
return "A" return "A"
else: else:
return self.radius_to_char_fine[radius] return self.radius_to_char_fine[radius]
def create_aa_circle(self, x_pos, y_pos, radius, angle=0, fill="white", tags="", anchor=tkinter.CENTER) -> str: def create_aa_circle(self, x_pos: int, y_pos: int, radius: int, angle: int = 0, fill: str = "white",
tags: Union[str, tuple[str, ...]] = "", anchor: str = tkinter.CENTER) -> int:
# create a circle with a font element # create a circle with a font element
circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill, circle_1 = self.create_text(x_pos, y_pos, text=self.get_char_from_radius(radius), anchor=anchor, fill=fill,
font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle) font=("CustomTkinter_shapes_font", -radius * 2), tags=tags, angle=angle)
@ -61,5 +88,3 @@ class CTkCanvas(tkinter.Canvas):
super().itemconfigure(configure_id, *args, **kwargs_except_outline) super().itemconfigure(configure_id, *args, **kwargs_except_outline)
else: else:
super().itemconfigure(configure_id, *args, **kwargs) super().itemconfigure(configure_id, *args, **kwargs)

View File

@ -83,7 +83,7 @@ class CTkCheckBox(CTkBaseClass):
width=self.apply_widget_scaling(self.desired_width), width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)) height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
if self.hover is True: if self.hover is True:
self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Enter>", self.on_enter)

View File

@ -50,7 +50,7 @@ class CTkEntry(CTkBaseClass):
width=self.apply_widget_scaling(self.current_width), width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height)) height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(column=0, row=0, sticky="we") self.canvas.grid(column=0, row=0, sticky="we")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.entry = tkinter.Entry(master=self, self.entry = tkinter.Entry(master=self,
bd=0, bd=0,

View File

@ -45,7 +45,7 @@ class CTkFrame(CTkBaseClass):
height=self.apply_widget_scaling(self.current_height)) height=self.apply_widget_scaling(self.current_height))
self.canvas.place(x=0, y=0, relwidth=1, relheight=1) self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode)) self.canvas.configure(bg=ThemeManager.single_color(self.bg_color, self.appearance_mode))
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.bind('<Configure>', self.update_dimensions_event) self.bind('<Configure>', self.update_dimensions_event)

View File

@ -48,7 +48,7 @@ class CTkLabel(CTkBaseClass):
width=self.apply_widget_scaling(self.desired_width), width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)) height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, sticky="nswe") self.canvas.grid(row=0, column=0, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.text_label = tkinter.Label(master=self, self.text_label = tkinter.Label(master=self,
highlightthickness=0, highlightthickness=0,

View File

@ -62,7 +62,7 @@ class CTkProgressBar(CTkBaseClass):
width=self.apply_widget_scaling(self.desired_width), width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)) height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe") self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget # Each time an item is resized due to pack position mode, the binding Configure is called on the widget
self.bind('<Configure>', self.update_dimensions_event) self.bind('<Configure>', self.update_dimensions_event)

View File

@ -79,7 +79,7 @@ class CTkRadioButton(CTkBaseClass):
width=self.apply_widget_scaling(self.current_width), width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height)) height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1) self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave) self.canvas.bind("<Leave>", self.on_leave)

View File

@ -84,7 +84,7 @@ class CTkSlider(CTkBaseClass):
width=self.apply_widget_scaling(self.desired_width), width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height)) height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe") self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave) self.canvas.bind("<Leave>", self.on_leave)

View File

@ -84,7 +84,7 @@ class CTkSwitch(CTkBaseClass):
width=self.apply_widget_scaling(self.current_width), width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height)) height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe") self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
self.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method) self.draw_engine = DrawEngine(self.canvas)
self.canvas.bind("<Enter>", self.on_enter) self.canvas.bind("<Enter>", self.on_enter)
self.canvas.bind("<Leave>", self.on_leave) self.canvas.bind("<Leave>", self.on_leave)

View File

@ -69,24 +69,21 @@ class CTk(tkinter.Tk):
if self.current_width != round(detected_width / self.window_scaling) or self.current_height != round(detected_height / self.window_scaling): 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_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 self.current_height = round(detected_height / self.window_scaling) # current_width and current_height are independent of the scale
print("update_dimensions_event:", self.current_width)
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling): def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling self.window_scaling = new_window_scaling
# reset min, max and resizable constraints for applying scaling # force new dimensions on window by using min, max, and geometry
if self.last_resizable_args is not None: super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
super().resizable(True, True) super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
if self.min_width is not None or self.min_height is not None: super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}")
super().minsize(0, 0) print("set_scaling:", self.apply_window_scaling(self.current_width), self.max_width, self.min_width)
if self.max_width is not None or self.max_height is not None:
super().maxsize(1_000_000, 1_000_000)
# set new window size by applying scaling to the current window size # set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
self.geometry(f"{self.current_width}x{self.current_height}") self.after(400, self.set_scaled_min_max)
# set scaled min, max sizes and reapply resizable def set_scaled_min_max(self):
if self.last_resizable_args is not None:
super().resizable(*self.last_resizable_args[0], **self.last_resizable_args[1]) # args, kwargs
if self.min_width is not None or self.min_height is not None: if self.min_width is not None or self.min_height is not None:
super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height)) super().minsize(self.apply_window_scaling(self.min_width), self.apply_window_scaling(self.min_height))
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:
@ -106,6 +103,7 @@ class CTk(tkinter.Tk):
def mainloop(self, *args, **kwargs): def mainloop(self, *args, **kwargs):
if not self.window_exists: if not self.window_exists:
print("deiconify")
self.deiconify() self.deiconify()
self.window_exists = True self.window_exists = True
super().mainloop(*args, **kwargs) super().mainloop(*args, **kwargs)
@ -135,6 +133,7 @@ class CTk(tkinter.Tk):
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 geometry(self, geometry_string): def geometry(self, geometry_string):
print("geometry:", geometry_string)
super().geometry(self.apply_geometry_scaling(geometry_string)) super().geometry(self.apply_geometry_scaling(geometry_string))
# update width and height attributes # update width and height attributes

View File

@ -1,206 +0,0 @@
import tkinter
import tkinter.messagebox
import customtkinter
customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
class App(customtkinter.CTk):
WIDTH = 780
HEIGHT = 520
def __init__(self):
super().__init__()
self.title("CustomTkinter complex example")
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
# self.minsize(App.WIDTH, App.HEIGHT)
self.protocol("WM_DELETE_WINDOW", self.on_closing) # call .on_closing() when app gets closed
# ============ create two frames ============
# configure grid layout (2x1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.frame_left = customtkinter.CTkFrame(master=self,
width=180,
corner_radius=0)
self.frame_left.grid(row=0, column=0, sticky="nswe")
self.frame_right = customtkinter.CTkFrame(master=self)
self.frame_right.grid(row=0, column=1, sticky="nswe", padx=20, pady=20)
# ============ frame_left ============
# configure grid layout (1x11)
self.frame_left.grid_rowconfigure(0, minsize=10) # empty row with minsize as spacing
self.frame_left.grid_rowconfigure(5, weight=1) # empty row as spacing
self.frame_left.grid_rowconfigure(8, minsize=20) # empty row with minsize as spacing
self.frame_left.grid_rowconfigure(11, minsize=10) # empty row with minsize as spacing
self.label_1 = customtkinter.CTkLabel(master=self.frame_left,
text="CustomTkinter",
text_font=("Roboto Medium", -16)) # font name and size in px
self.label_1.grid(row=1, column=0, pady=10, padx=10)
self.button_1 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 1",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_1.grid(row=2, column=0, pady=10, padx=20)
self.button_2 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 2",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_2.grid(row=3, column=0, pady=10, padx=20)
self.button_3 = customtkinter.CTkButton(master=self.frame_left,
text="CTkButton 3",
fg_color=("gray75", "gray30"), # <- custom tuple-color
command=self.button_event)
self.button_3.grid(row=4, column=0, pady=10, padx=20)
self.switch_1 = customtkinter.CTkSwitch(master=self.frame_left)
self.switch_1.grid(row=9, column=0, pady=10, padx=20, sticky="w")
self.switch_2 = customtkinter.CTkSwitch(master=self.frame_left,
text="Dark Mode",
command=self.change_mode)
self.switch_2.grid(row=10, column=0, pady=10, padx=20, sticky="w")
# ============ frame_right ============
# configure grid layout (3x7)
self.frame_right.rowconfigure((0, 1, 2, 3), weight=1)
self.frame_right.rowconfigure(7, weight=10)
self.frame_right.columnconfigure((0, 1), weight=1)
self.frame_right.columnconfigure(2, weight=0)
self.frame_info = customtkinter.CTkFrame(master=self.frame_right)
self.frame_info.grid(row=0, column=0, columnspan=2, rowspan=4, pady=20, padx=20, sticky="nsew")
# ============ frame_info ============
# configure grid layout (1x1)
self.frame_info.rowconfigure(0, weight=1)
self.frame_info.columnconfigure(0, weight=1)
self.label_info_1 = customtkinter.CTkLabel(master=self.frame_info,
text="CTkLabel: Lorem ipsum dolor sit,\n" +
"amet consetetur sadipscing elitr,\n" +
"sed diam nonumy eirmod tempor" ,
height=100,
fg_color=("white", "gray38"), # <- custom tuple-color
justify=tkinter.LEFT)
self.label_info_1.grid(column=0, row=0, sticky="nwe", padx=15, pady=15)
self.progressbar = customtkinter.CTkProgressBar(master=self.frame_info)
self.progressbar.grid(row=1, column=0, sticky="ew", padx=15, pady=15)
# ============ frame_right ============
self.radio_var = tkinter.IntVar(value=0)
self.label_radio_group = customtkinter.CTkLabel(master=self.frame_right,
text="CTkRadioButton Group:")
self.label_radio_group.grid(row=0, column=2, columnspan=1, pady=20, padx=10, sticky="")
self.radio_button_1 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=0)
self.radio_button_1.grid(row=1, column=2, pady=10, padx=20, sticky="n")
self.radio_button_2 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=1)
self.radio_button_2.grid(row=2, column=2, pady=10, padx=20, sticky="n")
self.radio_button_3 = customtkinter.CTkRadioButton(master=self.frame_right,
variable=self.radio_var,
value=2)
self.radio_button_3.grid(row=3, column=2, pady=10, padx=20, sticky="n")
self.slider_1 = customtkinter.CTkSlider(master=self.frame_right,
from_=0,
to=1,
number_of_steps=3,
command=None)
self.slider_1.grid(row=4, column=0, columnspan=2, pady=10, padx=20, sticky="we")
self.slider_2 = customtkinter.CTkSlider(master=self.frame_right,
command=lambda x: customtkinter.set_user_scaling(x * 2))
self.slider_2.grid(row=5, column=0, columnspan=2, pady=10, padx=20, sticky="we")
self.slider_button_1 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
command=self.button_event)
self.slider_button_1.grid(row=4, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.slider_button_2 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
command=self.button_event)
self.slider_button_2.grid(row=5, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.checkbox_button_1 = customtkinter.CTkButton(master=self.frame_right,
height=25,
text="CTkButton",
border_width=3, # <- custom border_width
fg_color=None, # <- no fg_color
command=self.button_event)
self.checkbox_button_1.grid(row=6, column=2, columnspan=1, pady=10, padx=20, sticky="we")
self.check_box_1 = customtkinter.CTkCheckBox(master=self.frame_right,
text="CTkCheckBox")
self.check_box_1.grid(row=6, column=0, pady=10, padx=20, sticky="w")
self.check_box_2 = customtkinter.CTkCheckBox(master=self.frame_right,
text="CTkCheckBox")
self.check_box_2.grid(row=6, column=1, pady=10, padx=20, sticky="w")
self.entry = customtkinter.CTkEntry(master=self.frame_right,
width=120,
placeholder_text="CTkEntry")
self.entry.grid(row=8, column=0, columnspan=2, pady=20, padx=20, sticky="we")
self.button_5 = customtkinter.CTkButton(master=self.frame_right,
text="CTkButton",
command=self.button_event)
self.button_5.grid(row=8, column=2, columnspan=1, pady=20, padx=20, sticky="we")
# set default values
self.radio_button_1.select()
self.switch_2.select()
self.slider_1.set(0.2)
self.slider_2.set(0.7)
self.progressbar.set(0.5)
self.slider_button_1.configure(state=tkinter.DISABLED, text="Disabled Button")
self.radio_button_3.configure(state=tkinter.DISABLED)
self.check_box_1.configure(state=tkinter.DISABLED, text="CheckBox disabled")
self.check_box_2.select()
def button_event(self):
print("Button pressed")
def change_mode(self):
if self.switch_2.get() == 1:
customtkinter.set_appearance_mode("dark")
else:
customtkinter.set_appearance_mode("light")
def on_closing(self, event=0):
self.destroy()
def start(self):
self.mainloop()
if __name__ == "__main__":
app = App()
app.start()

View File

@ -1,76 +0,0 @@
import tkinter
import tkinter.messagebox
import customtkinter
from PIL import Image, ImageTk
import os
customtkinter.ScalingTracker.set_window_scaling(1.5)
customtkinter.ScalingTracker.set_widget_scaling(1.5)
customtkinter.ScalingTracker.set_spacing_scaling(1.5)
customtkinter.set_appearance_mode("Dark") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
PATH = os.path.dirname(os.path.realpath(__file__))
class App(customtkinter.CTk):
APP_NAME = "CustomTkinter background gradient image"
WIDTH = 900
HEIGHT = 600
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title(App.APP_NAME)
self.geometry(f"{App.WIDTH}x{App.HEIGHT}")
self.minsize(App.WIDTH, App.HEIGHT)
self.maxsize(App.WIDTH, App.HEIGHT)
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.bind("<Command-q>", self.on_closing)
self.bind("<Command-w>", self.on_closing)
self.createcommand('tk::mac::Quit', self.on_closing)
# load image with PIL and convert to PhotoImage
image = Image.open(PATH + "/../test_images/bg_gradient.jpg").resize((self.WIDTH, self.HEIGHT))
self.bg_image = ImageTk.PhotoImage(image)
self.image_label = tkinter.Label(master=self, image=self.bg_image)
self.image_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
self.frame = customtkinter.CTkFrame(master=self,
width=300,
height=App.HEIGHT,
corner_radius=0)
self.frame.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
self.label_1 = customtkinter.CTkLabel(master=self.frame, width=200, height=60,
fg_color=("gray70", "gray35"), text="CustomTkinter\ninterface example")
self.label_1.place(relx=0.5, rely=0.3, anchor=tkinter.CENTER)
self.entry_1 = customtkinter.CTkEntry(master=self.frame, corner_radius=20, width=200, placeholder_text="username")
self.entry_1.place(relx=0.5, rely=0.52, anchor=tkinter.CENTER)
self.entry_2 = customtkinter.CTkEntry(master=self.frame, corner_radius=20, width=200, show="*", placeholder_text="password")
self.entry_2.place(relx=0.5, rely=0.6, anchor=tkinter.CENTER)
self.button_2 = customtkinter.CTkButton(master=self.frame, text="Login",
corner_radius=6, command=self.button_event, width=200)
self.button_2.place(relx=0.5, rely=0.7, anchor=tkinter.CENTER)
def button_event(self):
print("Login pressed - username:", self.entry_1.get(), "password:", self.entry_2.get())
def on_closing(self, event=0):
self.destroy()
def start(self):
self.mainloop()
if __name__ == "__main__":
app = App()
app.start()

View File

@ -1,21 +1,22 @@
import tkinter import tkinter
import customtkinter # <- import the CustomTkinter module import customtkinter # <- import the CustomTkinter module
customtkinter.ScalingTracker.set_window_scaling(1.5) customtkinter.ScalingTracker.set_window_scaling(0.5)
customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light" customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue" customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
root_tk = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window) root_tk = customtkinter.CTk() # create CTk window like you do with the Tk window (you can also use normal tkinter.Tk window)
root_tk.geometry("400x480") root_tk.geometry("400x480")
root_tk.title("CustomTkinter Test") root_tk.title("CustomTkinter manual scaling test")
root_tk.minsize(480, 480) #root_tk.minsize(200, 200)
root_tk.maxsize(520, 520) #root_tk.maxsize(520, 520)
root_tk.resizable(True, False)
print(customtkinter.ScalingTracker.get_window_scaling(root_tk))
def button_function(): def button_function():
root_tk.geometry(f"{200}x{200}")
print("Button click", label_1.text_label.cget("text")) print("Button click", label_1.text_label.cget("text"))
@ -31,7 +32,7 @@ def check_box_function():
y_padding = 13 y_padding = 13
frame_1 = customtkinter.CTkFrame(master=root_tk, corner_radius=15) frame_1 = customtkinter.CTkFrame(master=root_tk)
frame_1.pack(pady=20, padx=60, fill="both", expand=True) frame_1.pack(pady=20, padx=60, fill="both", expand=True)
label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT) label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT)