目录
文章出处来源:[
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。
