-
-
Notifications
You must be signed in to change notification settings - Fork 8k
Backends cleanup #8772
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
Backends cleanup #8772
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Follow a similar architecture to the Gtk3Agg backend: a default draw can be implemented as a blitting of the entire canvas. Also unify the Qt4Agg and Qt5Agg canvas classes a bit more.
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ | |
from matplotlib.figure import Figure | ||
|
||
|
||
from .backend_qt5agg import FigureCanvasQTAggBase as _FigureCanvasQTAggBase | ||
from .backend_qt5agg import FigureCanvasQTAggBase | ||
|
||
from .backend_agg import FigureCanvasAgg | ||
from .backend_qt4 import QtCore | ||
|
@@ -54,13 +54,7 @@ def new_figure_manager_given_figure(num, figure): | |
return FigureManagerQT(canvas, num) | ||
|
||
|
||
class FigureCanvasQTAggBase(_FigureCanvasQTAggBase): | ||
def __init__(self, figure): | ||
self._agg_draw_pending = False | ||
|
||
|
||
class FigureCanvasQTAgg(FigureCanvasQTAggBase, | ||
FigureCanvasQT, FigureCanvasAgg): | ||
class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT): | ||
""" | ||
The canvas the figure renders into. Calls the draw and print fig | ||
methods, creates the renderers, etc... | ||
|
@@ -72,16 +66,6 @@ class FigureCanvasQTAgg(FigureCanvasQTAggBase, | |
|
||
""" | ||
|
||
def __init__(self, figure): | ||
if DEBUG: | ||
print('FigureCanvasQtAgg: ', figure) | ||
FigureCanvasQT.__init__(self, figure) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this needs to be here or the MI does not work correctly (as pyqt5 made them cooperative, but they are not int pyqt4) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is further handled by the qtcairo commit, but in fact with the new MRO (specifically, due to FigureCanvasQT going after FigureCanvasAgg), this is not needed anymore, because the MRO FigureCanvasQT(4)Agg is
and Or you can just test by yourself that it works :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I was lost in the classes here. It looks like the calls out to the PyQt base classes happen in the 🐑 |
||
FigureCanvasQTAggBase.__init__(self, figure) | ||
FigureCanvasAgg.__init__(self, figure) | ||
self._drawRect = None | ||
self.blitbox = [] | ||
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) | ||
|
||
|
||
FigureCanvas = FigureCanvasQTAgg | ||
FigureManager = FigureManagerQT |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,10 +7,11 @@ | |
import six | ||
|
||
import ctypes | ||
import sys | ||
import traceback | ||
|
||
from matplotlib import cbook | ||
from matplotlib.figure import Figure | ||
from matplotlib.transforms import Bbox | ||
|
||
from .backend_agg import FigureCanvasAgg | ||
from .backend_qt5 import QtCore | ||
|
@@ -28,11 +29,6 @@ | |
|
||
DEBUG = False | ||
|
||
_decref = ctypes.pythonapi.Py_DecRef | ||
_decref.argtypes = [ctypes.py_object] | ||
_decref.restype = None | ||
|
||
|
||
def new_figure_manager(num, *args, **kwargs): | ||
""" | ||
Create a new figure manager instance | ||
|
@@ -52,7 +48,7 @@ def new_figure_manager_given_figure(num, figure): | |
return FigureManagerQT(canvas, num) | ||
|
||
|
||
class FigureCanvasQTAggBase(object): | ||
class FigureCanvasQTAggBase(FigureCanvasAgg): | ||
""" | ||
The canvas the figure renders into. Calls the draw and print fig | ||
methods, creates the renderers, etc... | ||
|
@@ -66,7 +62,10 @@ class FigureCanvasQTAggBase(object): | |
|
||
def __init__(self, figure): | ||
super(FigureCanvasQTAggBase, self).__init__(figure=figure) | ||
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) | ||
self._agg_draw_pending = False | ||
self._bbox_queue = [] | ||
self._drawRect = None | ||
|
||
def drawRectangle(self, rect): | ||
if rect is not None: | ||
|
@@ -75,6 +74,11 @@ def drawRectangle(self, rect): | |
self._drawRect = None | ||
self.update() | ||
|
||
@property | ||
@cbook.deprecated("2.1") | ||
def blitbox(self): | ||
return self._bbox_queue | ||
|
||
def paintEvent(self, e): | ||
""" | ||
Copy the image from the Agg canvas to the qt.drawable. | ||
|
@@ -86,88 +90,41 @@ def paintEvent(self, e): | |
if not hasattr(self, 'renderer'): | ||
return | ||
|
||
if DEBUG: | ||
print('FigureCanvasQtAgg.paintEvent: ', self, | ||
self.get_width_height()) | ||
|
||
if len(self.blitbox) == 0: | ||
# matplotlib is in rgba byte order. QImage wants to put the bytes | ||
# into argb format and is in a 4 byte unsigned int. Little endian | ||
# system is LSB first and expects the bytes in reverse order | ||
# (bgra). | ||
if QtCore.QSysInfo.ByteOrder == QtCore.QSysInfo.LittleEndian: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why drop the little endian support? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just reusing the blitting codepath for both cases, so either There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fair. There are not many little-endian systems left in practice (and they tend to be big main frame type systems these days). |
||
stringBuffer = self.renderer._renderer.tostring_bgra() | ||
else: | ||
stringBuffer = self.renderer._renderer.tostring_argb() | ||
|
||
refcnt = sys.getrefcount(stringBuffer) | ||
|
||
# convert the Agg rendered image -> qImage | ||
qImage = QtGui.QImage(stringBuffer, self.renderer.width, | ||
self.renderer.height, | ||
QtGui.QImage.Format_ARGB32) | ||
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) | ||
# reset the image area of the canvas to be the back-ground color | ||
p.eraseRect(rect) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is important if users have alpha in the final rgba. If this is not here text appears to 'get thicker' over time as the alpha stacks up. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch, restored the erase in the case where bbox_queue is empty. |
||
# draw the rendered image on to the canvas | ||
p.drawPixmap(QtCore.QPoint(0, 0), QtGui.QPixmap.fromImage(qImage)) | ||
|
||
# draw the zoom rectangle to the QPainter | ||
if self._drawRect is not None: | ||
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() | ||
|
||
# This works around a bug in PySide 1.1.2 on Python 3.x, | ||
# where the reference count of stringBuffer is incremented | ||
# but never decremented by QImage. | ||
# TODO: revert PR #1323 once the issue is fixed in PySide. | ||
del qImage | ||
if refcnt != sys.getrefcount(stringBuffer): | ||
_decref(stringBuffer) | ||
painter = QtGui.QPainter(self) | ||
|
||
if self._bbox_queue: | ||
bbox_queue = self._bbox_queue | ||
else: | ||
p = QtGui.QPainter(self) | ||
|
||
while len(self.blitbox): | ||
bbox = self.blitbox.pop() | ||
l, b, r, t = bbox.extents | ||
w = int(r) - int(l) | ||
h = int(t) - int(b) | ||
t = int(b) + h | ||
reg = self.copy_from_bbox(bbox) | ||
stringBuffer = reg.to_string_argb() | ||
qImage = QtGui.QImage(stringBuffer, w, h, | ||
QtGui.QImage.Format_ARGB32) | ||
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 | ||
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(origin / self._dpi_ratio, pixmap) | ||
|
||
# draw the zoom rectangle to the QPainter | ||
if self._drawRect is not None: | ||
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() | ||
painter.eraseRect(self.rect()) | ||
bbox_queue = [ | ||
Bbox([[0, 0], [self.renderer.width, self.renderer.height]])] | ||
self._bbox_queue = [] | ||
for bbox in bbox_queue: | ||
l, b, r, t = map(int, bbox.extents) | ||
w = r - l | ||
h = t - b | ||
reg = self.copy_from_bbox(bbox) | ||
buf = reg.to_string_argb() | ||
qimage = QtGui.QImage(buf, w, h, QtGui.QImage.Format_ARGB32) | ||
if hasattr(qimage, 'setDevicePixelRatio'): | ||
# Not available on Qt4 or some older Qt5. | ||
qimage.setDevicePixelRatio(self._dpi_ratio) | ||
origin = QtCore.QPoint(l, self.renderer.height - t) | ||
painter.drawImage(origin / self._dpi_ratio, qimage) | ||
# Adjust the buf reference count to work around a memory | ||
# leak bug in QImage under PySide on Python 3. | ||
if QT_API == 'PySide' and six.PY3: | ||
ctypes.c_long.from_address(id(buf)).value = 1 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious what the history on why we had this done two different ways was. |
||
|
||
# draw the zoom rectangle to the QPainter | ||
if self._drawRect is not None: | ||
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio, | ||
QtCore.Qt.DotLine) | ||
painter.setPen(pen) | ||
x, y, w, h = self._drawRect | ||
painter.drawRect(x, y, w, h) | ||
|
||
painter.end() | ||
|
||
def draw(self): | ||
""" | ||
|
@@ -213,7 +170,7 @@ def blit(self, bbox=None): | |
if bbox is None and self.figure: | ||
bbox = self.figure.bbox | ||
|
||
self.blitbox.append(bbox) | ||
self._bbox_queue.append(bbox) | ||
|
||
# repaint uses logical pixels, not physical pixels like the renderer. | ||
l, b, w, h = [pt / self._dpi_ratio for pt in bbox.bounds] | ||
|
@@ -225,8 +182,7 @@ def print_figure(self, *args, **kwargs): | |
self.draw() | ||
|
||
|
||
class FigureCanvasQTAgg(FigureCanvasQTAggBase, | ||
FigureCanvasQT, FigureCanvasAgg): | ||
class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT): | ||
""" | ||
The canvas the figure renders into. Calls the draw and print fig | ||
methods, creates the renderers, etc. | ||
|
@@ -244,14 +200,11 @@ def __init__(self, figure): | |
if DEBUG: | ||
print('FigureCanvasQtAgg: ', figure) | ||
super(FigureCanvasQTAgg, self).__init__(figure=figure) | ||
self._drawRect = None | ||
self.blitbox = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This going private needs to be documented and gracefully deprecated. Users who use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done (modulo comment below) |
||
# 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) | ||
|
||
|
||
FigureCanvas = FigureCanvasQTAgg | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think having the base class come in as the
_
version of this is clearer (I had to think a bit for why this should work).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually this MRO chain will get even further simplified in #8771 (to
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT)
, which seems as simple as one can do :-)), sorry for modifying the same thing twice.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am very much 👍 on simplifying the MRO.