窗口之间的主从关系与Z-Order
说明:这是本人2008年写的一篇旧文,从未公开发表过。其中除了一小段描述Window Mobile平台的内容已过时,大部分内容对于从事Win32开发的程序员还是很有参考价值的,也是对自己从事Windows开发工作的一个总结,欢迎指正。转载请注明:http://www.cnblogs.com/dhatbj/原创。
范围(Scope)
讨论Windows操作系统中窗口之间的关系(relationship between windows),除特别指明的部分之外,适用于各版本桌面平台和Windows Mobile平台。
概述(Summary)
窗口(Window)是Windows操作系统中用来显示信息和接受用户输入的基本单元(Block)。负责管理窗口相关功能的操作系统部件被称为窗口管理器(Window Manager)。Windows操作系统初始化时会生成一个窗口,叫做桌面窗口(Desktop Window),调用GetDesktopWindow函数可获得它的句柄。桌面窗口会覆盖整个屏幕,所有其它窗口都在其之上显示。
窗口类型(Window Type)
Windows中有3种类型的窗口:层叠窗口(Overlapped Window)、弹出窗口(Popup Window)、子窗口(Child Window),在生成窗口(调用CreateWindowEx)时分别以WS_OVERLAPPED、WS_POPUP或WS_CHILD窗口风格(Style)来表示。层叠窗口是窗口的缺省类型,如果不指定任何窗口类型则生成的是层叠窗口。
弹出窗口通常用于对话框。它隐含带有WS_CLIPSIBLINGS窗口风格(后面会详细描述)。
层叠窗口通常被用作应用程序的主窗口,也隐含带有WS_CLIPSIBLINGS窗口风格。在桌面平台上,层叠窗口还隐含带有WS_CAPTION窗口风格。带有标题栏的窗口都隐含带有边框(Border),至于原因嘛,想像一下“光秃秃的标题栏”+“没有边框的窗口”会是个什么样子。在Mobile平台上,层叠窗口与弹出窗口的界限已经很模糊了。
层叠窗口和弹出窗口统称为顶层窗口(top-level windows)。
剩下的一类是子窗口,例如常见的Button,Edit Box,List Box等窗口控件。
WS_OVERLAPPED的值
在桌面平台上,WS_OVERLAPPED定义为0,这与窗口的缺省类型为层叠窗口的事实相符;而在Windows Mobile平台上,WS_OVERLAPPED被定义为WS_BORDER | WS_CAPTION,这是怎么回事呢?我想这是微软为了保持桌面平台与Mobile平台软件的外观兼容性而使用的一个技巧,因为Mobile平台上的层叠窗口缺省是不带WS_CAPTION风格的,微软的意思应该是:(WS_OVERLAPPED in PC)=(WS_OVERLAPPED in Mobile)| WS_BORDER | WS_CAPTION,在字面上就会写成:
#define WS_OVERLAPPED WS_BORDER | WS_CAPTION
这样定义可以方便桌面平台上的代码移植到Mobile平台。但开发原生的Windows Mobile代码时就要注意了,由于Mobile上的典型窗口是不带标题栏的(Mobile界面最上方的Title Bar并不属于窗口的一部分),我们在生成层叠窗口时不应使用WS_OVERLAPPED标志(这一标志的实际意义是:PC style overlapped window)— 不指定任何窗口类型就好。
窗口层次结构(Window Hierarchy)
窗口管理器以一个树状结构组织和管理系统内所有窗口,如图:
图1.树状的窗口组织图
树形结构的根是桌面窗口,其下属第一层窗口是顶层窗口(层叠窗口+弹出窗口,见上一小节)。顶层窗口之下的所有层里只包含子窗口。从桌面窗口出发,通过一系列相关API函数的调用,可以遍历系统中的所有窗口。
窗口的从属关系
包括父/子(parent-child)关系、拥有/被拥有(owner-owned)关系及兄弟(siblings)关系。
父/子(parent-child)关系
类型为Child Window的窗口必须有一个父窗口,父窗口的类型可以是3种类型中的任意一种。子窗口的位置坐标都是相对于父窗口客户区的左上角(upper-left corner)计算的。子窗口会把它的notify消息发送到父窗口。父/子关系对窗口可见性的影响为:子窗口只能显示在它的父窗口的客户区中,超出父窗口客户区的部分将被裁减掉;父窗口被隐藏时,它的所有子窗口也被隐藏;最小化父窗口不影响子窗口的可见状态,子窗口会随着父窗口被最小化,但是它的WS_VISIBLE属性不变。父窗口被销毁的时候,它的所有子窗口都会被销毁。
窗口生成时通过CreateWindowEx函数的hWndParent参数可指定其父窗口,或在窗口产生后通过SetParent函数更改。通过GetParent函数可获取父窗口句柄。父窗口要查询其子窗口可使用GetWindow函数(指定GW_CHILD标志),该函数返回第一个子窗口的句柄。
桌面窗口与所有顶层窗口也是父/子关系,但又有其特殊性:对桌面窗口调用GetWindow函数(指定GW_CHILD标志)可得到第一个顶层窗口的句柄,但对某一个顶层窗口调用GetParent却会返回NULL,不会返回桌面窗口的句柄。由此可见,父/子关系中的“子”一方未必一定是子窗口(Child Window类型)。
拥有/被拥有(owner-owned)关系
顶层窗口之间可以存在owner-owned关系。owner-owned关系对窗口可见性的影响为:owned窗口永远显示在owner窗口的前面;当owner窗口最小化的时候,它所拥有的窗口都会被隐藏;隐藏owner窗口不影响它所拥有的窗口的可见状态。根据最后这一点,如果窗口A 拥有窗口B,窗口B拥有窗口C,则当窗口A最小化的时候,窗口B被隐藏,但是窗口 C还是可见的。当owner窗口被销毁的时候,它所拥有的窗口都会被销毁。
Owner窗口在owned窗口生成时通过CreateWindowEx函数的hWndParent参数指定,如果该参数传入的是一个子窗口,窗口管理器将找到容纳该子窗口的顶层窗口,以该顶层窗口作为owner窗口。Owner窗口一旦指定不能更改。通过GetWindow函数(指定GW_OWNER标志)可获取owner窗口的句柄(如果存在的话)。
兄弟(siblings)关系
同一个父窗口的所有直属子窗口之间是兄弟关系,也就是相互平等,没有主从之分。窗口管理器用链表(linked list)来管理每个父窗口的直属子窗口(见图1),这个链表叫子窗口链(child window list)。
调用GetWindow函数时使用GW_HWNDPREV或GW_HWNDNEXT标志可访问子窗口链中的前一个或后一个窗口;使用GW_HWNDFIRST或GW_HWNDLAST标志可访问子窗口链中的第一个或最后一个窗口。
Z-Order
窗口在子窗口链中的先后顺序也就是窗口在屏幕上显示时的前后顺序,在子窗口链里位置越靠前的窗口显示时也越靠前,这个前后顺序就是Z-Order。Z-Order在前的顶层窗口会遮挡Z-Order在后的顶层窗口;屏幕上的一块区域需要刷新(Update)时,同一个子窗口链中Z-Order在前的窗口先刷新,Z-Order在后的窗口后刷新。有父/子关系的窗口是父窗口先刷新,子窗口后刷新,
顶层窗口生成时,窗口管理器会把它加到(桌面窗口的)子窗口链的最前面,也就是Z-Order的最前面,使整个窗口都可见。子窗口的Z-Order要高于它的父窗口,因此会显示在父窗口前面,但任何一个子窗口的Z-Order都不会超过其父窗口的Z-Order更靠前的兄弟窗口。改变窗口的Z-Order可使用SetWindowPos函数。
子窗口生成时,与顶层窗口的情况有所不同,窗口管理器会把它加到父窗口的子窗口链的最后面。这似乎是反直觉的,为什么会这样呢?窗口管理器这样做是有原因的,其主要目的是让后生成的窗口能显示在前面(兄弟窗口间有重叠的情况下),并且子窗口间的Tab-Order与窗口的生成顺序相同,这样的效果才是符合直觉的。子窗口大多数情况下都共用其父窗口的显示DC(Device Context),所以在刷新时是可以在其兄弟窗口的客户区上绘画(draw)的,这就造成了Z-Order在后的子窗口因为刷新顺序在后,绘画能覆盖Z-Order在前的窗口,显示效果反而在前的现象,如下图所示:
图2.子窗口相互覆盖示意图(无WS_CLIPSIBLINGS风格)
如果想使Z-Order在前的子窗口显示时也在前(覆盖Z-Order在后的子窗口),需要使用WS_CLIPSIBLINGS窗口风格(后面详述)。
Topmost窗口
也就是具有WS_EX_TOPMOST扩展风格的窗口,仅适用于顶层窗口,子窗口无法使用。根据有无WS_EX_TOPMOST风格将所有顶层窗口分成了两个级别。有WS_EX_TOPMOST风格的顶层窗口Z-Order在前,普通顶层窗口Z-Order在后。普通顶层窗口要成为Topmost窗口可以调用SetWindowPos函数,指定hwndInsertAfter参数为HWND_TOPMOST,指定hwndInsertAfter参数为HWND_NOTOPMOST调用SetWindowPos则使Topmost窗口成为普通顶层窗口。
Tab-Order
兄弟窗口间的Tab-Order实际上是由Z-Order决定的(与Z-Order的顺序相同),因此如果想在程序运行过程中动态改变Tab-Order,可通过改变兄弟窗口间的Z-Order(使用SetWindowPos)来实现。反过来,在VC++的对话框编辑器里,如果控件的位置有重叠,通过调整Tab-Order也能调整控件间的遮挡关系。
WS_CLIPCHILDREN和WS_CLIPSIBLINGS
在窗口之间有重叠的情况下,这两个窗口风格会影响窗口刷新区域(Update Region)的计算方法。“Clip”一词是指从刷新区域中“剪切掉”被Z-Order在前的子窗口或兄弟窗口覆盖的区域,如图3所示:
图3. 窗口之间有重叠时的刷新区域
窗口B与窗口A是兄弟关系,B的Z-Order高于A。整个屏幕刷新时,B先刷新,A后刷新。如果A没有WS_CLIPSIBLINGS风格,则A的整个客户区都被刷新,A将重新刷新两窗口相交的C区域,造成A覆盖B的效果;但如果A有WS_CLIPSIBLINGS风格,则只有图中绿色的部分会被刷新,A就不会覆盖B。简单的说,WS_CLIPSIBLINGS可以控制Z-Order在前的窗口显示在前还是Z-Order在后的窗口显示在前。由于窗口管理器强制顶层窗口都有WS_CLIPSIBLINGS风格,所以顶层窗口总是Z-Order在前的显示在前(能覆盖Z-Order在后的窗口)。子窗口缺省不带WS_CLIPSIBLINGS风格,所以是Z-Order在后的窗口能覆盖Z-Order在前的兄弟窗口(如图2)。要想使子窗口的遮挡效果与Z-Order一致,可以把相关的子窗口都加上WS_CLIPSIBLINGS风格,之后的外观如下图:
图4.子窗口相互覆盖示意图(有WS_CLIPSIBLINGS风格)
由于父窗口总是先于子窗口执行刷新动作,所以无论是否使用WS_CLIPCHILDREN风格,父窗口都不会遮盖子窗口。WS_CLIPCHILDREN的作用主要是避免窗口重叠区域的重复刷新,有可能加快窗口显示速度以及减轻刷新时的“闪烁”问题。
窗口生成时使用缺省位置和大小
CreateWindowEx生成的窗口如果是层叠窗口,程序可以不指定窗口的初始位置和大小,而是由窗口管理器决定。
要让窗口管理器设置窗口的初始位置,需要使用一个特殊值CW_USEDEFAULT作为CreateWindowEx的参数“x”的值,参数“y”将被忽略,不起作用。对于桌面平台,如果窗口风格包含WS_VISIBLE,CreateWindowEx 函数内部将把“y”的值作为第二个参数(nCmdShow)传递给ShowWindow函数,这时的“y”作为一个隐藏参数使用。
要让窗口管理器设置窗口的初始大小,需要使用CW_USEDEFAULT作为CreateWindowEx的参数nWidth的值,参数nHeight将被忽略。
CW_USEDEFAULT只能用于层叠窗口,对于弹出窗口或子窗口,如果给“x”参数传递了CW_USEDEFAULT,则窗口位置不确定;如果给nWidth参数传递了CW_USEDEFAULT,则窗口大小不确定。
参考资料
1. Win32 Window Hierarchy and Styles,Kyle Marsh,1993
http://msdn.microsoft.com/en-us/library/ms997562.aspx