使用GDI+和C++减少闪烁

21

我在 C++/MFC 应用程序中使用 GDI+,但每当窗口调整大小时,似乎都无法避免闪烁。

我已经尝试过以下步骤:

  • OnEraseBkGnd() 中返回 TRUE;
  • OnCtlColor() 中返回 NULL;
  • 按照此示例代码使用双缓冲:

void vwView::OnDraw(CDC* pDC) 
{
   CRect rcClient;
   GetClientRect(rcClient);

   Bitmap bmp(rcClient.Width(), rcClient.Height());
   Graphics graphics(&bmp);

   graphics.DrawImage(m_image, rcClient.left, rcClient.top);

   Graphics grph(pDC->m_hDC);
   grph.DrawImage(&bmp, 0, 0);
}

我做错了什么吗?还是有其他方法可以实现这个目标?

6个回答

43
为了完全避免闪烁,您需要在屏幕更新之间的时间间隔内完成所有绘制。Windows没有提供任何简单的方法来实现这一点(Vista通过DWM提供复合绘图,但即使在运行Vista的系统上也不能依赖它)。因此,为了最大程度地减少闪烁,您可以尽快绘制所有内容(通过增加在刷新周期内完成所有绘制的机会来减少撕裂),并避免过度绘制(绘制屏幕的一部分,然后在其上绘制其他东西:可能会呈现给用户部分绘制的屏幕)。
让我们讨论到目前为止介绍的技术:
  • Do-nothing OnEraseBkgnd(): 避免在窗口的无效区域填充窗口背景颜色,从而帮助避免过度绘制。当您将在WM_PAINT处理中重新绘制整个区域时很有用,例如双缓冲绘制的情况...但请参见有关通过防止在WM_PAINT方法之后进行绘制以避免过度绘制的注释

  • 对于 OnCtlColor() 返回NULL: 除非您的窗体上有子控件,否则实际上不会做任何事情...在这种情况下,请参见有关通过防止在WM_PAINT方法之后进行绘制以避免过度绘制的注释

  • 双缓冲绘图: 通过将实际屏幕绘制减少到单个BitBLT来帮助避免撕裂(以及潜在的过度绘制)。但可能会损害绘制所需的时间:无法使用硬件加速(尽管使用GDI+,任何硬件辅助绘制的机会非常渺茫),必须为每次重绘创建并填充一个离屏位图,并且必须为每次重绘重新绘制整个窗口。请参见有关有效双缓冲的注释

  • 使用GDI调用而不是GDI+进行BitBlt: 这通常是个好主意 - Graphics::DrawImage()可能非常慢。在某些系统上,我甚至发现普通的GDI BitBlt()调用更快。尝试其他建议后再试验一下。

  • 避免窗口类样式强制在每次调整大小时进行完全重绘(CS_VREDRAWCS_HREDRAW: 这将有所帮助,但仅当您不需要在大小更改时重新绘制整个窗口时才有效。

避免在WM_PAINT方法之前绘制以避免过度绘制的注意事项

当窗口的所有或部分区域无效时,它将被擦除和重绘。如前所述,如果您计划重新绘制整个无效区域,则可以跳过擦除。 然而,如果您正在使用子窗口,则必须确保父窗口未擦除屏幕上的您的区域。应在所有父窗口上设置WS_CLIPCHILDREN样式-这将防止子窗口(包括您的视图)占用的区域被绘制。

避免在WM_PAINT方法之后绘制以避免过度绘制的注意事项

如果您在表单上托管了任何子控件,则希望使用WS_CLIPCHILDREN样式以避免覆盖它们(并随后被它们覆盖)。请注意,这会在某种程度上影响BitBlt例程的速度。

有效双缓冲的注意事项

现在,每次视图绘制自身时,您都会创建一个新的后备缓冲图像。对于较大的窗口,这可能代表分配和释放大量内存,并且导致显著的性能问题。我建议在您的视图对象中保留一个动态分配的位图,根据需要重新分配它以匹配您的视图的大小。

请注意,当窗口被调整大小时,这将导致与当前系统一样多的分配,因为每个新大小都需要分配一个新的后备缓冲位图来匹配它 - 您可以通过将维度舍入到下一个最大的4、8、16等倍数来减轻痛苦,从而避免在每个微小的尺寸变化时重新分配。
请注意,如果窗口的大小自上次渲染到后备缓冲区以来没有改变,那么在窗口失效时不需要重新渲染它 - 只需将已经渲染好的图像 Blt 到屏幕上即可。
此外,请分配一个与屏幕位深匹配的位图。您目前使用的 Bitmap 构造函数将默认为32bpp,ARGB布局;如果这与屏幕不匹配,则必须进行转换。考虑使用 GDI 方法 CreateCompatibleBitmap() 获取匹配的位图。
最后...我假设你的示例代码只是一个说明性的片段。但是,如果您实际上什么都不做,只是将现有图像呈现到屏幕上,那么您根本不需要维护后备缓冲区 - 只需直接从图像 Blt(并将图像格式提前转换以匹配屏幕)。

2
那么,“将实际的屏幕绘制缩减为单个 BitBLt”并不足以防止所有的撕裂?就像 Windows 可能允许这个单一的 bitblt 在屏幕刷新的中间发生,导致即使是一个单一操作也可能出现撕裂? - rogerdpack
2
当然。传输实际数据需要时间,这取决于特定机器的设置。 - Shog9

4

如果您已经缓冲了图像,可以尝试使用传统的GDI而不是GDI+来编写DC。使用Bitmap::LockBits访问原始位图数据,创建BITMAPINFO结构,并使用SetDIBitsToDevice显示位图。


3
您可以通过使用Direct3D来获取一些推进,让其“告诉”您何时发生垂直同步等,以便您在适当的时间进行BitBlt/更新。参见GDI vsync to avoid tearing(虽然对于某些情况而言,将事物降至单个小BitBlt可能已经足够了)。请注意,似乎GDI BitBlt与屏幕垂直同步不同步。请参见Faster than BitBlt。另请注意,如果使用CAPTUREBLT(允许您捕获透明窗口),则会导致鼠标闪烁(如果未使用Aero)。

1
那篇关于“比BitBlt更快”的论坛帖子似乎自2015年4月以来就不再可用了(https://web.archive.org/web/20150423163628/http://www.itlisting.org/4-windows-ce-embedded/7752b5a1dabede5f.aspx)。 - jrh

3

看起来窗口样式的描述已经被移动到窗口类样式 - jrh

2

这基本上是一个仅包含链接的答案,这是不被鼓励的。截至2018年6月11日,这看起来几乎是一个“死链接答案”,尽管读者应该注意到该网站并没有“维护”太长时间(它在2017年是有效的)。就我个人而言,我想这还好,页面提供了有用的建议。希望它能很快恢复。无论如何,我建议在你的回答中使用页面内容(用自己的话),以便更好地解释。 - jrh

0

表单上有子窗口吗?窗口管理器首先通过发送WM_ERASEBKGND消息让父窗口擦除其背景,然后发送WM_PAINT消息 - 可能对应于您的wx::OnDraw方法。然后它遍历每个子控件并让它们自己绘制。

如果这是您的情况...使用Vista的新Aero外观将解决您的问题,因为Aero桌面窗口管理器会自动进行窗口合成。对于旧的窗口管理器来说,这是一个麻烦事。


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