Skip to content

wx backends: don't use ClientDC any more #11944

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

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
Draft
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
15 changes: 15 additions & 0 deletions doc/api/next_api_changes/2018-09-01-DS-backends_wx.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
wx Backends
-----------

The internal implementation of the wx backends was changed to do all
the screen painting inside the ``_OnPaint`` method which handles wx
``EVT_PAINT`` events.
So for a screen update due to a call to ``draw`` or for drawing a
selection rubberband, the ``Refresh`` method is called to trigger
a later paint event instead of drawing directly to a ``ClientDC``.

The atribute ``_retinaFix`` has moved from ``NavigationToolbar2Wx``
to ``_FigureCanvasWxBase``.

The method ``gui_repaint`` of all wx canvases has been removed.
The ``draw`` method no longer accepts an argument ``drawDC``.
280 changes: 83 additions & 197 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,7 @@ class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel):
wx.WXK_NUMPAD_INSERT: 'insert',
wx.WXK_NUMPAD_DELETE: 'delete',
}
_retinaFix = 'wxMac' in wx.PlatformInfo

def __init__(self, parent, id, figure):
"""
Expand All @@ -531,6 +532,8 @@ def __init__(self, parent, id, figure):
_log.debug("%s - __init__() - bitmap w:%d h:%d", type(self), w, h)
# TODO: Add support for 'point' inspection and plot navigation.
self._isDrawn = False
self._rubberband = None # a selection rectangle to be drawn
self._overlay = None

self.Bind(wx.EVT_SIZE, self._onSize)
self.Bind(wx.EVT_PAINT, self._onPaint)
Expand Down Expand Up @@ -619,29 +622,6 @@ def _get_imagesave_wildcards(self):
wildcards = '|'.join(wildcards)
return wildcards, extensions, filter_index

def gui_repaint(self, drawDC=None, origin='WX'):
Copy link
Contributor

Choose a reason for hiding this comment

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

Need to add an API change note (it's mostly an implementation detail so not going to ask a deprecation period, which probably wouldn't make sense anyways).

"""
Performs update of the displayed image on the GUI canvas, using the
supplied wx.PaintDC device context.

The 'WXAgg' backend sets origin accordingly.
"""
_log.debug("%s - gui_repaint()", type(self))
if self.IsShownOnScreen():
if not drawDC:
# not called from OnPaint use a ClientDC
drawDC = wx.ClientDC(self)

# following is for 'WX' backend on Windows
# the bitmap can not be in use by another DC,
# see GraphicsContextWx._cache
if wx.Platform == '__WXMSW__' and origin == 'WX':
img = self.bitmap.ConvertToImage()
bmp = img.ConvertToBitmap()
drawDC.DrawBitmap(bmp, 0, 0)
else:
drawDC.DrawBitmap(self.bitmap, 0, 0)

filetypes = {
**FigureCanvasBase.filetypes,
'bmp': 'Windows bitmap',
Expand All @@ -666,12 +646,30 @@ def print_figure(self, filename, *args, **kwargs):
def _onPaint(self, event):
"""Called when wxPaintEvt is generated."""
_log.debug("%s - _onPaint()", type(self))
drawDC = wx.PaintDC(self)
if not self._isDrawn:
self.draw(drawDC=drawDC)
else:
self.gui_repaint(drawDC=drawDC)
drawDC.Destroy()
self._draw()

# the bitmap can not be in use by another DC
img = self.bitmap.ConvertToImage()
bmp = img.ConvertToBitmap()
dc = wx.BufferedPaintDC(self, bmp)

if not self._rubberband:
return

# draw rubberband / selection: a box with border and 50% transparency
if not self._retinaFix:
dc = wx.GCDC(dc)
# Set the pen, for the box's border
bc = wx.BLUE
dc.SetPen(wx.Pen(colour=bc, width=1, style=wx.PENSTYLE_SOLID))
# Create a brush (for the box's interior) with the same colour,
# but 50% transparency.
bc = wx.Colour(bc.red, bc.green, bc.blue, 0x80)
dc.SetBrush(wx.Brush(bc))

Copy link
Contributor

@newville newville Oct 6, 2018

Choose a reason for hiding this comment

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

some comments and suggestions:

a) use dc.SetLogicalFunction(wx.XOR) and a white (or light) Pen color so that the border shows on axes with different background face colors. Similarly, let the brush be more transparent.

b) use the explicit 'wxMac' in wx.PlatformInfo -- it's only used once anyway.

c) why is dc overwritten with wx.GCDC(dc)? That seems unnecessary.

Perhaps something like this:

        odc = wx.DCOverlay(self._overlay, dc)

        dc.SetLogicalFunction(wx.XOR)
        dc.SetPen(wx.Pen(colour=wx.WHITE, width=2,
                         style=wx.PENSTYLE_SOLID))

        # Create a semi-transparent brush for the box's interior
        # except on Macs, as that is buggy on Retina displays
        if 'wxMac' in wx.PlatformInfo:
            dc.SetBrush(wx.TRANSPARENT_BRUSH)
        else:
            dc.SetBrush(wx.Brush(wx.Colour(0, 0, 255, 0x20)))

        # Draw the rectangle

# Draw the rectangle
dc.DrawRectangle(*self._rubberband)

def _onSize(self, event):
"""
Expand Down Expand Up @@ -824,20 +822,59 @@ def _onEnter(self, event):
event.Skip()
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))

def _draw_rubberband(self, x0, y0, x1, y1):
# trigger a refresh to draw a rubberband-like selection box
width = abs(x1 - x0)
x0 = min(x0, x1)
height = abs(y1 - y0)
y0 = self.figure.bbox.height - max(y0, y1)
previous_rubberband = self._rubberband
self._rubberband = (x0, y0, width, height)
self._refresh_rubberband(previous_rubberband)

def _remove_rubberband(self):
# end drawing of a rubberband-like selection box
if not self._rubberband:
return
previous_rubberband = self._rubberband
self._rubberband = None
self._refresh_rubberband(previous_rubberband) # trigger a later redraw

def _refresh_rubberband(self, previous_rubberband=None):
# initiate a refresh on the area that contains the previous and
# current selection / rubberband
if self._rubberband:
rect = wx.Rect(self._rubberband)
else:
rect = None
if previous_rubberband:
previous_rubberband = wx.Rect(*previous_rubberband)
# extend by one pixel to avoid residuals on Mac OS
previous_rubberband.Inflate(1, 1)
if rect:
rect.Union(previous_rubberband)
else:
rect = previous_rubberband
rect.Intersect(self.Rect)
self.Refresh(False, rect)


class FigureCanvasWx(_FigureCanvasWxBase):
# Rendering to a Wx canvas using the deprecated Wx renderer.

def draw(self, drawDC=None):
def _draw(self):
_log.debug("%s - _draw()", type(self))
self.renderer = RendererWx(self.bitmap, self.figure.dpi)
self.figure.draw(self.renderer)
self._isDrawn = True

def draw(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Add note re: API change.

"""
Render the figure using RendererWx instance renderer, or using a
previously defined renderer if none is specified.
"""
_log.debug("%s - draw()", type(self))
self.renderer = RendererWx(self.bitmap, self.figure.dpi)
self.figure.draw(self.renderer)
self._isDrawn = True
self.gui_repaint(drawDC=drawDC)
self._draw()
self.Refresh()

def print_bmp(self, filename, *args, **kwargs):
return self._print_image(filename, wx.BITMAP_TYPE_BMP, *args, **kwargs)
Expand Down Expand Up @@ -908,7 +945,7 @@ def _print_image(self, filename, filetype, *args, **kwargs):
# been cleaned up. The artist contains() methods will fail
# otherwise.
if self._isDrawn:
self.draw()
self._draw()
self.Refresh()


Expand Down Expand Up @@ -1120,11 +1157,6 @@ def __init__(self, canvas):
self.canvas = canvas
self._idle = True
self.prevZoomRect = None
# for now, use alternate zoom-rectangle drawing on all
# Macs. N.B. In future versions of wx it may be possible to
# detect Retina displays with window.GetContentScaleFactor()
# and/or dc.GetContentScaleFactor()
self.retinaFix = 'wxMac' in wx.PlatformInfo
Copy link
Contributor

Choose a reason for hiding this comment

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

All these attributes need removal notices.


def get_canvas(self, frame, fig):
return type(self.canvas)(frame, -1, fig)
Expand Down Expand Up @@ -1216,85 +1248,14 @@ def set_cursor(self, cursor):
self.canvas.Update()

def press(self, event):
if self._active == 'ZOOM':
if not self.retinaFix:
self.wxoverlay = wx.Overlay()
else:
if event.inaxes is not None:
self.savedRetinaImage = self.canvas.copy_from_bbox(
event.inaxes.bbox)
self.zoomStartX = event.xdata
self.zoomStartY = event.ydata
self.zoomAxes = event.inaxes
pass

def release(self, event):
if self._active == 'ZOOM':
# When the mouse is released we reset the overlay and it
# restores the former content to the window.
if not self.retinaFix:
self.wxoverlay.Reset()
del self.wxoverlay
else:
del self.savedRetinaImage
if self.prevZoomRect:
self.prevZoomRect.pop(0).remove()
self.prevZoomRect = None
if self.zoomAxes:
self.zoomAxes = None
self.canvas._remove_rubberband()

def draw_rubberband(self, event, x0, y0, x1, y1):
if self.retinaFix: # On Macs, use the following code
# wx.DCOverlay does not work properly on Retina displays.
rubberBandColor = '#C0C0FF'
if self.prevZoomRect:
self.prevZoomRect.pop(0).remove()
self.canvas.restore_region(self.savedRetinaImage)
X0, X1 = self.zoomStartX, event.xdata
Y0, Y1 = self.zoomStartY, event.ydata
lineX = (X0, X0, X1, X1, X0)
lineY = (Y0, Y1, Y1, Y0, Y0)
self.prevZoomRect = self.zoomAxes.plot(
lineX, lineY, '-', color=rubberBandColor)
self.zoomAxes.draw_artist(self.prevZoomRect[0])
self.canvas.blit(self.zoomAxes.bbox)
return

# Use an Overlay to draw a rubberband-like bounding box.

dc = wx.ClientDC(self.canvas)
odc = wx.DCOverlay(self.wxoverlay, dc)
odc.Clear()

# Mac's DC is already the same as a GCDC, and it causes
# problems with the overlay if we try to use an actual
# wx.GCDC so don't try it.
if 'wxMac' not in wx.PlatformInfo:
dc = wx.GCDC(dc)

height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0

if y1 < y0:
y0, y1 = y1, y0
if x1 < x0:
x0, x1 = x1, x0

w = x1 - x0
h = y1 - y0
rect = wx.Rect(x0, y0, w, h)

rubberBandColor = '#C0C0FF' # or load from config?

# Set a pen for the border
color = wx.Colour(rubberBandColor)
dc.SetPen(wx.Pen(color, 1))

# use the same color, plus alpha for the brush
r, g, b, a = color.Get(True)
color.Set(r, g, b, 0x60)
dc.SetBrush(wx.Brush(color))
dc.DrawRectangle(rect)
self.canvas._draw_rubberband(x0, y0, x1, y1)

@cbook.deprecated("3.2")
def set_status_bar(self, statbar):
Expand Down Expand Up @@ -1451,91 +1412,16 @@ def set_cursor(self, cursor):
self._make_classic_style_pseudo_toolbar(), cursor)


if 'wxMac' not in wx.PlatformInfo:
# on most platforms, use overlay
class RubberbandWx(backend_tools.RubberbandBase):
def __init__(self, *args, **kwargs):
backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
self.wxoverlay = None

def draw_rubberband(self, x0, y0, x1, y1):
# Use an Overlay to draw a rubberband-like bounding box.
if self.wxoverlay is None:
self.wxoverlay = wx.Overlay()
dc = wx.ClientDC(self.canvas)
odc = wx.DCOverlay(self.wxoverlay, dc)
odc.Clear()

dc = wx.GCDC(dc)

height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0

if y1 < y0:
y0, y1 = y1, y0
if x1 < x0:
x0, x1 = x1, x0

w = x1 - x0
h = y1 - y0
rect = wx.Rect(x0, y0, w, h)

rubberBandColor = '#C0C0FF' # or load from config?

# Set a pen for the border
color = wx.Colour(rubberBandColor)
dc.SetPen(wx.Pen(color, 1))
class RubberbandWx(backend_tools.RubberbandBase):
def __init__(self, *args, **kwargs):
backend_tools.RubberbandBase.__init__(self, *args, **kwargs)

# use the same color, plus alpha for the brush
r, g, b, a = color.Get(True)
color.Set(r, g, b, 0x60)
dc.SetBrush(wx.Brush(color))
dc.DrawRectangle(rect)
def draw_rubberband(self, x0, y0, x1, y1):
# Use an Overlay to draw a rubberband-like bounding box.
self.canvas._draw_rubberband(x0, y0, x1, y1)

def remove_rubberband(self):
if self.wxoverlay is None:
return
self.wxoverlay.Reset()
self.wxoverlay = None

else:
# on Mac OS retina displays DCOverlay does not work
# and dc.SetLogicalFunction does not have an effect on any display
# the workaround is to blit the full image for remove_rubberband
class RubberbandWx(backend_tools.RubberbandBase):
def __init__(self, *args, **kwargs):
backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
self._rect = None

def draw_rubberband(self, x0, y0, x1, y1):
dc = wx.ClientDC(self.canvas)
# this would be required if the Canvas is a ScrolledWindow,
# which is not the case for now
# self.PrepareDC(dc)

# delete old rubberband
if self._rect:
self.remove_rubberband(dc)

# draw new rubberband
dc.SetPen(wx.Pen(wx.BLACK, 1, wx.SOLID))
dc.SetBrush(wx.TRANSPARENT_BRUSH)
self._rect = (x0, self.canvas._height-y0, x1-x0, -y1+y0)
dc.DrawRectangle(self._rect)

def remove_rubberband(self, dc=None):
if not self._rect:
return
if self.canvas.bitmap:
if dc is None:
dc = wx.ClientDC(self.canvas)
dc.DrawBitmap(self.canvas.bitmap, 0, 0)
# for testing the method on Windows, use this code instead:
# img = self.canvas.bitmap.ConvertToImage()
# bmp = img.ConvertToBitmap()
# dc.DrawBitmap(bmp, 0, 0)
self._rect = None
def remove_rubberband(self):
self.canvas._remove_rubberband()


class _HelpDialog(wx.Dialog):
Expand Down
Loading