Win32 解决窗口闪烁

〇、前提本文采用 Win32 SDK 以及 C/C++ 语言描述,其中没有用到 C++ 语言的功能。采用标准 Win32 应用程序模型,即从 WinMain() 进入,然后使用 RegisterClassEx() 注册主窗口类,同时主窗口的消息处理回调过程是 WndProc(),其它的一些变量和函数在文章中描述。一、闪烁的分类与原因通常的闪烁分为:1. 窗口内容的闪烁,例如使用 TextOut() 直接在窗口客户端绘制文字等;2. 窗口子控件闪烁,例如窗口中的 Button、TabControl 闪烁等。所有闪烁的根本原因只有一个,就是同一个像素使用不同的颜色值多次绘制,造成视觉上的闪烁现象。而应用在上面两种闪烁的情况中,造成 Windows 为同一个像素绘制多次的成因就分为很多种了:1. 窗口首先在 WM_ERASEBKGND 消息中使用背景画刷(如果在类样式中指定的话)擦除背景,然后再在 WM_PAINT 中绘制内容(如之前说到的TextOut),造成了在内容处的同一个像素绘制两次,从而造成了 TextOut 输出的内容会不断闪烁;(这个现象在窗口每次需要 WM_PAINT 时就会出现);2. 窗口在 WM_PAINT 绘制完内容之后向所有的子窗口发送 WM_PAINT 消息,从而造成所有子窗口位置的像素首先被窗口的 WM_PAINT 绘制一次,其次被这个子窗口的内容绘制一次(或多次),造成闪烁现象;3. 子窗口本身也需要处理 WM_ERASEBKGND 和 WM_PAINT 消息,Windows 有些系统控件本身没有经过良好的优化,造成闪烁,例如 TabControl。下面就消除这几种闪烁情况进行说明。二、合适的类样式和窗体样式最简单的消除闪烁的方法就是首先需要指定合适的类样式(CS_*)和窗体样式(WS_* 以及 WS_EX_*),选用相应的样式时可以考虑一下几点:1. 所有具有子窗口的父窗口都需要加入 WS_CLIPCHILDREN 样式;2. 所有子窗口都需要加入 WS_CLIPSIBLINGS 样式;3. 谨慎考虑 CS_HREDRAW 和 CS_VREDRAW,需要根据窗口客户区绘制的内容来决定;有关这些样式的具体解释参见 MSDN 或其它相关参考资料。其中只有第三个需要经过考虑,另外两个几乎在所有的情况下都是需要遵循的。如果窗体绘制的内容之固定位置的,例如不管窗口大小是什么只是在 (15, 15) 位置处输出固定的字符串,那就不需要加入这两个类样式;但是如果窗口绘制的内容是需要根据窗口的大小不同而不同的,例如需要居中绘制一个字符串,那么就必须加入这两个样式。三、双缓冲技术双缓冲简而言之就是将绘制同一个像素的操作都在内存中悄悄进行,最后将整个内存图像一次性复制到屏幕上,这样从屏幕的角度来看就是所有的像素都只绘制了一次。更多有关双缓冲的介绍参考相关网站资料等,这里不再详述。双缓冲用于解决同一个窗口中的绘制问题,如 WM_ERASEBKGND 和 WM_PAINT 的处理、Windows 系统控件本身 WM_PAINT 的不足。详细来说就是使用 TextOut() 输出居中的字符串 和 Windows的TabControl控件本身的闪烁问题。首先处理 TextOut() 的闪烁,以下列出相关的代码片断(在主窗口的 WndProc 消息处理中的代码片断):case WM_ERASEBKGND:return TRUE; // 不进行擦除背景,在 WM_PAINT 消息中进行擦除case WM_PAINT:{ // 为了在 case 子句中声明局部变量,加入大括号HDC hdc = BeginPaint(hWnd, &ps);RECT rect;GetClientRect(hWnd, &rect);HDC dcBuffer = CreateCompatibleDC(hdc);HBITMAP memBM = CreateCompatibleBitmap(hdc, rect.right - rect.left, rect.bottom - rect.top); // 创建内存图像SelectObject(dcBuffer, memBM);FillRect(dcBuffer, &rect, (HBRUSH)(GetClassLong(hWnd, GCL_HBRBACKGROUND) - 1)); // 擦除背景,WM_ERASEBKGND标准的处理方法,但是这需要在窗口类的声明中将 wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1); 写入这样的代码DrawText(dcBuffer, TEXT("Hello, Windows!"), -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);BitBlt(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top, dcBuffer, 0, 0, SRCCOPY); // 复制内存图像到屏幕DeleteObject(memBM);DeleteDC(dcBuffer);EndPaint(hWnd, &ps);break;}窗口内容的绘制双缓冲就是这样的代码框架,然后是 Windows 系统控件 TabControl,可能是 Windows 在实现上的疏忽,即使父窗口用了 WS_CLIPCHILDREN 的情况下,在 Windows XP、Windows Vista/7 的经典主题样式 下,TabControl 依然会闪烁,经过一些简单的分析发现是由于TabControl 本身的 WM_ERASEBKGND 不像其它标准控件那样可以避免闪烁,同时还由于子窗口的 WM_PAINT 消息处理中也多次绘制了同一个像素造成了严重的闪烁。因此需要对这一个控件进行“特殊照顾”;在写出代码片断之前,首先需要了解“控件子类化”的概念,详细内容参见 MSDN 或其它相关文档,经典的方法是使用 SetWindowLongPtr() 结合 GWLP_WNDPROC 进行子类化,但在 Windows XP 以后可以用更为简单的 SetWindowSubclass() 子类化一个控件。还需要了解的就是“WM_PRINT、WM_PRINTCLIENT”消息,它们都允许控件/窗口将当前的状态绘制到一个指定的 HDC 中,而不是 WM_PAINT 中绘制到窗口 DC 中,而且 WM_PRINT 内部在某些情况下会调用 WM_PRINTCLIENT 进行绘制,因此我们的程序中只需要使用 WM_PRINT 消息即可,根据 MSDN 的描述,所有的 Windows 系统控件都实现了这两个消息。以下是处理TabControl闪烁的代码:// TabControl 的子类化回调函数(详细内容参见 MSDN 的 Subclass Controls 一章)LRESULT CALLBACK TabCtrlProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR, DWORD_PTR){HDC hdc, dcBuffer;HBITMAP memBM;PAINTSTRUCT ps;RECT rect;switch (uMsg) {case WM_ERASEBKGND:// 由于默认情况下本消息中 lParam 传入的是 0,因此默认情况下不进行绘制,以避免闪烁;但如果传入的是 TRUE,那么表示是由以下在处理 WM_PAINT 消息的代码调用的,因此需要调用默认的绘制代码,也就是 break 到 switch 语句之外if (!lParam)return TRUE;break;case WM_PAINT:GetClientRect(hWnd, &rect);hdc = BeginPaint(hWnd, &ps);dcBuffer = CreateCompatibleDC(hdc);memBM = CreateCompatibleBitmap(hdc, rect.right - rect.left, rect.bottom - rect.top);SelectObject(dcBuffer, memBM); // 以上双缓冲代码和之前的是一样的SendMessage(hWnd, WM_ERASEBKGND, (WPARAM)(dcBuffer), TRUE); // 发送 WM_ERASEBKGND 消息并传入 lParam 为 TRUE,wParam 为缓冲的 DC,要求默认处理程序将背景擦除过程应用到 dcBuffer 上,详细内容参见 MSDN 中关于 WM_ERASEBKGND 消息的说明SendMessage(hWnd, WM_PRINT, (WPARAM)(dcBuffer), PRF_CLIENT | PRF_NONCLIENT); // 发送 WM_PRINT,要求控件将当前状态绘制到 dcBuffer,详细内容参见 MSDN 中关于 WM_PRINT 和 WM_PRINTCLIENT 消息的说明BitBlt(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top, dcBuffer, 0, 0, SRCCOPY); // 复制到屏幕并清理内存图像DeleteObject(memBM);DeleteDC(dcBuffer);EndPaint(hWnd, &ps);return TRUE;}return DefSubclassProc(hWnd, uMsg, wParam, lParam);}// 需要导入头文件 #include // #pragma comment(lib, "ComCtl32.lib")// 并且需要在 WinMain 开始时使用 INITCOMMONCONTROLSEX icc;// icc.dwSize = sizeof(INITCOMMONCONTROLSEX);// icc.dwICC = ICC_TAB_CLASSES;// InitCommonControlsEx(&icc);// 这些代码来导入 Common Controls v6.0 的 DLLcase WM_CREATE:hTab = CreateWindowEx(0, WC_TABCONTROL, TEXT(""), WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 450, 100, 200, 500, hWnd, NULL, GetModuleHandle(NULL), NULL);tabItem.mask = TCIF_TEXT | TCIF_IMAGE;tabItem.iImage = -1;tabItem.pszText = TEXT("Tab Item 1");TabCtrl_InsertItem(hTab, 0, &tabItem);tabItem.pszText = TEXT("Tab Item 2");TabCtrl_InsertItem(hTab, 1, &tabItem);SetWindowSubclass(hTab, TabCtrlProc, 0, 0);break;WM_PRINT 消息绘制的内容和 WM_PAINT 绘制到窗口 DC 上的内容是几乎完全一致的,包括鼠标交互、键盘交互、焦点框、颜色变化等,但是之所以说“几乎”,是因为在处理 Windows Vista/7 的 Aero 主题下的一些动画过程不会在 WM_PRINT 过程中体现,但是由于 TabControl 在 Aero 主题下也根本没有动画效果,因此不会影响。四、消除子窗口闪烁这个方法一般被称为是一种“错觉”,即消除闪烁只是看上去的一种错觉而已,为何称为“错觉”在描述完之后再进行分析。该方法主要用于一些透明的窗体,最著名的就是例如 BS_GROUPBOX 样式的 Button,在所有的标准控件中,可以说 GroupBox 的闪烁问题是最为臭名昭著的,也是最难以解决的,因为如果像其它控件的父窗口一样在它的父窗口中加入 WS_CLIPCHILDREN 样式,那么它的背景就相当于完全没有绘制,因为事实上 BS_GROUPBOX 是透明的,具体结果可以自行编码试验。而如果将 WS_EX_TRANSPARENT 样式加入到 GroupBox 中,由于每次窗体都会绘制背景,所以这又会造成严重的闪烁问题。这里说到的一种解决方案就是在绘制父窗口的背景的时候将这个控件的内容也作为背景进行绘制,这样就算绘制两次,也是绘制两次同样颜色的像素,而造成闪烁的原因是由于绘制多次不同颜色的像素(参见第一节),即比如说父窗口 hWnd 有一个子控件 hGroup,它是一个 BS_GROUPBOX,然后在 hWnd 的客户区域除了绘制 TextOut() 的字符串以外,在 hGroup 相应位置的背景也需要使用第三节提到的 WM_PRINT 消息来绘制,即擦除窗口背景的时候就同时绘制了这个控件,而窗口背景实用双缓冲技术避免闪烁的,当然最后控件本身再在这个背景的基础上绘制自身,相当于用同样的像素再次覆盖这个背景;因此即使还是绘制了两次,但是由于用的是相同颜色的像素(因为 MSDN 中有提到 Windows 标准控件的 WM_PAINT 和 WM_PRINT 消息绘制的内容是一样的),因此也就给用户感觉没有闪烁。相关的代码片断如下(主窗口的 WndProc 消息处理中,代码中省略了之前两节的代码,最终可以将它们都合并起来,同时也省略了变量的声明,其中 hGroup 是全局变量,其它都是局部变量,rGroup是RECT类型):case WM_CREATE:hGroup = CreateWindowEx(WS_EX_TRANSPARENT, WC_BUTTON, TEXT("GroupBox"), WS_CHILD | WS_VISIBLE | BS_GROUPBOX | WS_CLIPSIBLINGS, 100, 100, 300, 500, hWnd, NULL, GetModuleHandle(NULL), NULL);break;case WM_CTLCOLORSTATIC:return NULL; // 将 GroupBox 的标题背景也设置为透明,否则将会有一些奇怪的颜色出现case WM_PAINT:GetClientRect(hWnd, &rect); // 获取窗口客户区矩形GetWindowRect(hGroup, &rGroup); // 获取 GroupBox 矩形(屏幕坐标)ScreenToClient(hWnd, (LPPOINT)(&rGroup.left)); // 将 GroupBox 矩形从屏幕坐标转换成客户区坐标hdc = BeginPaint(hWnd, &ps);dcBuffer = CreateCompatibleDC(hdc);memBM = CreateCompatibleBitmap(hdc, rect.right - rect.left, rect.bottom - rect.top);SelectObject(dcBuffer, memBM); // 双缓冲资源建立完毕FillRect(dcBuffer, &rect, (HBRUSH)(GetClassLong(hWnd, GCL_HBRBACKGROUND) - 1)); // 绘制窗口背景色SetWindowOrgEx(dcBuffer, -rGroup.left, -rGroup.top, NULL); // 将坐标系原点平移到 GroupBox 左上角SendMessage(hGroup, WM_PRINT, (WPARAM)(dcBuffer), PRF_CLIENT | PRF_NONCLIENT); // 调用系统默认的 GroupBox 绘制函数SetWindowOrgEx(dcBuffer, 0, 0, NULL); // 将坐标系恢复到原来的位置BitBlt(hdc, 0, 0, rect.right - rect.left, rect.bottom - rect.top, dcBuffer, 0, 0, SRCCOPY);DeleteObject(memBM); // 清理双缓冲资源DeleteDC(dcBuffer);EndPaint(hWnd, &ps);return TRUE;在代码中出现的相关 API 都可以参考 MSDN 或相关文档。和第三节提到的关于 Aero 主题下的动画问题一样,这个方法如果应用于一些具有不断动画的控件(如具有键盘焦点的按钮)上时会不尽如人意,但好在 BS_GROUPBOX 没有动画,而且所有有关透明的控件都没有动画。同样这个方法不仅仅适用于窗口上,也适用于子控件上,例如 TabControl。五、更多解决方案除了上面提到的几种方法以外,还有很多其它的解决方案,有些更为彻底,而有些有稍许的BUG。首先当然就是 Windows XP 引入的 WS_EX_COMPOSITED 样式,这个样式在 Windows 内部为你进行整个窗口(包括客户绘制和所有的子孙窗口)的双缓冲,然后一次性显示到屏幕上,在 Windows XP、Vista/7 以及它们的经典主题中都可以正确使用,而且闪烁效果能够得到完美的消除,性能也不错。但只有一个缺点,那就是在 Windows XP 主题,或 Win Vista/7 的 Aero Basic 主题下,标题栏的按钮不会有高亮效果,这个算是它的一个 BUG。而在 Windows Vista/7 的普通 Aero 主题(即开启透明效果)中,由于非客户区是交给 DWM 进行绘制的,因此不会影响,可以说在开启透明效果 Aero 主题下这个样式可以做到完美解决,而且非常的简便,只需要将父窗口的扩展样式添加WS_EX_COMPOSITED 即可(至今没有见到过有商业程序使用这个样式来避免闪烁)。第二种方法也是利用 Windows XP 引入的 WS_EX_LAYERED 样式,这个样式是用于设置窗口透明度或透明颜色掩码的,但是如果应用了这个样式,并且透明度设置到 254(255 表示完全不透明),这样用户察觉不到有那么大约0.5%不到的透明度,同时在内部 Windows 也会将其作为双缓冲处理,并且在所有主题下,非客户区的按钮(最小化、最大化、关闭按钮等)都能正确地进行交互,而不像 WS_EX_COMPOSITED 样式在某些主题下非客户区按钮在鼠标移动上去时没有高亮效果。这个方法的缺点就是性能很低,绘制效果非常慢(没有见过有使用这个样式来避免闪烁而不是处理透明的商业程序实例)。第三种方法就是使用其它的库,例如 WPF,它内部使用了无 HWND 的技术,整个窗体的子控件都没有 HWND,这样整个窗体的绘制工作就能由主窗体全权掌管,自然也就完全不闪烁了(例如 Visual Studio 2010)。第四种方法,就是要么忽略不计,允许窗口有烦人的闪烁问题,并期待今后的操作系统有更完美的解决方案,而事实上 Windows Vista/7 在避免闪烁的问题上确实做了不少很有成效的努力,但仍不够完美(例如 Visual Studio 2008 的查找对话框);要么就将窗口设置为不能调整大小,也能完全避免闪烁(例如 Office Word 等许多 Windows 附带程序的对话框)。六、应用利用第一节到第四节的一些功能,在 Windows 环境下的绝大多数闪烁问题都可以完美解决,但是也并不是所有的环境中都需要使用这个技术。例如在一些无法调整大小的对话框中,根本就无需考虑闪烁问题,因为根本就不会发生由于调整大小而产生的闪烁。对于 BS_GROUPBOX,需要使用第四节的方法,对于 TabControl 和窗口背景的绘制需要使用第三节的方法,对于所有其它的标准空间只需要在父窗口中包含 WS_CLIPCHILDREN 样式即可解决。下图就是使用以上方法绘制的一些控件和窗口背景,可以看到在处理 TabControl 时的一些问题,如 Button 按钮的背景周围有一圈蓝色,这是因为它的父窗口设置成为了主窗口的缘故,而如果把按钮的父窗口设置成为 TabControl,则背景就正常了,成为 Button2。GroupBox的父窗口也设置成为了 TabControl,这个程序在 XP 的 Luna 主题下、Win Vista/7 的 Aero 主题下,或任何 Windows 经典主题下到表现良好(右下角的调整框也是绘制在窗口背景中的,调用了相关的 Theme API,并且处理了 WM_NCHITTEST 消息,参考 MSDN)。Win32 解决窗口闪烁七、后续工作这里只解决了自己注册窗口类的一些窗口问题,而没有处理到对话框,但原理都是一样的。这里处理 TabControl 的方法是自己建立控件并进行子类化,并且把 TabControl 作为父窗口来承载它的子窗口,但在Windows中更为通用的办法是使用一些对话框单独编辑每一页的控件集合,并且和 TabControl 之间并不是父子关系,而是兄弟关系(Siblings),这一点在以后的文章中会讨论到。
  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值