今天老外报了个defect,跟窗体闪烁相关的,不过俺是看了好几遍也没察觉到,但是既然老外提出来了,还是要认真对待。一个如下图的窗体,由父窗体Pop-up和两个子窗体Title & TreeList组成,父窗体的边框由默认函数绘制,Title比较简单,画上一段文字和两个图标,TreeList部分使用了双缓冲绘制。设置了断点后,观察到pop-up边框和Title先绘制完成然后TreeList绘制之前,在Treelist那里是背景组成的一个框,然后在TreeList的backbuffer 绘制出来时整个TreeList显示出来,可能是这里造成的闪烁(还需要确认一下)。
问题的可能解决方法是对整个pop-up进行双缓冲绘制。
在网上搜索了一下,找到一片相当不错跟闪烁相关的文章,转载一下:
原文地址: http://www.cnblogs.com/loveshrek/archive/2007/03/23/685121.html
摘抄部分:
什么是闪烁
闪烁可以这样定义:当后面一幅图像以很快的速度画在前面一幅图像上时,在后面图像显示前,你可以很快看到前面那一个图像,这样的现象就是闪烁。我认为,闪烁会让使用者对程序很不满,原因是:如果用户接口编码如此糟糕,那么程序的其他部分呢,如何能相信数据的正确性呢?一个具有平滑,快速相应的程序会给用户带来信心,这个道理很简单。
程序出现闪烁可以由多种形式造成,最常见的原因是窗口大小发生改变时,其内容重画造成闪烁。
仅仅画一次
这是一个黄金法则,在任何计算机(Windows或者你使用的任何操作系统)上处理画法逻辑都需要遵循,即永远不要将同一像素画两次。一个懒惰的程序员常常不愿意在画法逻辑上投入过多精力,而是采用简单的处理逻辑。要避免闪烁,就需要确保不会出现重复绘制的情况发生。现在,WIndows和计算机还是很笨的,除非你给他们指令,否则他们不会做任何事情。如果闪烁的现象发生,那是因为你的程序刻意地多绘制了屏幕的某些区域造成的. 这个现象可能是因为一些明确的命令,或者一些被你忽视了的地方。如果程序有闪烁的现象出现,你需要你知道如何找到好的方案去解决这个问题。
如果还是不能解决
有些时候,当你花了很多努力去考虑非常好的画法时,发现窗口还是会被全部刷新。这通常是由两个Window 类的属性造成的:CS_VREDRAW 和 CS_HREDRAW。如果有其中一个标志被设置时,那么当窗口水平或者竖直方向有大小被改变时,其内容每次都会被重新刷新。所有,你需要关掉这两个标志,解决的唯一的方式是在创建窗体和窗体类被注册时,确保这两个属性不被设置。
WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
wc.style = 0; /* CS_VREDRAW | CS_HREDRAW; */
...
RegisterClassEx(&wc);
上面的例子描述了当窗体类被注册时,这两个属性不被设置的实现方法。
有一点需要注意:如果主窗体有了这两个属性,即使子窗体没有重画标志,会导致所有子窗体在其大小被改变时会被重绘。可以通过以下方式避免这个情况发生:
剪切子窗体
有时,闪烁的原因是因为当重画时,父窗体没有剪切其子窗体区域。这样的结果导致,整个父窗口内容被重画,而子窗体又被显示在了上面(造成闪烁)。这个可以通过在父窗体上设置WS_CLIPCHILDREN 来解决。当这个标志被设置时,被子窗体占据的任何区域将会被排除在更新区域外。因此,即使你尝试在子窗体所在的位置上绘制(父窗口的内容),BeginPaint中的剪切区域也会阻止其绘制效果。
双缓冲和内存设备描述表(Memory Device Context, 简称Memory-DC)
常见的彻底避免闪烁的方法是使用双缓冲。其基本的思路是:将窗体的内容画在屏幕外的一个缓冲区内,然后,将该缓冲区的内容再传递到屏幕上(使用BilBlt函数)。这是一个非常好的减少闪烁的方法,但是经常被滥用,特别是当程序员并不真正地理解如何有效地绘制窗口时。
典型的双缓冲代码如下:
HDC hdcMem; HBITMAP hbmMem; HANDLE hOld; PAINTSTRUCT ps; HDC hdc; .... case WM_PAINT: // Get DC for window hdc = BeginPaint(hwnd, &ps); // Create an off-screen DC for double-buffering hdcMem = CreateCompatibleDC(hdc); hbmMem = CreateCompatibleBitmap(hdc, win_width, win_height); hOld = SelectObject(hdcMem, hbmMem); // Draw into hdcMem // Transfer the off-screen DC to the screen BitBlt(hdc, 0, 0, win_width, win_height, hdcMem, 0, 0, SRCCOPY); // Free-up the off-screen DC SelectObject(hdcMem, hOld); DeleteObject(hbmMem); DeleteDC (hdcMem); EndPaint(hwnd, &ps); return 0;
这个方法比较慢,因为在每次窗体需要重画的时候内存设备描述表(Memory-DC)都需要被重新创建。更有效的方法是,仅仅创建内存设备描述表(Memory-DC)一次,并使其足够大到能满足任何时候的整个窗体刷新。当程序结束时,再销毁这个内存设备描述表(Memory-DC)。这两种方法都存在对内存开销的问题,特别是如果内存设备描述表(Memory-DC)是针对真个屏幕的大小。双缓冲也需要两倍的时间去画。这是因为其第一次是在内存设备描述表(Memory-DC)上画,然后再使用BitBlt画回到屏幕上。当然,好的显卡会使BitBlt更快,但是仍然会耗CPU 时间。
如果程序需要显示相当复杂的信息,比如像网页,那么你应该使用内存设备描述表(Memory-DC)。比如IE,如果不使用双缓冲,是没有办法在绘制网页时不闪烁的。
没有必要将双缓冲技术用于整个窗体的绘制中。可以这样设想,窗口中仅仅有一个小部分包含了复杂的图形对象(比如半透明的位图或者其他)。你应该将内存设备描述表(Memory-DC)仅仅用于着一个小区域,其他区域使用常规的方法。 有时,通过仔细的思考,经常可以避免使用双缓冲而直接将结果画到屏幕上。只要你不破坏黄金法则,即“永远不要将一个像素画两次”,就可以防止闪烁的出现。
对于双缓冲,上面的代码是基本的算法,老外的提示了一个更方便的方法,就是用CreatEx创建窗体是设置Extend style为 WS_EX_COMPOSITED,然后就默认的双缓冲绘制了。