//转载自:http://blog.csdn.net/guogangj/article/details/3460267
(本文尝试通过一些简单的实验,来分析Windows的窗口机制,并对微软的设计理由进行一定的猜测,需要读者具备C++、Windows编程及MFC经验,还得有一定动手能力。文中可能出现一些术语不统一的现象,比如“子窗口”,有时候我写作“child window”,有时候写作“child”,我想应该不会有太大影响,文章太长,不一一更正了)
问题开始于我的最近的一次开发经历,我打算把程序的一部分界面放在DLL中,而这部分界面又需要使用到Tooltip,但DLL中的虚函数PreTranslateMessage无法被调用到,原因大家可以在网上搜索一下,这并不是我这篇文章要讲的。PreTranslateMessage不能被调,那Tooltip也就不能起作用,因为Tooltip需要在PreTranslateMessage中加入tooltip.RelayEvent(&msg)来触发事件,方可正常显示。解决方法有好几个,我用的是比较麻烦的一个——完全自己手动编写Tooltip,然后用WM_MOUSEMOVE等事件来触发Tooltip显示,写好之后发现些小问题,那就是调试运行时候IDE给了个warning,说我在析构函数中调用了DestroyWindow,这样会导致窗口OnDestry和OnNcDestroy不被正常调用,这个问题我以前遇到过,当然解决方法也是显而易见的,只需要在窗口对象(C++概念,非Windows内核对象,下文同)销毁前,调用DestroyWindow即可。对于要销毁的这个窗口的子窗口,是不需要显式调用DestroyWindow的,因为父窗口在销毁的时候也会销毁掉它们,OK,我把这个过程用个示意图说明一下:
图1
上图表示了App Window及其子窗口的关系,现在假设我们要销毁Parent Window 1(对应的对象指针是m_pWndParent1),我们可以m_pWndParent1->DestroyWindow(),这样Child Window 1,Parent Window 2,Child Window 2都被销毁了,销毁的时候这些窗口的OnDestry和OnNcDestroy都被调用了,最后delete m_pWndParent1,此时m_pWndParent1->m_hWnd已经是NULL,不会再去调用Destroy,在析构的时候也就不会出现Warning。但如果不先执行m_pWndParent1->DestroyWindow()而直接delete m_pWndParent1,那么在CWnd::~CWnd中就会调用DestroyWindow(m_hWnd),这样会产生WM_DESTROY和WM_NCDESTROY,会尝试去调用OnDestry和OnNcDestroy,但由于是在CWnd的函数~CWnd()的内部调用这两个成员,此时的虚函数表指针并不指向派生类的虚函数表,因此调用的其实是CWnd::OnDestroy和CWnd::OnNcDestroy,派生类的OnDestry和OnNcDestroy不被调用,但我们很多时候把释放内存等操作写在派生类的OnDestroy和OnNcDestroy中,这样,就容易导致内存泄露和逻辑混乱了。
上面这些道理我当然是知道的,但Warning还是出现了,而且我用排除法确定了是跟我写的那个Tooltip有关,下面是关于我的Tooltip的截图:
图2
大家看到,Tooltip显示在我的图形窗口上,它是个弹出式(popup)窗口,其内容为当前鼠标光标的坐标值,图形窗口之外,我是不想让它显示的,那么按照我的思路,Tooltip就应该设计是图形窗口的子窗口,它的窗口对象就应该作为图形窗口对象的成员,在图形窗口OnCreate的时候创建,在图形窗口被DestroyWindow的时候自动销毁,前面提到过,父窗口被销毁的时候,其子窗口会被自动销毁,没错吧,所以不需要显式去对Tooltip调用DestroyWindow。可事实证明了这样是有问题的,因为Tooltip的父窗口根本不是,也不能是图形窗口。大家可以看到我的图形窗口是作为一个子窗口嵌入到别的窗口中去的,它的属性包含了WS_CHILD,通过实验,我发现Tooltip的父窗口只能指定为程序主窗口,如果企图指定为那个图形窗口的话,它就自动变为程序主窗口,再进一步研究发现,弹出式窗口的父窗口都不能是带WS_CHILD风格的窗口,然后打开spy++查看,弹出式窗口的上一级都是桌面,可是,通过GetParent函数,得到的弹出式窗口的父窗口却是程序主窗口而不是桌面,为什么?……问题越来越多,我糊涂了,上面说的都是在我深入理解前,所看到的现象,包括了我的一些概念认识方面的错误。
好吧,我们现在开始,一点点地通过实验去攻破这些难题!
一、神秘的WS_OVERLAPPED
我们从WinUser.h头文件中可以看出,窗口可分三种,其Window Styles定义如下:
- #define WS_OVERLAPPED 0x00000000L
- #define WS_POPUP 0x80000000L
- #define WS_CHILD 0x40000000L
那么我们很容易得到这个结论:style的最高位是1的,是一个popup窗口,style的次高位是1的,代表是一个child窗口,如果最高位次高位都是0,那这个窗口就是一个overlapped窗口,如果两位都是1,厄……MSDN告诉我们不能这么干,事实呢?我后面再讲。其实这个结论是有点过时的,甚至很能误导人,不是我们的原因,很可能是Windows的历史原因,为什么?具体也是后面讲。嘿嘿。
OK,我们现在开始来尝试,看看这些风格究竟影响窗口几何,对了,准备spy++,这是必备工具。
用VC++的向导创建一个Hello World的Windows程序,注意是Windows程序,不是MFC的Hello World,这样我们可以绕开MFC,专注于查看一些Windows的技术细节,编译,运行。
图3
然后用spy++查看这个窗口的风格,发现其风格显示为“WS_OVERLAPPEDWINDOW|WS_VISIBLE|WS_CLIPSIBLING|WS_OVERLAPPED”。此时它的创建函数为:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
只制定了一个WS_OVERLAPPEDWINDOW,但我们很快就找到了WS_OVERLAPPEDWINDOW的定义:
- #define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | /
- WS_CAPTION | /
- WS_SYSMENU | /
- WS_THICKFRAME | /
- WS_MINIMIZEBOX | /
- WS_MAXIMIZEBOX)
原来overlapped窗口就是有标题,系统菜单,最小最大化按钮和可调整大小边框的窗口,这个定义是正确的,但只是个我们认知上的概念的问题,因为popup和child窗口也同样可以拥有这些(后面证明)。由于WS_OVERLAPPED为0,那我们是不是可以把WS_OVERLAPPEDWINDOW定义中的WS_OVERLAPPED拿掉呢?那是肯定的,那也就是说WS_OVERLAPPED什么都不是!我们只作popup和child的区分,是不是这样?也不是,我们继续实验。
很简单,接下去我们只给这个向导生成的代码加一点点东西,就是把CreateWindow改成:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
对,给窗口风格增一个popup风格,看看会怎么样?运行!这回可不得了,窗口缩到了屏幕的左上角,并且宽度高度都变为了最小,当然,你还是可以用鼠标拖动窗口边缘来调整它的大小的。如图:
图4
这是为什么呢?观察CreateWindow的,第四、第五、第六和第七参数,分别为窗口的x坐标,y坐标,宽度,和高度,CW_USEDEFAULT被define成0,所以窗口被缩到左上角去也就不奇怪了,可没有popup,光是overlapped风格的窗口,为什么不会缩呢?看MSDN的说明,对第四个参数的说明:“If this parameter is set to CW_USEDEFAULT, the system selects the default position for the window's upper-left corner and ignores the y parameter. CW_USEDEFAULT is valid only for overlapped windows; if it is specified for a pop-up or child window, the x and y parameters are set to zero. ”其余几个参数也有类似的描述,这说明了什么?说明Windows对overlapped和popup还是作区分的,而这点,算是我们发现的第一个不同。哦,还有件事情,就是用spy++观察其风格,发现其确实多了一个WS_POPUP,其余没什么变化。
继续,这回还是老地方,把WS_POPUP改为WS_CHILD,试试看,这回创建窗口失败了,返回0,用GetLastError查看具体错误信息,得到的是:“1406:无法创建最上层子窗口。”看来桌面是不让我们随便搞的。继续,还是老地方,这回改成:
- hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW|WS_POPUP|WS_CHILD, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
嗯?有没搞错,又是popup又是child,肯定不能成功吧,不试不知道,居然成功了,这个创建出来的窗口乍一看,跟popup风格的很像,但用起来有些怪异,比如:当它被别的窗口挡住的时候,不能通过点击它的客户区来让它显示在前面,即使点击它的标题栏,也是要松开鼠标左键,它才能显示在前面,还有就是用spy++的“瞄准器”没法准确捕捉到这个窗口,瞄准器对准它的时候,就显示Caption为“Program Manager”,class为“Program”,“Program Manager”是什么?其实就是我们所看到的这个桌面(注意,不是桌面,我说的是我们说“看到的桌面”,就是显示桌面图标的这个所能看到的桌面窗口,和前面提到的桌面窗口是有区别的)的父窗口的父窗口,这个窗口一般情况下是不能直接“瞄准”到的,这点可以通过spy++证实,如图:
图5
图6
spy++不能直接“瞄准”这个popup和child并存的怪窗口,但我们有别的办法捕捉到它,<Alt>+<F3>,输入窗口的标题来查找(记得运行程序后刷新一下才能找到),结果见下图:
图7
我们从上图中清楚地看到,popup和child并存!用spy++逐个查看桌面窗口的下属,这种情况还是无独有偶的,但这样的窗口代表了什么意义,我就不清楚了,总之用起来怪怪的,对Microsoft来说,这可能就是Undocumented,OK,我们了解到这里就行了,但一般情况下,我们不要去创建这种奇怪的窗口。这几轮实验给我们什么启示?设计上的启示:一个应用程序的主窗口通常是一个Overlapped类型的窗口,当然有时可以是一个popup窗口,比如基于对话框的程序,但不应该是一个child窗口,尽管上面演示了如何给应用程序主窗口加入child风格。
那还有一个问题,我为什么认为WS_OVERLAPPED神秘呢?这还算是拜spy++所赐,按照我们一般的想法,如果一个窗口的风格的最高两位都是0,它既不是popup也不是child的时候,那它就是Overlapped。事实上spy++的判定不是这样的,就以刚才的实验为例,当使用WS_OVERLAPPEDWINDOW|WS_POPUP风格创建窗口的时候,WS_OVERLAPPED和WS_POPUP属性同时出现了,我做了很多很多的尝试,企图找出其中规律,看看spy++是怎么判定WS_OVERLAPPED的,但至今没结论,我到MSDN上search,未果,有人提起这个问题,但没有令我满意的答复,下面这段文字是我找到的可能有点线索的答复:
Actually, Microsoft Spy++ is wrong.
There are two bits in the window style that control its type. If the high-order bit of the style DWORD is set, the window is a popup window. If the next bit is set, the window is a child window. If neither is set, the window is overlapped. (If both are set, the result is undocumented.)
Look at these definitions from WinUser.h.
- #define WS_OVERLAPPED 0x00000000L
- #define WS_POPUP 0x80000000L
- #define WS_CHILD 0x40000000L
Your window style (0x94c00880) has the high-order bit set and the next bit clear so it is a popup window, not an overlapped window.
The correct way to identify all three types of windows (this is what Spy++ should do) is
- dwStyle = GetWindowLong(hWnd, GWL_STYLE);
- if (dwStyle&WS_POPUP)
- // it's a popup window
- else if (dwStyle&WS_CHILD)
- // it's a child window
- else
- // it's an overlapped window
这断描述跟我的想法一致。要知道,就算你只给窗口一个WS_POPUP的风格,WS_OVERLAPPED也会显示在spy++上的,我认为这十分有问题,究竟spy++如何判,估计得请教比尔盖茨了。还有一段有趣的描述,估计也有所帮助:
As long as...
WS_POPUP | WS_OVERLAPPED
...is absolutelly equivalent with...
WS_POPUP
... why do you care if Spy++ lists WS_OVERLAPPED or not?
Please stop playing "Thomas Unbeliever" with us.
Becomes too expensive to use "walking on the water" device here again, and again. ;)
虽然这么说,我还是认为,spy++给了我们不少误导,那么对WS_OVERLAPPED的讨论就暂时告一段落吧,作为一个技术人,很难容忍自己无法理解的逻辑,我就是这么种人……不过如果再扯下去的话这篇文章就不能结束了,所以姑且认为,这是spy++的错,而我们还是认为窗口分3种——popup,child和Overlapped。(Undocumented不在此列,也不在本文讲述之列)
二、Parent与Owner
这是内容最多的一节,做好心理准备。
微软和我们开了个玩笑,告诉我们,窗口和人一样,可以有父母,有主人……我们先来看一个最著名的Windows API:
- HWND CreateWindowEx(
- DWORD dwExStyle, // extended window style
- LPCTSTR lpClassName, // registered class name
- LPCTSTR lpWindowName, // window name
- DWORD dwStyle, // window style
- int x, // horizontal position of window
- int y, // vertical position of window
- int nWidth, // window width
- int nHeight, // window height
- HWND hWndParent, // handle to parent or owner window
- HMENU hMenu, // menu handle or child identifier
- HINSTANCE hInstance, // handle to application instance
- LPVOID lpParam // window-creation data
- );
猜对了,我就是从MSDN上copy下来的,看第九个参数的名字叫hWndParent,顾名思义哦,这就是Parent窗口了,不过我们中国人不喜欢称之“父母窗口”,我们喜欢叫它“父窗口”,简单一点。其实这个名字对我们造成了不少的误导,我只能说,可能也是由于历史原因,比如在Windows 1.0(1985年出的,当时没什么影响力)的时候,只有Parent这个概念,没有Owner的概念。
回头看看文章开始我提起的,我企图将Tooltip的父窗口设置为一个图形窗口,不能成功,Tooltip的父窗口会自动变成应用程序主窗口,这是为什么?好,现在开始讲概念了,都是我花了很多时间在互联网上搜索,筛选,确认,得出来的结论:
规则一:Owner window控制了Owned window的生存,当Owner window被销毁的时候,其所属的Owned window就会被销毁。
规则二:Parent window控制了Child window的绘制,Child window不可能显示在其Parent window的客户区之外。
规则三:Parent window同时控制了Child window的生存,当Parent window被销毁的时候,其所属的Child window就会被销毁。
规则四:Owner window不能是Child window。
规则五:Child window一定有Parent(否则怎么叫Child?),一定没有Owner。
规则六:非Child window的Parent一定是桌面,它们不一定有Owner。
这是比较重要的几点,如果你认为这跟你以前学到的,或者认知的有所不同,先别急着抗议,先看看我是怎么理解的。除了这几条规则,下面我还会逐步给出一些规则。
先说比较好理解的Child window,上文提到了,包含了WS_CHILD风格的窗口就叫Child window,我们中文叫“子窗口”。那么我前面提到的我写的那个Tooltip,是不是“子窗口”呢?——当然不是了,它没有WS_CHILD风格啊,它是popup风格的,我想当然地认为在创建它的时候给它指定了那个Parent参数,那它的Parent就是那个参数,其实是错的。这个实验最简单了,随便找些应用程序,比如“附件”里的计算器,用spy++的“瞄准器”观察上面的按钮等“子窗口”,在Styles标签中,我们可以看到WS_CHILD(或者WS_CHILDWINDOW,一样的)属性,然后在Windows标签中,我们可以清楚地看到,凡是包含了WS_CHILD属性的窗口(子窗口),都没有Owner window,不信还可以继续观察其它应用程序,省去自己编程了。再看它们的Parent window,是不是一定有的?——当然一定有。
前面说了,子窗口不能显示在父窗口客户区之外,我们最常见的子窗口就是那些摆在对话框上的控件,什么button啊,listbox啊,combobox啊……都有个共同特点,不能拖动的,除非你重写它们的window procedure,然后响应WM_MOUSEMOVE等消息,实现所谓“拖动”。那么有没有能够像应用程序主窗口那样有标题栏,能够被自由拖动的子窗口呢?——当然有!要创建是吗?简单,直接用MFC向导创建一个MDI程序即可,MDI的那些View其实就是可以自由拖动的子窗口,可以用spy++查看一下它们的属性,当然,你是不能把它们拖出主窗口的客户区的。也许你跟我一样,觉得MFC封装了过多的技术细节,想完全自己手动创建一个能拖动的子窗口,而且看起来就像个MDI的界面,OK,follow me。
首先当然是用应用程序向导生成最普通的Window应用程序了。然后增加一个窗口处理函数,也就是我们准备创建的子窗口的处理函数了。
- LRESULT CALLBACK WndProcDoNothing(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
- {
- return DefWindowProc(hWnd, message, wParam, lParam);
- }
DoNothing?好名字。注册之:
- WNDCLASSEX wcex;
- wcex.cbSize = sizeof(WNDCLASSEX);
- wcex.style = CS_HREDRAW | CS_VREDRAW;
- wcex.lpfnWndProc = (WNDPROC)WndProcDoNothing;
- wcex.cbClsExtra = 0;
- wcex.cbWndExtra = 0;
- wcex.hInstance = hInstance;
- wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_ALLWINDOWTEST);
- wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
- wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
- wcex.lpszMenuName = NULL; //子窗口不能拥有菜单,指定了也没有用
- wcex.lpszClassName = TEXT("child_window");
- wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
- RegisterClassEx(&wcex);
最后当然是把它给创建出来了:
- g_hwndChild = CreateWindowEx(NULL, TEXT("child_window"), TEXT(""), WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);
关于WS_CLIPSIBLINGS属性,下文将提到。好,就这样,大家看看运行效果:
图8
是不是很少遇到这种窗口组织结构?确实很少人这样用,而且哦,你会发现子窗口的标题栏没办法变为彩色,它一直是灰的,就表示它一直处于未激活状态,你怎么点它,拖它,调它,都没用的,而这个时候程序主窗口一直显示为激活状态,如何激活这个子窗口?我曾经对此苦思冥想,最后才知道,子窗口是无法被激活的,你立即反驳:“那MFC如何做到的?”哈哈,好,你反应够快,我下文会给你演示如何“激活”子窗口。(注意是加引号的)现在尝试移动主窗口,你会发现所有它的子窗口都会跟着主窗口移动的,这就好像我们看苹果落地一样,不会觉得奇怪,但你有没有想过,主窗口移动的时候,其子窗口对屏幕的位置也发生了变化,不变的是相对主窗口的客户区坐标。这就是子窗口的特性。再试试看启用/禁用主窗口,显示/隐藏主窗口看看,就不难得出结论:
规则七:子窗口会随着其父窗口移动,启用/禁用,显示/隐藏。
子窗口我们就暂时讲那么多,接着讲所有者窗口,就是Owner window,由于子窗口一定没有Owner,因此Owner window是对popup和Overlapped而言的,而popup和Overlapped前面也提到了,不一定有Owner,不像Child那样一定有Parent。现在进入我们下一个实验:
还是用向导生成最普通的Windows hello world程序,步骤和上一个实验很相似,仅仅改了一点点东西,改了哪点?就是把CreateWindowEx函数的第四个参数的WS_CHILD拿掉,其余不变,代码我就不贴了,大家编译并运行看看。大家会看到类似这个效果:
图9
弹出窗口的caption是蓝色的,说明它处于激活状态,如果你现在点击程序主窗口,那弹出窗口的标题栏就变灰,而程序主窗口的标题栏变蓝,两个窗口看起来就像并列的关系,但你很快发现它们其实不并列,因为如果它们有重叠部分的话,弹出窗口总是遮挡程序主窗口。用spy++观察之,发现程序主窗口就是弹出窗口的Owner。
规则八:非Child window总是显示在它们的Owner之前。
看到了没?这个时候CreateWindowEx的第九个参数的意义就不是Parent window,而是Owner,那把这个参数改为NULL,会有什么效果呢?马上试试看,反正这么容易。
图10
初一看没什么变化,其实变化大了,一是主窗口这回可以显示在弹出窗口之前了,二是任务栏上出现了两个button。
图11
用spy++观察到这两个窗口的Owner都是NULL。
规则九:Owner为NULL的非Child窗口能够(不是一定哦)在任务栏上出现它们的按钮。
这个时候,你应该清楚为什么给一个MessageBox正确指定一个Owner这么重要了吧?我以前有个同事,非常“厉害”,他创建了一个程序,一旦出现点什么问题,就能把MessageBox弹得满屏都是,而且把任务栏霸占得渣都不剩,他大概是没明白这个道理。MessageBox是一个非child窗口,如果不指定一个正确的Owner,那弹出MessageBox之后,Owner还是处于可操作的状态,两个窗口看起来是并列的,都在任务栏上有显示,如果再弹出MessageBox,先关闭那个MessageBox?我看先关哪个都没问题,因为界面操作上没有限制,但这样很容易导致逻辑混乱,如果不幸走入了个死循环,连续弹MessageBox,那就像这位同事写的那个程序那样,满屏皆是消息框了。
我们现在来进行一些稍微复杂点点的实验,就是创建A弹出窗口,其Owner为主窗口,创建B弹出窗口,其Owner为A窗口,创建C弹出窗口,其Owner为B窗口。步骤模仿上面的窗口创建步骤即可,好,编译,运行,效果大致如此:
图12
现在,把主窗口最小化,看看发生了什么事情。你会发现A窗口不见了,而B,C窗口尚在,A窗口究竟是跟随主窗口一起最小化了呢,或者被销毁了呢?还是被隐藏了呢?答案是被隐藏了,我们可以通过spy++找到它,发现它的属性里边没有WS_VISIBLE。那现在将主窗口还原,A这时候出现了,那现在我们最小化A,Oh?What happen?B不见了,主窗口和C都还在,我们还是老办法,用spy++看B,发现它没了WS_VISIBLE属性,现在还原A窗口,方法如下图所示:
图12_x
注意,最小化的A并不显示在任务栏上。还原A后B也出现了。
规则十:Owner窗口最小化后,被它拥有的窗口会被隐藏。
前面测试的是最小化,那我们现在不妨来测试一下,让A隐藏,会怎么样?在主窗口里创建一个button,点这个button,就执行ShowWindow(g_hwndA, SW_HIDE),如图:
图13
你会发现,被隐藏的只有A,A隐藏后主窗口,B和C都是可见的,你可以继续尝试,隐藏B和C,或者主窗口,不过,你隐藏了主窗口的话恐怕就没法通过主窗口的菜单来关闭程序了,只能打开任务管理器结束掉程序。
规则十一:Owner隐藏,不会影响其拥有的窗口。
现在不是最小化,也不是隐藏,而是测试“关闭”,即销毁窗口,尝试关闭A,发现B,C被关闭;尝试关闭B,发现C被关闭。这个规则也就是规则一了,不必再列。
好,我不可能把所有的规则都列出来,但我相信前面所写的这些东西,对大家起到了抛砖引玉的作用了,其它规则,也可以通过类似的实验得出,或者用已有的规则去推导。那在转入下一节前,我提点问题:
为什么子窗口没有Owner?(就是我们来猜猜微软为什么这样设计)试想一个Child既有Parent,又有Owner,Parent控制其绘制,Owner控制其存在,在Owner销毁的时候,子窗口就要被销毁,而其Parent有可能还继续存在,那这个子窗口的消失可能有点不明不白,这是其中一个原因,另一个原因也类似,如果Parent不控制子窗口的存在,只管其绘制,那么在Parent销毁的时候,Owner可以继续存在,这个时候的子窗口是存在,而又不能显示和访问的,这可能会导致别的怪异问题,既然起了Child这个名字,就应该把它全权交给Parent,由Parent来决定它的一切,我想这就是微软的道理。
那我们如何获取一个窗口的Parent和Owner?大家都知道API函数,GetParent,这是用来获取Parent窗口句柄的API——慢!这并不完全正确!大家再仔细点看看MSDN,再仔细点:
If the window is a child window, the return value is a handle to the parent window. If the window is a top-level window, the return value is a handle to the owner window.
什么是top-level window?就是非Child window,这个后面再详细谈这个,现在注意看了,GetParent返回的有可能不是parent,对于非child窗口来说,返回的就不是parent,为什么?因为非child窗口的parent恒定是Desktop啊(规则6),这还需要获取吗?我们接下去的实验是用来测试GetParent这个函数是否工作正常的,什么?测试M$提供的API,没错,呵呵,当一把微软的测试员吧。接上面那个实验:
//在窗口创建完成后,调用下面的代码,在第一个GetParent处设置个断点,查看返回值,如果返回NULL,按照MSDN所说的,用GetLastError看看是否有出错。
- {
- DWORD rtn;
- HWND hw = GetParent(hWnd); //获取主窗口的“Parent”
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetParent(g_hwndA); //获取A的“Parent”
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetParent(g_hwndB); //获取B的“Parent”
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetParent(g_hwndC); //获取C的“Parent”
- if(hw==NULL)
- rtn = GetLastError();
- }
我的实验结果有些令我不解,清一色返回0,包括GetLastError,也就是说没有出错,那GetParent返回0,根据MSDN上的描述,原因只可能是:这些窗口确实没有Owner。不对啊?难道前面的规则和推论都是错误的不成?我创建它们的时候,就明明白白地指定了hWndParent参数,而且上面的实验也表明了他们之间的Owner和Owned关系,那是不是GetParent错了?我想是的,你先别对着我扔砖头,想看到正确的情况么?好,我弄给你看。
我们是如何创建A,B和C这几个弹出窗口的?我再把创建它们的语句贴一下吧:
- g_hwndX = CreateWindowEx(NULL, TEXT("child_window"), TEXT("X"), WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);
现在把这个语句改为:
- g_hwndX = CreateWindowEx(NULL, TEXT("child_window"), TEXT("X"), WS_POPUP|WS_VISIBLE|WS_OVERLAPPEDWINDOW|WS_CLIPSIBLINGS, 30, 30, 400, 300, hWnd, NULL, hInstance, NULL);
对,就是加上一个WS_POPUP,看看情况变得怎么样?
很惊讶,对不?GetParent这回全部都正确地按照MSDN的描述工作了,这是我发现的popup和Overlapped的第二个差别,第一个差别?在文章开头附近,自己回去找。而spy++显示出来的那个Parent,其实就是GetParent返回的结果。记住,对于非child窗口来说,GetParent返回的并不是Parent,MSDN也是这么说的,你看看这个函数的名字是不是很有误导性?还有spy++也真是的,将错就错。好吧,就让它错去吧,但我们得记住:对非Child窗口来说,Parent一定是桌面。好,再有个问题,看刚刚这个实验,对于有WS_POPUP风格的非Child窗口来说,GetParent能够取回它的Owner,可对于没有WS_POPUP风格的非Child窗口来说,GetParent恒定返回0,那我们如何有效地取得非Child窗口真正的主人呢?方法当然是有的,看:
- {
- DWORD rtn;
- HWND hw = GetWindow(hWnd, GW_OWNER); //获取主窗口的Owner
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetWindow(g_hwndA, GW_OWNER); //获取A的Owner
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetWindow(g_hwndB, GW_OWNER); //获取B的Owner
- if(hw==NULL)
- rtn = GetLastError();
- hw = GetWindow(g_hwndC, GW_OWNER); //获取C的Owner
- if(hw==NULL)
- rtn = GetLastError();
- }
这么一来,无论是否带有WS_POPUP风格,都能够正常取得其所有者了,这个跟spy++的结果一致,用GetWindow取得的Owner总是正确的,那有没有一种方法,使得取得的Parent总是正确的?很遗憾,没有直接的API,包括使用GetWindowLong(hwnd, GWL_HWNDPARENT)都不能一直正确返回Parent,BTW,有位高人说,GetWindowLong(hwnd, GWL_HWNDPARENT)和GetParent(hwnd)有时候会得到不同的结果,不过这个我尝试不出来,我观察的,它们总是返回一样的结果,无论对什么窗口,真怀疑GetParent(hwnd)就是return (HWND)GetWindowLong(hwnd, GWL_HWNDPARENT),虽然我们不能直接一步获取正确的Parent,但我们可以写一个简单的函数:
- HWND GetTrueParent(HWND hwnd)
- {
- DWORD dwStyle = GetWindowLong(hwnd, GWL_STYLE);
- if((dwStyle & WS_CHILD) == WS_CHILD)
- return GetParent(hwnd);
- else
- return GetDesktopWindow();
- }
你终于憋不住了,对我大吼:“你有什么依据说非Child窗口的Parent一定是Desktop?”我当然是有依据的,首先是这些非child window的绘制,不能超出桌面,超出桌面就什么都看不见了,只能是桌面管理着它们的绘制,如果它们确实存在Parent的话,当然,聪明你认为这个理由并不充分,OK,我们编程来证明,先介绍一个API:
- HWND FindWindowEx(
- HWND hwndParent, // handle to parent window
- HWND hwndChildAfter, // handle to child window
- LPCTSTR lpszClass, // class name
- LPCTSTR lpszWindow // window name
- );
又被你猜对了,我是从MSDN上copy下来的(^_^),看MSDN对这个函数的说明:
hwndParent
[in] Handle to the parent window whose child windows are to be searched.
If hwndParent is NULL, the function uses the desktop window as the parent window. The function searches among windows that are child windows of the desktop.
hwndChildAfter
[in] Handle to a child window. The search begins with the next child window in the Z order. The child window must be a direct child window of hwndParent, not just a descendant window.
If hwndChildAfter is NULL, the search begins with the first child window of hwndParent.
lpszClass
窗口类名(我来翻译,简单点)
lpszWindow
窗口标题
关键是看第一个参数,如果hwndParent为NULL,函数就查找desktop的“子窗口”,但这个“子窗口”是加引号的,因为这里的“子窗口”和本文前面一直提到的子窗口确实不太一样,那就是这里的“子窗口”没有WS_CHILD风格,算是一个特殊吧,也难怪GetParent不愿意告诉我们desktop就是这些非Child的父窗口。好,有这个函数,我们就可以知道刚才创建的那几个弹出窗口的老爸究竟是不是桌面。代码十分简单:
- {
- DWORD rtn;
- HWND hw = FindWindowEx(NULL, NULL, TEXT("ALLWINDOWTEST"), TEXT("AllWindowTest")); //从桌面开始查找主窗口
- if(hw==NULL)
- rtn = GetLastError();
- hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("A")); //从桌面开始查找A
- if(hw==NULL)
- rtn = GetLastError();
- hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("B")); //从桌面开始查找B
- if(hw==NULL)
- rtn = GetLastError();
- hw = FindWindowEx(NULL, NULL, TEXT("child_window"), TEXT("C")); //从桌面开始查找C
- if(hw==NULL)
- rtn = GetLastError();
- }
结果如何?(是不是偷懒干脆不做,等着我说结果啊?)我的结果是全部找到了,和用spy++查找的结果一样,所以我有充分的理由认为,所有非child窗口其实是desktop的child,spy++的树形结构组织确实也是这么阐述的。你很厉害,你还是能够驳斥我:“根据规则三,Parent被销毁的时候,其Child将被销毁,你证明给我看?”这个……有点难:
- HWND hwndDesktop = GetDesktopWindow();
- BOOL rtn = DestroyWindow(hwndDesktop);
- if(!rtn)
- DWORD dwErr = GetLastError();
My god,Desktop没了,你说我们还能看到什么呢?当然微软不会没想到这点,DestroyWindow当然不能成功,错误代码为5,“拒绝访问”。好,我有些累了,不能再纠缠了,转入下一节!留个作业如何?尝试使用SetParent这个API,改变窗口的Parent,观察运行情况,并思考这样做有什么不好之处。
三、如何体现WS_CLIPSIBLING和WS_CLIPCHILD?
看了这个标题,应该怎么做?我想你十有八九是打开MSDN,输入这两个关键字去搜索吧?OK,不用了,我把MSDN对这两个窗口风格的说明贴出来:
WS_CLIPCHILDREN Excludes the area occupied by child windows when you draw within the parent window. Used when you create the parent window.
WS_CLIPSIBLINGS Clips child windows relative to each other; that is, when a particular child window receives a paint message, the WS_CLIPSIBLINGS style clips all other overlapped child windows out of the region of the child window to be updated. (If WS_CLIPSIBLINGS is not given and child windows overlap, when you draw within the client area of a child window, it is possible to draw within the client area of a neighboring child window.) For use with the
WS_CHILD style only.
找到是不难,但如果光看这个就明白的话我也不必要写这种文章了,没有适当的代码去实践,估计很多人是不懂这两个风格什么含义的。OK,现在我来带你实践。spy++开着不?哈,别关啊,后面还要用到。用spy++观察各个top-level window(非Child窗口)的属性,是不是都有个WS_CLIPSIBLINGS?想找个没有的都不行,如果你不服气,你要自己创建一个没有WS_CLIPSIBLINGS风格的顶层窗口,好吧,我在这里等你一会儿(……一会儿过去了……),你垂头丧气地回来了:“不行,即便我不指定这个风格,Windows也强制帮我加上。”那……你可以强制剥离掉这个风格啊,这样:
- DWORD dwStyle = GetWindowLong(hWnd, GWL_STYLE);
- dwStyle &= ~(WS_CLIPSIBLINGS);
- SetWindowLong(hWnd, GWL_STYLE);
执行后用spy++一看,还是没有把WS_CLIPSIBLINGS风格去掉,看来Windows是吃定你的了。嗯,前面说的都是top-level window,那对于child window呢?创建一个MFC对话框,在上面加几个button,然后增加/删除这几个button的WS_CLIPSIBLINGS风格?你除了发现child window对与WS_CLIPSIBLING风格不再是强制的之外,恐怕仍然一无所获吧。还是得Follow me,我还是不用MFC,用最简单的Windows API。模仿第二节的创建几个popup窗口A、B、C的那个例子,只不过现在的CreateWindowEx改成这样:
- g_hwndA = CreateWindowEx(NULL, TEXT("child_window"), TEXT("A"),
- WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 30, 30, 400, 300, hWnd, NULL, hInst, NULL);
- g_hwndB = CreateWindowEx(NULL, TEXT("child_window"), TEXT("B"),
- WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 60, 60, 400, 300, hWnd, NULL, hInst, NULL);
- g_hwndC = CreateWindowEx(NULL, TEXT("child_window"), TEXT("C"),
- WS_CHILD|WS_VISIBLE|WS_OVERLAPPEDWINDOW, 90, 90, 400, 300, hWnd, NULL, hInst, NULL);
创建出来的效果如图:
图14
一眼看没什么奇怪的,但尝试拖动里边的窗口就出现些问题了,首先是显示在最前端的C窗口不能拖动(其实是被挡住了),然后你发现B也不能拖动,A可以,A一拖,就出现这种情况:
图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("%s Paint/n"), szWindowTxt);
- 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("Main window paint/n"));
再看看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("child_window"), NULL);
- if (hwndFind==NULL)
- break;
- if (hwndFind==hWnd)
- PostMessage(hwndFind, WM_NCACTIVATE, TRUE, NULL);
- else
- PostMessage(hwndFind, WM_NCACTIVATE, FALSE, NULL);
- }
- }
- break;
现在再尝试运行程序,点击A,B,C窗口,是不是就可以把它们的caption变为彩色(我的是默认的浅蓝色)了?什么道理?虽然这几个子窗口不能真正地被激活(Windows机制决定的,只有top-level window才能被激活),但可以通过发WM_NCACTIVATE消息来欺骗它们,让它们以为自己被激活了,于是把自己的caption绘制为浅蓝色。如图:
图18
也许你还发现,点击子窗口的客户区不能让子窗口调整到其它子窗口的前面,窗口那个前,那个后的这种次序叫“Z order”,又译作“Z轴”,order是“序”的意思,这其实是窗口管理器维护的一个链表,没错,是链表,不是数组,不是队列,不是堆栈,为什么是链表?因为窗口的次序经常发生变化,链表是最方便修改次序的了,只需要改变节点的指针,这点性能考虑,微软是肯定做过的。下面是窗口的Z order的描述(我的描述,从MSDN改编):
桌面是最底层的窗口,不能改变的;对于top-level window,如果存在owner,一定会显示在owner之上(owner一定不会挡住它),不存在拥有关系的top-level窗口,互相之间都有可能会阻挡,用户的操作,窗口显示隐藏最大最小化还原,或者显式调用API设定等都有可能影响它们的次序,但微软为了使得有些窗口总是能够显示在最顶或最底,还设立了一套特殊的规则,那就是top most window,SetWindowPos这个API就有调整次序的功能,或者把某窗口设置为top most,top most总是显示在其它非top most窗口的上面,如果两个窗口同时是top most,那么谁更上面呢?——都有可能,top most之间又是“公平竞争”的关系了,虽然他们对非top most总是保持着优势,那把一个owner设置为top most,会怎么样呢?由于被拥有的窗口必须在其owner的上面,所以那些被拥有的窗口也都全部变成了top most,尽管你没有给他们指定top most,用spy++观察top most窗口的属性,在Extended Style栏目中,能看到一个“WS_EX_TOPMOST”属性,这就是top most窗口的标志了。OK,top-level window的情况看来都没什么问题了,那child window的情况呢?大家都知道,child是绘制在其parent的客户区中的,不可能超出其parent的界限,相当于是其parent的一部分,那我们可不能以认为其child的z order跟其parent的是一致的呢?对于其它top-level窗口来说,这样看是没问题的,因为一个top-level窗口被移到了前面,它的child也会跟着它显示在前面,反之亦然,但一个在Parent窗口内部,哪个child在前,哪个在后,又是有自己的一套private z order的,所谓国有国法,家有家规嘛,这样看,我想就没什么问题了。哦,不对,还有一点没说,对于child来说,不能是top most窗口,用SetWindowPos设置也是没用的。
那我们如何来知道整个Z order的链表?可以这样:
- void ListZOrder(HWND hParent)
- {
- TCHAR szOutput[10];
- HWND hwnd = GetTopWindow(hParent);
- while(hwnd!=NULL)
- {
- wsprintf(szOutput, TEXT("%08X/n"), (UINT)hwnd);
- OutputDebugString(szOutput);
- hwnd = GetNextWindow(hwnd, GW_HWNDNEXT);
- }
- }
这个函数会把某个Parent的子窗口句柄值,按照z order次序,从最顶打印到最底。如果hParent为NULL,那么就从桌面的最顶窗口开始,列出所有桌面的窗口,这样意义不大,为什么?因为你会找出来很多很多窗口,可见的,不可见的,奇奇怪怪的,变来变去的,所以这种列窗口的方法通常是用于列子窗口的。
最后我想提提Foreground、Active和Focus这三者,非常容易让人搞混的三个概念,我给出一些提示和方法,读者自己去编程序体验。
首先是Foreground窗口,说起Foreground就不能不说Foreground线程,Windows同时管理着很多线程,但为了给用户操作起来“爽”一些,需要更快地响应用户的操作,就弄了这么个Foreground线程的概念。比如用户在玩扫雷,那扫雷这个程序的某个线程(据我所知扫雷只有一个线程)就被提升为Foreground线程,这个线程拥有比别的线程略高的优先级,能获取更多的cpu时间片,以此更快一些地响应用户,用户正在使用的这个扫雷程序的主界面,就是Foreground窗口。那Active窗口是什么呢?Active窗口就是目前用户正在使用的那个窗口……厄,这种解释也未免太敷衍人了,那它跟Foreground窗口有什么异同啊?首先说“同”,那就是它们都必须是top-level window,而不能是child window,不同嘛……还是等等再说,那现在轮到Focus窗口了,Focus窗口就是目前直接接收到用户键盘输入消息的那个窗口,可以是child window。我就给那么多提示吧。
我不想直接告诉你它们究竟还有什么不同,我现在给出三个API:GetFocus、GetActiveWindow和GetForegroundWindow,大家用这三个API去做些实验就知道了。
后记
这篇文章我想已经足够长,我必须得结束了,这恐怕也是我写的最长的一篇技术文章(除了我的本科毕业论文),如果你能够从头到尾读到这里,我倍感荣幸,如果它能够给你些帮助,我想这份辛苦也是值得的。进入冬天了,天气很冷,注意保暖。(其实现在我觉得我的脚正踩在南极大陆上……)