3
3
import logging
4
4
import math
5
5
import os .path
6
+ import queue
6
7
import sys
7
8
import tkinter as tk
8
9
import tkinter .filedialog
@@ -409,6 +410,13 @@ def __init__(self, canvas, num, window):
409
410
if self .toolbar :
410
411
backend_tools .add_tools_to_container (self .toolbar )
411
412
413
+ # If the window has per-monitor DPI awareness, then setup a poll to
414
+ # check the DPI change queue, to then update the scaling.
415
+ dpi_queue = queue .SimpleQueue ()
416
+ if _tkagg .enable_dpi_awareness (int (window .frame (), 16 ), dpi_queue ):
417
+ self ._dpi_queue = dpi_queue
418
+ window .after (100 , self ._update_dpi_ratio )
419
+
412
420
self ._shown = False
413
421
414
422
def _get_toolbar (self ):
@@ -420,6 +428,24 @@ def _get_toolbar(self):
420
428
toolbar = None
421
429
return toolbar
422
430
431
+ def _update_dpi_ratio (self ):
432
+ """
433
+ Polling function to update DPI ratio for Tk.
434
+
435
+ This must continuously check the queue, instead of waiting for a Tk
436
+ event, due to thread locking issues in the Python/Tk interface.
437
+ """
438
+ try :
439
+ newdpi = self ._dpi_queue .get (block = False )
440
+ except queue .Empty :
441
+ self .window .after (100 , self ._update_dpi_ratio )
442
+ return
443
+ self .window .call ('tk' , 'scaling' , newdpi / 72 )
444
+ if self .toolbar and hasattr (self .toolbar , '_rescale' ):
445
+ self .toolbar ._rescale ()
446
+ self .canvas ._update_device_pixel_ratio ()
447
+ self .window .after (100 , self ._update_dpi_ratio )
448
+
423
449
def resize (self , width , height ):
424
450
max_size = 1_400_000 # the measured max on xorg 1.20.8 was 1_409_023
425
451
@@ -532,6 +558,32 @@ def __init__(self, canvas, window, *, pack_toolbar=True):
532
558
if pack_toolbar :
533
559
self .pack (side = tk .BOTTOM , fill = tk .X )
534
560
561
+ def _rescale (self ):
562
+ """
563
+ Scale all children of the toolbar to current DPI setting.
564
+
565
+ Before this is called, the Tk scaling setting will have been updated to
566
+ match the new DPI. Tk widgets do not update for changes to scaling, but
567
+ all measurements made after the change will match the new scaling. Thus
568
+ this function re-applies all the same sizes in physical units (points),
569
+ which Tk will scale correctly to pixels.
570
+ """
571
+ for widget in self .winfo_children ():
572
+ if isinstance (widget , (tk .Button , tk .Checkbutton )):
573
+ if hasattr (widget , '_image_file' ):
574
+ # Explicit class because ToolbarTk calls _rescale.
575
+ NavigationToolbar2Tk ._set_image_for_button (self , widget )
576
+ else :
577
+ pass # This is handled by the font setting instead.
578
+ elif isinstance (widget , tk .Frame ):
579
+ widget .configure (height = '22p' , pady = '1p' )
580
+ widget .pack_configure (padx = '4p' )
581
+ elif isinstance (widget , tk .Label ):
582
+ pass # This is handled by the font setting instead.
583
+ else :
584
+ _log .warning ('Unknown child class %s' , widget .winfo_class )
585
+ self ._label_font .configure (size = 10 )
586
+
535
587
def _update_buttons_checked (self ):
536
588
# sync button checkstates to match active mode
537
589
for text , mode in [('Zoom' , _Mode .ZOOM ), ('Pan' , _Mode .PAN )]:
@@ -573,6 +625,22 @@ def set_cursor(self, cursor):
573
625
except tkinter .TclError :
574
626
pass
575
627
628
+ def _set_image_for_button (self , button ):
629
+ """
630
+ Set the image for a button based on its pixel size.
631
+
632
+ The pixel size is determined by the DPI scaling of the window.
633
+ """
634
+ if button ._image_file is None :
635
+ return
636
+
637
+ size = button .winfo_pixels ('24p' )
638
+ with Image .open (button ._image_file .replace ('.png' , '_large.png' )
639
+ if size > 24 else button ._image_file ) as im :
640
+ image = ImageTk .PhotoImage (im .resize ((size , size )), master = self )
641
+ button .configure (image = image , height = '24p' , width = '24p' )
642
+ button ._ntimage = image # Prevent garbage collection.
643
+
576
644
def _Button (self , text , image_file , toggle , command ):
577
645
if not toggle :
578
646
b = tk .Button (master = self , text = text , command = command )
@@ -587,14 +655,10 @@ def _Button(self, text, image_file, toggle, command):
587
655
master = self , text = text , command = command ,
588
656
indicatoron = False , variable = var )
589
657
b .var = var
658
+ b ._image_file = image_file
590
659
if image_file is not None :
591
- size = b .winfo_pixels ('24p' )
592
- with Image .open (image_file .replace ('.png' , '_large.png' )
593
- if size > 24 else image_file ) as im :
594
- image = ImageTk .PhotoImage (im .resize ((size , size )),
595
- master = self )
596
- b .configure (image = image , height = '24p' , width = '24p' )
597
- b ._ntimage = image # Prevent garbage collection.
660
+ # Explicit class because ToolbarTk calls _Button.
661
+ NavigationToolbar2Tk ._set_image_for_button (self , b )
598
662
else :
599
663
b .configure (font = self ._label_font )
600
664
b .pack (side = tk .LEFT )
@@ -748,6 +812,9 @@ def __init__(self, toolmanager, window):
748
812
self .pack (side = tk .TOP , fill = tk .X )
749
813
self ._groups = {}
750
814
815
+ def _rescale (self ):
816
+ return NavigationToolbar2Tk ._rescale (self )
817
+
751
818
def add_toolitem (
752
819
self , name , group , position , image_file , description , toggle ):
753
820
frame = self ._get_groupframe (group )
0 commit comments