Skip to content
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

Image redraw problem on OSX with wx>2.9 & mpl>1.1 #3734

Closed
briantoby opened this issue Oct 28, 2014 · 6 comments
Closed

Image redraw problem on OSX with wx>2.9 & mpl>1.1 #3734

briantoby opened this issue Oct 28, 2014 · 6 comments

Comments

@briantoby
Copy link

I have seen a problem on more than one Mac running OS X 10.9.x where MPL images are not updated properly. As one example of this, when one zooms a plot, by selecting the zoom widget, the rectangle is never erased and after it is "released" the plot is never redrawn with the new limits. This happens when I embed a figure into a wx Panel, as shown in the script below.

I do not see the problem with EPD (python 2.7.3, wxpython 2.8.10.1, matplotlib 1.1.0), but do with fairly recent Anaconda (python 2.7.8, wxpython 3.0.0.0, matplotlib 1.4.2) and Canopy (python 2.7.6, wxpython 2.9.2.4, matplotlib 1.3.1) dists.

mplbug

# simple demo that fails on Mac (at least on 10.9.5) and later wx/mpl combos
#
# problem is that if one selects the zoom button and drags a region,
# the rectangles never disappear and the image is not updated to
# show the zoomed in region
#
# works (not quite 100%) w/EPD (python 2.7.3, wxpython 2.8.10.1, matplotlib 1.1.0)
# fails w/Anaconda (python 2.7.8, wxpython 3.0.0.0, matplotlib 1.4.2
# fails w/Canopy (python 2.7.6, wxpython 2.9.2.4, matplotlib 1.3.1)
#

import sys
import wx
import wx.aui
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as mplCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2Wx as mplToolbar
print "python:     ",sys.version[:5]
print "wxpython:   ",wx.__version__
print "matplotlib: ",mpl.__version__

app = wx.PySimpleApp()

# create frame with notebook
frame = wx.Frame(None, size=(400,400)) 
win = wx.Panel(frame)
win.nb = wx.aui.AuiNotebook(win)
sizer = wx.BoxSizer(wx.VERTICAL) 
sizer.Add(win.nb, 1, wx.EXPAND)
win.SetSizer(sizer)

# create a panel for the plot with a plot in it
tab = wx.Panel(win.nb, id=-1)
figure = mpl.figure.Figure(dpi=None)
# create a quick figure for the plot
ax = figure.gca()
ax.plot(range(10),range(10),'o-')
canvas = mplCanvas(tab, -1, figure) 
toolbar = mplToolbar(canvas)
toolbar.Realize()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(canvas,1,wx.EXPAND)
sizer.Add(toolbar, 0 , wx.LEFT | wx.EXPAND)
tab.SetSizer(sizer)
win.nb.AddPage(tab,'plot1') # add the panel as a notebook tab

frame.Show()
app.MainLoop()

@briantoby
Copy link
Author

I have spent some more time on this and have noticed that in _onDrawIdle() in backend_wx.py, the wx.GetApp().Pending() call seems to always evaluate as True.

I have also seen that I can find a similar bug in Windows w/only 64 bit wx, where after the first zoom, home or arrow use, .Pending also never stops returning as True.

Patching draw_idle() to call self.draw() directly seems to solve at least some of the problems:

def draw_idle(self):
    """
    Delay rendering until the GUI is idle.
    """
    DEBUG_MSG("draw_idle()", 1, self)
    self._isDrawn = False  # Force redraw
    if major >= 2 or (major == 2 and minor >= 8):
        wx.CallAfter(self.draw)
    # Create a timer for handling draw_idle requests
    # If there are events pending when the timer is
    # complete, reset the timer and continue.  The
    # alternative approach, binding to wx.EVT_IDLE,
    # doesn't behave as nicely.
    elif hasattr(self,'_idletimer'):
     ...

I suspect this causes other problems. This also does not address the uglyness when dragging the zoom box.

@RobinD42
Copy link
Contributor

RobinD42 commented Dec 1, 2014

The docstring for draw_idle is: "Delay rendering until the GUI is idle." To do that in wx you do not need to mess with Pending and timers at all. Simply using Refresh will do basically the same thing. It causes the system to add a paint event to the queue, but it will not be processed immediately. It will typically be delayed until there are no other pending events, or until painting is forced for some reason. On OSX the paint event will be scheduled into the active display pipeline so it will happen at the best possible moment taking the whole display into account.

On a former job I worked around this issue by subclassing FigureCanvasWxAgg and overriding draw_idle like this:

    def draw_idle(self, *args, **kw):
        # Instead of messing with timers and wx.App.Pending, just trigger a
        # paint event.
        self._isDrawn = False
        self.Refresh(eraseBackground=False)

It worked well for at least the use cases needed by that application (several histogram, scatter and img plots) and was tested on Windows and OSX. wxGTK ports should also do fine with this change.

NOTE: the above will not fix the trails left behind while dragging the rubberband. I have another fix for that which I'll try to track down.

@RobinD42
Copy link
Contributor

RobinD42 commented Dec 1, 2014

To get a better rubberband effect that also doesn't leave trails on OSX, you can use a wx.Overlay instead of directly drawing to a wx.ClientDC (which is just a bad idea in most cases these days.) I've implemented this in an old project by overriding the following methods in a class derived from NavigationToolbar2Wx. This should work in at least wxPython 2.8.12, 2.9, and 3.0+, and possibly earlier 2.x versions. You may want to encorporate these changes directly into NavigationToolbar2Wx.

    def press(self, event):
        if self._active == 'ZOOM':
            self.wxoverlay = wx.Overlay()

    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.
            self.wxoverlay.Reset()
            del self.wxoverlay

    def draw_rubberband(self, event, x0, y0, x1, y1):
        # Use an Overlay to draw a more awesome rubberband.

        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<y0: 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.NamedColour(rubberBandColor)
        dc.SetPen(wx.Pen(color, 1))

        # use the same color, plus alpha for the brush
        r, g, b = color.Get()
        color.Set(r,g,b, 0x60)
        dc.SetBrush(wx.Brush(color))
        dc.DrawRectangleRect(rect)

@efiring
Copy link
Member

efiring commented Dec 1, 2014

@RobinD42 Thanks very much for the tips. If you would like to turn them into a PR, that would be even better; but if not, I expect they will get in one way or another.

@RobinD42
Copy link
Contributor

RobinD42 commented Dec 2, 2014

Yeah, I figured that would be the first question. 😉

I didn't have time to do a real PR today, but I'll try to do it soonish.

@tacaswell
Copy link
Member

closed by #3905

@tacaswell tacaswell modified the milestones: v1.4.x, v1.4.3 Jan 15, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants