diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 5af60bddf9c9..44da34ef06f9 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1729,6 +1729,9 @@ def __init__(self, figure=None): self.mouse_grabber = None # the axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me self._is_idle_drawing = False + # We don't want to scale up the figure DPI more than once. + figure._original_dpi = figure.dpi + self._device_pixel_ratio = 1 callbacks = property(lambda self: self.figure._canvas_callbacks) button_pick_id = property(lambda self: self.figure._button_pick_id) @@ -2054,12 +2057,73 @@ def draw_idle(self, *args, **kwargs): with self._idle_draw_cntx(): self.draw(*args, **kwargs) + @property + def device_pixel_ratio(self): + """ + The ratio of physical to logical pixels used for the canvas on screen. + + By default, this is 1, meaning physical and logical pixels are the same + size. Subclasses that support High DPI screens may set this property to + indicate that said ratio is different. All Matplotlib interaction, + unless working directly with the canvas, remains in logical pixels. + + """ + return self._device_pixel_ratio + + def _set_device_pixel_ratio(self, ratio): + """ + Set the ratio of physical to logical pixels used for the canvas. + + Subclasses that support High DPI screens can set this property to + indicate that said ratio is different. The canvas itself will be + created at the physical size, while the client side will use the + logical size. Thus the DPI of the Figure will change to be scaled by + this ratio. Implementations that support High DPI screens should use + physical pixels for events so that transforms back to Axes space are + correct. + + By default, this is 1, meaning physical and logical pixels are the same + size. + + Parameters + ---------- + ratio : float + The ratio of logical to physical pixels used for the canvas. + + Returns + ------- + bool + Whether the ratio has changed. Backends may interpret this as a + signal to resize the window, repaint the canvas, or change any + other relevant properties. + """ + if self._device_pixel_ratio == ratio: + return False + # In cases with mixed resolution displays, we need to be careful if the + # device pixel ratio changes - in this case we need to resize the + # canvas accordingly. Some backends provide events that indicate a + # change in DPI, but those that don't will update this before drawing. + dpi = ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + self._device_pixel_ratio = ratio + return True + def get_width_height(self): """ - Return the figure width and height in points or pixels - (depending on the backend), truncated to integers. + Return the figure width and height in integral points or pixels. + + When the figure is used on High DPI screens (and the backend supports + it), the truncation to integers occurs after scaling by the device + pixel ratio. + + Returns + ------- + width, height : int + The size of the figure, in points or pixels, depending on the + backend. """ - return int(self.figure.bbox.width), int(self.figure.bbox.height) + return tuple(int(size / self.device_pixel_ratio) + for size in self.figure.bbox.max) @classmethod def get_supported_filetypes(cls): diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 0c2d32e1e8d1..2d4e6aa62cb2 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -213,15 +213,6 @@ def __init__(self, figure=None): _create_qApp() super().__init__(figure=figure) - # We don't want to scale up the figure DPI more than once. - # Note, we don't handle a signal for changing DPI yet. - self.figure._original_dpi = self.figure.dpi - self._update_figure_dpi() - # In cases with mixed resolution displays, we need to be careful if the - # dpi_ratio changes - in this case we need to resize the canvas - # accordingly. - self._dpi_ratio_prev = self._dpi_ratio - self._draw_pending = False self._is_drawing = False self._draw_rect_callback = lambda painter: None @@ -233,28 +224,13 @@ def __init__(self, figure=None): palette = QtGui.QPalette(QtCore.Qt.white) self.setPalette(palette) - def _update_figure_dpi(self): - dpi = self._dpi_ratio * self.figure._original_dpi - self.figure._set_dpi(dpi, forward=False) - - @property - def _dpi_ratio(self): - return _devicePixelRatioF(self) - def _update_pixel_ratio(self): - # We need to be careful in cases with mixed resolution displays if - # dpi_ratio changes. - if self._dpi_ratio != self._dpi_ratio_prev: - # We need to update the figure DPI. - self._update_figure_dpi() - self._dpi_ratio_prev = self._dpi_ratio + if self._set_device_pixel_ratio(_devicePixelRatioF(self)): # The easiest way to resize the canvas is to emit a resizeEvent # since we implement all the logic for resizing the canvas for # that event. event = QtGui.QResizeEvent(self.size(), self.size()) self.resizeEvent(event) - # resizeEvent triggers a paintEvent itself, so we exit this one - # (after making sure that the event is immediately handled). def _update_screen(self, screen): # Handler for changes to a window's attached screen. @@ -270,10 +246,6 @@ def showEvent(self, event): window.screenChanged.connect(self._update_screen) self._update_screen(window.screen()) - def get_width_height(self): - w, h = FigureCanvasBase.get_width_height(self) - return int(w / self._dpi_ratio), int(h / self._dpi_ratio) - def enterEvent(self, event): try: x, y = self.mouseEventCoords(event.pos()) @@ -296,11 +268,10 @@ def mouseEventCoords(self, pos): Also, the origin is different and needs to be corrected. """ - dpi_ratio = self._dpi_ratio x = pos.x() # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height / dpi_ratio - pos.y() - return x * dpi_ratio, y * dpi_ratio + y = self.figure.bbox.height / self.device_pixel_ratio - pos.y() + return x * self.device_pixel_ratio, y * self.device_pixel_ratio def mousePressEvent(self, event): x, y = self.mouseEventCoords(event.pos()) @@ -361,8 +332,8 @@ def keyReleaseEvent(self, event): FigureCanvasBase.key_release_event(self, key, guiEvent=event) def resizeEvent(self, event): - w = event.size().width() * self._dpi_ratio - h = event.size().height() * self._dpi_ratio + w = event.size().width() * self.device_pixel_ratio + h = event.size().height() * self.device_pixel_ratio dpival = self.figure.dpi winch = w / dpival hinch = h / dpival @@ -460,7 +431,7 @@ def blit(self, bbox=None): if bbox is None and self.figure: bbox = self.figure.bbox # Blit the entire canvas if bbox is None. # repaint uses logical pixels, not physical pixels like the renderer. - l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds] + l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds] t = b + h self.repaint(l, self.rect().height() - t, w, h) @@ -481,11 +452,11 @@ def drawRectangle(self, rect): # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs # to be called at the end of paintEvent. if rect is not None: - x0, y0, w, h = [int(pt / self._dpi_ratio) for pt in rect] + x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect] x1 = x0 + w y1 = y0 + h def _draw_rect_callback(painter): - pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio) + pen = QtGui.QPen(QtCore.Qt.black, 1 / self.device_pixel_ratio) pen.setDashPattern([3, 3]) for color, offset in [ (QtCore.Qt.black, 0), (QtCore.Qt.white, 3)]: diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 897c28c38f04..3c5de72f7697 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -42,8 +42,8 @@ def paintEvent(self, event): # scale rect dimensions using the screen dpi ratio to get # correct values for the Figure coordinates (rather than # QT5's coords) - width = rect.width() * self._dpi_ratio - height = rect.height() * self._dpi_ratio + width = rect.width() * self.device_pixel_ratio + height = rect.height() * self.device_pixel_ratio left, top = self.mouseEventCoords(rect.topLeft()) # shift the "top" by the height of the image to get the # correct corner for our coordinate system @@ -61,7 +61,7 @@ def paintEvent(self, event): qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0], QtGui.QImage.Format_ARGB32_Premultiplied) - _setDevicePixelRatio(qimage, self._dpi_ratio) + _setDevicePixelRatio(qimage, self.device_pixel_ratio) # set origin using original QT coordinates origin = QtCore.QPoint(rect.left(), rect.top()) painter.drawImage(origin, qimage) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 4b6d7305e7c1..e15e0d858ad8 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -17,9 +17,8 @@ def draw(self): super().draw() def paintEvent(self, event): - dpi_ratio = self._dpi_ratio - width = int(dpi_ratio * self.width()) - height = int(dpi_ratio * self.height()) + width = int(self.device_pixel_ratio * self.width()) + height = int(self.device_pixel_ratio * self.height()) if (width, height) != self._renderer.get_canvas_width_height(): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) @@ -32,7 +31,7 @@ def paintEvent(self, event): # QImage under PySide on Python 3. if QT_API == 'PySide': ctypes.c_long.from_address(id(buf)).value = 1 - _setDevicePixelRatio(qimage, dpi_ratio) + _setDevicePixelRatio(qimage, self.device_pixel_ratio) painter = QtGui.QPainter(self) painter.eraseRect(event.rect()) painter.drawImage(0, 0, qimage) diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index ed0d6173a6fe..dbdd30e1aa50 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -138,10 +138,6 @@ def __init__(self, *args, **kwargs): # to the connected clients. self._current_image_mode = 'full' - # Store the DPI ratio of the browser. This is the scaling that - # occurs automatically for all images on a HiDPI display. - self._dpi_ratio = 1 - def show(self): # show the figure window from matplotlib.pyplot import show @@ -311,8 +307,8 @@ def handle_refresh(self, event): self.draw_idle() def handle_resize(self, event): - x, y = event.get('width', 800), event.get('height', 800) - x, y = int(x) * self._dpi_ratio, int(y) * self._dpi_ratio + x = int(event.get('width', 800)) * self.device_pixel_ratio + y = int(event.get('height', 800)) * self.device_pixel_ratio fig = self.figure # An attempt at approximating the figure size in pixels. fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False) @@ -327,14 +323,15 @@ def handle_send_image_mode(self, event): # The client requests notification of what the current image mode is. self.send_event('image_mode', mode=self._current_image_mode) + def handle_set_device_pixel_ratio(self, event): + self._handle_set_device_pixel_ratio(event.get('device_pixel_ratio', 1)) + def handle_set_dpi_ratio(self, event): - dpi_ratio = event.get('dpi_ratio', 1) - if dpi_ratio != self._dpi_ratio: - # We don't want to scale up the figure dpi more than once. - if not hasattr(self.figure, '_original_dpi'): - self.figure._original_dpi = self.figure.dpi - self.figure.dpi = dpi_ratio * self.figure._original_dpi - self._dpi_ratio = dpi_ratio + # This handler is for backwards-compatibility with older ipympl. + self._handle_set_device_pixel_ratio(event.get('dpi_ratio', 1)) + + def _handle_set_device_pixel_ratio(self, device_pixel_ratio): + if self._set_device_pixel_ratio(device_pixel_ratio): self._force_full = True self.draw_idle() @@ -426,7 +423,8 @@ def _get_toolbar(self, canvas): def resize(self, w, h, forward=True): self._send_event( 'resize', - size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio), + size=(w / self.canvas.device_pixel_ratio, + h / self.canvas.device_pixel_ratio), forward=forward) def set_window_title(self, title): diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 4adeb987653f..8b716a6410f6 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -63,7 +63,9 @@ mpl.figure = function (figure_id, websocket, ondownload, parent_element) { fig.send_message('supports_binary', { value: fig.supports_binary }); fig.send_message('send_image_mode', {}); if (fig.ratio !== 1) { - fig.send_message('set_dpi_ratio', { dpi_ratio: fig.ratio }); + fig.send_message('set_device_pixel_ratio', { + device_pixel_ratio: fig.ratio, + }); } fig.send_message('refresh', {}); }; diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index d0d997e1ce2e..050781b87151 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -166,10 +166,10 @@ def on_key_press(event): @pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_pixel_ratio_change(): +def test_device_pixel_ratio_change(): """ Make sure that if the pixel ratio changes, the figure dpi changes but the - widget remains the same physical size. + widget remains the same logical size. """ prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF' @@ -180,10 +180,8 @@ def test_pixel_ratio_change(): qt_canvas = fig.canvas qt_canvas.show() - def set_pixel_ratio(ratio): + def set_device_pixel_ratio(ratio): p.return_value = ratio - # Make sure the mocking worked - assert qt_canvas._dpi_ratio == ratio # The value here doesn't matter, as we can't mock the C++ QScreen # object, but can override the functional wrapper around it. @@ -194,43 +192,46 @@ def set_pixel_ratio(ratio): qt_canvas.draw() qt_canvas.flush_events() + # Make sure the mocking worked + assert qt_canvas.device_pixel_ratio == ratio + qt_canvas.manager.show() size = qt_canvas.size() screen = qt_canvas.window().windowHandle().screen() - set_pixel_ratio(3) + set_device_pixel_ratio(3) # The DPI and the renderer width/height change assert fig.dpi == 360 assert qt_canvas.renderer.width == 1800 assert qt_canvas.renderer.height == 720 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - set_pixel_ratio(2) + set_device_pixel_ratio(2) # The DPI and the renderer width/height change assert fig.dpi == 240 assert qt_canvas.renderer.width == 1200 assert qt_canvas.renderer.height == 480 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - set_pixel_ratio(1.5) + set_device_pixel_ratio(1.5) # The DPI and the renderer width/height change assert fig.dpi == 180 assert qt_canvas.renderer.width == 900 assert qt_canvas.renderer.height == 360 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy