Skip to content

Support fractional HiDpi scaling with Qt backends #15656

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions lib/matplotlib/backends/backend_qt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'),
Expand Down
6 changes: 2 additions & 4 deletions lib/matplotlib/backends/backend_qt5agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 4 additions & 6 deletions lib/matplotlib/backends/backend_qt5cairo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to use round here rather than int to move the jumps away from the integers.

However, this may have issues with not being in sync with what we do else were in the code (which is int).

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)
Expand All @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions lib/matplotlib/backends/qt_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be elif, no? Or you'll set pixel ratio two different ways.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #17640 with this change.

# Not available on Qt4 or some older Qt5.
obj.setDevicePixelRatio(val)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to cast to int here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would think so, given your other Qt PR, though that's for new Qt that wouldn't call this. Though I think it would come from getDevicePixelRatio, so we may expect int anyway?

23 changes: 23 additions & 0 deletions lib/matplotlib/tests/test_backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down