completed scaling

This commit is contained in:
Tom Schimansky 2022-05-01 23:29:14 +02:00
parent c3c7d1a5de
commit cb12711b5c
22 changed files with 485 additions and 217 deletions

View File

@ -1,18 +1,19 @@
__version__ = "3.12"
from .widgets.ctk_input_dialog import CTkInputDialog
from .widgets.ctk_button import CTkButton
from .widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_entry import CTkEntry
from .widgets.ctk_slider import CTkSlider
from .widgets.ctk_frame import CTkFrame
from .widgets.ctk_progressbar import CTkProgressBar
from .widgets.ctk_label import CTkLabel
from .widgets.ctk_entry import CTkEntry
from .widgets.ctk_checkbox import CTkCheckBox
from .widgets.ctk_radiobutton import CTkRadioButton
from .widgets.ctk_tk import CTk
from .widgets.ctk_canvas import CTkCanvas
from .widgets.ctk_switch import CTkSwitch
from .widgets.ctk_toplevel import CTkToplevel
from .windows.ctk_tk import CTk
from .windows.ctk_toplevel import CTkToplevel
from .windows.ctk_input_dialog import CTkInputDialog
from .ctk_settings import CTkSettings
from .appearance_mode_tracker import AppearanceModeTracker

View File

@ -1,12 +1,16 @@
import sys
import tkinter
from distutils.version import StrictVersion as Version
import darkdetect
if Version(darkdetect.__version__) < Version("0.3.1"):
sys.stderr.write("WARNING: You have to update the darkdetect library: pip3 install --upgrade darkdetect\n")
if sys.platform != "darwin":
exit()
try:
import darkdetect
if Version(darkdetect.__version__) < Version("0.3.1"):
sys.stderr.write("WARNING: You have to update the darkdetect library: pip3 install --upgrade darkdetect\n")
if sys.platform != "darwin":
exit()
except:
pass
class AppearanceModeTracker:

View File

@ -56,19 +56,13 @@ class CTkDrawEngine:
returns bool if recoloring is necessary """
print("before", width, height)
width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only
width = math.floor(width / 2) * 2 # round (floor) current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
corner_radius = round(corner_radius)
print("after", width, height)
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
corner_radius = min(width / 2, height / 2)
print("corner", corner_radius)
border_width = round(border_width)
corner_radius = self._calc_optimal_corner_radius(corner_radius) # optimize corner_radius for different drawing methods (different rounding)
@ -357,7 +351,7 @@ class CTkDrawEngine:
returns bool if recoloring is necessary """
width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only
width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger
@ -521,7 +515,7 @@ class CTkDrawEngine:
button_length: Union[float, int], button_corner_radius: Union[float, int], slider_value: float,
orientation: str) -> bool:
width = math.floor(width / 2) * 2 # round width and height and restrict them to even values only
width = math.floor(width / 2) * 2 # round current_width and current_height and restrict them to even values only
height = math.floor(height / 2) * 2
if corner_radius > width / 2 or corner_radius > height / 2: # restrict corner_radius if it's too larger

View File

@ -4,10 +4,13 @@ import sys
class CTkSettings:
circle_font_is_ready = False
hand_cursor_enabled = True
preferred_drawing_method = None
radius_to_char_fine = None
cursor_manipulation_enabled = True
deactivate_macos_window_header_manipulation = False
deactivate_windows_window_header_manipulation = False
@classmethod
def init_font_character_mapping(cls):
radius_to_char_warped = {19: 'B', 18: 'B', 17: 'B', 16: 'B', 15: 'B', 14: 'B', 13: 'B', 12: 'B', 11: 'B', 10: 'B',
@ -41,7 +44,7 @@ class CTkSettings:
def print_settings(cls):
print(f"CTkSettings current values:")
print(f"circle_font_is_ready = {cls.circle_font_is_ready}")
print(f"hand_cursor_enabled = {cls.hand_cursor_enabled}")
print(f"hand_cursor_enabled = {cls.cursor_manipulation_enabled}")
print(f"preferred_drawing_method = {cls.preferred_drawing_method}")
print(f"radius_to_char_fine = {cls.radius_to_char_fine}")

View File

@ -6,23 +6,42 @@ class ScalingTracker:
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 DPI values
user_scaling = 1 # scale change of all widgets and windows
window_dpi_scaling_dict = {} # contains window objects as keys and corresponding scaling factors
widget_scaling = 1 # user values which multiply to detected window scaling factor
window_scaling = 1
spacing_scaling = 1
update_loop_running = False
@classmethod
def get_widget_scaling(cls, widget):
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.user_scaling
return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling
@classmethod
def get_spacing_scaling(cls, widget):
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.spacing_scaling
@classmethod
def get_window_scaling(cls, window):
return cls.window_dpi_scaling_dict[window] * cls.user_scaling
window_root = cls.get_window_root_of_widget(window)
return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling
@classmethod
def set_user_scaling(cls, user_scaling_factor):
cls.user_scaling = user_scaling_factor
def set_widget_scaling(cls, widget_scaling_factor):
cls.widget_scaling = widget_scaling_factor
cls.update_scaling_callbacks()
@classmethod
def set_spacing_scaling(cls, spacing_scaling_factor):
cls.spacing_scaling = spacing_scaling_factor
cls.update_scaling_callbacks()
@classmethod
def set_window_scaling(cls, window_scaling_factor):
cls.window_scaling = window_scaling_factor
cls.update_scaling_callbacks()
@classmethod
@ -39,7 +58,9 @@ class ScalingTracker:
def update_scaling_callbacks(cls):
for window, callback_list in cls.window_widgets_dict.items():
for callback in callback_list:
callback(cls.window_dpi_scaling_dict[window])
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)
@classmethod
def add_widget(cls, widget_callback, widget):
@ -51,7 +72,7 @@ class ScalingTracker:
cls.window_widgets_dict[window_root].append(widget_callback)
if window_root not in cls.window_dpi_scaling_dict:
cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_value(window_root)
cls.window_dpi_scaling_dict[window_root] = cls.get_window_dpi_scaling(window_root)
if not cls.update_loop_running:
window_root.after(100, cls.check_dpi_scaling)
@ -65,10 +86,10 @@ class ScalingTracker:
cls.window_widgets_dict[window].append(window_callback)
if window not in cls.window_dpi_scaling_dict:
cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_value(window)
cls.window_dpi_scaling_dict[window] = cls.get_window_dpi_scaling(window)
@classmethod
def get_window_dpi_value(cls, window):
def get_window_dpi_scaling(cls, window):
if sys.platform == "darwin":
return 1 # scaling works automatically on macOS

View File

@ -1,5 +1,6 @@
import tkinter
import sys
import math
from .ctk_canvas import CTkCanvas
from ..theme_manager import CTkThemeManager
@ -65,8 +66,8 @@ class CTkButton(CTkBaseClass):
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="nsew")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -87,11 +88,24 @@ class CTkButton(CTkBaseClass):
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
def set_scaling(self, *args, **kwargs):
super().set_scaling( *args, **kwargs)
if self.text_label is not None:
self.text_label.destroy()
self.text_label = None
if self.image_label is not None:
self.image_label.destroy()
self.image_label = None
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling)
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),
self.apply_widget_scaling(self.border_width))
if no_color_updates is False or requires_recoloring:
@ -175,35 +189,35 @@ class CTkButton(CTkBaseClass):
# create grid layout with just an image given
if self.image_label is not None and self.text_label is None:
self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", pady=self.border_width * self.scaling)
self.image_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="", pady=self.apply_widget_scaling(self.border_width))
# create grid layout with just text given
if self.image_label is None and self.text_label is not None:
self.text_label.grid(row=0, column=0, rowspan=2, columnspan=2, sticky="",
padx=self.corner_radius * self.scaling, pady=self.border_width * self.scaling)
padx=self.apply_widget_scaling(self.corner_radius), pady=self.apply_widget_scaling(self.border_width) + 1)
# create grid layout of image and text label in 2x2 grid system with given compound
if self.image_label is not None and self.text_label is not None:
if self.compound == tkinter.LEFT or self.compound == "left":
self.image_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.corner_radius * self.scaling, self.border_width * self.scaling), 2), pady=self.border_width * self.scaling)
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), pady=self.apply_widget_scaling(self.border_width))
self.text_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.corner_radius * self.scaling, self.border_width * self.scaling)), pady=self.border_width * self.scaling)
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), pady=self.apply_widget_scaling(self.border_width))
elif self.compound == tkinter.TOP or self.compound == "top":
self.image_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(self.border_width * self.scaling, 2))
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(self.apply_widget_scaling(self.border_width), 2))
self.text_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(2, self.border_width * self.scaling))
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(2, self.apply_widget_scaling(self.border_width)))
elif self.compound == tkinter.RIGHT or self.compound == "right":
self.image_label.grid(row=0, column=1, sticky="w", rowspan=2, columnspan=1,
padx=(2, max(self.corner_radius * self.scaling, self.border_width * self.scaling)), pady=self.border_width * self.scaling)
padx=(2, max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width))), pady=self.apply_widget_scaling(self.border_width))
self.text_label.grid(row=0, column=0, sticky="e", rowspan=2, columnspan=1,
padx=(max(self.corner_radius * self.scaling, self.border_width * self.scaling), 2), pady=self.border_width * self.scaling)
padx=(max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), 2), pady=self.apply_widget_scaling(self.border_width))
elif self.compound == tkinter.BOTTOM or self.compound == "bottom":
self.image_label.grid(row=1, column=0, sticky="n", columnspan=2, rowspan=1,
padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(2, self.border_width * self.scaling))
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(2, self.apply_widget_scaling(self.border_width)))
self.text_label.grid(row=0, column=0, sticky="s", columnspan=2, rowspan=1,
padx=max(self.corner_radius * self.scaling, self.border_width * self.scaling), pady=(self.border_width * self.scaling, 2))
padx=max(self.apply_widget_scaling(self.corner_radius), self.apply_widget_scaling(self.border_width)), pady=(self.apply_widget_scaling(self.border_width), 2))
def configure(self, *args, **kwargs):
require_redraw = False # some attribute changes require a call of self.draw() at the end
@ -271,17 +285,18 @@ class CTkButton(CTkBaseClass):
self.draw()
def set_cursor(self):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and self.function is not None and CTkSettings.hand_cursor_enabled:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self.function is not None and CTkSettings.hand_cursor_enabled:
self.configure(cursor="arrow")
if CTkSettings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and self.function is not None and CTkSettings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif sys.platform.startswith("win") and self.function is not None and CTkSettings.cursor_manipulation_enabled:
self.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and self.function is not None and CTkSettings.hand_cursor_enabled:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self.function is not None and CTkSettings.hand_cursor_enabled:
self.configure(cursor="hand2")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and self.function is not None and CTkSettings.cursor_manipulation_enabled:
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and self.function is not None and CTkSettings.cursor_manipulation_enabled:
self.configure(cursor="hand2")
def set_text(self, text):
self.text = text

View File

@ -68,20 +68,20 @@ class CTkCheckBox(CTkBaseClass):
# configure grid system (1x3)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.grid_rowconfigure(0, weight=1)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, rowspan=1)
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -103,6 +103,16 @@ class CTkCheckBox(CTkBaseClass):
self.set_cursor()
self.draw() # initial draw
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
@ -110,15 +120,15 @@ class CTkCheckBox(CTkBaseClass):
super().destroy()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling)
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),
self.apply_widget_scaling(self.border_width))
if self.check_state is True:
self.draw_engine.draw_checkmark(self.width * self.scaling,
self.height * self.scaling,
self.height * 0.58 * self.scaling)
self.draw_engine.draw_checkmark(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.current_height * 0.58))
else:
self.canvas.delete("checkmark")
@ -146,7 +156,6 @@ class CTkCheckBox(CTkBaseClass):
fill=CTkThemeManager.single_color(self.border_color, self.appearance_mode))
if self.text_label is None:
print("create label")
self.text_label = tkinter.Label(master=self,
bd=0,
text=self.text,
@ -231,17 +240,18 @@ class CTkCheckBox(CTkBaseClass):
self.draw()
def set_cursor(self):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
if CTkSettings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="hand2")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def set_text(self, text):
self.text = text

View File

@ -46,8 +46,8 @@ class CTkEntry(CTkBaseClass):
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(column=0, row=0, sticky="we")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -58,7 +58,7 @@ class CTkEntry(CTkBaseClass):
font=self.apply_font_scaling(self.text_font),
**kwargs)
self.entry.grid(column=0, row=0, sticky="we",
padx=self.corner_radius * self.scaling if self.corner_radius >= 6 * self.scaling else 6 * self.scaling)
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
super().bind('<Configure>', self.update_dimensions_event)
self.entry.bind('<FocusOut>', self.set_placeholder)
@ -67,6 +67,16 @@ class CTkEntry(CTkBaseClass):
self.draw()
self.set_placeholder()
def set_scaling(self, *args, **kwargs):
super().set_scaling( *args, **kwargs)
self.entry.configure(font=self.apply_font_scaling(self.text_font))
self.entry.grid(column=0, row=0, sticky="we",
padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def configure_basic_grid(self):
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
@ -91,10 +101,10 @@ class CTkEntry(CTkBaseClass):
def draw(self, no_color_updates=False):
self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling)
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),
self.apply_widget_scaling(self.border_width))
if CTkThemeManager.single_color(self.fg_color, self.appearance_mode) is not None:
self.canvas.itemconfig("inner_parts",
@ -147,12 +157,12 @@ class CTkEntry(CTkBaseClass):
if "corner_radius" in kwargs:
self.corner_radius = kwargs["corner_radius"]
if self.corner_radius * 2 > self.height:
self.corner_radius = self.height / 2
elif self.corner_radius * 2 > self.width:
self.corner_radius = self.width / 2
if self.corner_radius * 2 > self.current_height:
self.corner_radius = self.current_height / 2
elif self.corner_radius * 2 > self.current_width:
self.corner_radius = self.current_width / 2
self.entry.grid(column=0, row=0, sticky="we", padx=self.corner_radius if self.corner_radius >= 6 else 6)
self.entry.grid(column=0, row=0, sticky="we", padx=self.apply_widget_scaling(self.corner_radius) if self.corner_radius >= 6 else self.apply_widget_scaling(6))
del kwargs["corner_radius"]
require_redraw = True

View File

@ -41,8 +41,8 @@ class CTkFrame(CTkBaseClass):
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.place(x=0, y=0, relwidth=1, relheight=1)
self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -62,12 +62,18 @@ class CTkFrame(CTkBaseClass):
except ValueError:
return child_widgets
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling)
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),
self.apply_widget_scaling(self.border_width))
if no_color_updates is False or requires_recoloring:
self.canvas.itemconfig("inner_parts",

View File

@ -45,8 +45,8 @@ class CTkLabel(CTkBaseClass):
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, sticky="nswe")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -56,15 +56,22 @@ class CTkLabel(CTkBaseClass):
text=self.text,
font=self.apply_font_scaling(self.text_font),
**kwargs)
self.text_label.grid(row=0, column=0, padx=self.corner_radius)
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius))
self.bind('<Configure>', self.update_dimensions_event)
self.draw()
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.text_label.grid(row=0, column=0, padx=self.apply_widget_scaling(self.corner_radius))
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
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),
0)
self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))

View File

@ -40,14 +40,14 @@ class CTkProgressBar(CTkBaseClass):
self.border_width = CTkThemeManager.theme["shape"]["progressbar_border_width"] if border_width == "default_theme" else border_width
self.value = 0.5
self.grid_rowconfigure(1, weight=1)
self.grid_columnconfigure(1, weight=1)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1)
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(row=0, column=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
# Each time an item is resized due to pack position mode, the binding Configure is called on the widget
@ -61,6 +61,12 @@ class CTkProgressBar(CTkBaseClass):
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
@ -68,12 +74,10 @@ class CTkProgressBar(CTkBaseClass):
super().destroy()
def draw(self, no_color_updates=False):
print("progress", self.scaling)
requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling,
requires_recoloring = self.draw_engine.draw_rounded_progress_bar_with_border(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_width),
self.value, "w")
if no_color_updates is False or requires_recoloring:

View File

@ -65,19 +65,19 @@ class CTkRadioButton(CTkBaseClass):
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=0)
self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=1)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1)
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -96,6 +96,16 @@ class CTkRadioButton(CTkBaseClass):
else:
self.deselect(from_variable_callback=True)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
if self.variable is not None:
self.variable.trace_remove("write", self.variable_callback_name)
@ -103,10 +113,10 @@ class CTkRadioButton(CTkBaseClass):
super().destroy()
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_rect_with_border(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling)
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),
self.apply_widget_scaling(self.border_width))
self.bg_canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
self.canvas.configure(bg=CTkThemeManager.single_color(self.bg_color, self.appearance_mode))
@ -215,17 +225,18 @@ class CTkRadioButton(CTkBaseClass):
self.draw()
def set_cursor(self):
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="arrow")
if CTkSettings.cursor_manipulation_enabled:
if self.state == tkinter.DISABLED:
if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="arrow")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="hand2")
elif self.state == tkinter.NORMAL:
if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def set_text(self, text):
self.text = text

View File

@ -62,11 +62,14 @@ class CTkSlider(CTkBaseClass):
self.variable_callback_blocked = False
self.variable_callback_name = None
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
self.canvas.grid(column=0, row=0, sticky="nswe")
width=self.apply_widget_scaling(self.desired_width),
height=self.apply_widget_scaling(self.desired_height))
self.canvas.grid(column=0, row=0, rowspan=1, columnspan=1, sticky="nswe")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
self.canvas.bind("<Enter>", self.on_enter)
@ -86,6 +89,12 @@ class CTkSlider(CTkBaseClass):
self.set(self.variable.get(), from_variable_callback=True)
self.variable_callback_blocked = False
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self.variable is not None:
@ -93,23 +102,20 @@ class CTkSlider(CTkBaseClass):
super().destroy()
def configure_basic_grid(self):
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
def set_cursor(self):
if sys.platform == "darwin":
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self.configure(cursor="hand2")
if CTkSettings.cursor_manipulation_enabled:
if sys.platform == "darwin":
self.configure(cursor="pointinghand")
elif sys.platform.startswith("win"):
self.configure(cursor="hand2")
def draw(self, no_color_updates=False):
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling,
self.button_length * self.scaling,
self.button_corner_radius * self.scaling,
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_width),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.button_corner_radius),
self.value, "w")
if no_color_updates is False or requires_recoloring:
@ -136,7 +142,7 @@ class CTkSlider(CTkBaseClass):
outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode))
def clicked(self, event=None):
self.value = (event.x / self.width) / self.scaling
self.value = (event.x / self.current_width) / self.widget_scaling
if self.value > 1:
self.value = 1
@ -159,12 +165,12 @@ class CTkSlider(CTkBaseClass):
def on_enter(self, event=0):
self.hover_state = True
self.canvas.itemconfig("slider_parts", fill=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode))
outline=CTkThemeManager.single_color(self.button_hover_color, self.appearance_mode))
def on_leave(self, event=0):
self.hover_state = False
self.canvas.itemconfig("slider_parts", fill=CTkThemeManager.single_color(self.button_color, self.appearance_mode),
outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode))
outline=CTkThemeManager.single_color(self.button_color, self.appearance_mode))
def round_to_step_size(self, value):
if self.number_of_steps is not None:

View File

@ -70,19 +70,19 @@ class CTkSwitch(CTkBaseClass):
# configure grid system (3x1)
self.grid_columnconfigure(0, weight=1)
self.grid_columnconfigure(1, weight=0, minsize=6 * self.scaling)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.grid_columnconfigure(2, weight=0)
self.bg_canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.bg_canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=3, rowspan=1, sticky="nswe")
self.canvas = CTkCanvas(master=self,
highlightthickness=0,
width=self.width * self.scaling,
height=self.height * self.scaling)
width=self.apply_widget_scaling(self.current_width),
height=self.apply_widget_scaling(self.current_height))
self.canvas.grid(row=0, column=0, padx=0, pady=0, columnspan=1, sticky="nswe")
self.draw_engine = CTkDrawEngine(self.canvas, CTkSettings.preferred_drawing_method)
@ -100,6 +100,16 @@ class CTkSwitch(CTkBaseClass):
elif self.variable.get() == self.offvalue:
self.deselect(from_variable_callback=True)
def set_scaling(self, *args, **kwargs):
super().set_scaling(*args, **kwargs)
self.grid_columnconfigure(1, weight=0, minsize=self.apply_widget_scaling(6))
self.text_label.configure(font=self.apply_font_scaling(self.text_font))
self.bg_canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.canvas.configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
self.draw()
def destroy(self):
# remove variable_callback from variable callbacks if variable exists
if self.variable is not None:
@ -108,28 +118,29 @@ class CTkSwitch(CTkBaseClass):
super().destroy()
def set_cursor(self):
if sys.platform == "darwin" and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.hand_cursor_enabled:
self.canvas.configure(cursor="hand2")
if CTkSettings.cursor_manipulation_enabled:
if sys.platform == "darwin" and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="pointinghand")
elif sys.platform.startswith("win") and CTkSettings.cursor_manipulation_enabled:
self.canvas.configure(cursor="hand2")
def draw(self, no_color_updates=False):
if self.check_state is True:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling,
self.button_length * self.scaling,
self.corner_radius * self.scaling
, 1, "w")
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_width),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.corner_radius),
1, "w")
else:
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.width * self.scaling,
self.height * self.scaling,
self.corner_radius * self.scaling,
self.border_width * self.scaling,
self.button_length * self.scaling,
self.corner_radius * self.scaling,
requires_recoloring = self.draw_engine.draw_rounded_slider_with_border_and_button(self.apply_widget_scaling(self.current_width),
self.apply_widget_scaling(self.current_height),
self.apply_widget_scaling(self.corner_radius),
self.apply_widget_scaling(self.border_width),
self.apply_widget_scaling(self.button_length),
self.apply_widget_scaling(self.corner_radius),
0, "w")
if no_color_updates is False or requires_recoloring:

View File

@ -2,10 +2,10 @@ import tkinter
import tkinter.ttk as ttk
import copy
import re
import math
from typing import Callable, Union, TypedDict
from .ctk_tk import CTk
from .ctk_toplevel import CTkToplevel
from ..windows.ctk_tk import CTk
from ..windows.ctk_toplevel import CTkToplevel
from ..appearance_mode_tracker import AppearanceModeTracker
from ..scaling_tracker import ScalingTracker
from ..theme_manager import CTkThemeManager
@ -16,12 +16,23 @@ class CTkBaseClass(tkinter.Frame):
super().__init__(*args, width=width, height=height, **kwargs) # set desired size of underlying tkinter.Frame
self.bg_color = self.detect_color_of_master() if bg_color is None else bg_color
self.width = width # width and height in pixel, represent current size of the widget (not the desired size by init)
self.height = height # width and height are independent of the scale
self.current_width = width # current_width and current_height in pixel, represent current size of the widget (not the desired size by init)
self.current_height = height # current_width and current_height are independent of the scale
self.desired_width = width
self.desired_height = height
# add set_scaling method to callback list of ScalingTracker for automatic scaling changes
ScalingTracker.add_widget(self.set_scaling, self)
self.scaling = ScalingTracker.get_widget_scaling(self)
self.widget_scaling = ScalingTracker.get_widget_scaling(self)
self.spacing_scaling = ScalingTracker.get_spacing_scaling(self)
# save latest geometry function and kwargs
class GeometryCallDict(TypedDict):
function: Callable
kwargs: dict
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)
@ -54,6 +65,39 @@ class CTkBaseClass(tkinter.Frame):
AppearanceModeTracker.remove(self.set_appearance_mode)
super().destroy()
def place(self, **kwargs):
self.last_geometry_manager_call = {"function": super().place, "kwargs": kwargs}
super().place(**self.apply_argument_scaling(kwargs))
def pack(self, **kwargs):
self.last_geometry_manager_call = {"function": super().pack, "kwargs": kwargs}
super().pack(**self.apply_argument_scaling(kwargs))
def grid(self, **kwargs):
self.last_geometry_manager_call = {"function": super().grid, "kwargs": kwargs}
super().grid(**self.apply_argument_scaling(kwargs))
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_spacing_scaling(scaled_kwargs["pady"])
elif isinstance(scaled_kwargs["pady"], tuple):
scaled_kwargs["pady"] = tuple([self.apply_spacing_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_spacing_scaling(scaled_kwargs["padx"])
elif isinstance(scaled_kwargs["padx"], tuple):
scaled_kwargs["padx"] = tuple([self.apply_spacing_scaling(v) for v in scaled_kwargs["padx"]])
if "x" in scaled_kwargs:
scaled_kwargs["x"] = self.apply_spacing_scaling(scaled_kwargs["x"])
if "y" in scaled_kwargs:
scaled_kwargs["y"] = self.apply_spacing_scaling(scaled_kwargs["y"])
return scaled_kwargs
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
@ -77,9 +121,9 @@ class CTkBaseClass(tkinter.Frame):
def update_dimensions_event(self, event):
# only redraw if dimensions changed (for performance)
if self.width != math.floor(event.width * self.scaling) or self.height != math.floor(event.height * self.scaling):
self.width = event.width / self.scaling # adjust current size according to new size given by event
self.height = event.height / self.scaling # width and height are independent of the scale
if self.current_width != round(event.width / self.widget_scaling) or self.current_height != round(event.height / self.widget_scaling):
self.current_width = round(event.width / self.widget_scaling) # adjust current size according to new size given by event
self.current_height = round(event.height / self.widget_scaling) # current_width and current_height are independent of the scale
self.draw(no_color_updates=True) # faster drawing without color changes
@ -115,28 +159,44 @@ class CTkBaseClass(tkinter.Frame):
self.draw()
def set_scaling(self, new_scaling):
self.scaling = new_scaling
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.widget_scaling = new_widget_scaling
self.spacing_scaling = new_spacing_scaling
super().configure(width=self.width * self.scaling, height=self.height * self.scaling)
super().configure(width=self.apply_widget_scaling(self.desired_width), height=self.apply_widget_scaling(self.desired_height))
if self.last_geometry_manager_call is not None:
self.last_geometry_manager_call["function"](**self.apply_argument_scaling(self.last_geometry_manager_call["kwargs"]))
def apply_widget_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.widget_scaling
else:
return value
def apply_spacing_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.spacing_scaling
else:
return value
def apply_font_scaling(self, font):
if type(font) == tuple or type(font) == list:
font_list = list(font)
for i in range(len(font_list)):
if (type(font_list[i]) == int or type(font_list[i]) == float) and font_list[i] < 0:
font_list[i] = int(font_list[i] * self.scaling)
font_list[i] = int(font_list[i] * self.widget_scaling)
return tuple(font_list)
elif type(font) == str:
for negative_number in re.findall(r" -\d* ", font):
font = font.replace(negative_number, f" {int(int(negative_number) * self.scaling)} ")
font = font.replace(negative_number, f" {int(int(negative_number) * self.widget_scaling)} ")
return font
elif isinstance(font, tkinter.font.Font):
new_font_object = copy.copy(font)
if font.cget("size") < 0:
new_font_object.config(size=int(font.cget("size") * self.scaling))
new_font_object.config(size=int(font.cget("size") * self.widget_scaling))
return new_font_object
else:

View File

View File

@ -1,11 +1,11 @@
import tkinter
import time
from .ctk_label import CTkLabel
from .ctk_entry import CTkEntry
from .ctk_frame import CTkFrame
from .ctk_toplevel import CTkToplevel
from .ctk_button import CTkButton
from ..widgets.ctk_label import CTkLabel
from ..widgets.ctk_entry import CTkEntry
from ..widgets.ctk_frame import CTkFrame
from ..windows.ctk_toplevel import CTkToplevel
from ..widgets.ctk_button import CTkButton
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import CTkThemeManager
@ -33,7 +33,7 @@ class CTkInputDialog:
self.border_color = CTkThemeManager.theme["color"]["button_hover"] if border_color == "default_theme" else border_color
self.top = CTkToplevel()
self.top.geometry(f"280x{self.height}")
self.top.geometry(f"{280}x{self.height}")
self.top.resizable(False, False)
self.top.title(title)
self.top.lift()

View File

@ -4,9 +4,12 @@ import sys
import os
import platform
import ctypes
import re
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import CTkThemeManager
from ..scaling_tracker import ScalingTracker
from ..ctk_settings import CTkSettings
class CTk(tkinter.Tk):
@ -15,9 +18,20 @@ class CTk(tkinter.Tk):
**kwargs):
self.enable_macos_dark_title_bar()
super().__init__(*args, **kwargs)
# 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 = 600 # initial window size, always without scaling
self.current_height = 500
self.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
if "bg" in kwargs:
@ -27,10 +41,9 @@ class CTk(tkinter.Tk):
self.fg_color = kwargs["background"]
del kwargs["background"]
# add set_appearance_mode method to callback list of AppearanceModeTracker for appearance mode changes
AppearanceModeTracker.add(self.set_appearance_mode, self)
super().configure(bg=CTkThemeManager.single_color(self.fg_color, self.appearance_mode))
super().title("CTk")
self.geometry(f"{self.current_width}x{self.current_height}")
self.window_exists = False # indicates if the window is already shown through .update or .mainloop
@ -40,6 +53,22 @@ class CTk(tkinter.Tk):
else:
self.windows_set_titlebar_color("light")
self.bind('<Configure>', self.update_dimensions_event)
def update_dimensions_event(self, event=None):
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
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
# set new window size by applying scaling to the current window size
self.geometry(f"{self.current_width}x{self.current_height}")
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
self.disable_macos_dark_title_bar()
@ -52,7 +81,7 @@ class CTk(tkinter.Tk):
super().update()
def mainloop(self, *args, **kwargs):
if self.window_exists is False:
if not self.window_exists:
self.deiconify()
self.window_exists = True
super().mainloop(*args, **kwargs)
@ -66,6 +95,33 @@ class CTk(tkinter.Tk):
else:
self.windows_set_titlebar_color("light")
def geometry(self, geometry_string):
super().geometry(self.apply_geometry_scaling(geometry_string))
# update width and height attributes
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
self.current_width, self.current_height = numbers[0], numbers[1]
def apply_geometry_scaling(self, geometry_string):
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
if len(numbers) == 2:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}"
elif len(numbers) == 4:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}+" +\
f"{self.apply_window_scaling(numbers[2]):.0f}+" +\
f"{self.apply_window_scaling(numbers[3]):.0f}"
else:
return geometry_string
def apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.window_scaling
else:
return value
def config(self, *args, **kwargs):
self.configure(*args, **kwargs)
@ -97,30 +153,25 @@ class CTk(tkinter.Tk):
args[0]["background"] = CTkThemeManager.single_color(self.fg_color, self.appearance_mode)
if bg_changed:
from .ctk_slider import CTkSlider
from .ctk_progressbar import CTkProgressBar
from .ctk_label import CTkLabel
from .ctk_frame import CTkFrame
from .ctk_entry import CTkEntry
from customtkinter.widgets.ctk_checkbox import CTkCheckBox
from customtkinter.widgets.ctk_button import CTkButton
from ..widgets.widget_base_class import CTkBaseClass
for child in self.winfo_children():
if isinstance(child, (CTkFrame, CTkButton, CTkLabel, CTkSlider, CTkCheckBox, CTkEntry, CTkProgressBar)):
if isinstance(child, CTkBaseClass):
child.configure(bg_color=self.fg_color)
super().configure(*args, **kwargs)
@staticmethod
def enable_macos_dark_title_bar():
if sys.platform == "darwin": # macOS
if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
# This command allows dark-mode for all programs
@staticmethod
def disable_macos_dark_title_bar():
if sys.platform == "darwin": # macOS
if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
@ -137,7 +188,7 @@ class CTk(tkinter.Tk):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
if sys.platform.startswith("win"):
if sys.platform.startswith("win") and not CTkSettings.deactivate_windows_window_header_manipulation:
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
if not self.window_exists:

View File

@ -4,9 +4,12 @@ import sys
import os
import platform
import ctypes
import re
from ..appearance_mode_tracker import AppearanceModeTracker
from ..theme_manager import CTkThemeManager
from ..ctk_settings import CTkSettings
from ..scaling_tracker import ScalingTracker
class CTkToplevel(tkinter.Toplevel):
@ -18,6 +21,13 @@ class CTkToplevel(tkinter.Toplevel):
super().__init__(*args, **kwargs)
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.fg_color = CTkThemeManager.theme["color"]["window_bg_color"] if fg_color == "default_theme" else fg_color
if "bg" in kwargs:
@ -38,6 +48,47 @@ class CTkToplevel(tkinter.Toplevel):
else:
self.windows_set_titlebar_color("light")
def update_dimensions_event(self, event=None):
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
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
self.window_scaling = new_window_scaling
# set new window size by applying scaling to the current window size
self.geometry(f"{self.current_width}x{self.current_height}")
def apply_geometry_scaling(self, geometry_string):
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
if len(numbers) == 2:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}"
elif len(numbers) == 4:
return f"{self.apply_window_scaling(numbers[0]):.0f}x" +\
f"{self.apply_window_scaling(numbers[1]):.0f}+" +\
f"{self.apply_window_scaling(numbers[2]):.0f}+" +\
f"{self.apply_window_scaling(numbers[3]):.0f}"
else:
return geometry_string
def apply_window_scaling(self, value):
if isinstance(value, (int, float)):
return value * self.window_scaling
else:
return value
def geometry(self, geometry_string):
super().geometry(self.apply_geometry_scaling(geometry_string))
# update width and height attributes
numbers = list(map(int, re.split(r"[x+]", geometry_string))) # split geometry string into list of numbers
self.current_width, self.current_height = numbers[0], numbers[1]
def destroy(self):
AppearanceModeTracker.remove(self.set_appearance_mode)
self.disable_macos_dark_title_bar()
@ -99,14 +150,14 @@ class CTkToplevel(tkinter.Toplevel):
@staticmethod
def enable_macos_dark_title_bar():
if sys.platform == "darwin": # macOS
if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults write -g NSRequiresAquaSystemAppearance -bool No")
@staticmethod
def disable_macos_dark_title_bar():
if sys.platform == "darwin": # macOS
if sys.platform == "darwin" and not CTkSettings.deactivate_macos_window_header_manipulation: # macOS
if Version(platform.python_version()) < Version("3.10"):
if Version(tkinter.Tcl().call("info", "patchlevel")) >= Version("8.6.9"): # Tcl/Tk >= 8.6.9
os.system("defaults delete -g NSRequiresAquaSystemAppearance")
@ -123,7 +174,7 @@ class CTkToplevel(tkinter.Toplevel):
https://docs.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
"""
if sys.platform.startswith("win"):
if sys.platform.startswith("win") and not CTkSettings.deactivate_windows_window_header_manipulation:
super().withdraw() # hide window so that it can be redrawn after the titlebar change so that the color change is visible
super().update()

View File

@ -3,6 +3,7 @@ import tkinter.messagebox
import customtkinter
import sys
customtkinter.ScalingTracker.set_user_scaling(0.5)
customtkinter.set_appearance_mode("System") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"

View File

@ -4,8 +4,6 @@ import customtkinter # <- import the CustomTkinter module
customtkinter.set_appearance_mode("dark") # Modes: "System" (standard), "Dark", "Light"
customtkinter.set_default_color_theme("blue") # Themes: "blue" (standard), "green", "dark-blue"
customtkinter.ScalingTracker.set_user_scaling(2.5)
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.title("CustomTkinter Test")
@ -16,6 +14,10 @@ def button_function():
def slider_function(value):
customtkinter.ScalingTracker.set_widget_scaling(value * 2)
customtkinter.ScalingTracker.set_spacing_scaling(value * 2)
customtkinter.ScalingTracker.set_window_scaling(value * 2)
progressbar_1.set(value)

View File

@ -29,7 +29,7 @@ f4.grid(row=0, column=3, rowspan=1, columnspan=1, sticky="nsew")
f4.grid_columnconfigure(0, weight=1)
for i in range(0, 21, 1):
#b = customtkinter.CTkButton(f1, corner_radius=i, height=34, border_width=2, text=f"{i} {i-2}",
#b = customtkinter.CTkButton(f1, corner_radius=i, current_height=34, border_width=2, text=f"{i} {i-2}",
# border_color="white", fg_color=None, text_color="white")
b = tkinter.Button(f1, text=f"{i} {i-2}", width=20)
b.grid(row=i, column=0, pady=5, padx=15, sticky="nsew")