27
27
#endif
28
28
29
29
#ifdef WIN32_DLL
30
+ #include < string>
30
31
#include < windows.h>
32
+ #include < commctrl.h>
31
33
#define PSAPI_VERSION 1
32
34
#include < psapi.h> // Must be linked with 'psapi' library
33
35
#define dlsym GetProcAddress
@@ -49,6 +51,11 @@ static int convert_voidptr(PyObject *obj, void *p)
49
51
// extension module or loaded Tk libraries at run-time.
50
52
static Tk_FindPhoto_t TK_FIND_PHOTO;
51
53
static Tk_PhotoPutBlock_NoComposite_t TK_PHOTO_PUT_BLOCK_NO_COMPOSITE;
54
+ #ifdef WIN32_DLL
55
+ // Global vars for Tcl functions. We load these symbols from the tkinter
56
+ // extension module or loaded Tcl libraries at run-time.
57
+ static Tcl_SetVar_t TCL_SETVAR;
58
+ #endif
52
59
53
60
static PyObject *mpl_tk_blit (PyObject *self, PyObject *args)
54
61
{
@@ -95,17 +102,119 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
95
102
}
96
103
}
97
104
105
+ #ifdef WIN32_DLL
106
+ LRESULT CALLBACK
107
+ DpiSubclassProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
108
+ UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
109
+ {
110
+ switch (uMsg) {
111
+ case WM_DPICHANGED:
112
+ // This function is a subclassed window procedure, and so is run during
113
+ // the Tcl/Tk event loop. Unfortunately, Tkinter has a *second* lock on
114
+ // Tcl threading that is not exposed publicly, but is currently taken
115
+ // while we're in the window procedure. So while we can take the GIL to
116
+ // call Python code, we must not also call *any* Tk code from Python.
117
+ // So stay with Tcl calls in C only.
118
+ {
119
+ // This variable naming must match the name used in
120
+ // lib/matplotlib/backends/_backend_tk.py:FigureManagerTk.
121
+ std::string var_name (" window_dpi" );
122
+ var_name += std::to_string ((unsigned long long )hwnd);
123
+
124
+ // X is high word, Y is low word, but they are always equal.
125
+ std::string dpi = std::to_string (LOWORD (wParam));
126
+
127
+ Tcl_Interp* interp = (Tcl_Interp*)dwRefData;
128
+ TCL_SETVAR (interp, var_name.c_str (), dpi.c_str (), 0 );
129
+ }
130
+ return 0 ;
131
+ case WM_NCDESTROY:
132
+ RemoveWindowSubclass (hwnd, DpiSubclassProc, uIdSubclass);
133
+ break ;
134
+ }
135
+
136
+ return DefSubclassProc (hwnd, uMsg, wParam, lParam);
137
+ }
138
+ #endif
139
+
140
+ static PyObject*
141
+ mpl_tk_enable_dpi_awareness (PyObject* self, PyObject*const * args,
142
+ Py_ssize_t nargs)
143
+ {
144
+ if (nargs != 2 ) {
145
+ return PyErr_Format (PyExc_TypeError,
146
+ " enable_dpi_awareness() takes 2 positional "
147
+ " arguments but %zd were given" ,
148
+ nargs);
149
+ }
150
+
151
+ #ifdef WIN32_DLL
152
+ HWND frame_handle = NULL ;
153
+ Tcl_Interp *interp = NULL ;
154
+
155
+ if (!convert_voidptr (args[0 ], &frame_handle)) {
156
+ return NULL ;
157
+ }
158
+ if (!convert_voidptr (args[1 ], &interp)) {
159
+ return NULL ;
160
+ }
161
+
162
+ #ifdef _DPI_AWARENESS_CONTEXTS_
163
+ HMODULE user32 = LoadLibrary (" user32.dll" );
164
+
165
+ typedef DPI_AWARENESS_CONTEXT (WINAPI *GetWindowDpiAwarenessContext_t)(HWND);
166
+ GetWindowDpiAwarenessContext_t GetWindowDpiAwarenessContextPtr =
167
+ (GetWindowDpiAwarenessContext_t)GetProcAddress (
168
+ user32, " GetWindowDpiAwarenessContext" );
169
+ if (GetWindowDpiAwarenessContextPtr == NULL ) {
170
+ FreeLibrary (user32);
171
+ Py_RETURN_FALSE;
172
+ }
173
+
174
+ typedef BOOL (WINAPI *AreDpiAwarenessContextsEqual_t)(DPI_AWARENESS_CONTEXT,
175
+ DPI_AWARENESS_CONTEXT);
176
+ AreDpiAwarenessContextsEqual_t AreDpiAwarenessContextsEqualPtr =
177
+ (AreDpiAwarenessContextsEqual_t)GetProcAddress (
178
+ user32, " AreDpiAwarenessContextsEqual" );
179
+ if (AreDpiAwarenessContextsEqualPtr == NULL ) {
180
+ FreeLibrary (user32);
181
+ Py_RETURN_FALSE;
182
+ }
183
+
184
+ DPI_AWARENESS_CONTEXT ctx = GetWindowDpiAwarenessContextPtr (frame_handle);
185
+ bool per_monitor = (
186
+ AreDpiAwarenessContextsEqualPtr (
187
+ ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) ||
188
+ AreDpiAwarenessContextsEqualPtr (
189
+ ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE));
190
+
191
+ if (per_monitor) {
192
+ // Per monitor aware means we need to handle WM_DPICHANGED by wrapping
193
+ // the Window Procedure, and the Python side needs to trace the Tk
194
+ // window_dpi variable stored on interp.
195
+ SetWindowSubclass (frame_handle, DpiSubclassProc, 0 , (DWORD_PTR)interp);
196
+ }
197
+ FreeLibrary (user32);
198
+ return PyBool_FromLong (per_monitor);
199
+ #endif
200
+ #endif
201
+
202
+ Py_RETURN_NONE;
203
+ }
204
+
98
205
static PyMethodDef functions[] = {
99
206
{ " blit" , (PyCFunction)mpl_tk_blit, METH_VARARGS },
207
+ { " enable_dpi_awareness" , (PyCFunction)mpl_tk_enable_dpi_awareness,
208
+ METH_FASTCALL },
100
209
{ NULL , NULL } /* sentinel */
101
210
};
102
211
103
- // Functions to fill global Tk function pointers by dynamic loading
212
+ // Functions to fill global Tcl/ Tk function pointers by dynamic loading.
104
213
105
214
template <class T >
106
215
int load_tk (T lib)
107
216
{
108
- // Try to fill Tk global vars with function pointers. Return the number of
217
+ // Try to fill Tk global vars with function pointers. Return the number of
109
218
// functions found.
110
219
return
111
220
!!(TK_FIND_PHOTO =
@@ -116,27 +225,40 @@ int load_tk(T lib)
116
225
117
226
#ifdef WIN32_DLL
118
227
119
- /*
120
- * On Windows, we can't load the tkinter module to get the Tk symbols, because
121
- * Windows does not load symbols into the library name-space of importing
122
- * modules. So, knowing that tkinter has already been imported by Python, we
123
- * scan all modules in the running process for the Tk function names.
228
+ template <class T >
229
+ int load_tcl (T lib)
230
+ {
231
+ // Try to fill Tcl global vars with function pointers. Return the number of
232
+ // functions found.
233
+ return
234
+ !!(TCL_SETVAR = (Tcl_SetVar_t)dlsym (lib, " Tcl_SetVar" ));
235
+ }
236
+
237
+ /* On Windows, we can't load the tkinter module to get the Tcl/Tk symbols,
238
+ * because Windows does not load symbols into the library name-space of
239
+ * importing modules. So, knowing that tkinter has already been imported by
240
+ * Python, we scan all modules in the running process for the Tcl/Tk function
241
+ * names.
124
242
*/
125
243
126
244
void load_tkinter_funcs (void )
127
245
{
128
- // Load Tk functions by searching all modules in current process.
246
+ // Load Tcl/ Tk functions by searching all modules in current process.
129
247
HMODULE hMods[1024 ];
130
248
HANDLE hProcess;
131
249
DWORD cbNeeded;
132
250
unsigned int i;
251
+ bool tcl_ok = false , tk_ok = false ;
133
252
// Returns pseudo-handle that does not need to be closed
134
253
hProcess = GetCurrentProcess ();
135
- // Iterate through modules in this process looking for Tk names.
254
+ // Iterate through modules in this process looking for Tcl/ Tk names.
136
255
if (EnumProcessModules (hProcess, hMods, sizeof (hMods), &cbNeeded)) {
137
256
for (i = 0 ; i < (cbNeeded / sizeof (HMODULE)); i++) {
138
- if (load_tk (hMods[i])) {
139
- return ;
257
+ if (!tcl_ok) {
258
+ tcl_ok = load_tcl (hMods[i]);
259
+ }
260
+ if (!tk_ok) {
261
+ tk_ok = load_tk (hMods[i]);
140
262
}
141
263
}
142
264
}
@@ -211,6 +333,11 @@ PyMODINIT_FUNC PyInit__tkagg(void)
211
333
load_tkinter_funcs ();
212
334
if (PyErr_Occurred ()) {
213
335
return NULL ;
336
+ #ifdef WIN32_DLL
337
+ } else if (!TCL_SETVAR) {
338
+ PyErr_SetString (PyExc_RuntimeError, " Failed to load Tcl_SetVar" );
339
+ return NULL ;
340
+ #endif
214
341
} else if (!TK_FIND_PHOTO) {
215
342
PyErr_SetString (PyExc_RuntimeError, " Failed to load Tk_FindPhoto" );
216
343
return NULL ;
0 commit comments