5
5
import os .path
6
6
import sys
7
7
import tkinter as tk
8
- from tkinter .simpledialog import SimpleDialog
9
8
import tkinter .filedialog
9
+ import tkinter .font
10
10
import tkinter .messagebox
11
+ from tkinter .simpledialog import SimpleDialog
11
12
12
13
import numpy as np
14
+ from PIL import Image , ImageTk
13
15
14
16
import matplotlib as mpl
15
17
from matplotlib import _api , backend_tools , cbook , _c_internal_utils
@@ -164,10 +166,9 @@ class FigureCanvasTk(FigureCanvasBase):
164
166
alternative = "get_tk_widget().bind('<Configure>', ..., True)" )
165
167
def __init__ (self , figure = None , master = None , resize_callback = None ):
166
168
super ().__init__ (figure )
167
- self ._idle = True
168
- self ._idle_callback = None
169
+ self ._idle_draw_id = None
169
170
self ._event_loop_id = None
170
- w , h = self .figure . bbox . size . astype ( int )
171
+ w , h = self .get_width_height ( physical = True )
171
172
self ._tkcanvas = tk .Canvas (
172
173
master = master , background = "white" ,
173
174
width = w , height = h , borderwidth = 0 , highlightthickness = 0 )
@@ -176,6 +177,7 @@ def __init__(self, figure=None, master=None, resize_callback=None):
176
177
self ._tkcanvas .create_image (w // 2 , h // 2 , image = self ._tkphoto )
177
178
self ._resize_callback = resize_callback
178
179
self ._tkcanvas .bind ("<Configure>" , self .resize )
180
+ self ._tkcanvas .bind ("<Map>" , self ._update_device_pixel_ratio )
179
181
self ._tkcanvas .bind ("<Key>" , self .key_press )
180
182
self ._tkcanvas .bind ("<Motion>" , self .motion_notify_event )
181
183
self ._tkcanvas .bind ("<Enter>" , self .enter_notify_event )
@@ -210,6 +212,18 @@ def filter_destroy(event):
210
212
self ._master = master
211
213
self ._tkcanvas .focus_set ()
212
214
215
+ def _update_device_pixel_ratio (self , event = None ):
216
+ # Tk gives scaling with respect to 72 DPI, but most (all?) screens are
217
+ # scaled vs 96 dpi, and pixel ratio settings are given in whole
218
+ # percentages, so round to 2 digits.
219
+ ratio = round (self ._master .call ('tk' , 'scaling' ) / (96 / 72 ), 2 )
220
+ if self ._set_device_pixel_ratio (ratio ):
221
+ # The easiest way to resize the canvas is to resize the canvas
222
+ # widget itself, since we implement all the logic for resizing the
223
+ # canvas backing store on that event.
224
+ w , h = self .get_width_height (physical = True )
225
+ self ._tkcanvas .configure (width = w , height = h )
226
+
213
227
def resize (self , event ):
214
228
width , height = event .width , event .height
215
229
if self ._resize_callback is not None :
@@ -230,18 +244,16 @@ def resize(self, event):
230
244
231
245
def draw_idle (self ):
232
246
# docstring inherited
233
- if not self ._idle :
247
+ if self ._idle_draw_id :
234
248
return
235
249
236
- self ._idle = False
237
-
238
250
def idle_draw (* args ):
239
251
try :
240
252
self .draw ()
241
253
finally :
242
- self ._idle = True
254
+ self ._idle_draw_id = None
243
255
244
- self ._idle_callback = self ._tkcanvas .after_idle (idle_draw )
256
+ self ._idle_draw_id = self ._tkcanvas .after_idle (idle_draw )
245
257
246
258
def get_tk_widget (self ):
247
259
"""
@@ -407,6 +419,16 @@ def __init__(self, canvas, num, window):
407
419
if self .toolbar :
408
420
backend_tools .add_tools_to_container (self .toolbar )
409
421
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
+
410
432
self ._shown = False
411
433
412
434
def _get_toolbar (self ):
@@ -418,6 +440,13 @@ def _get_toolbar(self):
418
440
toolbar = None
<
92B1
/tr>419
441
return toolbar
420
442
443
+ def _update_window_dpi (self , * args ):
444
+ newdpi = self ._window_dpi .get ()
445
+ self .window .call ('tk' , 'scaling' , newdpi / 72 )
446
+ if self .toolbar and hasattr (self .toolbar , '_rescale' ):
447
+ self .toolbar ._rescale ()
448
+ self .canvas ._update_device_pixel_ratio ()
449
+
421
450
def resize (self , width , height ):
422
451
max_size = 1_400_000 # the measured max on xorg 1.20.8 was 1_409_023
423
452
@@ -447,8 +476,8 @@ def destroy(*args):
447
476
self ._shown = True
448
477
449
478
def destroy (self , * args ):
450
- if self .canvas ._idle_callback :
451
- self .canvas ._tkcanvas .after_cancel (self .canvas ._idle_callback )
479
+ if self .canvas ._idle_draw_id :
480
+ self .canvas ._tkcanvas .after_cancel (self .canvas ._idle_draw_id )
452
481
if self .canvas ._event_loop_id :
453
482
self .canvas ._tkcanvas .after_cancel (self .canvas ._event_loop_id )
454
483
@@ -514,22 +543,52 @@ def __init__(self, canvas, window, *, pack_toolbar=True):
514
543
if tooltip_text is not None :
515
544
ToolTip .createToolTip (button , tooltip_text )
516
545
546
+ self ._label_font = tkinter .font .Font (size = 10 )
547
+
517
548
# This filler item ensures the toolbar is always at least two text
518
549
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
519
550
# over images because those use two-line messages which resize the
520
551
# toolbar.
521
- label = tk .Label (master = self ,
552
+ label = tk .Label (master = self , font = self . _label_font ,
522
553
text = '\N{NO-BREAK SPACE} \n \N{NO-BREAK SPACE} ' )
523
554
label .pack (side = tk .RIGHT )
524
555
525
556
self .message = tk .StringVar (master = self )
526
- self ._message_label = tk .Label (master = self , textvariable = self .message )
557
+ self ._message_label = tk .Label (master = self , font = self ._label_font ,
558
+ textvariable = self .message )
527
559
self ._message_label .pack (side = tk .RIGHT )
528
560
529
561
NavigationToolbar2 .__init__ (self , canvas )
530
562
if pack_toolbar :
531
563
self .pack (side = tk .BOTTOM , fill = tk .X )
532
564
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
+ for widget in self .winfo_children ():
576
+ if isinstance (widget , (tk .Button , tk .Checkbutton )):
577
+ if hasattr (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
+ elif isinstance (widget , tk .Frame ):
584
+ widget .configure (height = '22p' , pady = '1p' )
585
+ widget .pack_configure (padx = '4p' )
586
+ elif isinstance (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
+
533
592
def _update_buttons_checked (self ):
534
593
# sync button checkstates to match active mode
535
594
for text , mode in [('Zoom' , _Mode .ZOOM ), ('Pan' , _Mode .PAN )]:
@@ -571,15 +630,25 @@ def set_cursor(self, cursor):
571
630
except tkinter .TclError :
572
631
pass
573
632
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
+ if button ._image_file is None :
640
+ return
641
+
642
+ size = button .winfo_pixels ('18p' )
643
+ with Image .open (button ._image_file .replace ('.png' , '_large.png' )
644
+ if size > 24 else button ._image_file ) as im :
645
+ image = ImageTk .PhotoImage (im .resize ((size , size )), master = self )
646
+ button .configure (image = image , height = '18p' , width = '18p' )
647
+ button ._ntimage = image # Prevent garbage collection.
648
+
574
649
def _Button (self , text , image_file , toggle , command ):
575
- if tk .TkVersion >= 8.6 :
576
- PhotoImage = tk .PhotoImage
577
- else :
578
- from PIL .ImageTk import PhotoImage
579
- image = (PhotoImage (master = self , file = image_file )
580
- if image_file is not None else None )
581
650
if not toggle :
582
- b = tk .Button (master = self , text = text , image = image , command = command )
651
+ b = tk .Button (master = self , text = text , command = command )
583
652
else :
584
653
# There is a bug in tkinter included in some python 3.6 versions
585
654
# that without this variable, produces a "visual" toggling of
@@ -588,18 +657,22 @@ def _Button(self, text, image_file, toggle, command):
588
657
# https://bugs.python.org/issue25684
589
658
var = tk .IntVar (master = self )
590
659
b = tk .Checkbutton (
591
- master = self , text = text , image = image , command = command ,
660
+ master = self , text = text , command = command ,
592
661
indicatoron = False , variable = var )
593
662
b .var = var
594
- b ._ntimage = image
663
+ b ._image_file = image_file
664
+ if image_file is not None :
665
+ # Explicit class because ToolbarTk calls _Button.
666
+ NavigationToolbar2Tk ._set_image_for_button (self , b )
667
+ else :
668
+ b .configure (font = self ._label_font )
595
669
b .pack (side = tk .LEFT )
596
670
return b
597
671
598
672
def _Spacer (self ):
599
- # Buttons are 30px high. Make this 26px tall +2px padding to center it.
600
- s = tk .Frame (
601
- master = self , height = 26 , relief = tk .RIDGE , pady = 2 , bg = "DarkGray" )
602
- s .pack (side = tk .LEFT , padx = 5 )
673
+ # Buttons are also 18pt high.
674
+ s = tk .Frame (master = self , height = '18p' , relief = tk .RIDGE , bg = 'DarkGray' )
675
+ s .pack (side = tk .LEFT , padx = '3p' )
603
676
return s
604
677
605
678
def save_figure (self , * args ):
@@ -734,13 +807,18 @@ def __init__(self, toolmanager, window):
734
807
tk .Frame .__init__ (self , master = window ,
735
808
width = int (width ), height = int (height ),
736
809
borderwidth = 2 )
810
+ self ._label_font = tkinter .font .Font (size = 10 )
737
811
self ._message = tk .StringVar (master = self )
738
- self ._message_label = tk .Label (master = self , textvariable = self ._message )
812
+ self ._message_label = tk .Label (master = self , font = self ._label_font ,
813
+ textvariable = self ._message )
739
814
self ._message_label .pack (side = tk .RIGHT )
740
815
self ._toolitems = {}
741
816
self .pack (side = tk .TOP , fill = tk .X )
742
817
self ._groups = {}
743
818
819
+ def _rescale (self ):
820
+ return NavigationToolbar2Tk ._rescale (self )
821
+
744
822
def add_toolitem (
745
823
self , name , group , position , image_file , description , toggle ):
746
824
frame = self ._get_groupframe (group )
@@ -847,6 +925,7 @@ def new_figure_manager_given_figure(cls, num, figure):
847
925
with _restore_foreground_window_at_end ():
848
926
if cbook ._get_running_interactive_framework () is None :
849
927
cbook ._setup_new_guiapp ()
928
+ _c_internal_utils .Win32_SetProcessDpiAwareness_max ()
850
929
window = tk .Tk (className = "matplotlib" )
851
930
window .withdraw ()
852
931
0 commit comments