From f8cb5c9e47f2a254f71146124b80d66e3a72db61 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 24 Jul 2017 16:08:06 +0100 Subject: [PATCH 1/5] Fix a bug with the Qt5 backend with mixed resolution displays When mixed-resolution displays are present, the _dpi_ratio attribute on the canvas may change between paintEvents, which means that we need to change the size of the canvas. However, the underlying canvas is only resized if a resizeEvent is emitted, so we check whether _dpi_ratio has changed between paintEvents, and if so we emit a fake resizeEvent for the widget. --- lib/matplotlib/backends/backend_qt5agg.py | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index ec9a70c5bb68..29ec2a28689f 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -38,6 +38,14 @@ def __init__(self, figure): self._bbox_queue = [] self._drawRect = None + # 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 + def drawRectangle(self, rect): if rect is not None: self._drawRect = [pt / self._dpi_ratio for pt in rect] @@ -56,6 +64,23 @@ def paintEvent(self, e): In Qt, all drawing should be done inside of here when a widget is shown onscreen. """ + + # As described in __init__ above, we need to be careful in cases with + # mixed resolution displays if dpi_ratio is changing between painting + # events. + if self._dpi_ratio_prev is None: + self._dpi_ratio_prev = self._dpi_ratio + elif self._dpi_ratio != self._dpi_ratio_prev: + # 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()) + # We use self.resizeEvent here instead of QApplication.postEvent + # since the latter doesn't guarantee that the event will be emitted + # straight away, and this causes visual delays in the changes. + self.resizeEvent(event) + self._dpi_ratio_prev = self._dpi_ratio + # if the canvas does not have a renderer, then give up and wait for # FigureCanvasAgg.draw(self) to be called if not hasattr(self, 'renderer'): From 7f4a73a8eb3229aa54210799e688af063ad2e9c0 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Mon, 24 Jul 2017 17:24:01 +0100 Subject: [PATCH 2/5] Update figure DPI when dpi_ratio changes Similarly to resizeEvent, we tell _set_dpi to not change the canvas size since we are doing it ourselves. --- lib/matplotlib/backends/backend_qt5.py | 2 +- lib/matplotlib/backends/backend_qt5agg.py | 14 ++++++++++---- lib/matplotlib/figure.py | 9 +++++++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index e3a9a77019b0..ab8c255680c7 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -674,7 +674,7 @@ def save_figure(self, *args): fname, filter = _getSaveFileName(self.parent, "Choose a filename to save to", - start, filters, selectedFilter) + start, filters, selectedFilter) if fname: if startpath == '': # explicitly missing key or empty str signals to use cwd diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 29ec2a28689f..a974be0b6e4b 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -43,7 +43,8 @@ def __init__(self, figure): # 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. + # dpi_ratio value here and in paintEvent we resize the canvas if + # needed. self._dpi_ratio_prev = None def drawRectangle(self, rect): @@ -68,9 +69,10 @@ def paintEvent(self, e): # As described in __init__ above, we need to be careful in cases with # mixed resolution displays if dpi_ratio is changing between painting # events. - if self._dpi_ratio_prev is None: - self._dpi_ratio_prev = self._dpi_ratio - elif self._dpi_ratio != self._dpi_ratio_prev: + if (self._dpi_ratio_prev is None or + self._dpi_ratio != self._dpi_ratio_prev): + # We need to update the figure DPI + self._update_figure_dpi() # 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. @@ -196,6 +198,10 @@ def __init__(self, figure): self.figure._original_dpi = self.figure.dpi self.figure.dpi = self._dpi_ratio * self.figure._original_dpi + def _update_figure_dpi(self): + dpi = self._dpi_ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + @_BackendQT5.export class _BackendQT5Agg(_BackendQT5): diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 3067acea87e2..63412660cd38 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -418,11 +418,16 @@ def _get_axes(self): def _get_dpi(self): return self._dpi - def _set_dpi(self, dpi): + def _set_dpi(self, dpi, forward=True): + """ + The forward kwarg is passed on to set_size_inches + """ self._dpi = dpi self.dpi_scale_trans.clear().scale(dpi, dpi) - self.set_size_inches(*self.get_size_inches()) + w, h = self.get_size_inches() + self.set_size_inches(w, h, forward=forward) self.callbacks.process('dpi_changed', self) + dpi = property(_get_dpi, _set_dpi) def get_tight_layout(self): From 31408af24887d7e7e430c2e235536e27fd884754 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 25 Jul 2017 09:45:12 +0100 Subject: [PATCH 3/5] Added a regression test to make sure the renderer size and figure dpi change when _dpi_ratio changes --- lib/matplotlib/tests/test_backend_qt5.py | 64 +++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt5.py b/lib/matplotlib/tests/test_backend_qt5.py index 538391b51088..dc47afecc719 100644 --- a/lib/matplotlib/tests/test_backend_qt5.py +++ b/lib/matplotlib/tests/test_backend_qt5.py @@ -1,10 +1,13 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) +import copy + +import matplotlib from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf -import matplotlib -import copy + +from numpy.testing import assert_equal import pytest try: @@ -95,3 +98,60 @@ def receive(event): qt_canvas.mpl_connect('key_press_event', receive) qt_canvas.keyPressEvent(event) + + +@pytest.mark.backend('Qt5Agg') +def test_dpi_ratio_change(): + """ + Make sure that if _dpi_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: + + 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 + + # Make sure the mocking worked + assert qt_canvas._dpi_ratio == 3 + + size = qt_canvas.size() + + qt_canvas.manager.show() + qApp.processEvents() + + # 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 + assert size.width() == 200 + assert size.height() == 80 + assert_equal(qt_canvas.get_width_height(), (600, 240)) + assert_equal(fig.get_size_inches(), (5, 2)) + + p.return_value = 2 + + assert qt_canvas._dpi_ratio == 2 + + qt_canvas.draw() + qApp.processEvents() + + # 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 + assert size.width() == 200 + assert size.height() == 80 + assert_equal(qt_canvas.get_width_height(), (600, 240)) + assert_equal(fig.get_size_inches(), (5, 2)) From 6b159846fdae8d7056738ea406ca8a58c318831b Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Tue, 25 Jul 2017 09:48:09 +0100 Subject: [PATCH 4/5] Fix order of execution in paintEvent --- lib/matplotlib/backends/backend_qt5agg.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index a974be0b6e4b..ed1def550632 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -66,6 +66,11 @@ def paintEvent(self, e): shown onscreen. """ + # if the canvas does not have a renderer, then give up and wait for + # FigureCanvasAgg.draw(self) to be called + if not hasattr(self, 'renderer'): + return + # As described in __init__ above, we need to be careful in cases with # mixed resolution displays if dpi_ratio is changing between painting # events. @@ -83,11 +88,6 @@ def paintEvent(self, e): self.resizeEvent(event) self._dpi_ratio_prev = self._dpi_ratio - # if the canvas does not have a renderer, then give up and wait for - # FigureCanvasAgg.draw(self) to be called - if not hasattr(self, 'renderer'): - return - painter = QtGui.QPainter(self) if self._bbox_queue: From 591a9e034acb400fbe15fa98fed784d89d497f36 Mon Sep 17 00:00:00 2001 From: Thomas Robitaille Date: Fri, 28 Jul 2017 18:04:49 +0100 Subject: [PATCH 5/5] Simplify FigureCanvasQTAgg.__init__ --- lib/matplotlib/backends/backend_qt5agg.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index ed1def550632..b7a90a8f4d68 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -194,9 +194,8 @@ def __init__(self, figure): super(FigureCanvasQTAgg, self).__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. - if not hasattr(self.figure, '_original_dpi'): - self.figure._original_dpi = self.figure.dpi - self.figure.dpi = self._dpi_ratio * self.figure._original_dpi + self.figure._original_dpi = self.figure.dpi + self._update_figure_dpi() def _update_figure_dpi(self): dpi = self._dpi_ratio * self.figure._original_dpi