Skip to content

Commit 0395bfe

Browse files
committed
qt{4,5}cairo backend: the minimal version.
1 parent 874116f commit 0395bfe

File tree

8 files changed

+161
-115
lines changed

8 files changed

+161
-115
lines changed

doc/users/next_whats_new/qtcairo.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Cairo rendering for Qt canvases
2+
-------------------------------
3+
4+
The new ``Qt4Cairo`` and ``Qt5Cairo`` backends allow Qt canvases to use Cairo
5+
rendering instead of Agg.

lib/matplotlib/backends/backend_agg.py

+3
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ def draw(self):
430430
if toolbar:
431431
toolbar.set_cursor(cursors.WAIT)
432432
self.figure.draw(self.renderer)
433+
# A GUI class may be need to update a window using this draw, so
434+
# don't forget to call the superclass.
435+
super(FigureCanvasAgg, self).draw()
433436
finally:
434437
if toolbar:
435438
toolbar.set_cursor(toolbar._lastCursor)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .backend_qt5cairo import _BackendQT5Cairo
2+
3+
4+
@_BackendQT5Cairo.export
5+
class _BackendQT4Cairo(_BackendQT5Cairo):
6+
pass

lib/matplotlib/backends/backend_qt5.py

+88-6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import signal
99
import sys
1010
from six import unichr
11+
import traceback
1112

1213
import matplotlib
1314

@@ -226,19 +227,16 @@ class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase):
226227
# QtCore.Qt.XButton2: None,
227228
}
228229

229-
def _update_figure_dpi(self):
230-
dpi = self._dpi_ratio * self.figure._original_dpi
231-
self.figure._set_dpi(dpi, forward=False)
232-
233230
@_allow_super_init
234231
def __init__(self, figure):
235232
_create_qApp()
236233
super(FigureCanvasQT, self).__init__(figure=figure)
237234

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

249+
self._draw_pending = False
250+
self._is_drawing = False
251+
self._draw_rect_callback = lambda painter: None
252+
253+
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
251254
self.setMouseTracking(True)
255+
self.resize(*self.get_width_height())
252256
# Key auto-repeat enabled by default
253257
self._keyautorepeat = True
254258

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

262+
def _update_figure_dpi(self):
263+
dpi = self._dpi_ratio * self.figure._original_dpi
264+
self.figure._set_dpi(dpi, forward=False)
265+
258266
@property
259267
def _dpi_ratio(self):
260268
# Not available on Qt4 or some older Qt5.
@@ -263,6 +271,26 @@ def _dpi_ratio(self):
263271
except AttributeError:
264272
return 1
265273

274+
def _update_dpi(self):
275+
# As described in __init__ above, we need to be careful in cases with
276+
# mixed resolution displays if dpi_ratio is changing between painting
277+
# events.
278+
# Return whether we triggered a resizeEvent (and thus a paintEvent)
279+
# from within this function.
280+
if self._dpi_ratio != self._dpi_ratio_prev:
281+
# We need to update the figure DPI.
282+
self._update_figure_dpi()
283+
self._dpi_ratio_prev = self._dpi_ratio
284+
# The easiest way to resize the canvas is to emit a resizeEvent
285+
# since we implement all the logic for resizing the canvas for
286+
# that event.
287+
event = QtGui.QResizeEvent(self.size(), self.size())
288+
self.resizeEvent(event)
289+
# resizeEvent triggers a paintEvent itself, so we exit this one
290+
# (after making sure that the event is immediately handled).
291+
return True
292+
return False
293+
266294
def get_width_height(self):
267295
w, h = FigureCanvasBase.get_width_height(self)
268296
return int(w / self._dpi_ratio), int(h / self._dpi_ratio)
@@ -453,6 +481,60 @@ def stop_event_loop(self, event=None):
453481
if hasattr(self, "_event_loop"):
454482
self._event_loop.quit()
455483

484+
def draw(self):
485+
"""Render the figure, and queue a request for a Qt draw.
486+
"""
487+
# The Agg draw is done here; delaying causes problems with code that
488+
# uses the result of the draw() to update plot elements.
489+
if self._is_drawing:
490+
return
491+
self._is_drawing = True
492+
try:
493+
super(FigureCanvasQT, self).draw()
494+
finally:
495+
self._is_drawing = False
496+
self.update()
497+
498+
def draw_idle(self):
499+
"""Queue redraw of the Agg buffer and request Qt paintEvent.
500+
"""
501+
# The Agg draw needs to be handled by the same thread matplotlib
502+
# modifies the scene graph from. Post Agg draw request to the
503+
# current event loop in order to ensure thread affinity and to
504+
# accumulate multiple draw requests from event handling.
505+
# TODO: queued signal connection might be safer than singleShot
506+
if not (self._draw_pending or self._is_drawing):
507+
self._draw_pending = True
508+
QtCore.QTimer.singleShot(0, self._draw_idle)
509+
510+
def _draw_idle(self):
511+
if self.height() < 0 or self.width() < 0:
512+
self._draw_pending = False
513+
if not self._draw_pending:
514+
return
515+
try:
516+
self.draw()
517+
except Exception:
518+
# Uncaught exceptions are fatal for PyQt5, so catch them instead.
519+
traceback.print_exc()
520+
finally:
521+
self._draw_pending = False
522+
523+
def drawRectangle(self, rect):
524+
# Draw the zoom rectangle to the QPainter. _draw_rect_callback needs
525+
# to be called at the end of paintEvent.
526+
if rect is not None:
527+
def _draw_rect_callback(painter):
528+
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio,
529+
QtCore.Qt.DotLine)
530+
painter.setPen(pen)
531+
painter.drawRect(*(pt / self._dpi_ratio for pt in rect))
532+
else:
533+
def _draw_rect_callback(painter):
534+
return
535+
self._draw_rect_callback = _draw_rect_callback
536+
self.update()
537+
456538

457539
class MainWindow(QtWidgets.QMainWindow):
458540
closing = QtCore.Signal()

lib/matplotlib/backends/backend_qt5agg.py

+12-103
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import six
88

99
import ctypes
10-
import traceback
1110

1211
from matplotlib import cbook
1312
from matplotlib.transforms import Bbox
@@ -19,32 +18,11 @@
1918
from .qt_compat import QT_API
2019

2120

22-
class FigureCanvasQTAggBase(FigureCanvasAgg):
23-
"""
24-
The canvas the figure renders into. Calls the draw and print fig
25-
methods, creates the renderers, etc...
26-
27-
Attributes
28-
----------
29-
figure : `matplotlib.figure.Figure`
30-
A high-level Figure instance
31-
32-
"""
21+
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):
3322

3423
def __init__(self, figure):
35-
super(FigureCanvasQTAggBase, self).__init__(figure=figure)
36-
self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent)
37-
self._agg_draw_pending = False
38-
self._agg_is_drawing = False
24+
super(FigureCanvasQTAgg, self).__init__(figure=figure)
3925
self._bbox_queue = []
40-
self._drawRect = None
41-
42-
def drawRectangle(self, rect):
43-
if rect is not None:
44-
self._drawRect = [pt / self._dpi_ratio for pt in rect]
45-
else:
46-
self._drawRect = None
47-
self.update()
4826

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

8243
# if the canvas does not have a renderer, then give up and wait for
8344
# FigureCanvasAgg.draw(self) to be called
@@ -100,23 +61,17 @@ def paintEvent(self, e):
10061
reg = self.copy_from_bbox(bbox)
10162
buf = reg.to_string_argb()
10263
qimage = QtGui.QImage(buf, w, h, QtGui.QImage.Format_ARGB32)
64+
# Adjust the buf reference count to work around a memory leak bug
65+
# in QImage under PySide on Python 3.
66+
if QT_API == 'PySide' and six.PY3:
67+
ctypes.c_long.from_address(id(buf)).value = 1
10368
if hasattr(qimage, 'setDevicePixelRatio'):
10469
# Not available on Qt4 or some older Qt5.
10570
qimage.setDevicePixelRatio(self._dpi_ratio)
10671
origin = QtCore.QPoint(l, self.renderer.height - t)
10772
painter.drawImage(origin / self._dpi_ratio, qimage)
108-
# Adjust the buf reference count to work around a memory
109-
# leak bug in QImage under PySide on Python 3.
110-
if QT_API == 'PySide' and six.PY3:
111-
ctypes.c_long.from_address(id(buf)).value = 1
11273

113-
# draw the zoom rectangle to the QPainter
114-
if self._drawRect is not None:
115-
pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio,
116-
QtCore.Qt.DotLine)
117-
painter.setPen(pen)
118-
x, y, w, h = self._drawRect
119-
painter.drawRect(x, y, w, h)
74+
self._draw_rect_callback(painter)
12075

12176
painter.end()
12277

@@ -130,42 +85,11 @@ def draw(self):
13085

13186
self._agg_is_drawing = True
13287
try:
133-
super(FigureCanvasQTAggBase, self).draw()
88+
super(FigureCanvasQTAgg, self).draw()
13489
finally:
13590
self._agg_is_drawing = False
13691
self.update()
13792

138-
def draw_idle(self):
139-
"""Queue redraw of the Agg buffer and request Qt paintEvent.
140-
"""
141-
# The Agg draw needs to be handled by the same thread matplotlib
142-
# modifies the scene graph from. Post Agg draw request to the
143-
# current event loop in order to ensure thread affinity and to
144-
# accumulate multiple draw requests from event handling.
145-
# TODO: queued signal connection might be safer than singleShot
146-
if not (self._agg_draw_pending or self._agg_is_drawing):
147-
self._agg_draw_pending = True
148-
QtCore.QTimer.singleShot(0, self.__draw_idle_agg)
149-
150-
def __draw_idle_agg(self, *args):
151-
# if nothing to do, bail
152-
if not self._agg_draw_pending:
153-
return
154-
# we have now tried this function at least once, do not run
155-
# again until re-armed. Doing this here rather than after
156-
# protects against recursive calls triggered through self.draw
157-
# The recursive call is via `repaintEvent`
158-
self._agg_draw_pending = False
159-
# if negative size, bail
160-
if self.height() < 0 or self.width() < 0:
161-
return
162-
try:
163-
# actually do the drawing
164-
self.draw()
165-
except Exception:
166-
# Uncaught exceptions are fatal for PyQt5, so catch them instead.
167-
traceback.print_exc()
168-
16993
def blit(self, bbox=None):
17094
"""Blit the region in bbox.
17195
"""
@@ -182,25 +106,10 @@ def blit(self, bbox=None):
182106
self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h)
183107

184108
def print_figure(self, *args, **kwargs):
185-
super(FigureCanvasQTAggBase, self).print_figure(*args, **kwargs)
109+
super(FigureCanvasQTAgg, self).print_figure(*args, **kwargs)
186110
self.draw()
187111

188112

189-
class FigureCanvasQTAgg(FigureCanvasQTAggBase, FigureCanvasQT):
190-
"""
191-
The canvas the figure renders into. Calls the draw and print fig
192-
methods, creates the renderers, etc.
193-
194-
Modified to import from Qt5 backend for new-style mouse events.
195-
196-
Attributes
197-
----------
198-
figure : `matplotlib.figure.Figure`
199-
A high-level Figure instance
200-
201-
"""
202-
203-
204113
@_BackendQT5.export
205114
class _BackendQT5Agg(_BackendQT5):
206115
FigureCanvas = FigureCanvasQTAgg
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
2+
from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT
3+
from .qt_compat import QT_API
4+
5+
6+
class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo):
7+
def __init__(self, figure):
8+
super(FigureCanvasQTCairo, self).__init__(figure=figure)
9+
self._renderer = RendererCairo(self.figure.dpi)
10+
11+
def paintEvent(self, event):
12+
self._update_dpi()
13+
dpi_ratio = self._dpi_ratio
14+
width = dpi_ratio * self.width()
15+
height = dpi_ratio * self.height()
16+
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
17+
self._renderer.set_ctx_from_surface(surface)
18+
self._renderer.set_width_height(width, height)
19+
self.figure.draw(self._renderer)
20+
buf = surface.get_data()
21+
qimage = QtGui.QImage(buf, width, height,
22+
QtGui.QImage.Format_ARGB32_Premultiplied)
23+
# Adjust the buf reference count to work around a memory leak bug in
24+
# QImage under PySide on Python 3.
25+
if QT_API == 'PySide' and six.PY3:
26+
ctypes.c_long.from_address(id(buf)).value = 1
27+
if hasattr(qimage, 'setDevicePixelRatio'):
28+
# Not available on Qt4 or some older Qt5.
29+
qimage.setDevicePixelRatio(dpi_ratio)
30+
painter = QtGui.QPainter(self)
31+
painter.drawImage(0, 0, qimage)
32+
self._draw_rect_callback(painter)
33+
painter.end()
34+
35+
36+
@_BackendQT5.export
37+
class _BackendQT5Cairo(_BackendQT5):
38+
FigureCanvas = FigureCanvasQTCairo

0 commit comments

Comments
 (0)