diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index c27c876373a0..3abe934a79f9 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -227,12 +227,8 @@ def __init__(self, figure): 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. We could watch for screenChanged events from Qt, but - # the issue is that we can't guarantee this will be emitted *before* - # the first paintEvent for the canvas, so instead we keep track of the - # dpi_ratio value here and in paintEvent we resize the canvas if - # needed. - self._dpi_ratio_prev = None + # accordingly. + self._dpi_ratio_prev = self._dpi_ratio self._draw_pending = False self._is_drawing = False @@ -253,12 +249,9 @@ def _update_figure_dpi(self): def _dpi_ratio(self): return _devicePixelRatioF(self) - def _update_dpi(self): - # As described in __init__ above, we need to be careful in cases with - # mixed resolution displays if dpi_ratio is changing between painting - # events. - # Return whether we triggered a resizeEvent (and thus a paintEvent) - # from within this function. + 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() @@ -270,8 +263,20 @@ def _update_dpi(self): self.resizeEvent(event) # resizeEvent triggers a paintEvent itself, so we exit this one # (after making sure that the event is immediately handled). - return True - return False + + def _update_screen(self, screen): + # Handler for changes to a window's attached screen. + self._update_pixel_ratio() + if screen is not None: + screen.physicalDotsPerInchChanged.connect(self._update_pixel_ratio) + screen.logicalDotsPerInchChanged.connect(self._update_pixel_ratio) + + def showEvent(self, event): + # Set up correct pixel ratio, and connect to any signal changes for it, + # once the window is shown (and thus has these attributes). + window = self.window().windowHandle() + window.screenChanged.connect(self._update_screen) + self._update_screen(window.screen()) def get_width_height(self): w, h = FigureCanvasBase.get_width_height(self) @@ -364,10 +369,6 @@ def keyReleaseEvent(self, event): FigureCanvasBase.key_release_event(self, key, guiEvent=event) def resizeEvent(self, event): - # _dpi_ratio_prev will be set the first time the canvas is painted, and - # the rendered buffer is useless before anyways. - if self._dpi_ratio_prev is None: - return w = event.size().width() * self._dpi_ratio h = event.size().height() * self._dpi_ratio dpival = self.figure.dpi diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 80e670b3f3e4..90eafca5334d 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -27,9 +27,6 @@ def paintEvent(self, event): In Qt, all drawing should be done inside of here when a widget is shown onscreen. """ - if self._update_dpi(): - # The dpi update triggered its own paintEvent. - return self._draw_idle() # Only does something if a draw is pending. # If the canvas does not have a renderer, then give up and wait for diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index c79b0bc22260..a375d1f4d1e2 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -17,7 +17,6 @@ def draw(self): super().draw() def paintEvent(self, event): - self._update_dpi() dpi_ratio = self._dpi_ratio width = int(dpi_ratio * self.width()) height = int(dpi_ratio * self.height()) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index ab90da74d51e..afc6711158a5 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -158,32 +158,38 @@ def on_key_press(event): @pytest.mark.backend('Qt5Agg') -def test_dpi_ratio_change(): +def test_pixel_ratio_change(): """ - Make sure that if _dpi_ratio changes, the figure dpi changes but the + Make sure that if the pixel ratio changes, the figure dpi changes but the widget remains the same physical size. """ - prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT._dpi_ratio' - - with mock.patch(prop, new_callable=mock.PropertyMock) as p: - + prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF' + with mock.patch(prop) as p: p.return_value = 3 fig = plt.figure(figsize=(5, 2), dpi=120) qt_canvas = fig.canvas qt_canvas.show() - from matplotlib.backends.backend_qt5 import qApp + def set_pixel_ratio(ratio): + p.return_value = ratio + # Make sure the mocking worked + assert qt_canvas._dpi_ratio == ratio - # Make sure the mocking worked - assert qt_canvas._dpi_ratio == 3 + # The value here doesn't matter, as we can't mock the C++ QScreen + # object, but can override the functional wrapper around it. + # Emitting this event is simply to trigger the DPI change handler + # in Matplotlib in the same manner that it would occur normally. + screen.logicalDotsPerInchChanged.emit(96) - size = qt_canvas.size() + qt_canvas.draw() + qt_canvas.flush_events() qt_canvas.manager.show() - qt_canvas.draw() - qApp.processEvents() + size = qt_canvas.size() + screen = qt_canvas.window().windowHandle().screen() + set_pixel_ratio(3) # The DPI and the renderer width/height change assert fig.dpi == 360 @@ -196,17 +202,7 @@ def test_dpi_ratio_change(): assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - p.return_value = 2 - - assert qt_canvas._dpi_ratio == 2 - - qt_canvas.draw() - qApp.processEvents() - # this second processEvents is required to fully run the draw. - # On `update` we notice the DPI has changed and trigger a - # resize event to refresh, the second processEvents is - # required to process that and fully update the window sizes. - qApp.processEvents() + set_pixel_ratio(2) # The DPI and the renderer width/height change assert fig.dpi == 240 @@ -219,17 +215,7 @@ def test_dpi_ratio_change(): assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - p.return_value = 1.5 - - assert qt_canvas._dpi_ratio == 1.5 - - qt_canvas.draw() - qApp.processEvents() - # this second processEvents is required to fully run the draw. - # On `update` we notice the DPI has changed and trigger a - # resize event to refresh, the second processEvents is - # required to process that and fully update the window sizes. - qApp.processEvents() + set_pixel_ratio(1.5) # The DPI and the renderer width/height change assert fig.dpi == 180