Widget is not cleared on redraw

Hello!

I draw a grid, in the cells of which the images (wx.Bitmap) and several rectangles with text (cell numbers) will be located.

I need to use double buffering to get rid of the flickering (the flickering appears when I draw the cursor (a square on the selected cell)).

I noticed that when using double buffering, the EVT_PAINT handler is called even when I hover over the scrollbar. In this case, the animation of the scroll bar (smooth color change) occurs in jerks. Without double buffering, the animation is smooth.

Code
import wx
 
 
class PanelDraw(wx.ScrolledCanvas):
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
 
        self.SetDoubleBuffered(True)
        self.SetScrollRate(42, 42)
 
        self.cnt = 25
        self.padding = 32
        self.SetVirtualSize(self.cnt * 64 + 2 * self.padding,
                            self.cnt * 64 + 2 * self.padding)
 
        self.Bind(wx.EVT_PAINT, self.on_paint)
 
    def on_paint(self, event):
        dc = wx.PaintDC(self)
        dc.Clear()
        self.DoPrepareDC(dc)
 
        gc = wx.GraphicsContext.Create(dc)  # type: wx.GraphicsContext
 
        gc.SetBrush(wx.TRANSPARENT_BRUSH)
        pen = wx.Pen(wx.Colour(80, 80, 80, 127), 1, wx.PENSTYLE_USER_DASH)
        pen.SetDashes([2, 2])
        gc.SetPen(pen)
 
        for i in range(self.cnt + 1):
            gc.StrokeLine(self.padding + i * 64, self.padding,
                          self.padding + i * 64, self.padding + self.cnt * 64)
            gc.StrokeLine(self.padding, self.padding + i * 64,
                          self.padding+ self.cnt * 64, self.padding + i * 64)
 
 
class Frame(wx.Frame):
 
    def __init__(self):
        super().__init__(None)
 
        self.canvas = PanelDraw(self)
 
 
if __name__ == '__main__':
    app = wx.App()
    Frame().Show()
    app.MainLoop()

I managed to solve this problem by removing the call to SetDoubleBuffered and also using wx.AutoBufferedPaintDC instead of wx.PaintDC.

But now I am getting strange behavior when drawing a rectangle. After scrolling the page, the old rectangles are not deleted.

Code
import wx
 
 
class PanelDraw(wx.ScrolledCanvas):
 
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
 
        # self.SetDoubleBuffered(True)
        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)
        self.SetScrollRate(42, 42)
 
        self.cnt = 25
        self.padding = 32
        self.SetVirtualSize(self.cnt * 64 + 2 * self.padding,
                            self.cnt * 64 + 2 * self.padding)
 
        self.Bind(wx.EVT_PAINT, self.on_paint)
 
    def on_paint(self, event):
        dc = wx.AutoBufferedPaintDC(self)
        dc.Clear()
        self.DoPrepareDC(dc)
 
        gc = wx.GraphicsContext.Create(dc)  # type: wx.GraphicsContext
 
        gc.SetBrush(wx.TRANSPARENT_BRUSH)
        pen = wx.Pen(wx.Colour(80, 80, 80, 127), 1, wx.PENSTYLE_USER_DASH)
        pen.SetDashes([2, 2])
        gc.SetPen(pen)
 
        for i in range(self.cnt + 1):
            gc.StrokeLine(self.padding + i * 64, self.padding,
                          self.padding + i * 64, self.padding + self.cnt * 64)
            gc.StrokeLine(self.padding, self.padding + i * 64,
                          self.padding + self.cnt * 64, self.padding + i * 64)
 
        self.draw_angle(dc)
 
    def draw_angle(self, dc: wx.DC):
        size_x, size_y = self.GetClientSize()
        sx, sy, *_ = dc.GetClippingRect()
 
        dc.SetBrush(wx.Brush(wx.RED))
        dc.SetPen(wx.TRANSPARENT_PEN)
 
        dc.DrawRectangle(sx, sy, size_x, 20)
        dc.DrawRectangle(sx, sy, 20, size_y)
 
 
class Frame(wx.Frame):
 
    def __init__(self):
        super().__init__(None)
 
        self.canvas = PanelDraw(self)
 
 
if __name__ == '__main__':
    app = wx.App()
    Frame().Show()
    app.MainLoop()

How can I fix this?

Thanks.

You don’t mention the platform but it sounds like you are on Windows. I’ve seen similar problems with the system double buffering on Windows. I expect there are ways to mitigate that and take advantage of the feature, but like you I’ve found that it is much simpler to just handle the buffering in the application instead.

I was going to suggest that you call self.SetBackgroundStyle(wx.BG_STYLE_PAINT) since you are clearing and redrawing the full window area in your paint event handler. But I see in your 2nd example that you are now doing that. :slight_smile:

There are a couple contributing factors to this problem:

  1. When scrolling on Windows the system “helps” you out by by moving the existing pixels and then sending a paint event with the update region set to just the newly exposed area. You can see this in action by printing the value returned from self.GetUpdateRegion().GetBox() at the beginning of the paint handler. After a scroll then the region will not cover the whole window but just the portion that is “new”. This region is separate from the DC’s clipping region, so doing dc.DestroyClippingRegion() doesn’t help. I thought there was a way to reset the update region from the paint event but I haven’t been able to find that so far. I’ll describe some other workarounds below.

  2. The other problem is your sx, sy values in draw_angle. I think you want to be using self.CalcUnscrolledPosition(0, 0) for that, so the values you use as the rectangle origins will be the logical coordinates that correspond to the physical coordinates of (0,0).

Possible workarounds:

  1. The wx.grid.Grid class works around this issue by using separate subwindows for the components of the whole grid widget. So there’s one subwindow for the main area of the grid, one each for the column and row labels, and one for the bit in the corner where the label subwindows meet. The wx.ScrolledWindow class has the ability to do the scrolling on some other window than itself. See the SetTargetWindow method.

  2. A very simple fix would be to send an extra paint event after the window has been scrolled. This will still have a bit of flicker on Windows, but it may not be too bad. The widget’s update region will be set to the rectangle passed to Refresh and defaults to the whole client area of the widget. In your sample this can be accomplished by adding a wx.EVT_SCROLLWIN handler that looks something like this:

    def on_scroll(self, event):
        wx.CallAfter(self.Refresh)
        event.Skip()
  1. Another idea, that I haven’t tried yet, would be to just avoid wx.ScrolledWindow. You could use a wx.Panel with some standalone scrollbars. Don’t call self.DoPrepareDC and instead translate the drawing positions yourself based on the scrollbar positions. If you convert everything to be drawn via the wx.GraphicsContext instead of a mixture of GC and DC drawing, then you can use gc.Translate to offset the drawing of the grid area, and then gc.Translate again to restore the origin to (0,0) and then draw your top and side rectangles. There’s probably some additional work that will be needed to make this work, but it’s an idea to consider.
1 Like

Thanks for the answer!
(Yes. My operating system: Windows 7. wxPython 4.1.1)