Skip to content

Commit c205b60

Browse files
committed
Support fractional HiDpi scaling with Qt bakcends
1 parent f73f336 commit c205b60

File tree

5 files changed

+67
-21
lines changed

5 files changed

+67
-21
lines changed

lib/matplotlib/backends/backend_qt5.py

+5-11
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
from matplotlib.backend_managers import ToolManager
1818

1919
from .qt_compat import (
20-
QtCore, QtGui, QtWidgets, _getSaveFileName, is_pyqt5, __version__, QT_API)
20+
QtCore, QtGui, QtWidgets, _getSaveFileName, is_pyqt5, __version__, QT_API,
21+
_devicePixelRatioF, _setDevicePixelRatioF)
2122

2223
backend_version = __version__
2324

@@ -260,12 +261,7 @@ def _update_figure_dpi(self):
260261

261262
@property
262263
def _dpi_ratio(self):
263-
# Not available on Qt4 or some older Qt5.
264-
try:
265-
# self.devicePixelRatio() returns 0 in rare cases
266-
return self.devicePixelRatio() or 1
267-
except AttributeError:
268-
return 1
264+
return _devicePixelRatioF(self)
269265

270266
def _update_dpi(self):
271267
# As described in __init__ above, we need to be careful in cases with
@@ -683,8 +679,7 @@ def _icon(self, name, color=None):
683679
if is_pyqt5():
684680
name = name.replace('.png', '_large.png')
685681
pm = QtGui.QPixmap(os.path.join(self.basedir, name))
686-
if hasattr(pm, 'setDevicePixelRatio'):
687-
pm.setDevicePixelRatio(self.canvas._dpi_ratio)
682+
_setDevicePixelRatioF(pm, self.canvas._dpi_ratio)
688683
if color is not None:
689684
mask = pm.createMaskFromColor(QtGui.QColor('black'),
690685
QtCore.Qt.MaskOutColor)
@@ -957,8 +952,7 @@ def _add_to_group(self, group, name, button, position):
957952

958953
def _icon(self, name):
959954
pm = QtGui.QPixmap(name)
960-
if hasattr(pm, 'setDevicePixelRatio'):
961-
pm.setDevicePixelRatio(self.toolmanager.canvas._dpi_ratio)
955+
_setDevicePixelRatioF(pm, self.toolmanager.canvas._dpi_ratio)
962956
return QtGui.QIcon(pm)
963957

964958
def toggle_toolitem(self, name, toggled):

lib/matplotlib/backends/backend_qt5agg.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .backend_qt5 import (
1212
QtCore, QtGui, QtWidgets, _BackendQT5, FigureCanvasQT, FigureManagerQT,
1313
NavigationToolbar2QT, backend_version)
14-
from .qt_compat import QT_API
14+
from .qt_compat import QT_API, _setDevicePixelRatioF
1515

1616

1717
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):
@@ -63,9 +63,7 @@ def paintEvent(self, event):
6363

6464
qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0],
6565
QtGui.QImage.Format_ARGB32_Premultiplied)
66-
if hasattr(qimage, 'setDevicePixelRatio'):
67-
# Not available on Qt4 or some older Qt5.
68-
qimage.setDevicePixelRatio(self._dpi_ratio)
66+
_setDevicePixelRatioF(qimage, self._dpi_ratio)
6967
# set origin using original QT coordinates
7068
origin = QtCore.QPoint(rect.left(), rect.top())
7169
painter.drawImage(origin, qimage)

lib/matplotlib/backends/backend_qt5cairo.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
44
from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT
5-
from .qt_compat import QT_API
5+
from .qt_compat import QT_API, _setDevicePixelRatioF
66

77

88
class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo):
@@ -19,8 +19,8 @@ def draw(self):
1919
def paintEvent(self, event):
2020
self._update_dpi()
2121
dpi_ratio = self._dpi_ratio
22-
width = dpi_ratio * self.width()
23-
height = dpi_ratio * self.height()
22+
width = int(dpi_ratio * self.width())
23+
height = int(dpi_ratio * self.height())
2424
if (width, height) != self._renderer.get_canvas_width_height():
2525
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
2626
self._renderer.set_ctx_from_surface(surface)
@@ -33,9 +33,7 @@ def paintEvent(self, event):
3333
# QImage under PySide on Python 3.
3434
if QT_API == 'PySide':
3535
ctypes.c_long.from_address(id(buf)).value = 1
36-
if hasattr(qimage, 'setDevicePixelRatio'):
37-
# Not available on Qt4 or some older Qt5.
38-
qimage.setDevicePixelRatio(dpi_ratio)
36+
_setDevicePixelRatioF(qimage, dpi_ratio)
3937
painter = QtGui.QPainter(self)
4038
painter.eraseRect(event.rect())
4139
painter.drawImage(0, 0, qimage)

lib/matplotlib/backends/qt_compat.py

+33
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,36 @@ def is_pyqt5():
164164
ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
165165
pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
166166
QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4
167+
168+
169+
def _devicePixelRatioF(obj):
170+
"""
171+
Return obj.devicePixelRatioF() with graceful fallback for older Qt.
172+
173+
This can be replaced by the direct call when we require Qt>=5.6.
174+
"""
175+
try:
176+
# Not available on Qt<5.6
177+
return obj.devicePixelRatioF() or 1
178+
except AttributeError:
179+
pass
180+
try:
181+
# Not available on Qt4 or some older Qt5.
182+
# self.devicePixelRatio() returns 0 in rare cases
183+
return obj.devicePixelRatio() or 1
184+
except AttributeError:
185+
return 1
186+
187+
188+
def _setDevicePixelRatioF(obj, val):
189+
"""
190+
Call obj.setDevicePixelRatioF(val) with graceful fallback for older Qt.
191+
192+
This can be replaced by the direct call when we require Qt>=5.6.
193+
"""
194+
if hasattr(obj, 'setDevicePixelRatioF'):
195+
# Not available on Qt<5.6
196+
obj.setDevicePixelRatioF(val)
197+
if hasattr(obj, 'setDevicePixelRatio'):
198+
# Not available on Qt4 or some older Qt5.
199+
obj.setDevicePixelRatio(val)

lib/matplotlib/tests/test_backend_qt.py

+23
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,29 @@ def test_dpi_ratio_change():
253253
assert qt_canvas.get_width_height() == (600, 240)
254254
assert (fig.get_size_inches() == (5, 2)).all()
255255

256+
p.return_value = 1.5
257+
258+
assert qt_canvas._dpi_ratio == 1.5
259+
260+
qt_canvas.draw()
261+
qApp.processEvents()
262+
# this second processEvents is required to fully run the draw.
263+
# On `update` we notice the DPI has changed and trigger a
264+
# resize event to refresh, the second processEvents is
265+
# required to process that and fully update the window sizes.
266+
qApp.processEvents()
267+
268+
# The DPI and the renderer width/height change
269+
assert fig.dpi == 180
270+
assert qt_canvas.renderer.width == 900
271+
assert qt_canvas.renderer.height == 360
272+
273+
# The actual widget size and figure physical size don't change
274+
assert size.width() == 600
275+
assert size.height() == 240
276+
assert qt_canvas.get_width_height() == (600, 240)
277+
assert (fig.get_size_inches() == (5, 2)).all()
278+
256279

257280
@pytest.mark.backend('Qt5Agg')
258281
def test_subplottool():

0 commit comments

Comments
 (0)