forked from TomSchimansky/CustomTkinter
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscaling_tracker.py
More file actions
181 lines (147 loc) · 7.4 KB
/
scaling_tracker.py
File metadata and controls
181 lines (147 loc) · 7.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import tkinter
import sys
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
widget_scaling = 1 # user values which multiply to detected window scaling factor
window_scaling = 1
spacing_scaling = 1
update_loop_running = False
update_loop_interval = 150 # milliseconds
@classmethod
def get_widget_scaling(cls, widget) -> float:
window_root = cls.get_window_root_of_widget(widget)
return cls.window_dpi_scaling_dict[window_root] * cls.widget_scaling
@classmethod
def get_spacing_scaling(cls, widget) -> float:
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) -> float:
window_root = cls.get_window_root_of_widget(window)
return cls.window_dpi_scaling_dict[window_root] * cls.window_scaling
@classmethod
def set_widget_scaling(cls, widget_scaling_factor: float):
cls.widget_scaling = max(widget_scaling_factor, 0.4)
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_all()
@classmethod
def set_window_scaling(cls, window_scaling_factor: float):
cls.window_scaling = max(window_scaling_factor, 0.4)
cls.update_scaling_callbacks_all()
@classmethod
def get_window_root_of_widget(cls, widget):
current_widget = widget
while isinstance(current_widget, tkinter.Tk) is False and\
isinstance(current_widget, tkinter.Toplevel) is False:
current_widget = current_widget.master
return current_widget
@classmethod
def update_scaling_callbacks_all(cls):
for window, callback_list in cls.window_widgets_dict.items():
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:
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)
@classmethod
def add_widget(cls, widget_callback: Callable, widget):
window_root = cls.get_window_root_of_widget(widget)
if window_root not in cls.window_widgets_dict:
cls.window_widgets_dict[window_root] = [widget_callback]
else:
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_scaling(window_root)
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):
window_root = cls.get_window_root_of_widget(widget)
try:
cls.window_widgets_dict[window_root].remove(widget_callback)
except:
pass
@classmethod
def remove_window(cls, window_callback, window):
try:
del cls.window_widgets_dict[window]
except:
pass
@classmethod
def add_window(cls, window_callback, window):
if window not in cls.window_widgets_dict:
cls.window_widgets_dict[window] = [window_callback]
else:
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_scaling(window)
@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):
# check for every window if scaling value changed
for window in cls.window_widgets_dict:
if window.winfo_exists():
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 app in cls.window_widgets_dict.keys():
try:
app.after(cls.update_loop_interval, cls.check_dpi_scaling)
return
except Exception:
continue
cls.update_loop_running = False