Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
qt{4,5}cairo backend: the minimal version.
  • Loading branch information
anntzer committed Jan 29, 2018
commit 984fba18bcce543f3dd65c5fe5cadfc40e80e690
8 changes: 8 additions & 0 deletions doc/api/backend_qt4cairo_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

:mod:`matplotlib.backends.backend_qt4cairo`
===========================================

.. automodule:: matplotlib.backends.backend_qt4cairo
:members:
:undoc-members:
:show-inheritance:
8 changes: 8 additions & 0 deletions doc/api/backend_qt5cairo_api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

:mod:`matplotlib.backends.backend_qt5cairo`
===========================================

.. automodule:: matplotlib.backends.backend_qt5cairo
:members:
:undoc-members:
:show-inheritance:
2 changes: 2 additions & 0 deletions doc/api/index_backend_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ backends
backend_pgf_api.rst
backend_ps_api.rst
backend_qt4agg_api.rst
backend_qt4cairo_api.rst
backend_qt5agg_api.rst
backend_qt5cairo_api.rst
backend_svg_api.rst
backend_tkagg_api.rst
backend_webagg_api.rst
Expand Down
5 changes: 5 additions & 0 deletions doc/users/next_whats_new/more-cairo.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Cairo rendering for Qt canvases
-------------------------------

The new ``Qt4Cairo`` and ``Qt5Cairo`` backends allow Qt canvases to use Cairo
rendering instead of Agg.
3 changes: 3 additions & 0 deletions lib/matplotlib/backends/backend_agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,9 @@ def draw(self):
# if toolbar:
# toolbar.set_cursor(cursors.WAIT)
self.figure.draw(self.renderer)
# A GUI class may be need to update a window using this draw, so
# don't forget to call the superclass.
super(FigureCanvasAgg, self).draw()
finally:
# if toolbar:
# toolbar.set_cursor(toolbar._lastCursor)
Expand Down
6 changes: 6 additions & 0 deletions lib/matplotlib/backends/backend_qt4cairo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .backend_qt5cairo import _BackendQT5Cairo


@_BackendQT5Cairo.export
class _BackendQT4Cairo(_BackendQT5Cairo):
pass
94 changes: 88 additions & 6 deletions lib/matplotlib/backends/backend_qt5.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import signal
import sys
from six import unichr
import traceback

import matplotlib

Expand Down Expand Up @@ -226,19 +227,16 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase):
# QtCore.Qt.XButton2: None,
}

def _update_figure_dpi(self):
dpi = self._dpi_ratio * self.figure._original_dpi
self.figure._set_dpi(dpi, forward=False)

@_allow_super_init
def __init__(self, figure):
_create_qApp()
super(FigureCanvasQT, self).__init__(figure=figure)

figure._original_dpi = figure.dpi
self.figure = figure
# We don't want to scale up the figure DPI more than once.
# Note, we don't handle a signal for changing DPI yet.
figure._original_dpi = figure.dpi
self._update_figure_dpi()
self.resize(*self.get_width_height())
# In cases with mixed resolution displays, we need to be careful if the
# dpi_ratio changes - in this case we need to resize the canvas
# accordingly. We could watch for screenChanged events from Qt, but
Expand All @@ -248,13 +246,23 @@ def __init__(self, figure):
# needed.
self._dpi_ratio_prev = None

self._draw_pending = False
self._is_drawing = False
self._draw_rect_callback = lambda painter: None

self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
self.setMouseTracking(True)
self.resize(*self.get_width_height())
# Key auto-repeat enabled by default
self._keyautorepeat = True

palette = QtGui.QPalette(QtCore.Qt.white)
self.setPalette(palette)

def _update_figure_dpi(self):
dpi = self._dpi_ratio * self.figure._original_dpi
self.figure._set_dpi(dpi, forward=False)

@property
def _dpi_ratio(self):
# Not available on Qt4 or some older Qt5.
Expand All @@ -263,6 +271,26 @@ def _dpi_ratio(self):
except AttributeError:
return 1

def _update_dpi(self):
# As described in __init__ above, we need to be careful in cases with
# mixed resolution displays if dpi_ratio is changing between painting
# events.
# Return whether we triggered a resizeEvent (and thus a paintEvent)
# from within this function.
if self._dpi_ratio != self._dpi_ratio_prev:
# We need to update the figure DPI.
self._update_figure_dpi()
self._dpi_ratio_prev = self._dpi_ratio
# The easiest way to resize the canvas is to emit a resizeEvent
# since we implement all the logic for resizing the canvas for
# that event.
event = QtGui.QResizeEvent(self.size(), self.size())
self.resizeEvent(event)
# resizeEvent triggers a paintEvent itself, so we exit this one
# (after making sure that the event is immediately handled).
return True
return False

def get_width_height(self):
w, h = FigureCanvasBase.get_width_height(self)
return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
Expand Down Expand Up @@ -453,6 +481,60 @@ def stop_event_loop(self, event=None):
if hasattr(self, "_event_loop"):
self._event_loop.quit()

def draw(self):
"""Render the figure, and queue a request for a Qt draw.
"""
# The renderer draw is done here; delaying causes problems with code
# that uses the result of the draw() to update plot elements.
if self._is_drawing:
return
self._is_drawing = True
try:
super(FigureCanvasQT, self).draw()
finally:
self._is_drawing = False
self.update()

def draw_idle(self):
"""Queue redraw of the Agg buffer and request Qt paintEvent.
"""
# The Agg draw needs to be handled by the same thread matplotlib
# modifies the scene graph from. Post Agg draw request to the
# current event loop in order to ensure thread affinity and to
# accumulate multiple draw requests from event handling.
# TODO: queued signal connection might be safer than singleShot
if not (self._draw_pending or self._is_drawing):
self._draw_pending = True
QtCore.QTimer.singleShot(0, self._draw_idle)

def _draw_idle(self):
if self.height() < 0 or self.width() < 0:
self._draw_pending = False
if not self._draw_pending:
return
try:
self.draw()
except Exception:
# Uncaught exceptions are fatal for PyQt5, so catch them instead.
traceback.print_exc()
finally:
self._draw_pending = False

def drawRectangle(self, rect):
# Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
# to be called at the end of paintEvent.
if rect is not None:
def _draw_rect_callback(painter):
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio,
QtCore.Qt.DotLine)
painter.setPen(pen)
painter.drawRect(*(pt / self._dpi_ratio for pt in rect))
else:
def _draw_rect_callback(painter):
return
self._draw_rect_callback = _draw_rect_callback
self.update()


class MainWindow(QtWidgets.QMainWindow):
closing = QtCore.Signal()
Expand Down
129 changes: 14 additions & 115 deletions lib/matplotlib/backends/backend_qt5agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import six

import ctypes
import traceback

from matplotlib import cbook
from matplotlib.transforms import Bbox
Expand All @@ -19,32 +18,11 @@
from .qt_compat import QT_API


class FigureCanvasQTAggBase(FigureCanvasAgg):
"""
The canvas the figure renders into. Calls the draw and print fig
methods, creates the renderers, etc...

Attributes
----------
figure : `matplotlib.figure.Figure`
A high-level Figure instance

"""
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):

def __init__(self, figure):
super(FigureCanvasQTAggBase, self).__init__(figure=figure)
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
self._agg_draw_pending = False
self._agg_is_drawing = False
super(FigureCanvasQTAgg, self).__init__(figure=figure)
self._bbox_queue = []
self._drawRect = None

def drawRectangle(self, rect):
if rect is not None:
self._drawRect = [pt / self._dpi_ratio for pt in rect]
else:
self._drawRect = None
self.update()

@property
@cbook.deprecated("2.1")
Expand All @@ -57,27 +35,10 @@ def paintEvent(self, e):
In Qt, all drawing should be done inside of here when a widget is
shown onscreen.
"""
# if there is a pending draw, run it now as we need the updated render
# to paint the widget
if self._agg_draw_pending:
self.__draw_idle_agg()
# As described in __init__ above, we need to be careful in cases with
# mixed resolution displays if dpi_ratio is changing between painting
# events.
if self._dpi_ratio != self._dpi_ratio_prev:
# We need to update the figure DPI
self._update_figure_dpi()
self._dpi_ratio_prev = self._dpi_ratio
# The easiest way to resize the canvas is to emit a resizeEvent
# since we implement all the logic for resizing the canvas for
# that event.
event = QtGui.QResizeEvent(self.size(), self.size())
# We use self.resizeEvent here instead of QApplication.postEvent
# since the latter doesn't guarantee that the event will be emitted
# straight away, and this causes visual delays in the changes.
self.resizeEvent(event)
# resizeEvent triggers a paintEvent itself, so we exit this one.
if self._update_dpi():
# The dpi update triggered its own paintEvent.
return
self._draw_idle() # Only does something if a draw is pending.

# if the canvas does not have a renderer, then give up and wait for
# FigureCanvasAgg.draw(self) to be called
Expand All @@ -100,72 +61,20 @@ def paintEvent(self, e):
reg = self.copy_from_bbox(bbox)
buf = reg.to_string_argb()
qimage = QtGui.QImage(buf, w, h, QtGui.QImage.Format_ARGB32)
# 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
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

# 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)
self._draw_rect_callback(painter)

painter.end()

def draw(self):
"""Draw the figure with Agg, and queue a request for a Qt draw.
"""
# The Agg draw is done here; delaying causes problems with code that
# uses the result of the draw() to update plot elements.
if self._agg_is_drawing:
return

self._agg_is_drawing = True
try:
super(FigureCanvasQTAggBase, self).draw()
finally:
self._agg_is_drawing = False
self.update()

def draw_idle(self):
"""Queue redraw of the Agg buffer and request Qt paintEvent.
"""
# The Agg draw needs to be handled by the same thread matplotlib
# modifies the scene graph from. Post Agg draw request to the
# current event loop in order to ensure thread affinity and to
# accumulate multiple draw requests from event handling.
# TODO: queued signal connection might be safer than singleShot
if not (self._agg_draw_pending or self._agg_is_drawing):
self._agg_draw_pending = True
QtCore.QTimer.singleShot(0, self.__draw_idle_agg)

def __draw_idle_agg(self, *args):
# if nothing to do, bail
if not self._agg_draw_pending:
return
# we have now tried this function at least once, do not run
# again until re-armed. Doing this here rather than after
# protects against recursive calls triggered through self.draw
# The recursive call is via `repaintEvent`
self._agg_draw_pending = False
# if negative size, bail
if self.height() < 0 or self.width() < 0:
return
try:
# actually do the drawing
self.draw()
except Exception:
# Uncaught exceptions are fatal for PyQt5, so catch them instead.
traceback.print_exc()

def blit(self, bbox=None):
"""Blit the region in bbox.
"""
Expand All @@ -182,23 +91,13 @@ def blit(self, bbox=None):
self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h)

def print_figure(self, *args, **kwargs):
super(FigureCanvasQTAggBase, self).print_figure(*args, **kwargs)
super(FigureCanvasQTAgg, self).print_figure(*args, **kwargs)
self.draw()


class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT):
"""
The canvas the figure renders into. Calls the draw and print fig
methods, creates the renderers, etc.

Modified to import from Qt5 backend for new-style mouse events.

Attributes
----------
figure : `matplotlib.figure.Figure`
A high-level Figure instance

"""
@cbook.deprecated("2.2")
class FigureCanvasQTAggBase(FigureCanvasQTAgg):
pass


@_BackendQT5.export
Expand Down
Loading