如何处理WM_ERASEBKGND以避免闪烁?

9

我在一个表单上有一些自定义进度条,每秒更新/刷新两次,但它们会闪烁。

TMyProgressBar = class(TCustomControl)

我从 TCustomControl 继承了控件,因为我需要 Handle 和一些 TWinControl 事件。这些控件(最多 64 个)是动态创建的,并放置在一个 ScrollBox 上。当进度更新时,我首先调用 InvalidateRect

所有绘画工作(一组矩形、DrawText等——灵感来自于这里)都在内存 DC 中执行,然后通过 BitBlt 复制到控件的 DC 上。尽管如此,它仍然会闪烁,好像组件消失又重新出现。我认为这是由于背景擦除引起的。

这份无闪烁绘图建议中,写道要按照以下方式处理 WM_ERASEBKGND:

type
  TMyProgressBar = class(TCustomControl)
    procedure WMEraseBkGnd(var Message:TMessage); message WM_ERASEBKGND;

procedure TMyProgressBar.WMEraseBkGnd(var Message: TMessage);
begin
  Message.Result := 1;
end;

但是在另一个组件中,由TMS (TAdvProgressBar),相同的消息Result被设置为0

现在,Windows文档指出:

如果应用程序擦除背景,则应返回非零;否则,应返回零。

我测试了两个变体(Result = 0、1),令人惊讶的是,两者都避免了闪烁问题。

那么,现在我该在Delphi代码中放置什么?什么是正确的方法?


如果返回1,你很可能不会从保留更新区域的标记中受益。 - Sertac Akyuz
这并不是万能的解决方案。在WM_ERASEBACKGROUND中可能需要真正完成的工作。如果您什么都不绘制,那么就不会有闪烁。 - David Heffernan
2
这里有一个非常好的自定义TScrollBox的示例,可以调整以解决闪烁问题:https://dev59.com/f3LYa4cB1Zd3GeqPTyLD#16569324 - J...
3个回答

8
没关系,重要的是只要不调用inherited,默认窗口过程就不会擦除背景。由于你正在绘制控件的整个表面,因此不需要默认处理。
当你返回'0'或'1'(而不是'0')时发生了什么变化,系统在调用BeginPaint时相应地设置PAINTSTRUCTfErase成员。当你返回'0'时,它被设置为'True',表示必须在绘制过程中擦除背景。对于'1',它被设置为'False',表示不需要擦除。 TWinControl.PaintHandler中调用了BeginPaint。没有人会检查fErase是什么,VCL只使用BeginPaint返回的设备上下文,因此你返回的内容没有任何区别。
尽管如此,我建议返回'1',这意味着擦除已经处理完毕。

为什么这里0等于TRUE,1等于FALSE - 这让我感到困惑? :) - ALZ
2
@ALZ - 在 'WM_ERASEBKGND' 中返回 'True' (1),表示 "好的,擦除完成"。因此,当调用 'BeginPaint' 时,系统会说 "擦除:不需要(False)"。换句话说,'0' 是对该消息的响应。而 'True' 则完全不同。 - Sertac Akyuz
在未被覆盖的方法中是否可以调用inherited(例如procedure WMEraseBkGnd(var Message:TMessage); message WM_ERASEBKGND)? - ALZ
2
@ALZ - 在Delphi中,消息处理在这方面是特殊的。请参见这里的#7:http://docwiki.embarcadero.com/RADStudio/XE5/en/Declaring_a_New_Message-handling_Method - Sertac Akyuz

4
当背景没有(完全)被擦除时,您应该返回0,当认为背景已经被擦除时,您应该返回另一个值。这是您必须遵守的约定。 更重要的是不在此消息处理程序中调用inherited 1),因为这会调用继承的消息处理程序,最终调用默认的Windows过程,如果有的话,将设备上下文与窗口创建时赋予的画刷一起绘制。
现在,在实践中,尤其是在您自定义控件的示例中,您返回的值并不重要,因为只有您执行擦除任务。但是考虑设计基础控件类或部署控件:您可能希望向后代或控件用户指示背景并未完全验证。这就是Message.Result = 0的含义。您还可以发送Message.Result = ebLeftSide,表示在当前控件状态下,仅左侧(无论这意味着什么)被“擦除”。
请记住,在这种情况下,“擦除”也意味着“绘制”,但我认为这超出了问题的范围。 1)Inherited对于消息处理程序与虚拟方法有所不同。虽然它的含义相同-将调用继承链中的第一个处理程序-但是在其声明中没有覆盖指令,并且不能添加方法名称。

感谢回复和您提供的其他进度的想法。 现在对我来说更清楚了 - 我对UI /图形开发还很陌生。 在我的情况下,我正在覆盖旧的状态(视图)以显示进度,这不会产生闪烁的幻象,因为背景不再被抹掉。 - ALZ

2
WM_ERASEBKND的返回值只是确定在后续的WM_PAINT处理程序中调用BeginPaint时,PAINTSTRUCTfErase成员如何初始化。如果您的绘图处理程序忽略了该成员,则WM_ERASEBKND的返回值并不重要。

通过一次绘制而不是两次来避免闪烁。如果您在WM_ERASEBKGND中使用颜色填充区域,然后在WM_PAINT中稍后再进行位块传输,那么会出现闪烁。如果您在WM_ERASEBKGND中没有进行任何绘制,只是在WM_PAINT中进行位块传输,则不会出现闪烁。唯一的技巧是确保您的位块传输覆盖整个无效区域,并初始化像素。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接