Skip to content

Commit de11026

Browse files
authored
Merge pull request #26710 from jmoraleda/wx-hidpi
Add support for High DPI displays to wxAgg backend
2 parents ae73ddb + 6c1991d commit de11026

File tree

2 files changed

+51
-34
lines changed

2 files changed

+51
-34
lines changed

lib/matplotlib/backends/backend_wx.py

+42-27
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,20 @@
1414
import sys
1515
import weakref
1616

17-
import numpy as np
18-
import PIL.Image
19-
2017
import matplotlib as mpl
2118
from matplotlib.backend_bases import (
2219
_Backend, FigureCanvasBase, FigureManagerBase,
2320
GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase,
2421
TimerBase, ToolContainerBase, cursors,
2522
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
2623

27-
from matplotlib import _api, cbook, backend_tools
24+
from matplotlib import _api, cbook, backend_tools, _c_internal_utils
2825
from matplotlib._pylab_helpers import Gcf
2926
from matplotlib.path import Path
3027
from matplotlib.transforms import Affine2D
3128

3229
import wx
30+
import wx.svg
3331

3432
_log = logging.getLogger(__name__)
3533

@@ -45,6 +43,8 @@ def _create_wxapp():
4543
wxapp = wx.App(False)
4644
wxapp.SetExitOnFrameDelete(True)
4745
cbook._setup_new_guiapp()
46+
# Set per-process DPI awareness. This is a NoOp except in MSW
47+
_c_internal_utils.Win32_SetProcessDpiAwareness_max()
4848
return wxapp
4949

5050

@@ -471,12 +471,12 @@ def __init__(self, parent, id, figure=None):
471471
"""
472472

473473
FigureCanvasBase.__init__(self, figure)
474-
w, h = map(math.ceil, self.figure.bbox.size)
474+
size = wx.Size(*map(math.ceil, self.figure.bbox.size))
475+
if wx.Platform != '__WXMSW__':
476+
size = parent.FromDIP(size)
475477
# Set preferred window size hint - helps the sizer, if one is connected
476-
wx.Panel.__init__(self, parent, id, size=wx.Size(w, h))
477-
# Create the drawing bitmap
478-
self.bitmap = wx.Bitmap(w, h)
479-
_log.debug("%s - __init__() - bitmap w:%d h:%d", type(self), w, h)
478+
wx.Panel.__init__(self, parent, id, size=size)
479+
self.bitmap = None
480480
self._isDrawn = False
481481
self._rubberband_rect = None
482482
self._rubberband_pen_black = wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH)
@@ -512,6 +512,12 @@ def __init__(self, parent, id, figure=None):
512512
self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker.
513513
self.SetBackgroundColour(wx.WHITE)
514514

515+
if wx.Platform == '__WXMAC__':
516+
# Initial scaling. Other platforms handle this automatically
517+
dpiScale = self.GetDPIScaleFactor()
518+
self.SetInitialSize(self.GetSize()*(1/dpiScale))
519+
self._set_device_pixel_ratio(dpiScale)
520+
515521
def Copy_to_Clipboard(self, event=None):
516522
"""Copy bitmap of canvas to system clipboard."""
517523
bmp_obj = wx.BitmapDataObject()
@@ -524,6 +530,12 @@ def Copy_to_Clipboard(self, event=None):
524530
wx.TheClipboard.Flush()
525531
wx.TheClipboard.Close()
526532

533+
def _update_device_pixel_ratio(self, *args, **kwargs):
534+
# We need to be careful in cases with mixed resolution displays if
535+
# device_pixel_ratio changes.
536+
if self._set_device_pixel_ratio(self.GetDPIScaleFactor()):
537+
self.draw()
538+
527539
def draw_idle(self):
528540
# docstring inherited
529541
_log.debug("%s - draw_idle()", type(self))
@@ -631,7 +643,7 @@ def _on_size(self, event):
631643
In this application we attempt to resize to fit the window, so it
632644
is better to take the performance hit and redraw the whole window.
633645
"""
634-
646+
self._update_device_pixel_ratio()
635647
_log.debug("%s - _on_size()", type(self))
636648
sz = self.GetParent().GetSizer()
637649
if sz:
@@ -655,9 +667,10 @@ def _on_size(self, event):
655667
return # Empty figure
656668

657669
# Create a new, correctly sized bitmap
658-
self.bitmap = wx.Bitmap(self._width, self._height)
659-
660670
dpival = self.figure.dpi
671+
if not wx.Platform == '__WXMSW__':
672+
scale = self.GetDPIScaleFactor()
673+
dpival /= scale
661674
winch = self._width / dpival
662675
hinch = self._height / dpival
663676
self.figure.set_size_inches(winch, hinch, forward=False)
@@ -712,7 +725,11 @@ def _mpl_coords(self, pos=None):
712725
else:
713726
x, y = pos.X, pos.Y
714727
# flip y so y=0 is bottom of canvas
715-
return x, self.figure.bbox.height - y
728+
if not wx.Platform == '__WXMSW__':
729+
scale = self.GetDPIScaleFactor()
730+
return x*scale, self.figure.bbox.height - y*scale
731+
else:
732+
return x, self.figure.bbox.height - y
716733

717734
def _on_key_down(self, event):
718735
"""Capture key press."""
@@ -898,8 +915,8 @@ def __init__(self, num, fig, *, canvas_class):
898915
# On Windows, canvas sizing must occur after toolbar addition;
899916
# otherwise the toolbar further resizes the canvas.
900917
w, h = map(math.ceil, fig.bbox.size)
901-
self.canvas.SetInitialSize(wx.Size(w, h))
902-
self.canvas.SetMinSize((2, 2))
918+
self.canvas.SetInitialSize(self.FromDIP(wx.Size(w, h)))
919+
self.canvas.SetMinSize(self.FromDIP(wx.Size(2, 2)))
903920
self.canvas.SetFocus()
904921

905922
self.Fit()
@@ -1017,9 +1034,9 @@ def _set_frame_icon(frame):
10171034
class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar):
10181035
def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM):
10191036
wx.ToolBar.__init__(self, canvas.GetParent(), -1, style=style)
1037+
if wx.Platform == '__WXMAC__':
1038+
self.SetToolBitmapSize(self.GetToolBitmapSize()*self.GetDPIScaleFactor())
10201039

1021-
if 'wxMac' in wx.PlatformInfo:
1022-
self.SetToolBitmapSize((24, 24))
10231040
self.wx_ids = {}
10241041
for text, tooltip_text, image_file, callback in self.toolitems:
10251042
if text is None:
@@ -1028,7 +1045,7 @@ def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM):
10281045
self.wx_ids[text] = (
10291046
self.AddTool(
10301047
-1,
1031-
bitmap=self._icon(f"{image_file}.png"),
1048+
bitmap=self._icon(f"{image_file}.svg"),
10321049
bmpDisabled=wx.NullBitmap,
10331050
label=text, shortHelp=tooltip_text,
10341051
kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"]
@@ -1054,9 +1071,7 @@ def _icon(name):
10541071
*name*, including the extension and relative to Matplotlib's "images"
10551072
data directory.
10561073
"""
1057-
pilimg = PIL.Image.open(cbook._get_data_path("images", name))
1058-
# ensure RGBA as wx BitMap expects RGBA format
1059-
image = np.array(pilimg.convert("RGBA"))
1074+
svg = cbook._get_data_path("images", name).read_bytes()
10601075
try:
10611076
dark = wx.SystemSettings.GetAppearance().IsDark()
10621077
except AttributeError: # wxpython < 4.1
@@ -1068,11 +1083,9 @@ def _icon(name):
10681083
fg_lum = (.299 * fg.red + .587 * fg.green + .114 * fg.blue) / 255
10691084
dark = fg_lum - bg_lum > .2
10701085
if dark:
1071-
fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)
1072-
black_mask = (image[..., :3] == 0).all(axis=-1)
1073-
image[black_mask, :3] = (fg.Red(), fg.Green(), fg.Blue())
1074-
return wx.Bitmap.FromBufferRGBA(
1075-
image.shape[1], image.shape[0], image.tobytes())
1086+
svg = svg.replace(b'fill:black;', b'fill:white;')
1087+
toolbarIconSize = wx.ArtProvider().GetDIPSizeHint(wx.ART_TOOLBAR)
1088+
return wx.BitmapBundle.FromSVG(svg, toolbarIconSize)
10761089

10771090
def _update_buttons_checked(self):
10781091
if "Pan" in self.wx_ids:
@@ -1123,7 +1136,9 @@ def save_figure(self, *args):
11231136

11241137
def draw_rubberband(self, event, x0, y0, x1, y1):
11251138
height = self.canvas.figure.bbox.height
1126-
self.canvas._rubberband_rect = (x0, height - y0, x1, height - y1)
1139+
sf = 1 if wx.Platform == '__WXMSW__' else self.GetDPIScaleFactor()
1140+
self.canvas._rubberband_rect = (x0/sf, (height - y0)/sf,
1141+
x1/sf, (height - y1)/sf)
11271142
self.canvas.Refresh()
11281143

11291144
def remove_rubberband(self):

lib/matplotlib/backends/backend_wxagg.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ def draw(self, drawDC=None):
1212
Render the figure using agg.
1313
"""
1414
FigureCanvasAgg.draw(self)
15-
self.bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba())
15+
self.bitmap = self._create_bitmap()
1616
self._isDrawn = True
1717
self.gui_repaint(drawDC=drawDC)
1818

1919
def blit(self, bbox=None):
2020
# docstring inherited
21-
bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba())
21+
bitmap = self._create_bitmap()
2222
if bbox is None:
2323
self.bitmap = bitmap
2424
else:
@@ -31,11 +31,13 @@ def blit(self, bbox=None):
3131
srcDC.SelectObject(wx.NullBitmap)
3232
self.gui_repaint()
3333

34-
35-
def _rgba_to_wx_bitmap(rgba):
36-
"""Convert an RGBA buffer to a wx.Bitmap."""
37-
h, w, _ = rgba.shape
38-
return wx.Bitmap.FromBufferRGBA(w, h, rgba)
34+
def _create_bitmap(self):
35+
"""Create a wx.Bitmap from the renderer RGBA buffer"""
36+
rgba = self.get_renderer().buffer_rgba()
37+
h, w, _ = rgba.shape
38+
bitmap = wx.Bitmap.FromBufferRGBA(w, h, rgba)
39+
bitmap.SetScaleFactor(self.GetDPIScaleFactor())
40+
return bitmap
3941

4042

4143
@_BackendWx.export

0 commit comments

Comments
 (0)