本文所采用的方法属于本人试验方案(目前没有在 Win7 及 XP 尝试过,但在 NT 的较高版本Win10 在不发送 052C 消息,即不使用系统生成的 WorkerW 分层窗口实践成功。Win 8 开始 DWM 无法关闭,虽然 Win11 可以关闭 Aero 和动画,但我是在 Win10 上尝试的,和不发送 052C消息的效果是一样的),目的是在 DWM(桌面窗口管理器)的 Aero 配色方案被关闭的情况下脱离 DWM 实现分层窗口的部分功能,以便于能够在不支持透明配色方案的系统上实现在桌面图标列表窗口下面嵌入任意窗口,该项目可能应用于桌面壁纸等Web前端。
2023.10.19:WorkerW 窗口的创建是有导出函数的 SHCreateWorkerWindowW/A 但是里面的窗口回调以及细节微软并未公开,我准备有空逆向分析细节,用于最大程度还原现场。此外,发现在 Windows Vista SP1 上设置 WS_EX_LAYERED 属性会失败,暂时还未找到原因。Win10 1903 发送 0x052C 消息无效,0x052C 消息发送时候资源管理器的窗口回调里面会对桌面壁纸窗口的切换做淡入淡出的平滑移动动画,早期系统上回调里面并没有这个消息的处理。
首先,回顾一下Aero开启时桌面分层窗口的层次,打开任务视图,然后退出视图界面:
用Spy++ 刷新 看一下窗口层次:
做过桌面Web小程序的开发者都知道Program是桌面窗口(总窗口),Program被隐藏或关闭时候,它应该是这样(我同时将图标列表也隐藏了,实际上可以看见图标):
而在分层窗口没有诞生时候,桌面窗口层次应该这样:
我们知道这种窗口是无法嵌入窗口的,动态壁纸只能用刷HDC缓存的方法不停刷到桌面。
然而真的就无法嵌入一个可见的窗口了吗?
我的想法是,自己创建分层WorkerWs,几经尝试,发现可行。
以下给一个思路:
首先创建一个空的桌面窗口程序,以VS2019为例:
创建新项目→选择Windows桌面应用程序→把程序原有的代码全部删除,就可以写了。
以下是创建一个空白窗口(无任何增加功能,不适用于WorkerW)的示例代码:
#include "windows.h"
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //窗口函数说明
//------------------------------以下是入口函数的代码------------------------------
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wcex;
HWND hWnd;
MSG msg;
TCHAR szWindowClass[] = L"窗口示例"; //窗口类名
TCHAR szTitle[] = L"My Windows"; //窗口标题名
//------------------------------以下初始化窗口类------------------------------
wcex.cbSize = sizeof(WNDCLASSEX); //窗口类的大小
wcex.style = 0; //窗口类型为默认类型
wcex.lpfnWndProc = WndProc; //窗口处理函数为WndProc
wcex.cbClsExtra = 0; //窗口类无扩展
wcex.cbWndExtra = 0; //窗口实例无扩展
wcex.hInstance = hInstance; //当前实例句柄
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
//窗口的图标为默认图标
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
//窗口采用箭头光标
wcex.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //窗口背景为白色
wcex.lpszMenuName = NULL; //窗口中无菜单
wcex.lpszClassName = szWindowClass; //窗口类名为“窗口示例”
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
//窗口的小图标为默认图标
//------------------------------以下进行窗口类的注册------------------------------
if (!RegisterClassEx(&wcex)) //如果注册失败则发出警告
{
MessageBox(NULL, L"窗口类注册失败!", L"窗口注册", NULL);
return 1;
}
//------------------------------以下创建窗口------------------------------
hWnd = CreateWindow(
szWindowClass, //窗口类名
szTitle, //窗口实例的标题名
WS_OVERLAPPEDWINDOW, //窗口的风格
CW_USEDEFAULT, CW_USEDEFAULT, //窗口左下角坐标为默认值
CW_USEDEFAULT, CW_USEDEFAULT, //窗口的高和宽为默认值
NULL, //此窗口无父窗口
NULL, //此窗口无主菜单
hInstance, //创建此窗口应用程序的当前句柄
NULL //不使用该值
);
if (!hWnd) //如果创建窗口失败则发出警告
{
MessageBox(NULL, L"创建窗口失败!", L"创建窗口", NULL);
return 1;
}
ShowWindow(hWnd, nCmdShow); //显示窗口
UpdateWindow(hWnd); //绘制用户区
while (GetMessage(&msg, NULL, 0, 0)) //消息循环
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return(int)msg.wParam; //程序终止时将信息返回系统
}
//------------------------------以下是窗口函数的代码------------------------------
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0); //调用PostQuitMessage发出WM_QUIT消息
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
//默认时采用系统消息默认处理函数
break;
}
return 0;
}
生成窗口如下图所示:
值得注意的是我们的自定义WorkerW窗口必须满足以下条件:
1. 窗口可视(可以自己设置透明度)
2. 窗口类、风格与系统生成的要大致相同。
3. 窗口类名、窗口标题与系统生成窗口相同。
4. 窗口背景画刷与系统设定一致
5. 能处理相应的消息
两个WorkerW都是无标题类名为“WorkerW”的窗口,由于系统有很多WorkerW窗口(DWM开启时才有),为了使我们找自定义的窗口方便,第一个WorkerW的标题我们设置为“ ”(一个单空格),第二个WorkerW 我们设置为“ ”(两个单空格)。Spy++可以看到这几个系统窗口的信息,请把它们一一记录下来,这里有我已经整理好的:桌面窗口层次(Z-Order 记录文档)。
好了,现在我们已经知道开如何创建WorkerW窗口了,以WorkerW 1为例:
我们的操作是:创建WorerW 1窗口》找到SHELLDLL_DefView和SysListView32窗口》同步窗口风格否则SetParent会出现意料之外的结果(这个MSDN未讲,属个人方案,对Win10桌面要这样,如果你的DWM可能仍然处于未知的正常的话)》SetParent重置父窗口。
代码如下:
#include "windows.h"
HWND workerw1 = nullptr; // SHELLDLL_DefView窗口句柄
HWND workerw2 = nullptr; // 第二个WorkerW窗口句柄
HWND workerw3 = nullptr; // FolderView
HWND defview = nullptr;
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); //窗口函数说明
//------------------------------以下是窗口函数的代码------------------------------
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0); //调用PostQuitMessage发出WM_QUIT消息
break;
default:
HWND windowHandle = FindWindow(L"Progman", NULL);
HWND defview1 = FindWindowEx(windowHandle, 0, L"SHELLDLL_DefView", NULL);
//PostMessageA(windowHandle,message,wParam,lParam);
PostMessageA(defview1, message, wParam, lParam);
Sleep(100);//Sleep 可无,时间不可超过100,否则阻塞
return DefWindowProc(hWnd, message, wParam, lParam);
//默认时采用系统消息默认处理函数
break;
}
return 0;
}
BOOL CALLBACK EnumWindowsProc(HWND handle, LPARAM lparam)
{
// 获取Progman窗口
HWND windowHandle = FindWindow(L"Progman", NULL);
defview = FindWindowEx(windowHandle, 0, L"SHELLDLL_DefView", NULL);
if (defview != nullptr) // 找到SHELLDLL_DefView窗口
{
workerw1 = defview;
//获取第二个WorkerW窗口的窗口句柄
workerw2 = FindWindowEx(0, handle, L"WorkerW", 0);
workerw3 = FindWindowEx(defview, nullptr, L"SysListView32", L"FolderView");
}
return true;
}
//------------------------------以下是入口函数的代码------------------------------
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASSEX wcex;
HWND hWnd, hwnd;
MSG msg;
TCHAR szWindowClass[] = L"WorkerW"; //窗口类名
TCHAR szTitle[] = L" "; //窗口标题名
//------------------------------以下初始化窗口类------------------------------
wcex.cbSize = sizeof(WNDCLASSEX); //窗口类的大小
wcex.style = 0; //窗口类型为默认类型
wcex.lpfnWndProc = WndProc; //窗口处理函数为WndProc
wcex.cbClsExtra = 0; //窗口类无扩展
wcex.cbWndExtra = 0; //窗口实例无扩展
wcex.hInstance = hInstance; //当前实例句柄
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
//窗口的图标为默认图标
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
//窗口采用箭头光标
wcex.hbrBackground = (HBRUSH)(COLOR_3DFACE); // 窗口背景3dface
wcex.lpszMenuName = NULL; //窗口中无菜单
wcex.lpszClassName = szWindowClass; //窗口类名为“WorkerW”
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
//窗口的小图标为默认图标
//------------------------------以下进行窗口类的注册------------------------------
if (!RegisterClassEx(&wcex)) //如果注册失败则发出警告
{
MessageBox(NULL, L"窗口类注册失败!", L"窗口注册", NULL);
return 1;
}
//------------------------------以下创建窗口------------------------------
//RECT rect;
int Width = GetSystemMetrics(SM_CXSCREEN);
int Height = GetSystemMetrics(SM_CYSCREEN);
hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, // 窗口扩展风格,任务栏不显示图标
szWindowClass, //窗口类名
szTitle, //窗口实例的标题名
WS_POPUP, //窗口的风格,建议POPUP弹出窗口风格
CW_USEDEFAULT, CW_USEDEFAULT, //窗口左下角坐标为默认值
Width, Height, //窗口的高和宽为默认值
NULL, //此窗口无父窗口
NULL, //此窗口无主菜单
hInstance, //创建此窗口应用程序的当前句柄
NULL //不使用该值
);
if (!hWnd) //如果创建窗口失败则发出警告
{
MessageBox(NULL, L"创建窗口失败!", L"创建窗口", NULL);
return 1;
}
ShowWindow(hWnd, nCmdShow); //显示窗口
//枚举窗口
EnumWindows(EnumWindowsProc, (LPARAM)NULL);
hwnd = GetDesktopWindow();// 获取桌面
//同步窗口的风格,否则SetParent()将出现意料之外的结果。
SetWindowLongA(hWnd, GWL_EXSTYLE,
GetWindowLongA(hWnd, GWL_EXSTYLE));
SetWindowPos(hWnd, HWND_BOTTOM, 0, 0, Width, Height, SWP_FRAMECHANGED);// 立即生效
UpdateWindow(hWnd);// 立即更新窗口
SetParent(workerw3, workerw3);
SetWindowLongA(workerw3, GWL_EXSTYLE,
GetWindowLongA(workerw3, GWL_EXSTYLE) &~WS_CHILD|~WS_CHILDWINDOW);
SetParent(defview, hWnd);
UpdateWindow(defview);// 立即更新窗口
UpdateWindow(workerw3);// 立即更新窗口
while (GetMessage(&msg, NULL, 0, 0)) //消息循环
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return(int)msg.wParam; //程序终止时将信息返回系统
}
运行效果如图(由于Z-Order不正确,任务栏未显示):
刷新并使用Spy++的搜索功能,标题:单空格:类名WorkerW,注意:检查输入是否正确!
展开窗口层次:
至此WorkerW 1大体完成,但我们发现两个问题:
1. 任务栏未显示,原因是Z-Order错误
2. 鼠标移动到图标上无选中时的Highlight效果,原因可能是窗口CS类设置不正确,但主要在于没处理WM_MOUSEMOVE或者WM_SETCURSOR消息。但不影响双击打开功能,后面会将解决方案。
关于Z-Order的设置,你必须设置成:
关于 Z-Order 的实现细节其实是有一个 ZOrderManager_Service 的 COM 接口微软没有公开的,也就是直接管理 ZOrder 的细节环节是被保护的。在不知道内部细节的情况下,我们只能使用 SetWindowPos 去间接调整窗口层次,并且有多种消息会将窗口层次改变,即使有时候设置了 SWP_NOZORDER。
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
);
关键参数:
hWndInsertAfter
类型:HWND
窗口的句柄,用于在 Z 顺序中定位的窗口之前。 此参数必须是窗口句柄或以下值之一。
Value 含义 HWND_BOTTOM
(HWND) 1
将窗口置于 Z 顺序的底部。 如果 hWnd 参数标识最顶层的窗口,该窗口将失去其最顶层的状态,并放置在所有其他窗口的底部。 HWND_NOTOPMOST
(HWND) -2
将窗口置于所有非最顶部窗口 (即) 所有最顶部窗口的后面。 如果窗口已经是非最顶部窗口,则此标志不起作用。 HWND_TOP
(HWND) 0
将窗口置于 Z 顺序的顶部。 HWND_TOPMOST
(HWND) -1
将窗口置于所有非最顶部窗口的上面。 该窗口即使已停用,也会保留在最高位置。
我们有两种方法实现将 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 等待窗口句柄
}
具体的细节会单独列出一篇分析,主要是还有一部分细节没有理解完整。
我需要构建一个完整的窗口关系图,并重写窗口层次调整的一些接口以实现更多需要的功能:
如有错误,欢迎指正,谢谢!
关注我的博客获取最新消息:博客