diff --git a/customtkinter/windows/ctk_tk.py b/customtkinter/windows/ctk_tk.py index 53b8467..4b74acd 100644 --- a/customtkinter/windows/ctk_tk.py +++ b/customtkinter/windows/ctk_tk.py @@ -86,10 +86,8 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def _update_dimensions_event(self, event=None): if not self._block_update_dimensions_event: - # removed this because of python stackoverflow error with many label widgets - # self.update_idletasks() - detected_width = self.winfo_width() # detect current window size - detected_height = self.winfo_height() + detected_width = super().winfo_width() # detect current window size + detected_height = super().winfo_height() # detected_width = event.width # detected_height = event.height @@ -101,22 +99,20 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # block update_dimensions_event to prevent current_width and current_height to get updated - self._block_update_dimensions_event = True - - # force new dimensions on window by using min, max, and geometry + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. 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{self._apply_window_scaling(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 new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) - # release the blocking of update_dimensions_event after a small amount of time (slight delay is necessary) - def set_block_update_dimensions_event_false(): - self._block_update_dimensions_event = False - self.after(100, lambda: set_block_update_dimensions_event_false()) + def block_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def unblock_update_dimensions_event(self): + self._block_update_dimensions_event = False def _set_scaled_min_max(self): if self._min_width is not None or self._min_height is not None: @@ -157,12 +153,14 @@ class CTk(tkinter.Tk, CTkAppearanceModeBaseClass, CTkScalingBaseClass): super().mainloop(*args, **kwargs) def resizable(self, width: bool = None, height: bool = None): - super().resizable(width, height) + current_resizable_values = super().resizable(width, height) self._last_resizable_args = ([], {"width": width, "height": height}) if sys.platform.startswith("win"): self._windows_set_titlebar_color(self._get_appearance_mode()) + return current_resizable_values + def minsize(self, width: int = None, height: int = None): self._min_width = width self._min_height = height diff --git a/customtkinter/windows/ctk_toplevel.py b/customtkinter/windows/ctk_toplevel.py index 1a50c0a..bb2e383 100644 --- a/customtkinter/windows/ctk_toplevel.py +++ b/customtkinter/windows/ctk_toplevel.py @@ -65,6 +65,8 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self.bind('', self._update_dimensions_event) self.bind('', self._focus_in_event) + self._block_update_dimensions_event = False + def destroy(self): self._disable_macos_dark_title_bar() @@ -79,24 +81,31 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl self.lift() def _update_dimensions_event(self, event=None): - detected_width = self.winfo_width() # detect current window size - detected_height = self.winfo_height() + if not self._block_update_dimensions_event: + detected_width = self.winfo_width() # detect current window size + detected_height = self.winfo_height() - if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): - self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event - self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale + if self._current_width != self._reverse_window_scaling(detected_width) or self._current_height != self._reverse_window_scaling(detected_height): + self._current_width = self._reverse_window_scaling(detected_width) # adjust current size according to new size given by event + self._current_height = self._reverse_window_scaling(detected_height) # _current_width and _current_height are independent of the scale def _set_scaling(self, new_widget_scaling, new_window_scaling): super()._set_scaling(new_widget_scaling, new_window_scaling) - # force new dimensions on window by using min, max, and geometry + # Force new dimensions on window by using min, max, and geometry. Without min, max it won't work. 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)}") - # 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) + super().geometry(f"{self._apply_window_scaling(self._current_width)}x{self._apply_window_scaling(self._current_height)}") + + # set new scaled min and max with delay (delay prevents weird bug where window dimensions snap to unscaled dimensions when mouse releases window) + self.after(1000, self._set_scaled_min_max) # Why 1000ms delay? Experience! (Everything tested on Windows 11) + + def block_update_dimensions_event(self): + self._block_update_dimensions_event = False + + def unblock_update_dimensions_event(self): + self._block_update_dimensions_event = False def _set_scaled_min_max(self): if self._min_width is not None or self._min_height is not None: @@ -127,12 +136,14 @@ class CTkToplevel(tkinter.Toplevel, CTkAppearanceModeBaseClass, CTkScalingBaseCl super().iconify() def resizable(self, width: bool = None, height: bool = None): - super().resizable(width, height) + current_resizable_values = super().resizable(width, height) self._last_resizable_args = ([], {"width": width, "height": height}) if sys.platform.startswith("win"): self.after(10, lambda: self._windows_set_titlebar_color(self._get_appearance_mode())) + return current_resizable_values + def minsize(self, width=None, height=None): self._min_width = width self._min_height = height diff --git a/customtkinter/windows/widgets/scaling/scaling_tracker.py b/customtkinter/windows/widgets/scaling/scaling_tracker.py index babee5b..d3627c2 100644 --- a/customtkinter/windows/widgets/scaling/scaling_tracker.py +++ b/customtkinter/windows/widgets/scaling/scaling_tracker.py @@ -1,4 +1,3 @@ -import ctypes.wintypes import tkinter import sys from typing import Callable @@ -14,8 +13,8 @@ class ScalingTracker: window_scaling = 1 update_loop_running = False - update_loop_interval = 600 # ms - loop_pause_after_new_scaling = 1000 # ms + update_loop_interval = 100 # ms + loop_pause_after_new_scaling = 1500 # ms @classmethod def get_widget_scaling(cls, widget) -> float: @@ -119,9 +118,32 @@ class ScalingTracker: pass # high DPI scaling works automatically on macOS elif sys.platform.startswith("win"): - from ctypes import windll, wintypes - windll.shcore.SetProcessDpiAwareness(2) - # Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness + import ctypes + + # Values for SetProcessDpiAwareness and SetProcessDpiAwarenessContext: + # internal enum PROCESS_DPI_AWARENESS + # { + # Process_DPI_Unaware = 0, + # Process_System_DPI_Aware = 1, + # Process_Per_Monitor_DPI_Aware = 2 + # } + # + # internal enum DPI_AWARENESS_CONTEXT + # { + # DPI_AWARENESS_CONTEXT_UNAWARE = 16, + # DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = 17, + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = 18, + # DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = 34 + # } + + # ctypes.windll.user32.SetProcessDpiAwarenessContext(34) # Non client area scaling at runtime (titlebar) + # does not work with resizable(False, False), window starts growing on monitor with different scaling (weird tkinter bug...) + # ctypes.windll.user32.EnableNonClientDpiScaling(hwnd) does not work for some reason (tested on Windows 11) + + # It's too bad, that these Windows API methods don't work properly with tkinter. But I tested days with multiple monitor setups, + # and I don't think there is anything left to do. So this is the best option at the moment: + + ctypes.windll.shcore.SetProcessDpiAwareness(2) # Titlebar does not scale at runtime else: pass # DPI awareness on Linux not implemented @@ -161,10 +183,12 @@ class ScalingTracker: if sys.platform.startswith("win"): window.attributes("-alpha", 0.15) + window.block_update_dimensions_event() cls.update_scaling_callbacks_for_window(window) + window.unblock_update_dimensions_event() if sys.platform.startswith("win"): - window.after(200, lambda: window.attributes("-alpha", 1)) + window.attributes("-alpha", 1) new_scaling_detected = True diff --git a/examples/complex_example.py b/examples/complex_example.py index 4630f37..325909f 100644 --- a/examples/complex_example.py +++ b/examples/complex_example.py @@ -14,6 +14,7 @@ class App(customtkinter.CTk): # configure window self.title("CustomTkinter complex_example.py") self.geometry(f"{1100}x{580}") + #self.resizable(False, False) # configure grid layout (4x4) self.grid_columnconfigure(1, weight=1) diff --git a/test/manual_integration_tests/test_ctk_toplevel.py b/test/manual_integration_tests/test_ctk_toplevel.py index 88e8f1c..32bd490 100644 --- a/test/manual_integration_tests/test_ctk_toplevel.py +++ b/test/manual_integration_tests/test_ctk_toplevel.py @@ -8,6 +8,7 @@ class ToplevelWindow(customtkinter.CTkToplevel): super().__init__(*args, **kwargs) self.protocol("WM_DELETE_WINDOW", self.closing) self.geometry("500x300") + self.resizable(False, False) self.closing_event = closing_event self.label = customtkinter.CTkLabel(self, text="ToplevelWindow") @@ -26,6 +27,7 @@ class App(customtkinter.CTk): def __init__(self): super().__init__() self.geometry("500x400") + self.resizable(False, False) self.button_1 = customtkinter.CTkButton(self, text="Open CTkToplevel", command=self.open_toplevel) self.button_1.pack(side="top", padx=40, pady=40)