• 周五. 10 月 23rd, 2026

物嫩软件资讯网

软件资讯来物嫩

实现桌面动态壁纸(二)

由admin@wunen

4 月 1, 2025


目录


前言


一、关于 WorkerW 工作区窗口


二、关于窗口关系


2.1 窗口以及窗口隶属关系


2.2 桌面管理层窗口组分简析


2.3 厘清两个概念的区别


2.4 关于设置父窗口


三、编写代码以供在 Vista 上实现


3.1 方法二:子类化并自绘窗口背景


四、初步分析桌面管理层窗口创建的原理


4.1 桌面管理层窗口的创建流程


4.2 从管理层窗口回调看 0x052C 消息


总结


文章出处来源:[​

https://blog.csdn.net/qq_59075481/article/details/133801491

​]。

前言

这是实现

D2WT (Dynamic Desktop Wallpaper Tools)

系列的第二节,在本节中,我们进一步讨论

WorkerW

窗口的功能,介绍桌面窗口创建的流程,同时讨论为什么在

Vista

上无法嵌入窗口。


【提示】本文涉及的关于窗口的处理部分基于我曾经发的《桌面自定义 WorkerW 窗口》一文。里面的思路有类似的地方,但比那边讲的大概更加透彻。

需要查看第一节的可以点击这里:

实现桌面动态壁纸(一)

相关系列文章:



序号



文章标题(链接)



AID


1

实现桌面动态壁纸(一)

125361650

2

实现桌面动态壁纸(二)[本文]


133801491


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。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注