TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
11.58k stars 1.08k forks source link

Window changes size with multiple monitors and scalings on Windows #520

Open TomSchimansky opened 2 years ago

TomSchimansky commented 2 years ago

Window changes size when dragged from over to monitor with different scaling one Windows 11, maybe also Windows 10. This only happens when window is hold by the mouse for a second after moving to the other monitor, and is then released by the mouse.

Akascape commented 2 years ago

Try adding this:

import ctypes    
ctypes.windll.shcore.SetProcessDpiAwareness(0)
TomSchimansky commented 2 years ago

I already set the dpi awareness in the scaling tracker class, you can have a look if you are interested. These are the methods that manage the scaling:

@classmethod
    def activate_high_dpi_awareness(cls):
        """ make process DPI aware, customtkinter elements will get scaled automatically,
            only gets activated when CTk object is created """

        if not cls.deactivate_automatic_dpi_awareness:
            if sys.platform == "darwin":
                pass  # high DPI scaling works automatically on macOS

            elif sys.platform.startswith("win"):
                from ctypes import windll
                windll.shcore.SetProcessDpiAwareness(2)
                # Microsoft Docs: https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness
            else:
                pass  # DPI awareness on Linux not implemented
    @classmethod
    def get_window_dpi_scaling(cls, window) -> float:
        if not cls.deactivate_automatic_dpi_awareness:
            if sys.platform == "darwin":
                return 1  # scaling works automatically on macOS

            elif sys.platform.startswith("win"):
                from ctypes import windll, pointer, wintypes

                DPI100pc = 96  # DPI 96 is 100% scaling
                DPI_type = 0  # MDT_EFFECTIVE_DPI = 0, MDT_ANGULAR_DPI = 1, MDT_RAW_DPI = 2
                window_hwnd = wintypes.HWND(window.winfo_id())
                monitor_handle = windll.user32.MonitorFromWindow(window_hwnd, wintypes.DWORD(2))  # MONITOR_DEFAULTTONEAREST = 2
                x_dpi, y_dpi = wintypes.UINT(), wintypes.UINT()
                windll.shcore.GetDpiForMonitor(monitor_handle, DPI_type, pointer(x_dpi), pointer(y_dpi))
                return (x_dpi.value + y_dpi.value) / (2 * DPI100pc)

            else:
                return 1  # DPI awareness on Linux not implemented
        else:
            return 1
    @classmethod
    def check_dpi_scaling(cls):
        new_scaling_detected = False

        # check for every window if scaling value changed
        for window in cls.window_widgets_dict:
            if window.winfo_exists() and not window.state() == "iconic":
                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

                    if sys.platform.startswith("win"):
                        window.attributes("-alpha", 0.15)

                    cls.update_scaling_callbacks_for_window(window)

                    if sys.platform.startswith("win"):
                        window.after(200, lambda: window.attributes("-alpha", 1))

                    new_scaling_detected = True

        # find an existing tkinter object for the next call of .after()
        for app in cls.window_widgets_dict.keys():
            try:
                if new_scaling_detected:
                    app.after(cls.loop_pause_after_new_scaling, cls.check_dpi_scaling)
                else:
                    app.after(cls.update_loop_interval, cls.check_dpi_scaling)
                return
            except Exception:
                continue

        cls.update_loop_running = False

The set scaling method of the CTk window class looks like the following:

    def _set_scaling(self, new_widget_scaling, new_spacing_scaling, new_window_scaling):
        self._window_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
        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)

        # 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())

Its very difficult to spot any errors in this process I implemented, but maybe this is also just a tk bug, I don't know...

TomSchimansky commented 2 years ago

I will work on this later if I have the time