Problems with wx.Overlay on OSX 10.11

The sample code below draws a crosshair on top of a wx.Window which itself is drawn by means of a wx.BufferedDC. The crosshair can be placed by clicking with the left button (works ok on OSX and windows) and should maintain its position when the window is resized. This is achieved by listening to SIZE events. The handler redraws the background and forces a refresh of the window which triggers a PAINT event. The last line in the paint event handler is

wx.CallAfter(self._draw_crosshair)

which should draw the crosshair again. The latter works fine on windows, but not on OSX (function is called but nothing drawn). Am I doing something wrong? wxpython is 4.1.0

import wx
import numpy as np

from PIL import Image

X,Y = 19,27

class Map(wx.Window):
    def __init__(self, parent):
        super(Map, self).__init__(parent, style=wx.WANTS_CHARS)

        self._cross = [2, 4]
        self.overlay = wx.Overlay()
        self._buffer = None

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMapLeftDown)

    def _draw_crosshair(self, pt=None):
        if pt is not None:
            x, y = pt
            self._cross = pt
        else:
            x, y = self._cross

        print('draw overlay at pos {},{}'.format(x,y))

        w, h = self.canvas_size
        dx = np.diff(self.xsteps)[x]
        dy = np.diff(self.ysteps)[y]

        dc = wx.ClientDC(self)
        odc = wx.DCOverlay(self.overlay, dc)
        odc.Clear()
        if 'wxMac' not in wx.PlatformInfo:
            dc = wx.GCDC(dc)

        dc.SetBrush(wx.Brush(wx.Colour(255, 255, 0, 120)))
        dc.SetPen(wx.Pen(wx.Colour(255, 255, 0, 120), 1))
        dc.DrawRectangle(self.xsteps[x], 0, dx, self.ysteps[y])
        dc.DrawRectangle(self.xsteps[x], self.ysteps[y] + dy, dx, h - (self.ysteps[y] + dy))
        dc.DrawRectangle(0, self.ysteps[y], self.xsteps[x], dy)
        dc.DrawRectangle(self.xsteps[x] + dx, self.ysteps[y], w - (self.xsteps[x] + dx), dy)

        del odc
        # del dc

    def OnMapLeftDown(self, evt):
        self.SetFocus()
        w,h = self.canvas_size
        x, y = evt.GetX(), evt.GetY()
        if evt.LeftIsDown() and x < w and y < h:
            idx_x = int(float(x)/w*X)
            idx_y = int(float(y)/h*Y)
            self._draw_crosshair([idx_x, idx_y])

    def OnPaint(self, event):
        print('on paint')
        if hasattr(self, '_buffer') and self._buffer is not None:
            dc = wx.BufferedPaintDC(self, self._buffer)

        wx.CallAfter(self._draw_crosshair)

    def OnSize(self, evt):
        print('size evt')
        w, h = self.GetClientSize()
        w = max(1, w)
        h = max(1, h)
        self._buffer = wx.Bitmap(w, h)
        self.Draw()

    def Draw(self):
        print('draw')
        w, h = self.GetClientSize()
        if w < 1 and h < 1:
            return

        if self._buffer is not None:
            self.overlay.Reset()

            dc = wx.BufferedDC(wx.ClientDC(self), self._buffer)
            if 'wxMac' not in wx.PlatformInfo:
                dc = wx.GCDC(dc)

            self.canvas_size = cw, ch = int(h * X / Y), h

            checker = np.zeros((Y * X), dtype='uint8')
            checker[::2] = 1
            checker.shape = (Y, X)

            if ch > 0 and cw > 0:
                img = Image.fromarray(checker*100, mode='L').resize((cw, ch), Image.NEAREST)
                self._image = wx.Bitmap.FromBuffer(cw,ch,img.convert('RGB').tobytes())
                checker = np.array(img)
                self.xsteps = np.hstack(([0], np.where(checker[0, :-1] != checker[0, 1:])[0] + 1, [cw]))
                self.ysteps = np.hstack(([0], np.where(checker[:-1, 0] != checker[1:, 0])[0] + 1, [ch]))

                dc.Clear()
                dc.DrawBitmap(wx.Bitmap(self._image), 0, 0)
                self.Refresh()


if __name__ == '__main__':
    a = wx.App()
    f = wx.Frame(None)
    m = Map(f)
    b = wx.BoxSizer(wx.HORIZONTAL)
    b.Add(m,1,wx.EXPAND)
    f.SetSizer(b)
    m.Draw()
    f.Show()
    a.MainLoop()

Thanks in advance, Christian

Please try with wxPython 4.1.1. If I’m not misunderstanding your issue then it seems to be working correctly for me.

Thank you for the suggestion but unfortunately switching to 4.1.1 did not fix the problem. The crosshair should appear right after the window is shown. Resizing the window should trigger the drawing at the transformed coordinates.

I really cannot understand why it makes a difference whether the overlay is drawn from a LEFT_DOWN handler or when calling the function via wx.CallAfter. Seeking for an alternative I checked if it it works when called from an IDLE handler but it does not. Any ideas?

Best, Christian

Once again me. I just tried wxpython 4.0.7 and it works as expected.

It may be that recent updates to wx.Overlay or something related are depending on APIs or features that are not available in OSX 10.11…

On the other hand, unless your real application needs much more than what your sample is doing, you may not need the overlay classes at all. I’ve tweaked your sample a bit to show one way to do it. Basically it boils down to doing all drawing to the screen in the paint event handler, and all that needs to do is to draw the background image, and then draw the crosshair. Whenever the crosshair needs to change it just updates the self._cross attribute and calls Refresh. Just for fun I added a motion event handler so you can drag the crosshair around and check that updates are happening smoothly.

Everything else, like creating the background image from the size events, etc., is basically the same as your sample.

import wx
print(wx.version())
import numpy as np
from PIL import Image

X,Y = 25,25

class Map(wx.Window):
    def __init__(self, parent):
        super(Map, self).__init__(parent, style=wx.WANTS_CHARS)

        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)

        self._cross = [2, 4]
        self._buffer = None

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMapLeftDown)
        self.Bind(wx.EVT_MOTION, self.OnMouseMotion)

    def _draw_crosshair(self, dc):
        x, y = self._cross

        w, h = self.canvas_size
        dx = np.diff(self.xsteps)[x]
        dy = np.diff(self.ysteps)[y]

        if 'wxMac' not in wx.PlatformInfo:
            dc = wx.GCDC(dc)

        dc.SetBrush(wx.Brush(wx.Colour(255, 255, 0, 120)))
        dc.SetPen(wx.Pen(wx.Colour(255, 255, 0, 120), 1))
        dc.DrawRectangle(self.xsteps[x], 0, dx, self.ysteps[y])
        dc.DrawRectangle(self.xsteps[x], self.ysteps[y] + dy, dx, h - (self.ysteps[y] + dy))
        dc.DrawRectangle(0, self.ysteps[y], self.xsteps[x], dy)
        dc.DrawRectangle(self.xsteps[x] + dx, self.ysteps[y], w - (self.xsteps[x] + dx), dy)

    def _update_crosshair(self, evt):
        w,h = self.canvas_size
        x, y = evt.GetX(), evt.GetY()
        if x < w and y < h:
            idx_x = int(float(x)/w*X)
            idx_y = int(float(y)/h*Y)
            self._cross = [idx_x, idx_y]
            self.Refresh()

    def OnMapLeftDown(self, evt):
        self.SetFocus()
        if evt.LeftIsDown():
            self._update_crosshair(evt)

    def OnMouseMotion(self, evt):
        if evt.Dragging():
            self._update_crosshair(evt)

    def OnPaint(self, event):
        #print('on paint')
        dc = wx.PaintDC(self) if self.IsDoubleBuffered() else wx.BufferedPaintDC(self)
        if hasattr(self, '_buffer') and self._buffer is not None:
            dc.DrawBitmap(self._buffer, 0, 0)
            self._draw_crosshair(dc)

    def OnSize(self, evt):
        print('size evt')
        w, h = self.GetClientSize()
        w = max(1, w)
        h = max(1, h)
        self._buffer = wx.Bitmap(w, h)
        self.Draw()

    def Draw(self):
        print('draw')
        w, h = self.GetClientSize()
        if w < 1 and h < 1:
            return

        if self._buffer is not None:
            dc = wx.BufferedDC(None, self._buffer)
            if 'wxMac' not in wx.PlatformInfo:
                dc = wx.GCDC(dc)

            self.canvas_size = cw, ch = int(h * X / Y), h

            checker = np.zeros((Y * X), dtype='uint8')
            checker[::2] = 1
            checker.shape = (Y, X)

            if ch > 0 and cw > 0:
                img = Image.fromarray(checker*100, mode='L').resize((cw, ch), Image.NEAREST)
                self._image = wx.Bitmap.FromBuffer(cw,ch,img.convert('RGB').tobytes())
                checker = np.array(img)
                self.xsteps = np.hstack(([0], np.where(checker[0, :-1] != checker[0, 1:])[0] + 1, [cw]))
                self.ysteps = np.hstack(([0], np.where(checker[:-1, 0] != checker[1:, 0])[0] + 1, [ch]))

                dc.Clear()
                dc.DrawBitmap(wx.Bitmap(self._image), 0, 0)
                self.Refresh()


if __name__ == '__main__':
    a = wx.App()
    f = wx.Frame(None)
    m = Map(f)
    b = wx.BoxSizer(wx.HORIZONTAL)
    b.Add(m,1,wx.EXPAND)
    f.SetSizer(b)
    m.Draw()
    f.Show()
    a.MainLoop()

You are right, I do not need much more than that. Thank you for showing this alternative. Anyway, I just wanted to report my problems with wx.Overlay. Have you tried it on a more recent Mac? Does the issue persist?

Playing with the code I recognised that I do not fully understand the drawing mechanisms. Could you explain the difference between

dc = wx.BufferedDC(None, self._buffer)

and

dc = wx.BufferedDC(wx.ClientDC(self), self._buffer)

in Draw()? Also, as an exercise I tried to use a ClientDC in Draw() to draw directly to the window knowing that it will be overwritten when a PAINT event occurs. It does not seem to work, i.e. nothing is drawn to the window even if the PAINT event handler does nothing. Does it have to do with the fact that on OSX DoubleBuffering is on by default (and cannot be turned off)?

I use 10.14 on my primary Mac, and it never showed the problem with overlays that you seem to be seeing.

The latter form draws the bitmap to the given DC when the wx.BufferedDC goes out of scope and it is gc’d. If all you need is to draw to the buffer bitmap (as is the case in your sample) then creating and drawing to the client DC is just a waste of time and resources, especially on OSX where most uses of client DCs will appear to be ignored (even though they are not) due to how the system manages the display pipeline and buffering.

Thank you for the explanation. When trying the code on windows I noticed a huge speed difference between:

def Draw(self, ...):
    dc = wx.BufferedDC(wx.ClientDC(self), self._buffer)
    # do all the drawing

def OnPaint(self, evt): # will only be called if necessary
    dc = BufferedPaintDC(self, self._buffer)

and

def Draw(self, ...):
    dc = wx.BufferedDC(None, self._buffer)
    # do all the drawing
    self.Refresh() # triggers PAINT event

def OnPaint(self, evt):
    dc = BufferedPaintDC(self)
    dc.DrawBitmap(self._buffer, 0, 0)

The latter is way slower! On OSX I would say that there is no noticable difference.

Do you have an idea why? Is that by design?

Three things.

  1. Paint events are sent when the system decides it’s the right time to do so. My understanding that the criteria for that decision is quite different between OSX and Windows. If there are lots of pending events then it’s possible that WIndows is delaying the paint event in order to not starve out the processing of the other events.

  2. The OnPaint in your second snippet above is actually drawing the bitmap twice. Since your bitmap fills the visible space of the window then once would be enough. You can either do it like in your first snippet, or just change the BufferedPaintDC to a PaintDC and use DrawBitmap there.

  3. Calling Update after the Refresh will cause the paint event to be sent and processed immediately instead of waiting for the system to do it. This can help in some cases, but usually it’s best to just let the system manage the update of the display.