无闪烁的可绘制滚动窗口

3 投票
2 回答
2843 浏览
提问于 2025-04-16 00:36

我正在尝试创建一个可以用鼠标绘画的滚动窗口,这个功能是可以用的,但当用户在窗口上绘画时,如果滚动条没有回到“初始”位置,就会出现很烦人的闪烁现象。

要重现这个问题,运行附带的程序,稍微向下(或向右)滚动一下,然后按住左键“涂鸦”。你会发现偶尔会有闪烁的情况出现。

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()

我尝试过使用 SetBackgroundStyle(wx.BG_STYLE_CUSTOM),或者绑定 EVT_ERASE_BACKGROUND,还有用 RefreshRect 代替 Refresh,但闪烁的问题依然存在。你有什么建议可以让我尝试的吗?

我的环境是:Xubuntu 9.04,wxPython 2.8.9.1(但在Ubuntu 10.04上也测试过)

非常感谢你的时间!

2 个回答

2

根据Joril的建议,去掉了Refresh()这个函数后,界面不再闪烁了(即使放大窗口也没有问题)。

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()
5

这是Robin Dunn自己说的:

首先,默认情况下,Refresh()会在发送绘图事件之前清空背景(不过你可以通过设置背景样式或者捕捉清空事件来解决这个问题)。其次,可能最明显的问题是,在你的on_motion处理函数中,你没有根据滚动的偏移量来调整ClientDC的位置,而只是根据你在缓冲区中绘制线段的位置来处理。所以当缓冲区的内容被刷新到客户端DC时,它是以物理坐标(0,0)来绘制的,而不是虚拟坐标(0,0)。换句话说,你看到的闪烁是因为每次鼠标拖动事件后,缓冲区在错误的位置被绘制,然后又在on_paint中被触发的Refresh()中立即在正确的位置被绘制。

你可以通过在使用客户端DC之前调用PrepareDC来解决这个问题,像这样:

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

不过,由于你已经在使用RefreshRefreshRect,所以这里根本不需要使用客户端DC,只需在on_paint中处理缓冲区内容的刷新即可:

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

撰写回答