2727#endif
2828
2929#ifdef WIN32_DLL
30+ #include < string>
3031#include < windows.h>
32+ #include < commctrl.h>
3133#define PSAPI_VERSION 1
3234#include < psapi.h> // Must be linked with 'psapi' library
3335#define dlsym GetProcAddress
@@ -49,6 +51,11 @@ static int convert_voidptr(PyObject *obj, void *p)
4951// extension module or loaded Tk libraries at run-time.
5052static Tk_FindPhoto_t TK_FIND_PHOTO;
5153static 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
5259
5360static PyObject *mpl_tk_blit (PyObject *self, PyObject *args)
5461{
@@ -95,17 +102,119 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
95102 }
96103}
97104
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+
98205static PyMethodDef functions[] = {
99206 { " blit" , (PyCFunction)mpl_tk_blit, METH_VARARGS },
207+ { " enable_dpi_awareness" , (PyCFunction)mpl_tk_enable_dpi_awareness,
208+ METH_FASTCALL },
100209 { NULL , NULL } /* sentinel */
101210};
102211
103- // Functions to fill global Tk function pointers by dynamic loading
212+ // Functions to fill global Tcl/ Tk function pointers by dynamic loading.
104213
105214template <class T >
106215int load_tk (T lib)
107216{
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
109218 // functions found.
110219 return
111220 !!(TK_FIND_PHOTO =
@@ -116,27 +225,40 @@ int load_tk(T lib)
116225
117226#ifdef WIN32_DLL
118227
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.
124242 */
125243
126244void load_tkinter_funcs (void )
127245{
128- // Load Tk functions by searching all modules in current process.
246+ // Load Tcl/ Tk functions by searching all modules in current process.
129247 HMODULE hMods[1024 ];
130248 HANDLE hProcess;
131249 DWORD cbNeeded;
132250 unsigned int i;
251+ bool tcl_ok = false , tk_ok = false ;
133252 // Returns pseudo-handle that does not need to be closed
134253 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.
136255 if (EnumProcessModules (hProcess, hMods, sizeof (hMods), &cbNeeded)) {
137256 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]);
140262 }
141263 }
142264 }
@@ -211,6 +333,11 @@ PyMODINIT_FUNC PyInit__tkagg(void)
211333 load_tkinter_funcs ();
212334 if (PyErr_Occurred ()) {
213335 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
214341 } else if (!TK_FIND_PHOTO) {
215342 PyErr_SetString (PyExc_RuntimeError, " Failed to load Tk_FindPhoto" );
216343 return NULL ;
0 commit comments