mirror of
https://github.com/TomSchimansky/CustomTkinter.git
synced 2023-08-10 21:13:13 +03:00
restructured settings, small scaling fixes in CTk
This commit is contained in:
parent
4b48bf57b2
commit
35bdbed95c
@ -9,32 +9,30 @@ from .appearance_mode_tracker import AppearanceModeTracker
|
||||
from .theme_manager import ThemeManager
|
||||
from .scaling_tracker import ScalingTracker
|
||||
from .font_manager import FontManager
|
||||
|
||||
Settings.init_font_character_mapping()
|
||||
Settings.init_drawing_method()
|
||||
from .draw_engine import DrawEngine
|
||||
|
||||
AppearanceModeTracker.init_appearance_mode()
|
||||
|
||||
ThemeManager.load_theme("blue")
|
||||
|
||||
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
|
||||
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-Medium.ttf"))
|
||||
|
||||
# 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:
|
||||
Settings.circle_font_is_ready = True
|
||||
else:
|
||||
Settings.circle_font_is_ready = False
|
||||
|
||||
if Settings.preferred_drawing_method == "font_shapes":
|
||||
if FontManager.load_font(os.path.join(script_directory, "assets", "fonts", "CustomTkinter_shapes_font-fine.otf")) is False:
|
||||
if DrawEngine.preferred_drawing_method == "font_shapes":
|
||||
sys.stderr.write("customtkinter.__init__ warning: " +
|
||||
"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!")
|
||||
Settings.preferred_drawing_method = "circle_shapes"
|
||||
DrawEngine.preferred_drawing_method = "circle_shapes"
|
||||
|
||||
# import widgets
|
||||
from .widgets.ctk_button import CTkButton
|
||||
@ -69,10 +67,10 @@ def set_default_color_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):
|
||||
ScalingTracker.set_spacing_scaling(scaling_value)
|
||||
ScalingTracker.set_widget_scaling(scaling_value)
|
||||
|
||||
|
||||
def deactivate_automatic_dpi_awareness():
|
||||
ScalingTracker.deactivate_automatic_dpi_awareness = False
|
||||
|
@ -98,7 +98,7 @@ class AppearanceModeTracker:
|
||||
# find an existing tkinter.Tk object for the next call of .after()
|
||||
for root_tk in cls.root_tk_list:
|
||||
try:
|
||||
root_tk.after(200, cls.update)
|
||||
root_tk.after(500, cls.update)
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
@ -51,7 +51,7 @@
|
||||
"radiobutton_border_width_unchecked": 3,
|
||||
"radiobutton_border_width_checked": 6,
|
||||
"entry_border_width": 2,
|
||||
"frame_corner_radius": 10,
|
||||
"frame_corner_radius": 8,
|
||||
"frame_border_width": 0,
|
||||
"label_corner_radius": 8,
|
||||
"progressbar_border_width": 0,
|
||||
|
@ -1,9 +1,11 @@
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import math
|
||||
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:
|
||||
@ -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._rendering_method = rendering_method # "polygon_shapes" (macOS), "font_shapes" (Windows, Linux), "circle_shapes" (backup without fonts)
|
||||
self._existing_tags = set()
|
||||
|
||||
def _calc_optimal_corner_radius(self, user_corner_radius: Union[float, int]) -> Union[float, int]:
|
||||
# optimize for drawing with polygon shapes
|
||||
if self._rendering_method == "polygon_shapes":
|
||||
if self.preferred_drawing_method == "polygon_shapes":
|
||||
if sys.platform == "darwin":
|
||||
return user_corner_radius
|
||||
else:
|
||||
return round(user_corner_radius)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
# make sure the value is always with .5 at the end for smoother corners
|
||||
@ -71,11 +74,11 @@ class DrawEngine:
|
||||
else:
|
||||
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)
|
||||
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, ())
|
||||
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)
|
||||
|
||||
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:
|
||||
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,
|
||||
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,
|
||||
progress_value, orientation)
|
||||
|
||||
@ -534,10 +537,10 @@ class DrawEngine:
|
||||
else:
|
||||
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,
|
||||
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,
|
||||
button_length, button_corner_radius, slider_value, orientation)
|
||||
|
||||
@ -659,7 +662,7 @@ class DrawEngine:
|
||||
size = round(size)
|
||||
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
|
||||
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)
|
||||
@ -670,7 +673,7 @@ class DrawEngine:
|
||||
x + radius, y - radius,
|
||||
x - radius / 4, y + radius * 0.8,
|
||||
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"):
|
||||
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")
|
||||
|
@ -1,11 +1,10 @@
|
||||
import tkinter
|
||||
import sys
|
||||
from typing import Callable, Type
|
||||
|
||||
from .settings import Settings
|
||||
from typing import Callable
|
||||
|
||||
|
||||
class ScalingTracker:
|
||||
deactivate_automatic_dpi_awareness = False
|
||||
|
||||
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
|
||||
@ -34,17 +33,17 @@ class ScalingTracker:
|
||||
@classmethod
|
||||
def set_widget_scaling(cls, widget_scaling_factor: float):
|
||||
cls.widget_scaling = max(widget_scaling_factor, 0.4)
|
||||
cls.update_scaling_callbacks()
|
||||
cls.update_scaling_callbacks_all()
|
||||
|
||||
@classmethod
|
||||
def set_spacing_scaling(cls, spacing_scaling_factor: float):
|
||||
cls.spacing_scaling = max(spacing_scaling_factor, 0.4)
|
||||
cls.update_scaling_callbacks()
|
||||
cls.update_scaling_callbacks_all()
|
||||
|
||||
@classmethod
|
||||
def set_window_scaling(cls, window_scaling_factor: float):
|
||||
cls.window_scaling = max(window_scaling_factor, 0.4)
|
||||
cls.update_scaling_callbacks()
|
||||
cls.update_scaling_callbacks_all()
|
||||
|
||||
@classmethod
|
||||
def get_window_root_of_widget(cls, widget):
|
||||
@ -57,15 +56,27 @@ class ScalingTracker:
|
||||
return current_widget
|
||||
|
||||
@classmethod
|
||||
def update_scaling_callbacks(cls):
|
||||
def update_scaling_callbacks_all(cls):
|
||||
for window, callback_list in cls.window_widgets_dict.items():
|
||||
for callback in callback_list:
|
||||
if not Settings.deactivate_automatic_dpi_awareness:
|
||||
callback(cls.window_dpi_scaling_dict[window] * cls.widget_scaling,
|
||||
for set_scaling_callback in callback_list:
|
||||
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:
|
||||
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.window_scaling)
|
||||
|
||||
@ -81,9 +92,9 @@ class ScalingTracker:
|
||||
if window_root not in cls.window_dpi_scaling_dict:
|
||||
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)
|
||||
# cls.update_loop_running = True
|
||||
if not cls.update_loop_running:
|
||||
window_root.after(100, cls.check_dpi_scaling)
|
||||
cls.update_loop_running = True
|
||||
|
||||
@classmethod
|
||||
def remove_widget(cls, widget_callback, widget):
|
||||
@ -115,7 +126,7 @@ class ScalingTracker:
|
||||
""" make process DPI aware, customtkinter elemets will get scaled automatically,
|
||||
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":
|
||||
pass # high DPI scaling works automatically on macOS
|
||||
|
||||
@ -151,10 +162,16 @@ class ScalingTracker:
|
||||
# check for every window if scaling value changed
|
||||
# (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()
|
||||
for root_tk in cls.window_widgets_dict.keys():
|
||||
try:
|
||||
root_tk.after(500, cls.check_dpi_scaling)
|
||||
root_tk.after(200, cls.check_dpi_scaling)
|
||||
return
|
||||
except Exception:
|
||||
continue
|
||||
|
@ -1,42 +1,5 @@
|
||||
import sys
|
||||
|
||||
|
||||
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
|
||||
deactivate_macos_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"
|
||||
|
@ -0,0 +1,3 @@
|
||||
from .ctk_canvas import CTkCanvas
|
||||
|
||||
CTkCanvas.init_font_character_mapping()
|
@ -69,7 +69,7 @@ class CTkButton(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
# event bindings
|
||||
self.canvas.bind("<Enter>", self.on_enter)
|
||||
|
@ -1,23 +1,50 @@
|
||||
import tkinter
|
||||
from ..settings import Settings
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
|
||||
class CTkCanvas(tkinter.Canvas):
|
||||
|
||||
radius_to_char_fine = Settings.radius_to_char_fine # dict to map radius to font circle character
|
||||
radius_to_char_fine: dict = None # dict to map radius to font circle character
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
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:
|
||||
return "A"
|
||||
else:
|
||||
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
|
||||
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)
|
||||
@ -61,5 +88,3 @@ class CTkCanvas(tkinter.Canvas):
|
||||
super().itemconfigure(configure_id, *args, **kwargs_except_outline)
|
||||
else:
|
||||
super().itemconfigure(configure_id, *args, **kwargs)
|
||||
|
||||
|
||||
|
@ -83,7 +83,7 @@ class CTkCheckBox(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
if self.hover is True:
|
||||
self.canvas.bind("<Enter>", self.on_enter)
|
||||
|
@ -50,7 +50,7 @@ class CTkEntry(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.entry = tkinter.Entry(master=self,
|
||||
bd=0,
|
||||
|
@ -45,7 +45,7 @@ class CTkFrame(CTkBaseClass):
|
||||
height=self.apply_widget_scaling(self.current_height))
|
||||
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.draw_engine = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.bind('<Configure>', self.update_dimensions_event)
|
||||
|
||||
|
@ -48,7 +48,7 @@ class CTkLabel(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.text_label = tkinter.Label(master=self,
|
||||
highlightthickness=0,
|
||||
|
@ -62,7 +62,7 @@ class CTkProgressBar(CTkBaseClass):
|
||||
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 = 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
|
||||
self.bind('<Configure>', self.update_dimensions_event)
|
||||
|
@ -79,7 +79,7 @@ class CTkRadioButton(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.canvas.bind("<Enter>", self.on_enter)
|
||||
self.canvas.bind("<Leave>", self.on_leave)
|
||||
|
@ -84,7 +84,7 @@ class CTkSlider(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.canvas.bind("<Enter>", self.on_enter)
|
||||
self.canvas.bind("<Leave>", self.on_leave)
|
||||
|
@ -84,7 +84,7 @@ class CTkSwitch(CTkBaseClass):
|
||||
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 = DrawEngine(self.canvas, Settings.preferred_drawing_method)
|
||||
self.draw_engine = DrawEngine(self.canvas)
|
||||
|
||||
self.canvas.bind("<Enter>", self.on_enter)
|
||||
self.canvas.bind("<Leave>", self.on_leave)
|
||||
|
@ -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):
|
||||
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
|
||||
print("update_dimensions_event:", self.current_width)
|
||||
|
||||
def set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
|
||||
self.window_scaling = new_window_scaling
|
||||
|
||||
# reset min, max and resizable constraints for applying scaling
|
||||
if self.last_resizable_args is not None:
|
||||
super().resizable(True, True)
|
||||
if self.min_width is not None or self.min_height is not None:
|
||||
super().minsize(0, 0)
|
||||
if self.max_width is not None or self.max_height is not None:
|
||||
super().maxsize(1_000_000, 1_000_000)
|
||||
# force new dimensions on window by using min, max, and geometry
|
||||
super().minsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
|
||||
super().maxsize(self.apply_window_scaling(self.current_width), self.apply_window_scaling(self.current_height))
|
||||
super().geometry(f"{self.apply_window_scaling(self.current_width)}x"+f"{self.apply_window_scaling(self.current_height)}")
|
||||
print("set_scaling:", self.apply_window_scaling(self.current_width), self.max_width, self.min_width)
|
||||
|
||||
# set new window size by applying scaling to the current window size
|
||||
self.geometry(f"{self.current_width}x{self.current_height}")
|
||||
# set new scaled min and max with 400ms delay (otherwise it won't work for some reason)
|
||||
self.after(400, self.set_scaled_min_max)
|
||||
|
||||
# set scaled min, max sizes and reapply resizable
|
||||
if self.last_resizable_args is not None:
|
||||
super().resizable(*self.last_resizable_args[0], **self.last_resizable_args[1]) # args, kwargs
|
||||
def set_scaled_min_max(self):
|
||||
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))
|
||||
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):
|
||||
if not self.window_exists:
|
||||
print("deiconify")
|
||||
self.deiconify()
|
||||
self.window_exists = True
|
||||
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))
|
||||
|
||||
def geometry(self, geometry_string):
|
||||
print("geometry:", geometry_string)
|
||||
super().geometry(self.apply_geometry_scaling(geometry_string))
|
||||
|
||||
# update width and height attributes
|
||||
|
@ -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()
|
@ -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()
|
@ -1,21 +1,22 @@
|
||||
import tkinter
|
||||
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_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.geometry("400x480")
|
||||
root_tk.title("CustomTkinter Test")
|
||||
root_tk.title("CustomTkinter manual scaling test")
|
||||
|
||||
root_tk.minsize(480, 480)
|
||||
root_tk.maxsize(520, 520)
|
||||
#root_tk.minsize(200, 200)
|
||||
#root_tk.maxsize(520, 520)
|
||||
root_tk.resizable(True, False)
|
||||
|
||||
print(customtkinter.ScalingTracker.get_window_scaling(root_tk))
|
||||
|
||||
def button_function():
|
||||
root_tk.geometry(f"{200}x{200}")
|
||||
print("Button click", label_1.text_label.cget("text"))
|
||||
|
||||
|
||||
@ -31,7 +32,7 @@ def check_box_function():
|
||||
|
||||
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)
|
||||
|
||||
label_1 = customtkinter.CTkLabel(master=frame_1, justify=tkinter.LEFT)
|
||||
|
Loading…
x
Reference in New Issue
Block a user