图15
如果你尝试拖动B,C,情况可能更奇怪,总之就是窗口似乎不能正常绘制。那如何才能正常呢?我不说你都知道了,就是这节的主题,给这几个child window加上WS_CLIPSIBLINGS风格,就OK了,那如何解释?现在看图14,表面上看是C叠在B上面,而B叠在A上面,事实上正好相反不是,(关于窗口Z order的问题看下一节)事实是B叠在C之上,A叠在B上面,所以企图拖C,其实点到的是A的客户区,C当然“拖不动”,那为什么看起来是C叠B,B叠A?这跟绘制顺序有关系,A先绘,然后B,最后C,也许你又要我验证了,好,我改一下代码,打个log出来给你看。把Do nothing的那个窗口过程改为:
LRESULT CALLBACK WndProcDoNothing(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch(message) {
case WM_PAINT: {
TCHAR szOut[20];
TCHAR szWindowTxt[10];
GetWindowText(hWnd, szWindowTxt, 10);
wsprintf(szOut, TEXT(\
OutputDebugString(szOut); }
break; }
return DefWindowProc(hWnd, message, wParam, lParam); }
打印结果为: A Paint B Paint C Paint
那B为什么绘在A的上面?那就是因为没有指定WS_CLIPSIBLINGS,WS_CLIPSIBLINGS这个风格会在窗口绘制的时候裁掉“它被它的兄弟姐妹挡住的区域”,被裁掉的区域当然不会被绘制。对子窗口来说,这个风格不是一定有的,因为微软考虑到大多数子窗口,比如dialog上的控件,基本上都是固定不会移动的,不会产生互相叠起来的现象。那对于top-level窗口,如果可以没有这个风格,那我们的界面可能很容易混乱,所以这个风格是强制的。也许你要问:“那为什么我移动A的时候,A自己不会重绘?”当然不会了,因为我移动A,A本来就是在最顶层,完全可见的,没有什么区域变得无效需要重新绘制,所以它不会被重绘,这个可以通过log看出来。
现在分析下一个风格WS_CLIPCHILDREN,前一个是裁兄弟姐妹,这个是裁孩子,微软也够狠的。不多说了,直接改代码来体会这个风格的作用,按照这个意思,有这个风格的父窗口在绘制的时候,不会把东西绘到子窗口的区域上去,这个嘛,简单,我们只要在父窗口的WM_PAINT里画点东西试试看就好了。代码还是前面的代码,把A,B,C都加上WS_CLIPSIBLINGS,主窗口不要WS_CLIPCHILDREN风格,我们看看是不是能把东西画到子窗口的区域去。
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps); RECT rt;
GetClientRect(hWnd, &rt);
DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER); MoveToEx(hdc, 0, 0, NULL);
LineTo(hdc, 600, 400); //To be simple, just a line. EndPaint(hWnd, &ps); break;
运行结果如图:
图16
嗯?没有穿过啊?为什么?先动脑想想半分钟。 那是因为我们的实验不够严谨,现在在主窗口WM_PAINT消息的处理中加入一个Debug内容:
OutputDebugString(TEXT(\再看看debug出来的log: Main window paint A Paint B Paint C Paint
因为是主窗口先绘制,然后才是子窗口,所以即便这根线是穿过子窗口区域的,恐怕也看不出来了。那我们就不要在WM_PAINT里绘制,我们增加一个菜单项,叫paint a line,点这个菜单就执行下面的代码:
//在主窗口的WM_COMMAND消息处理中 switch (wmId) { //...
case ID_PAINT_A_LINE: {
HDC hdc = GetDC(hWnd); MoveToEx(hdc, 0, 0, NULL);
LineTo(hdc, 600, 400); //To be simple, just a line. ReleaseDC(hWnd, hdc); } }
运行程序,点菜单“paint a line”,看运行效果:
图17
算是“成功穿越”了,这时候你再给父窗口加上WS_CLIPCHILDREN看看,结果我就不说了,就算不尝试其实也能想得到。相信大家到此为止都理解了这两个风格的作用了。
再顺便说些实践经验,有时候我们会发觉程序在频繁重绘的时候闪烁比较厉害,还是拿这个例子改装一下吧,先把主窗口的WS_CLIPCHILDREN风格拿掉,然后在其窗口处理函数中加入些代码:
case WM_CREATE: //...
SetTimer(hWnd, 1, 200, NULL); break;
case WM_TIMER: if (wParam==1)
InvalidateRect(hWnd, NULL, TRUE); break;
意思是说每0.2秒重绘一次主窗口,大家看看,是不是闪烁得厉害,闪烁过程中,我们依稀看到了这根线穿过了子窗口的区域??然后把WS_CLIPCHILDREN风格赋予主窗口,其余不变,再看看,是不是闪烁现象大为减少?通过这个例子告诉大家什么叫“把现有的技术用得最好”(参考我上一篇博文),有时候就差那么一点点。
四、Foreground、Active、Focus及对Z order的理解
看前面的这个“MDI”例子,也许你发现它跟MFC向导创建出来的MDI界面的最大不同就是子窗口无法“激活”,你怎么点,怎么拖都不行,它们的caption恒定是灰色的,我曾经为此苦思冥想??spy++是个好东西,前面主要是用它来查看窗口的属性,现在我们用它来查看窗口消息,(不知道怎么做的看看spy++的帮助)在消息过滤中,我们只选择一个消息,就是WM_NCACTIVATE,MSDN对这个消息的说明是:The WM_NCACTIVATE message is sent to a window when its nonclient area needs to be changed to indicate an active or inactive state. 那就是窗口激活状态改变的时候,会收到这个消息啰?而我观察下来的结果是,The WM_NCACTIVATE never came.
办法总该是有的,比如利用SetActiveWindow这个API,在主界面上做个按钮,点一下这个按钮,就SetActiveWindow(g_hwndA),这样来激活A窗口,而事实上这样做是徒劳,A既没有被激活,也没有收到WM_NCACTIVATE。但我还是有办法的,大家看下面的代码,在那个叫WndProcDoNothing的窗口里加入对WM_MOUSEACTIVATE消息的处理:
case WM_MOUSEACTIVATE: {
HWND hwndFind=NULL; while(TRUE) {
hwndFind = FindWindowEx(g_hwndMain, hwndFind, TEXT(\ if (hwndFind==NULL) break;
if (hwndFind==hWnd)
PostMessage(hwndFind, WM_NCACTIVATE, TRUE, NULL); else
PostMessage(hwndFind, WM_NCACTIVATE, FALSE, NULL); } }