From 5400888e9c5e6ee25284faa8e6b0bb5c8c50d470 Mon Sep 17 00:00:00 2001 From: Peter Iannucci Date: Mon, 9 May 2016 11:38:52 -0400 Subject: [PATCH 1/6] Initial HiDPI support for qt5agg backend --- lib/matplotlib/backends/backend_qt5.py | 31 ++++++++++------------- lib/matplotlib/backends/backend_qt5agg.py | 3 +++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 4bde67f8eea8..542b6aa19c9e 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -250,10 +250,14 @@ def leaveEvent(self, event): QtWidgets.QApplication.restoreOverrideCursor() FigureCanvasBase.leave_notify_event(self, guiEvent=event) + def mouseEventCoords(self, pos): + x = pos.x() * self.devicePixelRatio() + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height - pos.y() * self.devicePixelRatio() + return x, y + def mousePressEvent(self, event): - x = event.pos().x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.pos().y() + x, y = self.mouseEventCoords(event.pos()) button = self.buttond.get(event.button()) if button is not None: FigureCanvasBase.button_press_event(self, x, y, button, @@ -262,9 +266,7 @@ def mousePressEvent(self, event): print('button pressed:', event.button()) def mouseDoubleClickEvent(self, event): - x = event.pos().x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.pos().y() + x, y = self.mouseEventCoords(event.pos()) button = self.buttond.get(event.button()) if button is not None: FigureCanvasBase.button_press_event(self, x, y, @@ -274,16 +276,12 @@ def mouseDoubleClickEvent(self, event): print('button doubleclicked:', event.button()) def mouseMoveEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() + x, y = self.mouseEventCoords(event) FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) # if DEBUG: print('mouse move') def mouseReleaseEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() + x, y = self.mouseEventCoords(event) button = self.buttond.get(event.button()) if button is not None: FigureCanvasBase.button_release_event(self, x, y, button, @@ -292,9 +290,7 @@ def mouseReleaseEvent(self, event): print('button released') def wheelEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() + x, y = self.mouseEventCoords(event) # from QWheelEvent::delta doc if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: steps = event.angleDelta().y() / 120 @@ -324,8 +320,9 @@ def keyReleaseEvent(self, event): print('key release', key) def resizeEvent(self, event): - w = event.size().width() - h = event.size().height() + dpi_ratio = getattr(self, '_dpi_ratio', 1) + w = event.size().width() * dpi_ratio + h = event.size().height() * dpi_ratio if DEBUG: print('resize (%d x %d)' % (w, h)) print("FigureCanvasQt.resizeEvent(%d, %d)" % (w, h)) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index ecc49840b19a..eadaf8bf9d0e 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -101,6 +101,7 @@ def paintEvent(self, e): qImage = QtGui.QImage(stringBuffer, self.renderer.width, self.renderer.height, QtGui.QImage.Format_ARGB32) + qImage.setDevicePixelRatio(self._dpi_ratio) # get the rectangle for the image rect = qImage.rect() p = QtGui.QPainter(self) @@ -136,6 +137,7 @@ def paintEvent(self, e): stringBuffer = reg.to_string_argb() qImage = QtGui.QImage(stringBuffer, w, h, QtGui.QImage.Format_ARGB32) + qImage.setDevicePixelRatio(self._dpi_ratio) # Adjust the stringBuffer reference count to work # around a memory leak bug in QImage() under PySide on # Python 3.x @@ -226,6 +228,7 @@ def __init__(self, figure): super(FigureCanvasQTAgg, self).__init__(figure=figure) self._drawRect = None self.blitbox = [] + self._dpi_ratio = self.devicePixelRatio() self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) From e5fab1c9cee67eea3c0a260bde0a0bdce82a3b8f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 24 Nov 2016 02:42:39 -0500 Subject: [PATCH 2/6] Pass HighDPI info from Qt5 to underlying Agg figure. --- lib/matplotlib/backends/backend_qt5.py | 7 ++++++- lib/matplotlib/backends/backend_qt5agg.py | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 542b6aa19c9e..d4febcb26540 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -243,6 +243,11 @@ def __init__(self, figure): w, h = self.get_width_height() self.resize(w, h) + def get_width_height(self): + dpi_ratio = self.devicePixelRatio() + w, h = FigureCanvasBase.get_width_height(self) + return int(w / dpi_ratio), int(h / dpi_ratio) + def enterEvent(self, event): FigureCanvasBase.enter_notify_event(self, guiEvent=event) @@ -320,7 +325,7 @@ def keyReleaseEvent(self, event): print('key release', key) def resizeEvent(self, event): - dpi_ratio = getattr(self, '_dpi_ratio', 1) + dpi_ratio = self.devicePixelRatio() w = event.size().width() * dpi_ratio h = event.size().height() * dpi_ratio if DEBUG: diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index eadaf8bf9d0e..c36c25193595 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -229,6 +229,11 @@ def __init__(self, figure): self._drawRect = None self.blitbox = [] self._dpi_ratio = self.devicePixelRatio() + # 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.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) From b1228095fdd5ab163caaa1016b6eeda4f7379c24 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 24 Nov 2016 02:43:57 -0500 Subject: [PATCH 3/6] Scale Qt5 rubberband by DPI ratio. --- lib/matplotlib/backends/backend_qt5agg.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index c36c25193595..82963e5f2bdd 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -67,7 +67,10 @@ def __init__(self, figure): self._agg_draw_pending = False def drawRectangle(self, rect): - self._drawRect = rect + if rect is not None: + self._drawRect = [pt / self._dpi_ratio for pt in rect] + else: + self._drawRect = None self.update() def paintEvent(self, e): @@ -112,7 +115,9 @@ def paintEvent(self, e): # draw the zoom rectangle to the QPainter if self._drawRect is not None: - p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) + pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, + QtCore.Qt.DotLine) + p.setPen(pen) x, y, w, h = self._drawRect p.drawRect(x, y, w, h) p.end() @@ -149,7 +154,9 @@ def paintEvent(self, e): # draw the zoom rectangle to the QPainter if self._drawRect is not None: - p.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.DotLine)) + pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, + QtCore.Qt.DotLine) + p.setPen(pen) x, y, w, h = self._drawRect p.drawRect(x, y, w, h) From 4d4b84c5b1524ef6124bd54b72efb569af125010 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 24 Nov 2016 03:06:20 -0500 Subject: [PATCH 4/6] Fix blitting with HighDPI Qt5. The renderer and backing QPixmap use doubled (or whatever) pixels, but the painter continues to use logical pixels for positioning. --- lib/matplotlib/backends/backend_qt5agg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 82963e5f2bdd..9a91ba4c045d 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -149,8 +149,9 @@ def paintEvent(self, e): if QT_API == 'PySide' and six.PY3: ctypes.c_long.from_address(id(stringBuffer)).value = 1 + origin = QtCore.QPoint(l, self.renderer.height - t) pixmap = QtGui.QPixmap.fromImage(qImage) - p.drawPixmap(QtCore.QPoint(l, self.renderer.height-t), pixmap) + p.drawPixmap(origin / self._dpi_ratio, pixmap) # draw the zoom rectangle to the QPainter if self._drawRect is not None: @@ -207,9 +208,11 @@ def blit(self, bbox=None): bbox = self.figure.bbox self.blitbox.append(bbox) - l, b, w, h = bbox.bounds + + # repaint uses logical pixels, not physical pixels like the renderer. + l, b, w, h = [pt / self._dpi_ratio for pt in bbox.bounds] t = b + h - self.repaint(l, self.renderer.height-t, w, h) + self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h) def print_figure(self, *args, **kwargs): FigureCanvasAgg.print_figure(self, *args, **kwargs) From d1d2c66d1a70f9fabdac17e219bb617458d6ceb4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 24 Nov 2016 03:34:17 -0500 Subject: [PATCH 5/6] Fix backwards compatibility with Qt4. --- lib/matplotlib/backends/backend_qt5.py | 20 +++++++++++++------- lib/matplotlib/backends/backend_qt5agg.py | 9 ++++++--- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index d4febcb26540..8150e5a6262c 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -243,10 +243,17 @@ def __init__(self, figure): w, h = self.get_width_height() self.resize(w, h) + @property + def _dpi_ratio(self): + # Not available on Qt4 or some older Qt5. + try: + return self.devicePixelRatio() + except AttributeError: + return 1 + def get_width_height(self): - dpi_ratio = self.devicePixelRatio() w, h = FigureCanvasBase.get_width_height(self) - return int(w / dpi_ratio), int(h / dpi_ratio) + return int(w / self._dpi_ratio), int(h / self._dpi_ratio) def enterEvent(self, event): FigureCanvasBase.enter_notify_event(self, guiEvent=event) @@ -256,9 +263,9 @@ def leaveEvent(self, event): FigureCanvasBase.leave_notify_event(self, guiEvent=event) def mouseEventCoords(self, pos): - x = pos.x() * self.devicePixelRatio() + x = pos.x() * self._dpi_ratio # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height - pos.y() * self.devicePixelRatio() + y = self.figure.bbox.height - pos.y() * self._dpi_ratio return x, y def mousePressEvent(self, event): @@ -325,9 +332,8 @@ def keyReleaseEvent(self, event): print('key release', key) def resizeEvent(self, event): - dpi_ratio = self.devicePixelRatio() - w = event.size().width() * dpi_ratio - h = event.size().height() * dpi_ratio + w = event.size().width() * self._dpi_ratio + h = event.size().height() * self._dpi_ratio if DEBUG: print('resize (%d x %d)' % (w, h)) print("FigureCanvasQt.resizeEvent(%d, %d)" % (w, h)) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 9a91ba4c045d..55b09d78c4ef 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -104,7 +104,9 @@ def paintEvent(self, e): qImage = QtGui.QImage(stringBuffer, self.renderer.width, self.renderer.height, QtGui.QImage.Format_ARGB32) - qImage.setDevicePixelRatio(self._dpi_ratio) + if hasattr(qImage, 'setDevicePixelRatio'): + # Not available on Qt4 or some older Qt5. + qImage.setDevicePixelRatio(self._dpi_ratio) # get the rectangle for the image rect = qImage.rect() p = QtGui.QPainter(self) @@ -142,7 +144,9 @@ def paintEvent(self, e): stringBuffer = reg.to_string_argb() qImage = QtGui.QImage(stringBuffer, w, h, QtGui.QImage.Format_ARGB32) - qImage.setDevicePixelRatio(self._dpi_ratio) + if hasattr(qImage, 'setDevicePixelRatio'): + # Not available on Qt4 or some older Qt5. + qImage.setDevicePixelRatio(self._dpi_ratio) # Adjust the stringBuffer reference count to work # around a memory leak bug in QImage() under PySide on # Python 3.x @@ -238,7 +242,6 @@ def __init__(self, figure): super(FigureCanvasQTAgg, self).__init__(figure=figure) self._drawRect = None self.blitbox = [] - self._dpi_ratio = self.devicePixelRatio() # 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'): From 001eaf9b95730fee9f8ae288834a791042f3ff07 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 2 Jan 2017 17:05:52 -0500 Subject: [PATCH 6/6] ENH: make Figure.set_size_inches dpi ratio aware If the underlying canvas as a `_dpi_ratio`, scale the dpi before computing the new size. --- lib/matplotlib/figure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 030d50263d87..f38fbcea58f6 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -713,7 +713,8 @@ def set_size_inches(self, w, h=None, forward=True): self.bbox_inches.p1 = w, h if forward: - dpival = self.dpi + ratio = getattr(self.canvas, '_dpi_ratio', 1) + dpival = self.dpi / ratio canvasw = w * dpival canvash = h * dpival manager = getattr(self.canvas, 'manager', None)