Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitebbc746

Browse files
committed
Handle DPI changes in TkAgg backend on Windows.
1 parent09f95f4 commitebbc746

File tree

4 files changed

+212
-20
lines changed

4 files changed

+212
-20
lines changed

‎lib/matplotlib/backends/_backend_tk.py

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,16 @@ def __init__(self, canvas, num, window):
419419
ifself.toolbar:
420420
backend_tools.add_tools_to_container(self.toolbar)
421421

422+
# If the window has per-monitor DPI awareness, then setup a Tk variable
423+
# to store the DPI, which will be updated by the C code, and the trace
424+
# will handle it on the Python side.
425+
window_frame=int(window.wm_frame(),16)
426+
window_dpi=tk.IntVar(master=window,value=96,
427+
name=f'window_dpi{window_frame}')
428+
if_tkagg.enable_dpi_awareness(window_frame,window.tk.interpaddr()):
429+
self._window_dpi=window_dpi# Prevent garbage collection.
430+
window_dpi.trace_add('write',self._update_window_dpi)
431+
422432
self._shown=False
423433

424434
def_get_toolbar(self):
@@ -430,6 +440,13 @@ def _get_toolbar(self):
430440
toolbar=None
431441
returntoolbar
432442

443+
def_update_window_dpi(self,*args):
444+
newdpi=self._window_dpi.get()
445+
self.window.call('tk','scaling',newdpi/72)
446+
ifself.toolbarandhasattr(self.toolbar,'_rescale'):
447+
self.toolbar._rescale()
448+
self.canvas._update_device_pixel_ratio()
449+
433450
defresize(self,width,height):
434451
max_size=1_400_000# the measured max on xorg 1.20.8 was 1_409_023
435452

@@ -545,6 +562,33 @@ def __init__(self, canvas, window, *, pack_toolbar=True):
545562
ifpack_toolbar:
546563
self.pack(side=tk.BOTTOM,fill=tk.X)
547564

565+
def_rescale(self):
566+
"""
567+
Scale all children of the toolbar to current DPI setting.
568+
569+
Before this is called, the Tk scaling setting will have been updated to
570+
match the new DPI. Tk widgets do not update for changes to scaling, but
571+
all measurements made after the change will match the new scaling. Thus
572+
this function re-applies all the same sizes in points, which Tk will
573+
scale correctly to pixels.
574+
"""
575+
forwidgetinself.winfo_children():
576+
ifisinstance(widget, (tk.Button,tk.Checkbutton)):
577+
ifhasattr(widget,'_image_file'):
578+
# Explicit class because ToolbarTk calls _rescale.
579+
NavigationToolbar2Tk._set_image_for_button(self,widget)
580+
else:
581+
# Text-only button is handled by the font setting instead.
582+
pass
583+
elifisinstance(widget,tk.Frame):
584+
widget.configure(height='22p',pady='1p')
585+
widget.pack_configure(padx='4p')
586+
elifisinstance(widget,tk.Label):
587+
pass# Text is handled by the font setting instead.
588+
else:
589+
_log.warning('Unknown child class %s',widget.winfo_class)
590+
self._label_font.configure(size=10)
591+
548592
def_update_buttons_checked(self):
549593
# sync button checkstates to match active mode
550594
fortext,modein [('Zoom',_Mode.ZOOM), ('Pan',_Mode.PAN)]:
@@ -586,6 +630,22 @@ def set_cursor(self, cursor):
586630
excepttkinter.TclError:
587631
pass
588632

633+
def_set_image_for_button(self,button):
634+
"""
635+
Set the image for a button based on its pixel size.
636+
637+
The pixel size is determined by the DPI scaling of the window.
638+
"""
639+
ifbutton._image_fileisNone:
640+
return
641+
642+
size=button.winfo_pixels('24p')
643+
withImage.open(button._image_file.replace('.png','_large.png')
644+
ifsize>24elsebutton._image_file)asim:
645+
image=ImageTk.PhotoImage(im.resize((size,size)),master=self)
646+
button.configure(image=image,height='24p',width='24p')
647+
button._ntimage=image# Prevent garbage collection.
648+
589649
def_Button(self,text,image_file,toggle,command):
590650
ifnottoggle:
591651
b=tk.Button(master=self,text=text,command=command)
@@ -600,14 +660,10 @@ def _Button(self, text, image_file, toggle, command):
600660
master=self,text=text,command=command,
601661
indicatoron=False,variable=var)
602662
b.var=var
663+
b._image_file=image_file
603664
ifimage_fileisnotNone:
604-
size=b.winfo_pixels('24p')
605-
withImage.open(image_file.replace('.png','_large.png')
606-
ifsize>24elseimage_file)asim:
607-
image=ImageTk.PhotoImage(im.resize((size,size)),
608-
master=self)
609-
b.configure(image=image,height='24p',width='24p')
610-
b._ntimage=image# Prevent garbage collection.
665+
# Explicit class because ToolbarTk calls _Button.
666+
NavigationToolbar2Tk._set_image_for_button(self,b)
611667
else:
612668
b.configure(font=self._label_font)
613669
b.pack(side=tk.LEFT)
@@ -761,6 +817,9 @@ def __init__(self, toolmanager, window):
761817
self.pack(side=tk.TOP,fill=tk.X)
762818
self._groups= {}
763819

820+
def_rescale(self):
821+
returnNavigationToolbar2Tk._rescale(self)
822+
764823
defadd_toolitem(
765824
self,name,group,position,image_file,description,toggle):
766825
frame=self._get_groupframe(group)

‎setupext.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,8 +444,8 @@ def get_extensions(self):
444444
],
445445
include_dirs=["src"],
446446
# psapi library needed for finding Tcl/Tk at run time.
447-
libraries=({"linux": ["dl"],"win32": ["psapi"],
448-
"cygwin": ["psapi"]}.get(sys.platform, [])),
447+
libraries={"linux": ["dl"],"win32": ["comctl32","psapi"],
448+
"cygwin": ["comctl32","psapi"]}.get(sys.platform, []),
449449
extra_link_args={"win32": ["-mwindows"]}.get(sys.platform, []))
450450
add_numpy_flags(ext)
451451
add_libagg_flags(ext)

‎src/_tkagg.cpp

Lines changed: 138 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
#endif
2828

2929
#ifdef WIN32_DLL
30+
#include<string>
3031
#include<windows.h>
32+
#include<commctrl.h>
3133
#definePSAPI_VERSION1
3234
#include<psapi.h>// Must be linked with 'psapi' library
3335
#definedlsym GetProcAddress
@@ -49,6 +51,11 @@ static int convert_voidptr(PyObject *obj, void *p)
4951
// extension module or loaded Tk libraries at run-time.
5052
static Tk_FindPhoto_t TK_FIND_PHOTO;
5153
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
5259

5360
static 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::stringvar_name("window_dpi");
122+
var_name +=std::to_string((unsignedlonglong)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+
return0;
131+
case WM_NCDESTROY:
132+
RemoveWindowSubclass(hwnd, DpiSubclassProc, uIdSubclass);
133+
break;
134+
}
135+
136+
returnDefSubclassProc(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+
returnPyErr_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+
returnNULL;
157+
}
158+
if (!convert_voidptr(args[1], &interp)) {
159+
returnNULL;
160+
}
161+
162+
#ifdef _DPI_AWARENESS_CONTEXTS_
163+
HMODULE user32 =LoadLibrary("user32.dll");
164+
165+
typedefDPI_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+
typedefBOOL (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+
returnPyBool_FromLong(per_monitor);
199+
#endif
200+
#endif
201+
202+
Py_RETURN_NONE;
203+
}
204+
98205
static 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 globalTcl/Tk function pointers by dynamic loading.
104213

105214
template<classT>
106215
intload_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<classT>
229+
intload_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

126244
voidload_tkinter_funcs(void)
127245
{
128-
// Load Tk functions by searching all modules in current process.
246+
// LoadTcl/Tk functions by searching all modules in current process.
129247
HMODULE hMods[1024];
130248
HANDLE hProcess;
131249
DWORD cbNeeded;
132250
unsignedint 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 forTcl/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
returnNULL;
336+
#ifdef WIN32_DLL
337+
}elseif (!TCL_SETVAR) {
338+
PyErr_SetString(PyExc_RuntimeError,"Failed to load Tcl_SetVar");
339+
returnNULL;
340+
#endif
214341
}elseif (!TK_FIND_PHOTO) {
215342
PyErr_SetString(PyExc_RuntimeError,"Failed to load Tk_FindPhoto");
216343
returnNULL;

‎src/_tkmini.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ typedef void (*Tk_PhotoPutBlock_NoComposite_t) (Tk_PhotoHandle handle,
9595
Tk_PhotoImageBlock*blockPtr,intx,inty,
9696
intwidth,intheight);
9797

98+
#ifdefWIN32_DLL
99+
/* Typedefs derived from function signatures in Tcl header */
100+
typedefconstchar*(*Tcl_SetVar_t)(Tcl_Interp*interp,constchar*varName,
101+
constchar*newValue,intflags);
102+
#endif
103+
98104
#ifdef__cplusplus
99105
}
100106
#endif

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp