From a96b4a11011dfd47a153fe99f101a06f7126a24a Mon Sep 17 00:00:00 2001 From: Jorge Moraleda Date: Mon, 4 Mar 2024 08:12:39 -0500 Subject: [PATCH 1/3] Add support for High DPI displays to wxAgg backend --- lib/matplotlib/backends/backend_wx.py | 61 ++++++++++++++---------- lib/matplotlib/backends/backend_wxagg.py | 16 ++++--- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 218be8947695..ed5d6c76d7c9 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -14,9 +14,6 @@ import sys import weakref -import numpy as np -import PIL.Image - import matplotlib as mpl from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, @@ -30,6 +27,7 @@ from matplotlib.transforms import Affine2D import wx +import wx.svg _log = logging.getLogger(__name__) @@ -473,10 +471,8 @@ def __init__(self, parent, id, figure=None): FigureCanvasBase.__init__(self, figure) w, h = map(math.ceil, self.figure.bbox.size) # Set preferred window size hint - helps the sizer, if one is connected - wx.Panel.__init__(self, parent, id, size=wx.Size(w, h)) - # Create the drawing bitmap - self.bitmap = wx.Bitmap(w, h) - _log.debug("%s - __init__() - bitmap w:%d h:%d", type(self), w, h) + wx.Panel.__init__(self, parent, id, size=parent.FromDIP(wx.Size(w, h))) + self.bitmap = None self._isDrawn = False self._rubberband_rect = None self._rubberband_pen_black = wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH) @@ -512,6 +508,12 @@ def __init__(self, parent, id, figure=None): self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker. self.SetBackgroundColour(wx.WHITE) + if wx.Platform == '__WXMAC__': + # Initial scaling. Other platforms handle this automatically + dpiScale = self.GetDPIScaleFactor() + self.SetInitialSize(self.GetSize()*(1/dpiScale)) + self._set_device_pixel_ratio(dpiScale) + def Copy_to_Clipboard(self, event=None): """Copy bitmap of canvas to system clipboard.""" bmp_obj = wx.BitmapDataObject() @@ -524,6 +526,12 @@ def Copy_to_Clipboard(self, event=None): wx.TheClipboard.Flush() wx.TheClipboard.Close() + def _update_device_pixel_ratio(self, *args, **kwargs): + # We need to be careful in cases with mixed resolution displays if + # device_pixel_ratio changes. + if self._set_device_pixel_ratio(self.GetDPIScaleFactor()): + self.draw() + def draw_idle(self): # docstring inherited _log.debug("%s - draw_idle()", type(self)) @@ -631,7 +639,7 @@ def _on_size(self, event): In this application we attempt to resize to fit the window, so it is better to take the performance hit and redraw the whole window. """ - + self._update_device_pixel_ratio() _log.debug("%s - _on_size()", type(self)) sz = self.GetParent().GetSizer() if sz: @@ -655,9 +663,10 @@ def _on_size(self, event): return # Empty figure # Create a new, correctly sized bitmap - self.bitmap = wx.Bitmap(self._width, self._height) - dpival = self.figure.dpi + if not wx.Platform == '__WXMSW__': + scale = self.GetDPIScaleFactor() + dpival /= scale winch = self._width / dpival hinch = self._height / dpival self.figure.set_size_inches(winch, hinch, forward=False) @@ -712,7 +721,11 @@ def _mpl_coords(self, pos=None): else: x, y = pos.X, pos.Y # flip y so y=0 is bottom of canvas - return x, self.figure.bbox.height - y + if not wx.Platform == '__WXMSW__': + scale = self.GetDPIScaleFactor() + return x*scale, self.figure.bbox.height - y*scale + else: + return x, self.figure.bbox.height - y def _on_key_down(self, event): """Capture key press.""" @@ -898,8 +911,8 @@ def __init__(self, num, fig, *, canvas_class): # On Windows, canvas sizing must occur after toolbar addition; # otherwise the toolbar further resizes the canvas. w, h = map(math.ceil, fig.bbox.size) - self.canvas.SetInitialSize(wx.Size(w, h)) - self.canvas.SetMinSize((2, 2)) + self.canvas.SetInitialSize(self.FromDIP(wx.Size(w, h))) + self.canvas.SetMinSize(self.FromDIP(wx.Size(2, 2))) self.canvas.SetFocus() self.Fit() @@ -1017,9 +1030,9 @@ def _set_frame_icon(frame): class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar): def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM): wx.ToolBar.__init__(self, canvas.GetParent(), -1, style=style) + if wx.Platform == '__WXMAC__': + self.SetToolBitmapSize(self.GetToolBitmapSize()*self.GetDPIScaleFactor()) - if 'wxMac' in wx.PlatformInfo: - self.SetToolBitmapSize((24, 24)) self.wx_ids = {} for text, tooltip_text, image_file, callback in self.toolitems: if text is None: @@ -1028,7 +1041,7 @@ def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM): self.wx_ids[text] = ( self.AddTool( -1, - bitmap=self._icon(f"{image_file}.png"), + bitmap=self._icon(f"{image_file}.svg"), bmpDisabled=wx.NullBitmap, label=text, shortHelp=tooltip_text, kind=(wx.ITEM_CHECK if text in ["Pan", "Zoom"] @@ -1054,9 +1067,7 @@ def _icon(name): *name*, including the extension and relative to Matplotlib's "images" data directory. """ - pilimg = PIL.Image.open(cbook._get_data_path("images", name)) - # ensure RGBA as wx BitMap expects RGBA format - image = np.array(pilimg.convert("RGBA")) + svg = cbook._get_data_path("images", name).read_bytes() try: dark = wx.SystemSettings.GetAppearance().IsDark() except AttributeError: # wxpython < 4.1 @@ -1068,11 +1079,9 @@ def _icon(name): fg_lum = (.299 * fg.red + .587 * fg.green + .114 * fg.blue) / 255 dark = fg_lum - bg_lum > .2 if dark: - fg = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) - black_mask = (image[..., :3] == 0).all(axis=-1) - image[black_mask, :3] = (fg.Red(), fg.Green(), fg.Blue()) - return wx.Bitmap.FromBufferRGBA( - image.shape[1], image.shape[0], image.tobytes()) + svg = svg.replace(b'fill:black;', b'fill:white;') + toolbarIconSize = wx.ArtProvider().GetDIPSizeHint(wx.ART_TOOLBAR) + return wx.BitmapBundle.FromSVG(svg, toolbarIconSize) def _update_buttons_checked(self): if "Pan" in self.wx_ids: @@ -1123,7 +1132,9 @@ def save_figure(self, *args): def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height - self.canvas._rubberband_rect = (x0, height - y0, x1, height - y1) + sf = self.GetDPIScaleFactor() + self.canvas._rubberband_rect = (x0/sf, (height - y0)/sf, + x1/sf, (height - y1)/sf) self.canvas.Refresh() def remove_rubberband(self): diff --git a/lib/matplotlib/backends/backend_wxagg.py b/lib/matplotlib/backends/backend_wxagg.py index a5a9de07153d..ab7703ffa02b 100644 --- a/lib/matplotlib/backends/backend_wxagg.py +++ b/lib/matplotlib/backends/backend_wxagg.py @@ -12,13 +12,13 @@ def draw(self, drawDC=None): Render the figure using agg. """ FigureCanvasAgg.draw(self) - self.bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba()) + self.bitmap = self._create_bitmap() self._isDrawn = True self.gui_repaint(drawDC=drawDC) def blit(self, bbox=None): # docstring inherited - bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba()) + bitmap = self._create_bitmap() if bbox is None: self.bitmap = bitmap else: @@ -31,11 +31,13 @@ def blit(self, bbox=None): srcDC.SelectObject(wx.NullBitmap) self.gui_repaint() - -def _rgba_to_wx_bitmap(rgba): - """Convert an RGBA buffer to a wx.Bitmap.""" - h, w, _ = rgba.shape - return wx.Bitmap.FromBufferRGBA(w, h, rgba) + def _create_bitmap(self): + """Create a wx.Bitmap from the renderer RGBA buffer""" + rgba = self.get_renderer().buffer_rgba() + h, w, _ = rgba.shape + bitmap = wx.Bitmap.FromBufferRGBA(w, h, rgba) + bitmap.SetScaleFactor(self.GetDPIScaleFactor()) + return bitmap @_BackendWx.export From 268392f33373c073ce1e742739e22da6d036fea9 Mon Sep 17 00:00:00 2001 From: Jorge Moraleda Date: Wed, 6 Mar 2024 18:07:30 -0500 Subject: [PATCH 2/3] Set per-monitor awareness on MSW when matplotlib creates the wx application. --- lib/matplotlib/backends/backend_wx.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index ed5d6c76d7c9..650694063f00 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -43,6 +43,10 @@ def _create_wxapp(): wxapp = wx.App(False) wxapp.SetExitOnFrameDelete(True) cbook._setup_new_guiapp() + if wx.Platform == '__WXMSW__': + # Set per-process DPI awareness. See https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness + import ctypes + ctypes.windll.shcore.SetProcessDpiAwareness(2) return wxapp @@ -469,9 +473,11 @@ def __init__(self, parent, id, figure=None): """ FigureCanvasBase.__init__(self, figure) - w, h = map(math.ceil, self.figure.bbox.size) + size = wx.Size(*map(math.ceil, self.figure.bbox.size)) + if wx.Platform != '__WXMSW__': + size = parent.FromDIP(size) # Set preferred window size hint - helps the sizer, if one is connected - wx.Panel.__init__(self, parent, id, size=parent.FromDIP(wx.Size(w, h))) + wx.Panel.__init__(self, parent, id, size=size) self.bitmap = None self._isDrawn = False self._rubberband_rect = None @@ -1132,7 +1138,7 @@ def save_figure(self, *args): def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height - sf = self.GetDPIScaleFactor() + sf = 1 if wx.Platform == '__WXMSW__' else self.GetDPIScaleFactor() self.canvas._rubberband_rect = (x0/sf, (height - y0)/sf, x1/sf, (height - y1)/sf) self.canvas.Refresh() From 6c1991d88c215e261d1a539bbdd03d2c972a477b Mon Sep 17 00:00:00 2001 From: Jorge Moraleda Date: Wed, 27 Mar 2024 08:19:59 -0400 Subject: [PATCH 3/3] Use wrapper to set max DPI awareness in MSW in wx backend --- lib/matplotlib/backends/backend_wx.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 650694063f00..0ae70707ac0d 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -21,7 +21,7 @@ TimerBase, ToolContainerBase, cursors, CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) -from matplotlib import _api, cbook, backend_tools +from matplotlib import _api, cbook, backend_tools, _c_internal_utils from matplotlib._pylab_helpers import Gcf from matplotlib.path import Path from matplotlib.transforms import Affine2D @@ -43,10 +43,8 @@ def _create_wxapp(): wxapp = wx.App(False) wxapp.SetExitOnFrameDelete(True) cbook._setup_new_guiapp() - if wx.Platform == '__WXMSW__': - # Set per-process DPI awareness. See https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness - import ctypes - ctypes.windll.shcore.SetProcessDpiAwareness(2) + # Set per-process DPI awareness. This is a NoOp except in MSW + _c_internal_utils.Win32_SetProcessDpiAwareness_max() return wxapp