PART1 桌面窗口层次(Z-Order 文本文档)
--------------------------------------------
Z-Order(Z 层次顺序):
Bottom-DesktopWindow:"Program Manager"Program
<-
""WorkerW(""SHELLDLL_DefView("FolderView"SysListView32))
"Program Manager"Program(""WorkerW)
->
--------------------------------------------
以下是开启Aero配色方案后(Win10)
桌面窗口的样式信息,嵌入窗口时要用到:
WorkerW 1(""WorkerW)
brush.blackground: COLOR_3DFACE
STYLE:96000000
WS_POPUP
WS_VISIBLE
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
EXSTYLE:00000080
WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
WS_EX_TOOLWINDOW
-----------------------------
WorkerW 2(CHILDWINDOW----""WorkerW)
brush.blackground: COLOR_3DFACE
STYLE:9E000000
WS_POPUP
WS_VISIBLE
WS_DISABLED
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
EXSTYLE:080000A0
WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
WS_EX_TRANSPARENT
WS_EX_TOOLWINDOW
WS_EX_NOACTIVATE
-----------------------------
""SHELLDLL_DefView
brush.blackground: COLOR_WINDOW
STYLE:56010000
WS_CHILDWINDOW
WS_VISIBLE
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
WS_MAXIMIZEBOX
EXSTYLE:00000000
WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
-----------------------------
"FolderView"SysListView32
brush.blackground: COLOR_WINDOW
STYLE:56003B40
WS_CHILDWINDOW
WS_VISIBLE
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
LVS_ICON
LVS_SHAREIMAGELISTS
LVS_AUTOARRANGE
LVS_EDITLABELS
LVS_ALIGNLEFT
LVS_OWNERDATA
LVS_NOSCROLL
EXSTYLE:00000000
WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
LVS_EX_HEADERDRAGDROP
LVS_EX_FULLROWSELECT
LVS_EX_INFOTIP
LVS_EX_UNDERLINEHOT
LVS_EX_LABELTIP
LVS_EX_DOUBLEBUFFER
LVS_EX_SNAPTOGRID
LVS_EX_TRANSPARENTSHADOWTEXT
LVS_EX_AUTOSIZECOLUMNS
-----------------------------
"Program Manager"Program
brush.blackground: COLOR_DESKTOP
STYLE:96000000
WS_POPUP
WS_VISIBLE
WS_CLIPSIBLINGS
WS_CLIPCHILDREN
EXSTYLE:00000080
WS_EX_LEFT
WS_EX_LTRREADING
WS_EX_RIGHTSCROLLBAR
WS_EX_TOOLWINDOW
PART2 VC中设置程序窗口的排列层级
1. Topmost 窗⼝和 Non-Topmost 窗⼝
Windows 中的应用程序窗口,可以按照显示效果分为 Topmost 和 Non-Topmost 两类。Topmost 类型的窗口,显示时位于 Non-Topmost 类型窗口的上方。当 Topmost 类型窗口不是活动窗口时,它失去了输入焦点,但是依然会位于 Non-Topmost 类型窗口之上。
下面举一个例子,假定某个应用程序 A 的窗口是 Topmost 类型,启动该程序,再启动 Windows 自带的记事本 Notepad.exe 程序。Notepad.exe 程序的窗口类型是 Non-Topmost,此时屏幕上的显示类似于下图:
由于先启动应用程序A(文件管理器 explorer.exe 被设置置顶),再启动记事本。所以当前活动窗口是记事本,输入焦点也在记事本中。如果我们敲击键盘,将在记事本中输入文字。应用程序A的窗口类型是 Topmost,所以它还是覆盖在记事本之上,有可能遮挡住一部分记事本窗口的内容。
上图显示的情景是存在一个 Topmost 窗口和一个 Non-Topmost 窗口。可能同时存在多个 Topmost 类型的窗口、以及多个 Non-Topmost 类型窗口,那么同一个类型的窗口会按照某种顺序上下排列起来。对于不同类型的窗口来说,在 Topmost 窗口集合中的任何一个窗口,都将位于所有 Non-Topmost 窗口之上。
2. Z-Order(Z坐标轴顺序)
我们可以将计算机屏幕看作一个二维平面,那么可以假想它有水平坐标轴X和垂直坐标轴 Y,如下图所示:
当计算机屏幕上有多个窗⼝时,我们可以将屏幕想象成⼀个拓展的三维空间,即增加⼀个坐标轴Z,如下图所⽰:
当应用程序新建了一个窗口时,操作系统将根据窗口的类型,将它放置到同类型窗口的最上方,即同类型窗口 Z-Order 的最顶层。当用户激活某一个窗口时(比如用鼠标点击该窗口的标题栏操作),该窗口的 Z-Order 也会被改变,操作系统会将该窗口的 Z-Order 提升到同类型窗口的最顶层。比如该窗口是一个 Non-Topmost 类型窗口,用户激活它之后,它将位于 Non-Topmost 类型窗口集的最上层,但仍然位于所有 Tompost 窗口之下。当一个程序窗口被操作系统调整到 Z-Order 的最顶层时,它的子窗口也会被放到最顶层。
3. VC中设置窗口排列层级的函数
VC中可以改变窗口排列层级的函数很多,最主要的⼀个函数是 SetWindowPos 。该函数定义如下:
BOOL WINAPI SetWindowPos(
_In_ HWND hWnd,
_In_opt_ HWND hWndInsertAfter,
_In_ int X,
_In_ int Y,
_In_ int cx,
_In_ int cy,
_In_ UINT uFlags
);
第1个参数HWND hWnd代表窗⼝的句柄,第2个参数hWndInsertAfter⽤来指定窗⼝将被调整到上下层中的哪⼀个层次中去,它有以下四种宏定义的取值:
HWND_BOTTOM——将该窗口放到 Z-Order 排列顺序的最底层。如果这个窗口是一个 Topmost 窗口,使用 HWND_BOTTOM 参数调用 SetWindowPos 函数之后,这个窗口将不再是 Topmost 类型,并被放到所有其他窗口之下,即如果此时还存在 Non-Topmost 类型的其他窗口,哪怕当前窗口是一个 Topmost 类型窗口,它也将被放到其他 Non-Topmost 类型窗口之下,也就是变为所有窗口中最底层的那⼀个。
HWND_NOTOPMOST——将该窗口放到所有 Non-Topmost 窗口之上、所有 Topmost 窗口之下。即该窗口将被放到 Non-Topmost 类型窗口集与 Topmost 窗口集之间的位置。
HWND_TOP——将该窗口放到同类型窗口中 Z-Order 顶层的位置。注意仅仅是同类型窗口集的最上层。
HWND_TOPMOST——将该窗口放到所有 Non-Topmost 窗口之上,即将该窗口变成一个 Topmost 类型的窗口,而且该窗口在 Topmost 窗口集中也处于最顶层,也就是说,将该窗口的位置放到屏幕Z坐标轴方向的最上层。这时如果再启动一个 Non-Topmost 类型的窗口(比如记事本程序),该窗口就不再是活动窗口,但它依然位于 Non-Topmost 窗口之上。读者可以回想⼀下前⾯举的应用程序A与记事本程序窗口前后排列的例子。
当 SetWindowPos 的第2个参数取值为 HWND_TOP 时,调用该函数与 BringWindowToTop 类似,都只能将窗口放到同类型窗口集中的最顶层。我们不能望文生义,以为 HWND_TOP 或BringWindowToTop 的意思就将窗口放到屏幕最前端。因为如果此时窗口类型是 Non-Topmost,而且存在 Topmost 类型的窗口,调用 BringWindowToTop 或将 SetWindowPos 的第2个参数设为HWND_TOP,其效果只是将该窗口放到了 Non-topmost 窗口集的最上层,但在该窗口之上还覆盖着 Topmost 窗口集。
如果我们希望将应用程序的窗口放到屏幕最前端,在其他所有窗口之上显示,可以调用SetWindowPos 函数并将其第2个参数取值为 HWND_TOPMOST,或者调用SetForegroundWindow 函数。SetForegroundWindow 函数有很多使用限制,详细信息可以查阅MSDN。
当 SetWindowPos 的第2个参数取值为 HWND_TOPMOST 时,调用该函数会将窗口放置于屏幕的最上层。但是并不能保证该窗口会⼀直保持在最上层。因为其他程序也可能会修改Z-order,比如再启动另⼀个程序,那个程序也用同样的方法(调用 SetWindowPos 并把第2个参数取值为HWND_TOPMOST)设置窗口,那么新启动的程序窗口将出现在最上层,即 Topmost 窗口集的最外层,而原来的程序窗口的层次会下调⼀级,即位于 Topmost 窗口集的次外层。
关于 Z-Order 的实现细节其实是有一个 ZOrderManager_Service 的 COM 接口微软没有公开的,也就是直接管理 ZOrder 的细节环节是被保护的。在不知道内部细节的情况下,我们只能使用 SetWindowPos 去间接调整窗口层次,并且有多种消息会将窗口层次改变,即使有时候设置了 SWP_NOZORDER。
我们有两种方法实现将 A 窗口插入到 B 窗口的前面或者后面一个槽位上,一种就是通过设置 HWND_BOTTOM 将窗口移动到 Z 序的底部,通过递归调用不停的将下方的窗口移动次序,实现窗口 Z 序的调整(或者设置 HWND_TOP 反向调整);另外一种就是利用窗口激活的时候 Z 序会移动到前面的方法来调整顺序。具体的细节之后会在新的文章中给出例子,关于桌面窗口的学习已经太久了,有些资料需要斟酌,不一定是正确的。
SetWindowPos(hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
// SWP_NOMOVE 设置后x,y参数无效,以原始设置为准,
// SWP_NOSIZE 设置后cx,cy参数无效,以原始设置为准
根据 IDA 分析 BingWindowToTop 函数就是指定了 NtUser32SetWindowPos 的第二个参数为 HWND_TOP 来实现的。
我们给出 BingWindowToTop 和 BringWindowToBottom 的定义:
// 移动窗口到同级窗口的 Z 序的顶部,直到下一个窗口被激活到顶部
BOOL WINAPI MyBringWindowToTop(HWND hWnd)
{
return SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
}
// 移动窗口到同级窗口的 Z 序的低部,直到下一个窗口被移动到低部
BOOL WINAPI MyBringWindowToBottom(HWND hWnd)
{
return SetWindowPos(hWnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
}
注意这里的同级窗口才能移动,如果不同级是会失败的。
当窗口层次发生变化的时候一般会首先收到 WM_WINDOWPOSCHANGING 消息,按照道理我们窗口需要在消息回调中处理该消息,并调用 BringWindowToXX 函数移动窗口,确保窗口层次不发生变化,这比 NOZORDER 标识更为有效。
但是我们也发现 W+D 或者显示桌面按钮按下时,窗口会先发生变化(具体机制还未研究),然后再收到 WM_WINDOWPOSCHANGING 消息,所以只处理该消息没有用,全局(系统的)窗口层次的调整不是在窗口回调中完成的,而是线程级别的,我们需要独立一个线程去检查窗口层次,并恢复窗口层次,比如:
while (true) // 不要用死循环
{
// 判断我们的窗口是否在 SHELLDLL_DefView 窗口的下方,
// 如果不是则调整窗口层次。
if (GetWindow(hNotepad, GW_HWNDNEXT) == hDefView)
{
ShowWindow(hNotepad, SW_SHOWMAXIMIZED); // 保证最大化显示
MyBringWindowToBottom(hNotepad);
}
Sleep(0);// 改成 WaitForSingleObject 等待窗口句柄
}
这一篇用于记录学习过程中发现的一些东西,没怎么排版。
更新于:2023.10.19