From 17ff6bae82f5e080f94c4b34a637c30e563d505c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 Nov 2019 16:55:34 +0100 Subject: [PATCH] Support fractional HiDpi scaling with Qt bakcends Author: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/backends/backend_qt5.py | 8 +++-- lib/matplotlib/backends/backend_qt5agg.py | 6 ++-- lib/matplotlib/backends/backend_qt5cairo.py | 10 +++--- lib/matplotlib/backends/qt_compat.py | 34 +++++++++++++++++++++ lib/matplotlib/tests/test_backend_qt.py | 23 ++++++++++++++ 5 files changed, 68 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 24f0c24fc02a..90ad4fd477ed 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -17,7 +17,9 @@ from matplotlib.backends.qt_editor._formsubplottool import UiSubplotTool from . import qt_compat from .qt_compat import ( - QtCore, QtGui, QtWidgets, _isdeleted, is_pyqt5, __version__, QT_API) + QtCore, QtGui, QtWidgets, __version__, QT_API, + _devicePixelRatioF, _isdeleted, +) backend_version = __version__ @@ -247,7 +249,7 @@ def _update_figure_dpi(self): @property def _dpi_ratio(self): - return qt_compat._devicePixelRatio(self) + return _devicePixelRatioF(self) def _update_dpi(self): # As described in __init__ above, we need to be careful in cases with @@ -707,7 +709,7 @@ def _icon(self, name): if QtCore.qVersion() >= '5.': name = name.replace('.png', '_large.png') pm = QtGui.QPixmap(str(cbook._get_data_path('images', name))) - qt_compat._setDevicePixelRatio(pm, qt_compat._devicePixelRatio(self)) + qt_compat._setDevicePixelRatio(pm, _devicePixelRatioF(self)) if self.palette().color(self.backgroundRole()).value() < 128: icon_color = self.palette().color(self.foregroundRole()) mask = pm.createMaskFromColor(QtGui.QColor('black'), diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 11b60378ac39..c46e493f9cf1 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -11,7 +11,7 @@ from .backend_qt5 import ( QtCore, QtGui, QtWidgets, _BackendQT5, FigureCanvasQT, FigureManagerQT, NavigationToolbar2QT, backend_version) -from .qt_compat import QT_API +from .qt_compat import QT_API, _setDevicePixelRatioF class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): @@ -64,9 +64,7 @@ def paintEvent(self, event): qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0], QtGui.QImage.Format_ARGB32_Premultiplied) - if hasattr(qimage, 'setDevicePixelRatio'): - # Not available on Qt4 or some older Qt5. - qimage.setDevicePixelRatio(self._dpi_ratio) + _setDevicePixelRatioF(qimage, self._dpi_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 5a38a80864be..d29997410323 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -2,7 +2,7 @@ from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT -from .qt_compat import QT_API +from .qt_compat import QT_API, _setDevicePixelRatioF class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo): @@ -19,8 +19,8 @@ def draw(self): def paintEvent(self, event): self._update_dpi() dpi_ratio = self._dpi_ratio - width = dpi_ratio * self.width() - height = dpi_ratio * self.height() + width = int(dpi_ratio * self.width()) + height = int(dpi_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) @@ -33,9 +33,7 @@ def paintEvent(self, event): # QImage under PySide on Python 3. if QT_API == 'PySide': ctypes.c_long.from_address(id(buf)).value = 1 - if hasattr(qimage, 'setDevicePixelRatio'): - # Not available on Qt4 or some older Qt5. - qimage.setDevicePixelRatio(dpi_ratio) + _setDevicePixelRatioF(qimage, dpi_ratio) painter = QtGui.QPainter(self) painter.eraseRect(event.rect()) painter.drawImage(0, 0, qimage) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 12797b602872..4252dad47335 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -185,7 +185,41 @@ def _setDevicePixelRatio(obj, factor): pass # These globals are only defined for backcompatibility purposes. ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) + QT_RC_MAJOR_VERSION = int(QtCore.qVersion().split(".")[0]) if QT_RC_MAJOR_VERSION == 4: mpl.cbook.warn_deprecated("3.3", name="support for Qt4") + + +def _devicePixelRatioF(obj): + """ + Return obj.devicePixelRatioF() with graceful fallback for older Qt. + + This can be replaced by the direct call when we require Qt>=5.6. + """ + try: + # Not available on Qt<5.6 + return obj.devicePixelRatioF() or 1 + except AttributeError: + pass + try: + # Not available on Qt4 or some older Qt5. + # self.devicePixelRatio() returns 0 in rare cases + return obj.devicePixelRatio() or 1 + except AttributeError: + return 1 + + +def _setDevicePixelRatioF(obj, val): + """ + Call obj.setDevicePixelRatioF(val) with graceful fallback for older Qt. + + This can be replaced by the direct call when we require Qt>=5.6. + """ + if hasattr(obj, 'setDevicePixelRatioF'): + # Not available on Qt<5.6 + obj.setDevicePixelRatioF(val) + if hasattr(obj, 'setDevicePixelRatio'): + # Not available on Qt4 or some older Qt5. + obj.setDevicePixelRatio(val) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index e1200adc1935..564192d6ec1d 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -213,6 +213,29 @@ 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() + + # 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 + assert size.width() == 600 + assert size.height() == 240 + assert qt_canvas.get_width_height() == (600, 240) + assert (fig.get_size_inches() == (5, 2)).all() + @pytest.mark.backend('Qt5Agg') def test_subplottool():