目录
文章出处来源:[https://blog.csdn.net/qq_59075481/article/details/133801491]。
前言
这是实现 D2WT (Dynamic Desktop Wallpaper Tools) 系列的第二节,在本节中,我们进一步讨论 WorkerW 窗口的功能,介绍桌面窗口创建的流程,同时讨论为什么在 Vista 上无法嵌入窗口。
【提示】本文涉及的关于窗口的处理部分基于我曾经发的《桌面自定义 WorkerW 窗口》一文。里面的思路有类似的地方,但比那边讲的大概更加透彻。
需要查看第一节的可以点击这里:实现桌面动态壁纸(一)
相关系列文章:
序号 | 文章标题(链接) | AID |
1 | 实现桌面动态壁纸(一) | 125361650 |
2 | 实现桌面动态壁纸(二)[本文] | |
3 | 实现桌面动态壁纸(三)[未来发布] | --- |
4 | 实现桌面动态壁纸——认识 WebView2 控件 | 138637909 |
一、关于 WorkerW 工作区窗口
WorkerW 是 Windows 操作系统中的一个窗口站 (Window Station) 和桌面 (Desktop) 的组合。它是用于用户界面的一个基础组件,用于管理和控制用户界面。WorkerW 从操作系统内核中获取资源,包括 CPU 资源和内存资源,并将其分配给用户进程,以便它们能够在屏幕上显示图形和交互元素。WorkerW 通过窗口管理器将窗口和界面元素显示在屏幕上,同时允许用户与它们进行交互。(以上这段来源于网络)
WorkerW/A 属于工作区窗口,它基本上通过调用 Shell API 函数中的 SHCreateWorkerWindowW/A 创建。其中 W 代表 WideChar (UNICODE) 版本的窗口,而 SHCreateWorkerWindowA 是该函数的 ASCII 版本。任何需要侦听窗口消息的应用程序都会调用此 API 来创建工作区窗口。 SHCreateWorkerWindowW 是为文档化的导出函数,通过分析 explorer.exe 发现该函数是从 api-ms-win-shlwapi-winrt-storage-l1-1-1.dll 中导入的,但是看到这个名称可能会很陌生。
在 explorer.exe 的导入表上,SHCreateWorkerWindowW 函数是通过解析名为 api-ms-win-shlwapi-winrt-storage-l1-1-1 的 API 集而重定向到 shlwapi.dll,所以,最终是需要分析 shlwapi.dll 里面的函数。
(API 集:微软推出的用高度命名的链接库名称分类 API 的最小唯一核心库,将 API 调用通过内置加载器转发到真实的 Dll 上,截止 Win11 已经更新到 V10 版本)
根据 ReactOS 的开发者文档可以知道 SHCreateWorkerWindow 的定义和内部实现。
HWND WINAPI SHCreateWorkerWindow(
WNDPROC wndProc,
HWND hWndParent,
DWORD dwExStyle,
DWORD dwStyle,
HMENU hMenu,
LONG_PTR wnd_extra
)
SHCreateWorkerWindowA/W 其实就是 CreateWindowExA/W 的封装:
HWND WINAPI SHCreateWorkerWindowA(
WNDPROC wndProc,
HWND hWndParent,
DWORD dwExStyle,
DWORD dwStyle,
HMENU hMenu,
LONG_PTR wnd_extra
)
{
static const char szClass[] = "WorkerA";
WNDCLASSA wc;
HWND hWnd;
TRACE("(%p, %p, 0x%08x, 0x%08x, %p, 0x%08lx)\n",
wndProc, hWndParent, dwExStyle, dwStyle, hMenu, wnd_extra);
/* Create Window class */
wc.style = 0;
wc.lpfnWndProc = DefWindowProcA;
wc.cbClsExtra = 0;
wc.cbWndExtra = sizeof(LONG_PTR);
wc.hInstance = shlwapi_hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursorA(NULL, (LPSTR)IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szClass;
SHRegisterClassA(&wc);
hWnd = CreateWindowExA(dwExStyle, szClass, 0, dwStyle, 0, 0, 0, 0,
hWndParent, hMenu, shlwapi_hInstance, 0);
if (hWnd)
{
SetWindowLongPtrA(hWnd, 0, wnd_extra);
if (wndProc) SetWindowLongPtrA(hWnd, GWLP_WNDPROC, (LONG_PTR)wndProc);
}
return hWnd;
}
HWND WINAPI SHCreateWorkerWindowW(
WNDPROC wndProc,
HWND hWndParent,
DWORD dwExStyle,
DWORD dwStyle,
HMENU hMenu,
LONG_PTR wnd_extra
)
{
static const WCHAR szClass[] = { 'W', 'o', 'r', 'k', 'e', 'r', 'W', 0 };
WNDCLASSW wc;
HWND hWnd;
TRACE("(%p, %p, 0x%08x, 0x%08x, %p, 0x%08lx)\n",
wndProc, hWndParent, dwExStyle, dwStyle, hMenu, wnd_extra);
/* If our OS is natively ANSI, use the ANSI version */
if (GetVersion() & 0x80000000) /* not NT */
{
TRACE("fallback to ANSI, ver 0x%08x\n", GetVersion());
return SHCreateWorkerWindowA(wndProc, hWndParent, dwExStyle, dwStyle, hMenu, wnd_extra);
}
/* Create Window class */
wc.style = 0;
wc.lpfnWndProc = DefWindowProcW;
wc.cbClsExtra = 0;
wc.cbWndExtra = sizeof(LONG_PTR);
wc.hInstance = shlwapi_hInstance;
wc.hIcon = NULL;
wc.hCursor = LoadCursorW(NULL, (LPWSTR)IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szClass;
SHRegisterClassW(&wc);
hWnd = CreateWindowExW(dwExStyle, szClass, 0, dwStyle, 0, 0, 0, 0,
hWndParent, hMenu, shlwapi_hInstance, 0);
if (hWnd)
{
SetWindowLongPtrW(hWnd, 0, wnd_extra);
if (wndProc) SetWindowLongPtrW(hWnd, GWLP_WNDPROC, (LONG_PTR)wndProc);
}
return hWnd;
}
在 DWM 机制完善之前的操作系统上,切换桌面壁纸或者系统主题的时候,窗口的绘制会出现卡顿、频闪现象。在切换主题的时候,微软通过 LockWindowUpdate 函数,阻止其他窗口的绘制,并显示一个“请稍后”窗口,来避免用户看到卡顿的桌面管理层窗口。但是,这给用户的体验并不是特别好,因为需要“等待”。随后,在 DWM 组件的支持下,切换壁纸前,首先将 DefView 窗口分离出来,然后利用 WorkerW 窗口去绘制 DefView 的背景,在内存中首先生成双缓冲,将新壁纸和旧壁纸的图案之间合成交叉溶解的图像动画,从而实现窗口背景的平滑处理。
下图展示了在切换主题的交叉阶段,桌面管理层窗口的变化(新旧壁纸的交叉溶解效果):
我们意识到,SHCreateWorkerWindow 只能创建类名是 WorkerW 的窗口,关键部分并不在于这个函数,想要知道系统是如何实现透明层次的,还需要研究其窗口过程以及后续的处理,我想这需要对桌面窗口有一个深入一点的理解。
二、关于窗口关系
2.1 窗口以及窗口隶属关系
(TODO:之后补充)
2.2 桌面管理层窗口组分简析
我们知道,桌面管理层窗口在未产生 WorkerW 分层时,窗口的层次应该如下所示:
我么可以通过简单的手法理解这些窗口的作用:
(1) SysHeader32 窗口
SysHeader32 窗口是一个不可见窗口,这个窗口主要负责在 ListView 上绘制每个图标的文本。
验证方法:通过 SendMessageW(hSysHead, WM_CLOSE, 0, 0) 即可关闭该窗口,按 F5 刷新桌面,可以观察到图标的文本已经消失,但是图标依然可以正常点击:
并且右键菜单依然是有效的:
(2) SysListView32 窗口
SysListView32 窗口主要负责控制图标列表的显示和操作,关闭或者隐藏后,图标列表将不可见。
验证方法:隐藏窗口 ShowWindow(hListView, SW_HIDE) 可以发现图标立即消失。
但是,右键菜单依然可用,说明右键菜单不归它管理:
(3) SHELLDLL_DefView 窗口
这个窗口我们需要通过两步验证它的功能。
SHELLDLL_DefView 窗口控制图标列表窗口的背景绘制工作,这可以从 SysListView32 的属性页看出:
SHELLDLL_DefView 还控制右键菜单,使用 ShowWindow(hDefView, SW_HIDE) 后无法打开右键菜单。
验证是否支持背景绘制工作:
第一步:进一步隐藏 Program Manager 窗口,桌面管理层窗口的背景变成白色:
这说明了,Program 窗口的背景是系统设置的壁纸。
第二步:将 DefView 窗口变成弹出式窗口(独立化),并恢复显示。
会发现,无论 Program 窗口是否可见,图标窗口的背景都是黑色的:
于是我们可以判断出,SHELLDLL_DefView 可以通过获取父窗口(会判断是不是 Progman 窗口)的图像缓冲,来绘制子窗口的背景。
(4) Program Manager 窗口
Program Manager 窗口是桌面管理层的主窗口,Program Manager 窗口响应 WM_CLOSE 时(不响应 SC_CLOSE ),会调用 Shell32.dll 中的符号并显示一个询问是否需要关闭计算机的对话框:
Program 还负责显示桌面壁纸,隐藏或者关闭后背景将变为白色:
至此,我们从窗口的可视化角度简单分析了各个桌面管理层窗口的基本作用。
2.3 厘清两个概念的区别
在这个系列的一开始,我们就用“桌面管理层窗口”来称呼包含桌面图标在内的几个窗口的集合:
但是,我们在第一篇中,我们也提到过桌面窗口这个名字,桌面窗口和桌面管理层窗口有什么区别呢?
桌面窗口是其他窗口的祖先,在系统启动时创建,类名为 “#32769” 。这个窗口由 csrss.exe 进程创建,所有父窗口显示为 NULL 的窗口其实是以该窗口作为父窗口。所有窗口都在这个窗口内。所以它是 Z 序最高的窗口。
而桌面管理层窗口,则是 Z 序最低的窗口。桌面管理层窗口是以名为 Program Manager 窗口为主窗口,管理左面文件夹图标列表的显示、操作、桌面壁纸等功能的一系列窗口。
而这本质上不是一类窗口。此外,通过 GetDesktopWindow 函数获取的窗口句柄是桌面窗口句柄,而不是桌面管理层的窗口句柄。(关于他们的详细内容,在接下来的文章中我们会一一介绍)
2.4 关于设置父窗口
在 Windows Vista 上,SHELL_DefView 不支持背景透明化,我们想到可以利用扩展属性 WS_EX_LAYERED 实现背景透明,但是 MSDN 上明确说明该扩展属性从 Windows 8 开始,才对子窗口有效果。也就是说,在 Vista 上,对子窗口 SHELL_DefView 设置分层属性是无效的。
这时候,我们就需要将 SHELL_DefView 独立出来,将其变成弹出式窗口,就可以设置该属性了。
这里我么可以使用 SetParent 并指定父窗口为 NULL,随后去除窗口的 WS_CHILD 属性,添加 WS_POPUP | WS_EX_TOOLWINDOW 等属性,来实现将窗口独立化。
HWND SetParent(
_In_ HWND hWndChild,
_In_opt_ HWND hWndNewParent
);
[in] hWndChild
类型:HWND
子窗口的句柄。
[in, optional] hWndNewParent
类型:HWND
新父窗口的句柄。 如果此参数为 NULL,桌面窗口将成为新的父窗口。 如果此参数 HWND_MESSAGE,则子窗口将成为 仅消息窗口。
部分资料对这里的参数为 NULL 时,SetParent 的行为认知可能有误解,这里不是指桌面管理层窗口,他不是 Progman 窗口,而是由 csrss.exe 进程创建的类名为 “#32769” 窗口,他是一切桌面顶级窗口的父窗口(不是所有者窗口),称为桌面窗口,然而顶级窗口的父窗口常常被标记为 NULL。
“#32769” 窗口是一切桌面窗口的祖先窗口,是系统启动的时候创建的第一个窗口。Spy++ 下可以看到第一个窗口就是它:
查看窗口对应的进程信息:
显然,窗口由 CSRSS 创建。
接下来,我们用一个很简单的例子测试一下就可以理解正在发生的事情:
#include <iostream>
#include <Windows.h>
int main()
{
HWND h32769Wnd = NULL;
HWND hDesktopwnd = NULL;
HWND hNewParent = NULL;
HWND hNotepad = NULL;
HWND hOwner = NULL;
SetLastError(0);
h32769Wnd = FindWindowW(L"#32769", NULL);
printf("FindDesktopWnd:[ 0x%I64X ], find #32769. err_code:[%d]\n",
(unsigned long long)h32769Wnd, GetLastError());
hNotepad = FindWindowA("Notepad", NULL);
if (hNotepad)
{
hNewParent = GetAncestor(hNotepad, GA_PARENT);
GetWindow(hNotepad,GW_OWNER);
printf("Notepad:[ 0x%I64X ]; GetAncestorParent:[ 0x%I64X ]; GetOwner:[ 0x%I64X ].\n",
(unsigned long long)hNotepad,
(unsigned long long)hNewParent,
(unsigned long long)hOwner);
hDesktopwnd = GetDesktopWindow();
printf("Desktopwnd:[ 0x%I64X ], use GetDesktopWindow.\n",
(unsigned long long)hDesktopwnd);
if (hDesktopwnd)
{
printf("SetParent use hDesktopwnd.\n");
hNewParent = SetParent(hNotepad, hDesktopwnd);
printf("LastParent:[ 0x%I64X ], retn by SetParent.\n",
(unsigned long long)hNewParent);
}
hNewParent = GetAncestor(hNotepad, GA_PARENT);
printf("Notepad:[ 0x%I64X ]; GetAncestorParent:[ 0x%I64X ]; GetOwner:[ 0x%I64X ].\n",
(unsigned long long)hNotepad,
(unsigned long long)hNewParent,
(unsigned long long)hOwner);
// --------------------------------------
printf("\n\nSetParent use (null) ptr.\n");
hNewParent = SetParent(hNotepad, NULL);
printf("LastParent:[ 0x%I64X ], retn by SetParent.\n",
(unsigned long long)hNewParent);
hNewParent = GetAncestor(hNotepad, GA_PARENT);
printf("Notepad:[ 0x%I64X ]; GetAncestorParent:[ 0x%I64X ]; GetOwner:[ 0x%I64X ].\n",
(unsigned long long)hNotepad,
(unsigned long long)hNewParent,
(unsigned long long)hOwner);
}
system("pause");
return 0;
}
我们首先尝试使用 FindWindow 查找类名,但是以失败告终,我们获得了无效句柄,这可能和FindWindow 的机制有关(没搞清楚原因,只知道他是 NtUserFindWindowEx 的封装。据我推断,它只从第一个顶级窗口开始检索,而且没有找到 GetLastError 并不能取到非零值)。
随后我们调用 SetParent 尝试设置 Notepad 的父窗口,这里我们进行了横向对比,第一次,我们使用 GetDesktopWindow 函数获取桌面窗口句柄,并把它作为第二参数传入 SetParent,通过分析父窗口和返回值,我们得到和 Spy++ 相同的结论(句柄指向 #32769 窗口);
第二次,我们按照 MSDN 上的说明,把第二个参数设置为 NULL,并再次获取信息,发现效果等同于传入 #32769 的有效句柄,这说明 SetParent 确实会在内部将 NULL 参数解释为桌面窗口( #32769 )的句柄。
下图展示了对 Notepad 窗口进行设置父窗口的操作前后,其父窗口的变化:
(关于 SetParent 的注意事项,在我之前的一篇博客中有详细分析,就不展开讨论了)
SetParent 的 NULL 传参其实有两个作用:
(1)设置窗口成为桌面顶级窗口;
(2)将窗口提升 Z 序至前端(替代 SetForegroundWindow ),甚至解决了 SetForegroundWindow 有时候失败的问题。
关于第二个相当于副产品,解决 SetForegroundWindow 失败网上给的代码一般是这样子的:
if(hWnd)
{
HWND hForeWnd = GetForegroundWindow();
DWORD dwForeID = GetWindowThreadProcessId(hForeWnd,NULL);
DWORD dwCurID = GetCurrentThreadId();
AttachThreadInput(dwCurID,dwForeID,TRUE);
ShowWindow(hWnd,SW_SHOWNORMAL);
SetWindowPos(hWnd,HWND_TOPMOST,0,0,0,0,SWP_NOSIZE|SWP_NOMOVE);
SetWindowPos(hWnd,HWND_NOTOPMOST,0,0,0,0, SWP_NOSIZE|SWP_NOMOVE);
SetForegroundWindow(hWnd);
AttachThreadInput(dwCurID,dwForeID,FALSE);
// hWnd 就是需要置前的窗口句柄
}
而我们只需要判断这个窗口是不是 POPUP 窗口,并 SetParent 传参 NULL 即可。
三、编写代码以供在 Vista 上实现
在 Vista 上,DWM 被首次引入操作系统,但是它的框架结构和现在的有很大的不同,比如它不能够响应 0x052C (WM_USER + 300)的消息,而创建 WorkerW 窗口。这就是为什么在第一篇章中,我们直言在 Vista 上即使有开启 DWM 也不能够通过窗口嵌入的方式实现动态壁纸。
那么,如果我们固执的想要在早期的系统环境下实现动态壁纸,我们该如何做呢?
我在之前研究过自己实现一个 WorkerW,那篇博客限于一些原因,一些实现细节没能公布。这里我们可以说,即使不使用 WorkerW 依然可以实现动态壁纸。
我们想到将壁纸主窗口设置为 Progman 的子窗口,但是 SetParent 函数有个坏毛病,它会自动“擦屁股”,自动调用 CZOrderManagerService 内部函数将我们的窗口 Z 序放在 SHELLDLL_DefView 的前面,这是一个非常糟糕的。因为我们的窗口将完全遮盖 SHELLDLL_DefView 窗口,这使得我们无法看到图标列表窗口,我们的窗口始终位于上方。怎么办呢?
别急,这里有几种方法解决问题:
3.1 方法二:子类化并自绘窗口背景
(TODO:后期补充)
四、初步分析桌面管理层窗口创建的原理
由于对桌面管理层窗口的逆向分析没有找到实质性的材料,而作者本人又是初学一些反汇编知识,如有分析错误的地方,还望指拨。
4.1 桌面管理层窗口的创建流程
首先,我们需要回顾一下桌面管理层窗口的组成:
桌面浏览器窗口( DesktopBrowser )主要包括 Progman 父窗口,和 DefView 窗口,DefView 窗口的子窗口 SysListView32 用于绘制桌面图标等相关组件。而 Progman 的背景则绘制为桌面壁纸。
打开 IDA Pro 并反汇编 explorer.exe 可以定位到入口函数 wWinMain,可以看到 wWinMain 调用了 CreateDesktopAndTray 函数,这个函数是对 SHCreateDesktop 的封装,用于创建桌面和 CTray 的相关成员。
从 F5 的信息可以看出函数调用了延迟加载的 Shell32.dll 中的 SHCreateDesktop 函数。
跟进 Shell32.dll 查看该函数的内部实现:
有三个函数调用是关键性的:
(1)CDesktopBrowser::CDesktopBrowser 初始化 DesktopBrowser,CDesktopBrowser 内部类实现了很多函数,包括图标窗口、任务栏控件、虚拟多桌面等等;
(2)RegisterDesktopClass 是对 RegisterClassW 的封装;
(3)SHFusionCreateWindowEx 是对 CreateWindowExW 的封装。
首先看 SHFusionCreateWindowEx 函数,前面谈到初始化 DesktopBrowser 的过程似乎在主窗口创建之前,然而分析上下文却能发现这两个实际上是并行操作。在 SHFusionCreateWindowEx 内首先激活并发上下文,然后尝试创建主窗口,同时初始化 DesktopBrowser 最后结束并发上下文,并返回窗口句柄。
然后,我们看一下 RegisterDesktopClass 函数,这个就比较简单了:
最重要的是 CDesktopBrowser 这个类,里面包含了有关桌面管理层窗口的很多未导出的内部函数。
RegisterDesktopClass 函数中调用的 CDesktopBrowser::s_DesktopWndProc 回调实现对 SysListView32 窗口的创建和处理。
调用树如下图所示:
SysListView32 窗口的创建和处理在 CreateDesktopView 中完成,流程比较复杂,暂不分析。
然后,继续跟踪,找到了 CDefView 类,一个关键的成员函数为 CDefView::CreateViewWindow,
他是对 CDefView::CreateViewWindow2 的封装,CDefView::CreateViewWindow2 进行了一些对参数的初始化处理,随后把工作交给了 CDefView::CreateViewWindow3,在 CDefView::CreateViewWindow3 里面最终实现了创建 SHELLDLL_DefView 窗口。
4.2 从管理层窗口回调看 0x052C 消息
【这部分将在之后完善】
总结
自此,我们的桌面管理层窗口的创建已经基本完成,以上分析只是简单梳理一下流程,其中大量调用通过 COM 类接口实现,这里暂不展开分析。
本文属于原创文章,转载请注明出处:
https://blog.csdn.net/qq_59075481/article/details/133801491
文章更新于:2023.10.20,2024.07.04。
文章发布于:2024.07.04。