views:

24

answers:

2

I'm trying to build a ScrolledWindow that you can draw on using the mouse, and it's working too, but I'm getting a nasty flicker when the user is drawing on the window while the scrollbars aren't in the "home" position..

To reproduce, run the attached program, scroll a bit down (or to the right) and "doodle" a bit by keeping the left mouse button pressed. You should see a flickering now and then..

import wx

class MainFrame(wx.Frame):
    """ Just a frame with a DrawPane """

    def __init__(self, *args, **kw):
        wx.Frame.__init__(self, *args, **kw)
        s = wx.BoxSizer(wx.VERTICAL)
        s.Add(DrawPane(self), 1, wx.EXPAND)
        self.SetSizer(s)

########################################################################
class DrawPane(wx.PyScrolledWindow):
    """ A PyScrolledWindow with a 1000x1000 drawable area """

    VSIZE = (1000, 1000)

    def __init__(self, *args, **kw):
        wx.PyScrolledWindow.__init__(self, *args, **kw)
        self.SetScrollbars(10, 10, 100, 100)
        self.prepare_buffer()

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
        self.Bind(wx.EVT_MOTION, self.on_motion)

    def prepare_buffer(self):
        self.buffer = wx.EmptyBitmap(*DrawPane.VSIZE)
        dc = wx.BufferedDC(None, self.buffer)
        dc.Clear()
        dc.DrawLine(0, 0, 999, 999) # Draw something to better show the flicker problem

    def on_paint(self, evt):
        dc = wx.BufferedPaintDC(self, self.buffer, wx.BUFFER_VIRTUAL_AREA)

    def on_mouse_down(self, evt):
        self.mouse_pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()

    def on_motion(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            dc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
            newpos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
            coords = self.mouse_pos + newpos
            dc.DrawLine(*coords)
            self.mouse_pos = newpos
            self.Refresh()

if __name__ == "__main__":
    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()
    MainFrame(None).Show()
    app.MainLoop()

I tried using SetBackgroundStyle(wx.BG_STYLE_CUSTOM), or binding EVT_ERASE_BACKGROUND, or using RefreshRect instead of Refresh, but the flicker is still there.. Any idea on what I might try next?

My environment: Xubuntu 9.04, wxPython 2.8.9.1 (but tested on Ubuntu 10.04 too)

Many thanks for your time!

+1  A: 

From Robin Dunn himself:

First, a Refresh() by default will erase the background before sending the paint event (although setting the BG style or catching the erase event would have taken care of that.) The second and probably most visible problem in this case is that in your on_motion handler you are not offsetting the ClientDC by the scroll offsets, just the position in the buffer that you are drawing the line segment at. So when the buffer is flushed out to the client DC it is drawn at the physical (0,0), not the virtual (0,0). In other words, the flicker you are seeing is coming from drawing the buffer at the wrong position after every mouse drag event, and then it immediately being drawn again at the right position in the on_paint triggered by the Refresh().

You should be able to fix this by calling PrepareDC on the client DC before using it, like this:

    cdc = wx.CLientDC(self)
    self.PrepareDC(cdc)
    dc = wx.BufferedDC(cdc, self.buffer)

However since you are doing a Refresh or RefreshRect anyway, there is no need to use a client DC here at all, just let the flushing of the buffer to the screen be done in on_paint instead:

    dc = wx.BufferedDC(None, self.buffer)
Joril
A: 

Using Joril recomendations and removing Refresh(), there is no flicker anymore (even enlarging the frame).

import wx


class MainFrame(wx.Frame):
    """ Just a frame with a DrawPane """

    def __init__(self, *args, **kw):
        wx.Frame.__init__(self, *args, **kw)
        s = wx.BoxSizer(wx.VERTICAL)
        s.Add(DrawPane(self), 1, wx.EXPAND)
        self.SetSizer(s)

########################################################################
class DrawPane(wx.PyScrolledWindow):
    """ A PyScrolledWindow with a 1000x1000 drawable area """

    VSIZE = (1000, 1000)

    def __init__(self, *args, **kw):
        wx.PyScrolledWindow.__init__(self, *args, **kw)
        self.SetScrollbars(10, 10, 100, 100)
        self.prepare_buffer()
        cdc = wx.ClientDC(self)
        self.PrepareDC(cdc)
        dc = wx.BufferedDC(cdc, self.buffer)

        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_mouse_down)
        self.Bind(wx.EVT_MOTION, self.on_motion)

    def prepare_buffer(self):
        self.buffer = wx.EmptyBitmap(*DrawPane.VSIZE)
        cdc = wx.ClientDC(self)
        self.PrepareDC(cdc)
        dc = wx.BufferedDC(cdc, self.buffer)
        dc.Clear()
        dc.DrawLine(0, 0, 999, 999) # Draw something to better show the flicker problem


    def on_paint(self, evt):
        dc = wx.BufferedPaintDC(self, self.buffer, wx.BUFFER_VIRTUAL_AREA)

    def on_mouse_down(self, evt):
        self.mouse_pos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()

    def on_motion(self, evt):
        if evt.Dragging() and evt.LeftIsDown():
            newpos = self.CalcUnscrolledPosition(evt.GetPosition()).Get()
            coords = self.mouse_pos + newpos
            cdc = wx.ClientDC(self)
            self.PrepareDC(cdc)
            dc = wx.BufferedDC(cdc, self.buffer)
            dc.DrawLine(*coords)
            self.mouse_pos = newpos

if __name__ == "__main__":
    app = wx.PySimpleApp()
    wx.InitAllImageHandlers()
    MainFrame(None).Show()
    app.MainLoop()
laurent-rpnet