实现桌面动态壁纸(一)

目录

一、前言

二、有哪些基础需要我掌握的?

1. Windows Aero 与桌面窗口管理器(DWM)

1.1 Windows Aero 配色方案

1.2 桌面窗口管理器(DWM)

1.3 DWM 服务(仅限 Win7)

1.4 通过接口启用/禁用 DWM 窗口合成

1.5 DWM 系统关键进程

2. 通过 Spy++ 工具研究桌面窗口层次

2.1 桌面窗口层次

2.2 如何形成 WorkerW 分层窗口

三、实战&编写代码

1. 如何为嵌入桌面管理层窗口做好准备?

1.1. 确保已经启动 DWM 窗口合成

1.2. 怎样启用工作区窗口(WorkerW)?

1.3 为什么说高版本必须以 WorkerW 2 为父窗口呢?

1.4 如何获取这些窗口的句柄?

2. 更改父窗口实现嵌入壁纸窗口

2.1 问题一解决方案(更新父窗口问题)

2.2 问题二解决方案(虚拟桌面和扩展屏分辨率问题)

2.3 如何获取并判断系统版本(未来可能更改)

2.4 解决退出程序时窗口残影问题

2.5 上文注意事项的一些补充

3 关于 0x052C 私有消息的兼容性问题(重要)

3.1 0x052C 处理机制的潜在兼容性问题

3.2 对已知兼容性问题的解决方案

4. 完整代码以及测试截图

4.1 代码

4.2 运行结果展示

5. 关于后续工作

四、处理向后兼容性问题(6 月 25 日更新)

4.1 问题提出

4.2 窗口显示解决方案(3 月 24 日)

4.3 最新的窗口显示解决方案(6 月 9 日)

4.4 窗口显示解决方案(6 月 11 日 | 实验性)

五、后记


本文链接:[https://blog.csdn.net/qq_59075481/article/details/125361650]。

一、前言

本文章以在 Windows 桌面管理层窗口(桌面图标后面)嵌入第三方窗口为主题,主要针对动态壁纸实现原理进行讲解。

提醒:作者将对使用本文方法(也是网上流传很广的方法)遇到的一些问题进行仔细研究分析,文章将不定期更新,目前遇到的一些问题在 3.3(3.1, 3.2) 小节和 3.4(4.1) 小节均有描述。(2024.06.04)

!!!!本文有好多地方需要修订,未来可能重新写这篇(06.10)!!!

Wallpaper Engine 是由 Kristjan Skutta 所开发的一款动态壁纸软件,区别于其他形式的壁纸软件,Wallpaper Engine 可以让用户通过其引擎深度的自定义或编辑来创作出符合个人需求的壁纸样式。如果你惊艳于 WallpaperEngine 的效果,并且自己也想制作个性化的桌面美化程序,那么请跟我一道学习相关理论和技术实现。

系列文章:

序号文章标题(链接)AID
1实现桌面动态壁纸(一)125361650
2实现桌面动态壁纸(二)133801491
3实现桌面动态壁纸(三)[未来发布]----
4实现桌面动态壁纸——认识 WebView2 控件138637909

二、有哪些基础需要我掌握的?

1. Windows Aero 与桌面窗口管理器(DWM)

1.1 Windows Aero 配色方案

Windows Aero 是从 Windows Vista 开始使用的新型用户界面,透明玻璃感让用户一眼贯穿。“Aero”为四个英文单字的首字母缩略字:Authentic(真实)、Energetic(动感)、Reflective(反射)及 Open(开阔)。意为 Aero 界面是具立体感、令人震撼、具透视感和阔大的用户界面。除了透明的接口外,Windows Aero 也包含了实时缩略图、实时动画等窗口特效,吸引用户的目光。

Windows Aero 是 Windows Vista 开始使用的新元素,包含重新设计 Windows Explorer 样式、Windows Aero 玻璃样式、Windows Flip 3D 窗口切换、以及实时缩略图还有新的字体。

Windows 7 所使用的 Windows Aero 有许多功能上的调整,以及新的触控接口和新的视觉效果及特效:

  • Aero Peek鼠标指针指向任务栏上图标,便会跳出该程序的缩略图预览,指向缩略图时还可看到该程序的全屏预览。此外,鼠标指向任务栏最右端的小按钮可看到桌面的预览。

  • Aero Shake点击某一窗口后,摇一下鼠标,可让其他打开中的窗口缩到最小,再晃动一次便可恢撤消貌。

  • Aero Snap点击窗口后并拖曳至桌面的左右边框,窗口变会填满该侧桌面的半部。拖曳至桌面上缘,窗口变会放到最大。此外,点击窗口的边框并拖曳至桌面上缘或下缘会使得窗口垂直放到最大,但宽度不变,逆向操作后窗口则会撤消回原貌。

  • 触控接口为了方便利用触控技术操作,些微放大了标题栏及任务栏的按钮。

  • 放到最大的窗口仍旧保持透明的边框,而以往 Windows Vista 中,窗口放到最大后,会以该主题的颜色(窗口颜色)填满边框,有相当大的不同。

  • 当鼠标滑过任务栏上的图标时,图标背景会浮现该图标最显著的 RGB 色彩,此外,鼠标的指针处会有更亮的颜色跟着指针移动。

  • 当移动窗口时,Aero 特效的窗口更新率会降低,减低 CPU 和 GPU 的负荷, 让程序能稳定的运作。

  • 用户可选择打开或关闭窗口边框阴影效果。

注:Windows7 Home Basic、Windows Vista Home Basic 隐藏了 Aero 特效,但可以通过修改注册表强行开启。Windows 8 正式版中取消了对系统窗口应用的 Aero Glass 特效。

1.2 桌面窗口管理器(DWM)

​桌面窗口管理器(Desktop Window Manager, DWM)是 Windows Vista 及更高版本的 Windows 桌面操作系统中的一个重要组件。

DWM 的桌面合成建立在 Composition 引擎基础之上,结合了 WPF 核心图形层组件基础。它的出现几乎改变了 Vista 中应用程序的屏幕象素显示方式。启用 DWM 后,提供的视觉效果有毛玻璃框架、3D 窗口变换动画、窗口翻转和高分辨率支持。

应用程序的显示不再是直接画到屏幕上,而是一个显示内存中的一个离屏 Surface 。然后由 DWM 将这些 Surface 合成显示到屏幕之上。

在Vista之前,Windows 要求应用程序画自己的可见区域,它们可以直接画在显卡的视频缓冲里面。而在 Vista,系统要求应用程序把整个表面画到离屏 Surface 当中。然后由 DWM 控制所有的离屏表面,并把它们合成到一起显示到真正的屏幕上。

DWM 的主要目标就是利用图形芯片的处理能力也给非游戏用户带来尽可能好的体验。因此 DWM 是基于 DirectX,特别是 Direct3D。更准确来说,DWM 是直接建立在一个称为 Milcore 的层次之上,而 Milcore 又建立在 DirectX 之上,最终是用 Direct3D 纹理来表示窗口内容和窗口框架。 DWM/Milcore 调用适当的 Direct3D 函数把所有的 Direct3D 纹理合成为最终的桌面。Vista 或 Win7 桌面就可以理解为一个全屏幕的 Direct3D 应用程序。

1.3 DWM 服务(仅限 Win7)

在 Win 7 上,桌面窗口管理器器,Desktop Window Manager Session Manager(DWMSMs)是一项服务,默认自动启动,若终止该服务,将导致 Aero 视觉效果消失。你可以在控制面板\管理工具\服务中查看服务状态:

Win7 UxSms 服务的显示名称

右键选项卡,点击属性栏,可以看到它的服务名称:UxSms。

服务设置页

 DWM 的目录位置为:C:\Windows\System32 ,其目录结构为:

 DWM 进程为 svchost.exe 的子进程,使用 Process Explorer 查看进程关系如下:

用户可以通过 NET 命令来操作 DWM 服务

控制台窗口键入以下指令:

1.启动 DWM 服务:net start UxSms

2.终止 DWM 服务:net stop UxSms

通过命令控制 DWM 服务进程

1.4 通过接口启用/禁用 DWM 窗口合成

Windows 7 以及 Vista 系统版本下,支持编程禁用 DWM,就是因为这个禁用功能导致我们的动态壁纸程序必须能够检测 DWM 的状态改变,除非程序不需要适配 Win7 系统。

完全控制桌面的应用程序(例如在全屏模式下运行的游戏)必须确定是否已启用 DWM,需要两个函数来检查 DWM 状态(下面函数仅在 Win7 有效):

fEnable 设置为 DWM_EC_DISABLECOMPOSITION 的情况下调用 DwmEnableComposition 会禁用 DWM 合成,直到调用进程已关闭,或者通过将 fEnable 设置为 DWM_EC_ENABLECOMPOSITION 调用 DwmEnableComposition 来重新启用合成。

注意:禁用组合的所有应用程序关闭后或通过调用 DwmEnableComposition 手动重新启用组合后,DWM 组合会自动重启。

1.5 DWM 系统关键进程

从 Windows 8 开始,当应用程序尝试直接绘制到主要显示图面时,DWM 会自动禁用合成。 组合将被禁用,直到该应用程序释放主设备图面。微软将该进程归为由 Winlogon.exe 进程启动和监视的关键进程,dwm.exe 不再是服务进程,不可以被编程终止,否则会黑屏卡死。主要原因是很多系统运行中的应用,尤其是 UWP 应用均依赖 DWM,如果 DWM 崩溃则它们的图形界面也将崩溃。

但是,有很多通过钩子或者修改链接库实现禁用 DWM 的例子,如果有空我会单独写一期。

2. 通过 Spy++ 工具研究桌面窗口层次

2.1 桌面窗口层次

Windows 的桌面由图标列表和背景窗口等组成,这些窗口以及控件窗口之间具有一定的层次。使用 Spy++ 可以获取到开机后普通的桌面窗口层次,结构如下所示:

"Program Manager" Progman

| -- "" SHELLDLL_DefView

     | -- "FolderView" SysListView32

          | -- "" SysHeader32    (Unvisible)

可以观察到在 Desktop 窗口中 Z-Order 位于最底层的窗口是 Progman 窗口, 其子窗口是 SHELLDLL_DefView 窗口,SHELLDLL_DefView 又有一个窗口类为 SysListView32 的子窗口最后 SysHeader32 窗口是不可见的。显而易见,桌面上的图标都在名为 SysListView32 的列表窗口中。如果熟悉 MFC,看到 SysListView32 会很眼熟,MFC中的 CListCtrl 控件窗口类也是SysListView32。

在这种层次下, 往 Progman 窗口中嵌入一个 WM_CHILDWINDOW 样式的窗口,将会覆盖在 SysLisView32 窗口上方,或者被前面的窗口挡住,无法通过嵌入窗口的方式实现类似 WallPaper Engine 那样的壁纸。我们现在看下 Wallpaper Engine 嵌入壁纸窗口时候桌面窗口层次,Wallpaper Engine 在 Win 7 上的行为和更高版本系统不一样。

首先是 Win8 / 8.1 系统:

"" WorkerW    (本文称作 WorkerW 1)

| -- "" WorkerW (WorkerW 0,顶级窗口为 Progman 时就默认具有)

| -- "" SHELLDLL_DefView

     | -- "FolderView" SysListView32

          | -- "" SysHeader32    (Unvisible)

"" WorkerW    (本文称作 WorkerW 2)

| -- "" CefBrowserWindow    (WallpaperEngine 的浏览器窗口)

"Program Manager" Progman

其次, Win 10 至 Win 11 23H2 操作系统:

"" WorkerW    (本文称作 WorkerW 1)

| -- "" SHELLDLL_DefView

     | -- "FolderView" SysListView32

          | -- "" SysHeader32    (Unvisible)

"" WorkerW    (本文称作 WorkerW 2)

| -- "" CefBrowserWindow    (WallpaperEngine 的浏览器窗口)

"Program Manager" Progman

然后,是 Win 7 系统,层次结构如下:

"" WorkerW 1    (Visible, Aero)

| -- "" SHELLDLL_DefView

     | -- "FolderView" SysListView32

          | -- "" SysHeader32    (Unvisible)

"" WorkerW 2    (Unvisible, White)

"Program Manager" Progman

| -- "" CefBrowserWindow    (Wallpaper Engine 的浏览器窗口)

最后,Win Vista 似乎无法通过发送消息产生类似的窗口层次。

我们发现 SHELLDLL_DefView 及其下面的桌面图标窗口成为一个 WorkerW 窗口的子窗口(我们称 WorkerW 1),和第一个 WorkerW 同级但 Z 序位于下方的 WorkerW 窗口(我们称 WorkerW 2),在Win 8至 Win 11上壁纸窗口设为了 WokerW 2 的子窗口,而在 Win 7 上则设置为 Progman 的子窗口。

从 Spy ++ 返回的信息来看,WorkerW 和 Progman 都是 NULL,也就是说它们是桌面顶级窗口,没有父窗口和所有者窗口。

在 Win 7 下用 Spy++ 分别看 WorkerW 1、WorkerW 2 的窗口样式,会发现 WorkerW 2 是一个 Popup 窗口,其 Parent 是 Progman 窗口 ,其上一个窗口句柄是 WorkerW 1。 WokerW 1 窗口也是一个Popup 窗口,其父窗口显示无,但是其下一个窗口显示的句柄正好是 WokerW 2。此时这三个窗口Z序很明显了:WorkerW1 > WorkerW2 > Progman 窗口。

在分析窗口样式的时候我还发现一个有趣的现象:

比如在 Win 11 下,WorkerW 2 的扩展样式中有一个叫 WS_EX_TRANSPARENT 的窗口样式,而在 WorkerW 1 下则没有:

Win 11 WorkerW 1 窗口样式
Win 11 WorkerW 2 窗口样式

对于 WS_EX_TRANSPARENT 样式,MSDN 是这样说的, 在窗口下方(由同一个线程创建)的兄弟窗口被绘制之前,不应该对窗口进行绘制。窗口显示为透明,因为底层兄弟窗口的位已经被绘制。要在没有这些限制的情况下实现透明度,请使用 SetWindowRgn 函数。

也就是说这个扩展样式,可以实现鼠标穿透(窗口的穿透性质和透明度之间的关系可以看 文章)。

再看看 Win 7 下的窗口样式:

可以看到 WorkerW 不仅有鼠标穿透,还有 WS_EX_LAYERED 分层窗口样式。用 GetLayeredWindowAttributes 函数检索透明度时候调用失败,猜测窗口是使用UpdateLayeredWindow 实现透明度的。但是Win 7/8上,WorkerW 1 并不是透明的,会遮挡 WorkerW 2,在 Win 8.1 及以上则不遮挡,单纯从窗口样式上很难判断窗口是否透明。

WorkerW 1、WorkerW 2 和 Progman 窗口一样都属于 explorer.exe 进程的窗口。实际上想要将自己的窗口嵌入到 Windows 桌面图标下方,桌面的窗口层次一定要正确,并且要保证高 Z 序窗口是透明的。总结以上分析,Win 7/8 窗口应嵌入 Progman 并且隐藏 WorkerW 2;Win 8.1开始的系统上,窗口应嵌入 WorkerW 2 。需要注意的是,用嵌入窗口的方式实现动态壁纸,只能在 WIn7 及其以上系统上实现,Vista/XP 以及更早的系统无法产生这种透明的窗口层次, XP是没有 DWM 框架且窗口不透明,Vista 是早期的 DWM 有些功能不支持,导致无法用 Worker 分层窗口嵌入壁纸窗口。

2.2 如何形成 WorkerW 分层窗口

WorkerW 分层窗口用于在切换桌面时产生淡入淡出动画,这主要通过在名为 WorkerW 的平滑移动窗口上绘制桌面 Progman 窗口的 HDC 信息得到。这种窗口是延时产生的,最典型的是在 Win 7 SP3 上更改显示器配色方案时 以及 Win 10 等打开“任务视图”时。下面演示两种情况下窗口层次是如何产生的。

(1)在 Win 7 SP3 上更改显示器配色方案

使用 Spy ++ 监视窗口消息,当更改显示器配色方案时,系统会调用 PostMessage 函数,并发送一条未公开的 WindowsMessage,即 0x052C (WM_SHELLPARENTCHANGING)

Windows Basic
Windows Aero
Aero Activated

(2)在 Win 11 上监视 Progman,并打开和关闭“任务视图”按钮

可以观察到,打开“任务视图”的时候,wParam 是 0x0D,lParam 是 0x01,关闭时则是 0x0.

目前看来,在不同操作系统上 Param 参数的功能不太一致,这需要更为详细的研究。

建议使用具有超时发送的 SendMessageTimeout 函数,并在指定的系统版本上使用以下建议的参数数值发送消息。

[2024.08.17 更新]

下面的更新内容根据 https://github.com/valinet/ExplorerPatcher/issues/525 的研究所得。在 Win11上需要确保 Program 窗口完全初始化后才能发送消息生成 WorkerW,因为 0x052C 的零参数消息是异步的,有些机器上可能因为性能原因将显现出问题。必须首先调用 0x052C 并指定 0xA(10) 或 0xB(11)。

原文:

jdp1024 commented  on Dec 22, 2021

You can have a look into CDesktopBrowser::_OnWallpaperUpdate which is called by CDesktopBrowser::_WndProcBS, there is something interesting.

If wParam is 10 or 11 and msg is 0x52cCDesktopBrowser::_IsDesktopWallpaperInitialized is called and 0x80004005 will be returned if _IsDesktopWallpaperInitialized returns FALSE.

【翻译】你可以检查正在被 CDesktopBrowser::_WndProcBS 调用的 CDesktopBrowser::_OnWallpaperUpdate,那里有一些有趣的东西。

如果消息是 0x52c 并且 wParam 是 10 或者 11,CDesktopBrowser::_IsDesktopWallpaperInitialized 函数将被调用以检查桌面初始化状态,当 _IsDesktopWallpaperInitialized 返回 FALSE 时,消息处理将返回错误代码 0x80004005。

这样的检查将避免使用不确定的做法,如 Sleep 进行延迟。 

BOOL sws_WindowHelpers_EnsureWallpaperHWND()
{
    // See: https://github.com/valinet/ExplorerPatcher/issues/525
	HWND progman = GetShellWindow();
	if (progman)
	{
		PDWORD_PTR res0 = -1, res1 = -1, res2 = -1, res3 = -1;
		// Call CDesktopBrowser::_IsDesktopWallpaperInitialized
		SendMessageTimeoutW(progman, 0x052C, 0xA, 0, SMTO_NORMAL, 1000, &res0);
		if (FAILED(res0))
		{
			return FALSE;
		}
		// Prepare to generate wallpaper window
		SendMessageTimeoutW(progman, 0x052C, 0xD, 0, SMTO_NORMAL, 1000, &res1);
		SendMessageTimeoutW(progman, 0x052C, 0XD, 1, SMTO_NORMAL, 1000, &res2);
		// "Animate desktop", which will make sure the wallpaper window is there
		SendMessageTimeoutW(progman, 0x052C, 0, 0, SMTO_NORMAL, 1000, &res3);  // 0 参数是必须的,对于早期系统(win7) 0xD 参数会导致处理失败。
		//printf("[sws] Wallpaper results: %d %d %d\n", res1, res2, res3);		
		return !res1 && !res2 && !res3;
	}
	return FALSE;
}

SendMessageTimeout 函数最早被提出只需要发送 0x052C 消息即可,但事实并不全部如此。

SendMessageTimeoutW(progman, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &res3);

其中,前 4 个参数和 SendMessage 相同,后面参数是控制超时的,0x03E8 等同于十进制的1000,表示等待超时时间是 1000 毫秒。

三、实战&编写代码

1. 如何为嵌入桌面管理层窗口做好准备?

只要能将我们自己的窗口嵌入到 Windows 图标下面并且可视,我们就可以在窗口上引入动画了!

1.1. 确保已经启动 DWM 窗口合成

2024.06.06:最新研究发现,只通过 DwmIsCompositionEnabled 函数和服务进程状态来检查是否启用了窗口合成是不精确的,当在使用类似 Basic 主题(通过一个没有文档化的 API 操作了 DWM 内部数据)或者在注册表和组策略中关闭了窗口合成,将对使用该 API 的简单检测产生逃逸,导致 0x052C 不能正常产生动作。作者将在未来更新此部分的环境检测策略。[截至目前未完全在更新中解决]

想让自己的窗口嵌入到桌面管理层窗口下面不被遮挡,你必须让桌面管理层窗口变成透明。说到窗口透明,做过客户端的程序员可能会想到 Windows 的 Layerd 窗口,但是实际上用 Spy ++ 去看 WorkerW 1 窗口样式, 他并不包含 WS_EX_LAYERD 风格。而且有个重要的问题,如果 Layerd 窗口采用 SetLayeredWindowAttributes 设置为全透明窗口,则其子窗口也会不可见。此外,使用 UpdateLayerdWindow 实现的透明窗口,完全透明的地方鼠标将会穿透。而用 Spy++ 查看桌面的 SysListView32 窗口,它是可以收到各种鼠标消息。那么,将原本 Progman 的子窗口 SHELLDLL_DefView(拥有类名为 SysListView32 的桌面管理层图标窗口)变成 WorkerW 1 的子窗口,并且让WorkerW 1 中除了桌面图标部分其他地方都是透明的,这个是怎么实现的呢?实际上 WorkerW 1 窗口这种透明效果是由 DWM( Desktop Window Manager) 来控制的(如何实现透明后文会说)。

Desktop Window Manager,它是 Vista 之后才出现的一个新的系统组件,它的进程名是dwm.exe。在 Win8 及以上系统,它会随系统自动启动, 并且一直运行。在 Vista/Win7 系统中,一般我们在使用 Aero 主题的时候才会启动这个服务。操作系统提供了 Desktop Window Manager 相关的 API ,相关接口都在 dwmapi.dll 中。DWM API 允许我们设置窗体在与其他窗体组合/重叠时候的显示特效,如全透明、半透明、模糊等效果。

所以回到在桌面管理层窗口嵌入窗口的问题,在 Win 7/8 上,我们首先要判断 DWM Compositon 是否开启,如果被禁止,则桌面管理层窗口层次是不满足要求的,DWM API 提供 DwmIsCompositionEnabled 函数来判断 DWM Composition 是否启用:

BOOL IsDwmCompositionEnabled()
{
	// 注意这DWM API在Vista/Win7系统以上才有的
	// win8/win10是不需要判断的会一直返回TRUE
	BOOL bEnabled = FALSE;
	typedef HRESULT(__stdcall *fnDwmIsCompositionEnabled)(BOOL* pfEnabled);
	HMODULE hModuleDwm = LoadLibraryA("dwmapi.dll");
	if (hModuleDwm != 0)
	{
		fnDwmIsCompositionEnabled pFunc = (fnDwmIsCompositionEnabled)GetProcAddress(hModuleDwm, "DwmIsCompositionEnabled");
		if (pFunc != 0)
		{
			BOOL result = FALSE;
			if (pFunc(&result) == S_OK)
			{
				bEnabled = result;
			}
		}
        else
        {
            SetLastError(ERROR_ACCESS_DENIED);
            bEnabled = TRUE;
        }

		FreeLibrary(hModuleDwm);
		hModuleDwm = 0;
	}
	return bEnabled;
}

如果 DWM Composition 未启用可以使用 DwmEnableComposition 启用它,这个函数参数有两个选择,DWM_EC_ENABLECOMPOSITION,Win 7 下将启用默认的Aero 主题;DWM_EC_DISABLECOMPOSITION,Win 7 下将启用Windows 7 Basic 主题。

样例代码:

/*
HRESULT DwmEnableComposition(
  UINT uCompositionAction
);
*/

#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")

// S1: 
...
HRESULT hr_db = S_OK;

// Disable DWM Composition 
hr_db = DwmEnableComposition(DWM_EC_DISABLECOMPOSITION);
if (SUCCEEDED(hr_db))
{
   // ...
}
...

// S2: 

...
HRESULT hr_eb = S_OK;

// Enable DWM Composition 
hr_eb = DwmEnableComposition(DWM_EC_ENABLECOMPOSITION);
if (SUCCEEDED(hr_eb))
{
   // ...
}
...

但是,在实际测试过程中发现,如果 Uxsms 服务没有启动,则 DwmEnableComposition 始终失败,如果 dwm.exe 进程崩溃,但 Uxsms 服务可能依然正常运行,这说明 Uxsms 服务相当于加载器,我们需要在调用窗口合成之前检查 dwm.exe 是否正在运行,如果没有运行就尝试重启 Uxsms 服务,然后再调用启用窗口合成 的函数。

DWORD FindProcessIDByName(LPCWSTR processName)
{
    DWORD processId = 0;
    HANDLE hProcessSnap;
    PROCESSENTRY32W pe32{};
    pe32.dwSize = sizeof(PROCESSENTRY32W);
    

    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE)
    {
        return(0);
    }
    
    if (!Process32FirstW(hProcessSnap, &pe32))
    {
        CloseHandle(hProcessSnap); // clean the snapshot object
        return(0);
    }
    
    do
    {
        if (!wcscmp(pe32.szExeFile, processName))//进程名称
        {
            processId = pe32.th32ProcessID;//进程ID
            break;
        }
    } while (Process32NextW(hProcessSnap, &pe32));

    CloseHandle(hProcessSnap);

    return processId;
}

BOOL QueryEnableDwmComposition()
{
    // 注意 DWM API 在 Vista/Win7 系统以上才有
    // win8 / win10 是不需要判断的会一直返回 TRUE

    BOOL bEnabled = FALSE;
    typedef HRESULT(__stdcall* fnDwmIsCompositionEnabled)(BOOL* pfEnabled);
    typedef HRESULT(__stdcall* fnDwmEnableComposition)(UINT uCompositionAction);

    HMODULE hModuleDwm = LoadLibraryA("dwmapi.dll");
    if (hModuleDwm != 0)
    {
        auto pFuncIsEnabled =
            (fnDwmIsCompositionEnabled)GetProcAddress(
                hModuleDwm, "DwmIsCompositionEnabled");
        auto pFuncEnableDwm =
            (fnDwmEnableComposition)GetProcAddress(
            hModuleDwm, "DwmEnableComposition");

        if (pFuncIsEnabled != 0)
        {
            BOOL result = FALSE;
            if (pFuncIsEnabled(&result) == S_OK)
            {
                // 没有启动就启动一下
                if(result == TRUE)
                    bEnabled = TRUE;
                else if (pFuncEnableDwm != 0)
                {
                    printf("[*] Attempt to start Dwm Service.\n");
                    if (!FindProcessIDByName(L"dwm.exe"))
                    {
                        system("SC stop UxSms");
                        WaitForSingleObject(GetCurrentProcess(), 1500);
                    }
                    system("SC start UxSms");
                    WaitForSingleObject(GetCurrentProcess(), 500);
                    // #define DWM_EC_ENABLECOMPOSITION 1
                    if (pFuncEnableDwm(TRUE) == S_OK)
                    {
                        bEnabled = TRUE;// 判断启动是否成功
                    }
                    else {
                        SetLastError(ERROR_INTERNAL_ERROR);
                    }
                }
            }
        }
        else {
            SetLastError(ERROR_ACCESS_DENIED);
            bEnabled = TRUE;
        }

        FreeLibrary(hModuleDwm);
        hModuleDwm = 0;
    }
    return bEnabled;
}

更好的方法是使用 SCM 配置函数监视和维持 UxSms 服务进程的启动状态。这里懒,就用 sc 命令代替了。

测试效果:

2024/07/09 更新:使用服务控制器 API 实现的状态检查:

// 检查服务状态
SERVICE_STATUS_PROCESS QueryServiceStatus(const WCHAR* serviceName) {
    SERVICE_STATUS_PROCESS ssp = {};
    SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (hSCManager == NULL) {
        std::wcerr << L"OpenSCManager failed, error: " << GetLastError() << std::endl;
        return ssp;
    }

    SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_QUERY_STATUS);
    if (hService == NULL) {
        std::wcerr << L"OpenService failed, error: " << GetLastError() << std::endl;
        CloseServiceHandle(hSCManager);
        return ssp;
    }

    DWORD dwBytesNeeded;
    if (!QueryServiceStatusEx(hService, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp, 
        sizeof(SERVICE_STATUS_PROCESS), &dwBytesNeeded)) {
        std::wcerr << L"QueryServiceStatusEx failed, error: " << GetLastError() << std::endl;
    }

    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);

    return ssp;
}

// 启动服务
bool CStartService(const WCHAR* serviceName) {
    SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (hSCManager == NULL) {
        std::wcerr << L"OpenSCManager failed, error: " << GetLastError() << std::endl;
        return false;
    }

    SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_START);
    if (hService == NULL) {
        std::wcerr << L"OpenService failed, error: " << GetLastError() << std::endl;
        CloseServiceHandle(hSCManager);
        return false;
    }

    if (!StartServiceW(hService, 0, NULL)) {
        std::wcerr << L"StartService failed, error: " << GetLastError() << std::endl;
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCManager);
        return false;
    }

    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return true;
}

// 停止服务
bool CStopService(const WCHAR* serviceName) {
    SC_HANDLE hSCManager = OpenSCManagerW(NULL, NULL, SC_MANAGER_ALL_ACCESS);
    if (hSCManager == NULL) {
        std::wcerr << L"OpenSCManager failed, error: " << GetLastError() << std::endl;
        return false;
    }

    SC_HANDLE hService = OpenServiceW(hSCManager, serviceName, SERVICE_STOP);
    if (hService == NULL) {
        std::wcerr << L"OpenService failed, error: " << GetLastError() << std::endl;
        CloseServiceHandle(hSCManager);
        return false;
    }

    SERVICE_STATUS status;
    if (!ControlService(hService, SERVICE_CONTROL_STOP, &status)) {
        std::wcerr << L"ControlService failed, error: " << GetLastError() << std::endl;
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCManager);
        return false;
    }

    CloseServiceHandle(hService);
    CloseServiceHandle(hSCManager);
    return true;
}

// 重新启动服务
bool CRestartService(const WCHAR* serviceName) {
    if (!CStopService(serviceName)) {
        std::wcerr << L"Failed to stop service: " << serviceName << std::endl;
        return false;
    }

    Sleep(1000); // 等待服务完全停止

    if (!CStartService(serviceName)) {
        std::wcerr << L"Failed to start service: " << serviceName << std::endl;
        return false;
    }

    return true;
}


// 只有 Win7 才需要判断窗口合成是否启用
BOOL QueryEnableDwmComposition()
{
    typedef HRESULT(__stdcall* fnDwmIsCompositionEnabled)(BOOL* pfEnabled);
    typedef HRESULT(__stdcall* fnDwmEnableComposition)(UINT uCompositionAction);

    HMODULE hModuleDwm = LoadLibraryW(L"dwmapi.dll");
    if (hModuleDwm == 0) {
        SetLastError(ERROR_MOD_NOT_FOUND);
        return FALSE;
    }
    
    auto pFuncIsEnabled =
        (fnDwmIsCompositionEnabled)GetProcAddress(
            hModuleDwm, "DwmIsCompositionEnabled");
    auto pFuncEnableDwm =
        (fnDwmEnableComposition)GetProcAddress(
            hModuleDwm, "DwmEnableComposition");

    if (!pFuncIsEnabled || !pFuncEnableDwm)
    {
        SetLastError(ERROR_ACCESS_DENIED);
        FreeLibrary(hModuleDwm);
        return FALSE;
    }


    BOOL bEnableComposition = FALSE;
    if (pFuncIsEnabled(&bEnableComposition) != S_OK)
    {
        FreeLibrary(hModuleDwm);
        SetLastError(ERROR_INTERNAL_ERROR);
        return FALSE;
    }

    // 没有启动就启动一下
    if (!bEnableComposition) {
        printf("[*] Attempt to start Dwm Service.\n");

        // 检查服务状态
        const WCHAR* dwmSvcName = _T("UxSms");
        SERVICE_STATUS_PROCESS ssp = QueryServiceStatus(dwmSvcName);
        if (ssp.dwCurrentState == SERVICE_STOPPED) {
            std::wcout << L"DWM Service is stopped. Starting service..." << std::endl;
            if (CStartService(dwmSvcName)) {
                std::wcout << L"DWM Service started successfully." << std::endl;
            }
        }
        else if (ssp.dwCurrentState == SERVICE_RUNNING) {
            std::wcout << L"Service is running. Restarting service..." << std::endl;
            if (CRestartService(dwmSvcName)) {
                std::wcout << L"DWM Service restarted successfully." << std::endl;
            }
        }
        else {
            std::wcout << L"DWM Service is in state: " 
                << ssp.dwCurrentState << std::endl;

            SetLastError(ERROR_INTERNAL_ERROR);
            FreeLibrary(hModuleDwm);
            return FALSE;
        }
        
        // #define DWM_EC_ENABLECOMPOSITION 1
        if (pFuncEnableDwm(TRUE) == S_OK)
        {
            FreeLibrary(hModuleDwm);
            return TRUE;
        }
        else {
            SetLastError(ERROR_INTERNAL_ERROR);
            FreeLibrary(hModuleDwm);
            return FALSE;
        }
    }

    return TRUE;
}

后续仍需要继续完善窗口合成检测的部分,目前已经去除了对 dwm.exe 进程的检测(这一步是多余的)。

1.2. 怎样启用工作区窗口(WorkerW)?

我们 Windows 系统已经开启 DWM Composition 了,那么我们怎么才能让桌面管理层窗口层次发生改变,能够让我们正常嵌入呢?上文说过桌面管理层窗口必须是双 WorkerW 的层次,而且 Z 序是固定的:

WorkerW 1 > WorkerW 2 > Progman

如果是嵌入到 WorkerW 2 窗口下面,会被 WorkerW 2 挡住;反之,如果是嵌入到 WorkerW 2 窗口上面,我们必确保 WorkerW 2 窗口是 Visible 的,否则嵌入的窗口也是不可见的。WorkerW 2 窗口创建出来后并没有在其上绘制背景,所以显示的还是 Progman 的背景。 但是在Win7 WorkerW 2 窗口并不是透明的,所以如果嵌入到 Progman 中,并且 WorkerW 2 是 Visible 的话,因为 Z 序的原因程序窗口确实会被挡住,并且在 Win 7/ win 8下 WorkerW 1 也是不透明的,也会遮挡,而 Win 8.1 则可以嵌入在 WorkerW 2 上。

根据前文研究结论:在采用 WorkerW 分组条件下,低版本系统嵌入窗口必须以 Program 为父窗口,并隐藏 WorkerW 2,否则无法显示窗口;而在高版本(不低于 Win 8.1)的桌面嵌入窗口则建议以 WorkerW 2 为父窗口。

1.3 为什么说高版本必须以 WorkerW 2 为父窗口呢?

在不低于 Win 8.1 上,如果以 Progman 作为父窗口嵌入壁纸窗口,虽然看似嵌入了,但是存在一些难以解决的问题。其一,当显示桌面时,嵌入的窗口可能被最小化。其二,当再次发送 0X052C 消息到 Progman 窗口的时候,我们窗口的文本会被强制同步,由 RGB 配色方案转为 ARGB。这会导致将黑色文本识别为透明色,导致窗口上的标准文本控件变得透明。

窗口的文本控件变为透明色了

 下面是另外一款软件出现的颜色错误:

桌面便签文字显示异常

关于这个问题,我是在: “桌面窗口嵌入应用黑色像素变为透明” 的解答中获知可能的原因:

这是因为 DWM 设置了 ARGB 型的透明色,而你的程序使用的是传统的 RGB ,黑色的Alpha 通道被误认为是完全透明。

辅助资料:以下内容来源自翻译文章:《DWM 窗体玻璃效果实现》,引用自原文章:Aero Glass:使用桌面窗口管理器创建特殊效果 |Microsoft学习

一个特殊问题是使用位模式 0x00000000 以黑色呈现 GDI 项目,在使用 Alpha 管道时也会碰巧出现完全透明的黑色。这意味着如果您使用黑色 GDI 画笔或笔进行绘制,将会得到透明的颜色,而不是黑色。当您尝试使用默认文本颜色控制位于玻璃区域中的文本标签时,这种问题表现得就特别明显。因为默认文本颜色通常为黑色,DWM 会认为它是透明的,因此文本将错误地写入玻璃区域。下图显示了一个这样的示例。第一行使用 GDI+ 编写,第二行是一个使用默认颜色的文本标签控件。可以看出,其中的内容几乎无法辨认,因为它实际上是错误呈现的文件,文本显示为灰色,而不是黑色。

文本控件的透明异常

此图片和描述来自:Aero Glass:使用桌面窗口管理器创建特殊效果 |Microsoft学习

(此处省略原文描述中已经过时的内容)


2024.06.10 更新原理解释

        嵌入在 Program 下时,会在第二次调用 0x052C 时触发控件幻影(穿透)的系统 BUG,这是由三个问题一起导致的结果。因为桌面使用 WM_ERASEBKGND 消息并处理 RedrawWindow、BeginPaint、EndPaint 等过程来将实现背景和图标层的图像缓冲区的刷新。当 Program 窗口接收到 0x052C 消息时会将其子窗口踢出到第一个 WorkerW 下(重新设置父窗口),新的 Z 序根据进入顺序会在 ListView(DefView) 窗口上面,导致嵌入的窗口现在在其绘制区内覆盖桌面图标窗口的绘制。另一方面,父窗口 WorkerW 是使用 DWM API 的扩展窗口,其颜色方案采用 ARGB,所以具有黑色 RBG 背景的子窗口上的控件就会被同步为透明色,导致文本控件透明异常。最后,由于父窗口(双缓冲)并不能够正确处理该窗口的绘制,所以拖动窗口时其边缘会出现重影。

        下图显示了在所创建的 Win32 窗口中嵌入 Procexp 窗口,通过类似的途径最终使得子窗口的文本控件穿透显示了根父窗口(这里复制了桌面背景作为演示)。我们称之为窗口幻影 BUG:

ARGB 色混淆问题复现

实现一个窗口,其 Z 序覆盖在桌面图标窗口之上,并且在其窗口回调中处理 WM_ERASEBKGND,将导致桌面壁纸被显示出来:

嵌入窗口实现重叠子窗口

在该窗口中嵌入子窗口,当拖动子窗口时,产生重影(没有正确处理缓冲区):

复现拖动窗口时重影残影问题

所以,多一事不如少一事,高版本系统嵌入在 WorkerW 2 下可以省去不必要的麻烦。

1.4 如何获取这些窗口的句柄?

前面分析过,要想让桌面管理层窗口变成适合嵌入的透明层次,我们需要向 Progman 窗口发送一个 0x052C 的消息,这个是 Windows 系统保留的一个消息,它在 Win 7 之后版本才有效(Vista SP1 没有响应这个消息)。当发送这个消息后,explorer.exe 就会生成 WorkerW 窗口(上文所说 WorkerWs ),Progman 上的 SHELLDLL_DefView 以及图标 SysListView32 窗口都会成为 WorkerW 1 的子窗口,同时还会产生另外一个 WorkerW 窗口(上文所说 WorkerW 2)。判断当前桌面管理层窗口是不是透明层次, 可以用 FindWindowEx 查找到WorkerW 2,至于WorkerW 1 可以 EnumWindows 枚举来查找,也可以 FindWindowEx 循环查找。如果两个都找到那就可以直接嵌入,否则需要发 0x052C 消息到 Progman 窗口,来启用 WorkerW 分组。

这里,我们使用 EnumWindows 函数枚举相关窗口句柄,并在 EnumWindowsProc 回调函数里面将窗口句柄压入结构体成员变量。

EnumWindows 函数通过将每个窗口的句柄依次传递给应用程序定义的回调函数来枚举屏幕上的所有顶级窗口。枚举 Windows 将一直持续到最后一个顶级窗口或回调函数返回 FALSE

BOOL EnumWindows(

[in] WNDENUMPROC lpEnumFunc,

[in] LPARAM lParam

);

参数:

[in] lpEnumFunc:指向应用程序定义的回调函数的指针。

[in] lParam:要传递给回调函数的应用程序定义值。

返回值:

BOOL dwErrorReturn

如果该函数成功,则返回值为非零值。

如果函数失败,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。

如果 EnumWindowsProc 返回零,则返回值也为零。 在这种情况下,回调函数应调用 SetLastError 以获取要返回给 EnumWindows 调用方有意义的错误代码。

根据上文桌面管理层窗口层次的理论分析,回调函数应该这样写:

​
inline BOOL CALLBACK EnumWindowsProc(HWND handle, LPARAM lparam)
{
    if ((void*)lparam == nullptr)
        return FALSE;
    auto WndInfo = (DesktopWndInfoPtr)lparam;
    DWORD_PTR result = 0;
    // 获取第一个WorkerW窗口(异常时获取到的是 Progman 窗口)
    HWND DefView = FindWindowExW(handle, 0, L"SHELLDLL_DefView", NULL);

    if (DefView != NULL)// 找到第一个WorkerW窗口
    {
        (*WndInfo).Workerw1 = handle;
        (*WndInfo).ShellDefView = DefView;
        // 获取第二个WorkerW窗口的窗口句柄
        (*WndInfo).Workerw2 = FindWindowExW(0, handle, L"WorkerW", 0);
        
        SetLastError(0);  // 确保不会误诊!!!要么就不立即返回。
        return FALSE;  // Patch:根据 MSDN 说明,应该立即返回 FALSE 以便于结束枚举。
        // 若要继续枚举,回调函数必须返回 TRUE;若要停止枚举,它必须返回 FALSE。
        // https://learn.microsoft.com/zh-cn/previous-versions/windows/desktop/legacy/ms633498(v=vs.85)
        
    } // Patch:在 EnumWindowsProc 回调函数中使用 SMTO 消息发送可能降低计算机性能
      // 并且可能因为超时而导致窗口遍历失败(后面代码对设置窗口的线程限制了执行时间,超时则停止运行)
//    else {// 如果不能找到第一个WorkerW, 则重新发送消息
//        HWND hProgman = (*WndInfo).Progman;
//        SendMessageTimeoutW(hProgman, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &result);
//    }
    return TRUE;
}

其中,DesktopWndInfo 是我自己定义的结构体,用于全局记录窗口信息:

typedef struct _DesktopWndInfo
{
    int  OSVersion = 1;               // 操作系统分类号
    bool IsCreatedWindow = false;     // 动态壁纸窗口是否创建完成
    HWND WallpaperWnd = NULL;         // 动态壁纸程序主窗口句柄
    HWND Workerw1 = NULL;             // 第一个WorkerW窗口句柄
    HWND Workerw2 = NULL;             // 第二个WorkerW窗口句柄
    HWND ShellDefView = NULL;         // ShellDefView窗口句柄
    HWND Progman = NULL;              // 总窗口句柄
}DesktopWndInfo, * DesktopWndInfoPtr;

为了解决 24H2 只有一个 WorkerW 特征的问题,我认为可以不再使用遍历窗口的方法获取所需窗口结构信息,而可以匹配已知的窗口结构层次来获取窗口。初步编写的搜索算法如下:

  • 查找 "Progman" 窗口

    • 通过 FindWindowW 查找类名为 "Progman" 的窗口句柄。
    • 如果找不到或句柄无效,返回错误信息并终止函数。
  • 获取前一个窗口

    • 使用 GetWindow 获取 Progman 窗口的前一个窗口句柄。
    • 如果找不到前一个窗口,返回错误信息并终止函数。
  • 根据前一个窗口的类名决定处理逻辑

    • 获取前一个窗口的类名。
    • 如果类名不是 "WorkerW",则在 Progman 窗口的子窗口中查找类名为 "SHELLDLL_DefView" 的窗口。
      • 如果找到 "SHELLDLL_DefView",则从子窗口中查找 "WorkerW" 窗口,保存到 DesktopWndInfo 结构体中。
      • 如果找不到 "SHELLDLL_DefView" 或 "WorkerW",返回错误信息并终止函数。
    • 如果类名是 "WorkerW",则进一步检查 WorkerW 窗口的子窗口中是否有 "SHELLDLL_DefView"。
      • 如果找到 "SHELLDLL_DefView",保存到 DesktopWndInfo 结构体中。
      • 如果找不到 "SHELLDLL_DefView",继续查找前一个 WorkerW 窗口,并查找其子窗口中的 "SHELLDLL_DefView"。

代码: 

BOOL OnInitFindDesktopWindows(DesktopWndInfoPtr lpWndInfo) {
    HWND hProgmanWnd = FindWindowW(L"Progman", nullptr);

    // 错误处理:找不到 "Progman" 窗口
    if (!hProgmanWnd || GetLastError() == ERROR_INVALID_WINDOW_HANDLE) {
        std::cerr << "[-] Fatal Error: No found Desktop Manager Window.\n";
        return FALSE;
    }
    lpWndInfo->Progman = hProgmanWnd;

    HWND hProgPrevWnd = GetWindow(hProgmanWnd, GW_HWNDPREV);
    // 错误处理:找不到前一个窗口
    if (!hProgPrevWnd) {
        std::cerr << "[-] Fatal Error: No found Desktop Manager Window.\n";
        return FALSE;
    }

    WCHAR wsTitle[50];
    GetClassNameW(hProgPrevWnd, wsTitle, 50);
    std::wstring strTitle = wsTitle;

    if (strTitle != L"WorkerW") {
        return FindShellDefViewInProgman(lpWndInfo, hProgmanWnd);
    } else {
        return FindShellDefViewInWorkerW(lpWndInfo, hProgPrevWnd);
    }
}

BOOL FindShellDefViewInProgman(DesktopWndInfoPtr lpWndInfo, HWND hProgmanWnd) {
    HWND hProgChildShellWnd = FindWindowExW(hProgmanWnd, nullptr, L"SHELLDLL_DefView", L"");
    if (!hProgChildShellWnd) {
        std::cerr << "[-] Error: No SHELLDLL_DefView found in Progman.\n";
        return FALSE;
    }

    HWND hProgChildWorkerW = FindWindowExW(hProgmanWnd, nullptr, L"WorkerW", L"");
    if (!hProgChildWorkerW) {
        std::cerr << "[-] Error: No WorkerW found in Progman.\n";
        return FALSE;
    }

    lpWndInfo->Workerw1 = hProgChildWorkerW;
    lpWndInfo->ShellDefView = hProgChildShellWnd;
    return TRUE;
}

BOOL FindShellDefViewInWorkerW(DesktopWndInfoPtr lpWndInfo, HWND hWorkerWnd) {
    HWND hWorkerChildWnd = FindWindowExW(hWorkerWnd, nullptr, L"SHELLDLL_DefView", L"");
    if (hWorkerChildWnd) {
        lpWndInfo->Workerw1 = hWorkerWnd;
        lpWndInfo->ShellDefView = hWorkerChildWnd;
        return TRUE;
    }

    HWND hWorkerPrevWnd = GetWindow(hWorkerWnd, GW_HWNDPREV);
    if (!hWorkerPrevWnd) {
        std::cerr << "[-] Error: No previous WorkerW found.\n";
        return FALSE;
    }

    hWorkerChildWnd = FindWindowExW(hWorkerPrevWnd, nullptr, L"SHELLDLL_DefView", L"");
    if (!hWorkerChildWnd) {
        std::cerr << "[-] Error: No SHELLDLL_DefView found in previous WorkerW.\n";
        return FALSE;
    }

    lpWndInfo->Workerw1 = hWorkerPrevWnd;
    lpWndInfo->Workerw2 = hWorkerWnd;
    lpWndInfo->ShellDefView = hWorkerChildWnd;
    return TRUE;
}

2. 更改父窗口实现嵌入壁纸窗口

此时桌面管理窗口的层次变成 WorkerW 分组层次,我们只需要把自己进程的窗口 “嵌入” 进去即可,使用 SetParent 更改指定窗口的父窗口。其定义如下:

HWND SetParent(

[in] HWND hWndChild,

[in] HWND hWndNewParent

);

/*
 * 参数
 * [in] hWndChild
 * 类型:HWND 子窗口的句柄。
 * [in, optional] hWndNewParent
 * 类型:HWND
 * 新父窗口的句柄。如果此参数为 NULL,则 桌面窗口 Program 成为新的父窗口。
 * 如果此参数为 HWND_MESSAGE,则子窗口将成为仅消息窗口。
 * 返回值
 * 类型:HWND
 * 如果函数成功,则返回值是前一个父窗口的句柄。
 * 如果函数失败,则返回值为 NULL 。
 * [#] 可以使用 GetParent 比对的方法获知 SetParent 设置是否成功
 *     使用 GetLastError 可能会显示 5 拒绝访问的错误( MSDN 写得有误)
 */

hWndChild 参数是我们自己窗口句柄, hWndNewParent 是 WorkerW 2 或者 Progman 窗口,若用 Progman 作为 Parent 窗口,记住需要隐藏掉 Worker 2 窗口。那么既然能把我们自己窗口嵌入到桌面图标下面, 那么在上面显示图片、视屏、动画等等都是可以的,下面是我把测试窗口嵌入到桌面管理层窗口下的效果:

然而,仔细观察图片,我们会发现测试窗口原来是最大化的(全桌面),但是在 SetParent 之后立即缩小到默认大小(Normal)。

此外只要一打开任务视图、按下组合键 “Win 徽标键 + Tab”,或者按下任务栏上的这个按钮

我们嵌入的窗口就会显示在桌面图标列表窗口上方。

2.1 问题一解决方案(更新父窗口问题)

通过再次分析 MSDN 的讲解,发现关键注释:

 (1)出于兼容性原因, SetParent 不会修改要更改其父级的窗口的 WS_CHILD 或 WS_POPUP 窗口样式。 因此,如果 hWndNewParent 为 NULL,则还应清除 WS_CHILD 位,并在调用 SetParent 后设置 WS_POPUP 样式。 相反,如果 hWndNewParent 不是 NULL,并且窗口以前是桌面的子级,则应在调用 SetParent 之前清除 WS_POPUP 样式并设置 WS_CHILD 样式。

(2)更改窗口的父级时,应同步两个窗口的 UISTATE。 有关详细信息,请参阅 WM_CHANGEUISTATE 和 WM_UPDATEUISTATE

(3)如果 hWndNewParent 和 hWndChild 以不同的 DPI 感知模式运行,则可能会出现意外行为或错误。 

所以,在设置父窗口前,一是:如果窗口是 POPUP 窗口,应该去除 WS_POPUP 样式,并手动添加 WS_CHILD 样式;二是,如果窗口线程的 DPI 设置不相同,则应该首先同步 DPI 设置,然后再调用 SetParent ;三是,如果窗口控件被键盘击中,需要在调用前同步 UI 状态。

我们应该按照要求来,不然会出现 WS_POPUP 和 WS_CHILDWINDOW 同时存在的情况,窗口的行为会比较奇怪,就像这里出现窗口变成弹出式窗口,解决方法是使用 GetWindowLong 和 SetWindowLong 组合来修改窗口样式,代码如下:

// 按照要求修改窗口样式
LONG style = GetWindowLongW(hClientWnd, GWL_STYLE);
style &= ~WS_POPUP;
style |= WS_CHILD;
SetWindowLongW(hClientWnd, GWL_STYLE, style);

// 设置父窗口
SetParent(hClientWnd, hNewParentWnd);

然后,我们可以使用 GetProcessDPIAwareness 检索进程的 DPI 设置,然后使用SetProcessDPIAwareness 同步进程 DPI,或者使用 SetThreadDPIContext 来同步线程 DPI(如果设置了清单文件,只需要检查线程的 DPI ,一般是默认同步的,不同步再去用线程同步 API 修改)。

那么,如何取消设置父窗口呢?

我们可以通过将 SetParent 的第二个参数设置为 NULL,并在调用前去除 WS_CHILD 样式,在调用 SetParent 后根据记录的旧的窗口样式来恢复样式。

一个恢复窗口独立性的代码如下:

// 取消 WS_CHILD 样式
style = GetWindowLongW(hClientWnd, GWL_STYLE);
style &= ~WS_CHILD;
SetWindowLongW(hClientWnd, GWL_STYLE, style);
// 父窗口设置为默认(主桌面)窗口
SetParent(hClientWnd, NULL);
// 恢复 WS_POPUP 样式
style = GetWindowLongW(hClientWnd, GWL_STYLE);
style |= WS_POPUP;
SetWindowLongW(hClientWnd, GWL_STYLE, style);

一个用于概念验证的完整代码如下:

#include <iostream>
#include <Windows.h>
#include <dwmapi.h>
#include <shellscalingapi.h>

#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "Shcore.lib")
using namespace std;

HWND hClientWnd = NULL;

LRESULT CALLBACK __WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
	HRESULT hr = S_OK;
	switch (msg) {
	case WM_CLOSE:
		MessageBoxW(NULL, L"WM_CLOSE", L"NOTICE:!!!", NULL);
		break;
	case WM_SYSCOMMAND:
	{
		if (wParam == SC_CLOSE)
		{
			MessageBoxW(NULL, L"SC_CLOSE", L"NOTICE:!!!", NULL);
		}
	}
	break;
	case WM_ACTIVATE:
	{
		// Extend the frame into the client area.
		MARGINS margins{};

		margins.cxLeftWidth = 8;      // 8
		margins.cxRightWidth = 8;    // 8
		margins.cyBottomHeight = 20; // 20
		margins.cyTopHeight = 27;       // 27

		hr = DwmExtendFrameIntoClientArea(hWnd, &margins);

		if (!SUCCEEDED(hr))
		{
			// Handle the error.
		}
	}
	break;
	default:
		break;
	}

	return DefWindowProc(hWnd, msg, wParam, lParam);
}


DWORD CreateTestChildWindow(void* args)
{
	// 窗口属性初始化
	HINSTANCE hIns = GetModuleHandleW(0);
	WNDCLASSEXW wc{};
	wc.cbSize = sizeof(wc);								// 定义结构大小
	wc.style = CS_HREDRAW | CS_VREDRAW;					// 如果改变了客户区域的宽度或高度,则重新绘制整个窗口 
	wc.cbClsExtra = 0;									// 窗口结构的附加字节数
	wc.cbWndExtra = 0;									// 窗口实例的附加字节数
	wc.hInstance = hIns;								// 本模块的实例句柄
	wc.hIcon = NULL;									// 图标的句柄
	wc.hIconSm = NULL;									// 和窗口类关联的小图标的句柄
	wc.hbrBackground = (HBRUSH)COLOR_WINDOW;			// 背景画刷的句柄
	wc.hCursor = NULL;									// 光标的句柄
	wc.lpfnWndProc = __WndProc;							// 窗口处理函数的指针
	wc.lpszMenuName = NULL;								// 指向菜单的指针
	wc.lpszClassName = L"TestWndClass";					// 指向类名称的指针

	// 为窗口注册一个窗口类
	if (!RegisterClassExW(&wc)) {
		cout << "RegisterClassEx error : " << GetLastError() << endl;
	}
	const auto cx{ GetSystemMetrics(SM_CXFULLSCREEN) }; // 取显示器屏幕高宽
	const auto cy{ GetSystemMetrics(SM_CYFULLSCREEN) };
	const auto x{ (cx >> 1 ) - 400 };
	const auto y{ (cy >> 1 ) - 300 };

	// 创建窗口
	HWND hWnd = CreateWindowExW(
		WS_EX_APPWINDOW,				// 窗口扩展样式:顶级窗口
		L"TestWndClass",				// 窗口类名
		L"TestWindows",				// 窗口标题
		WS_POPUP| \
		WS_ACTIVECAPTION | \
		WS_VISIBLE | \
		WS_CAPTION | \
		WS_SYSMENU | \
		WS_THICKFRAME | \
		WS_MINIMIZEBOX | \
		WS_MAXIMIZEBOX,		// 窗口样式:重叠窗口
		x,							// 窗口初始x坐标
		y,							// 窗口初始y坐标
		800,						// 窗口宽度
		600,						// 窗口高度
		0,							// 父窗口句柄
		0,							// 菜单句柄 
		hIns,						// 与窗口关联的模块实例的句柄
		0							// 用来传递给窗口WM_CREATE消息
	);

	if (hWnd == 0) {
		cout << "CreateWindowEx error : " << GetLastError() << endl;
		return 0;
	}
	hClientWnd = hWnd;
	UpdateWindow(hWnd);
	ShowWindow(hWnd, SW_SHOW);

	// 消息循环(没有会导致窗口卡死)
	MSG msg = { 0 };
	while (msg.message != WM_QUIT) {
		// 从消息队列中删除一条消息
		if (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE)) {
			DispatchMessageW(&msg);
		}
	}
	return 0;
}

int main()
{
	SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
	CreateThread(NULL, NULL, CreateTestChildWindow, NULL, 0, 0);

	while (!hClientWnd) {
		//Sleep(10);
	}
	if (!hClientWnd) return 0;
	system("pause");
	HWND hDesktop = FindWindowW(L"Notepad", NULL);
	HWND hChildTextWnd = FindWindowExW(hDesktop, NULL, 
		L"NotepadTextBox", NULL);
	printf("%2p %2p\n", hClientWnd, hDesktop);
	if (!hDesktop || !hChildTextWnd)
	{
		system("pause");
		return 0;
	}
	SetFocus(hChildTextWnd);
	RECT prtrc = { 0 },
		 cntrc = { 0 };
	GetWindowRect(hChildTextWnd, &prtrc);
	GetWindowRect(hClientWnd, &cntrc);
	int prtcx = prtrc.right - prtrc.left;
	int prtcy = prtrc.bottom - prtrc.top;
	int cntcx = cntrc.right - cntrc.left;
	int cntcy = cntrc.bottom - cntrc.top;
	int Child_X = (prtcx >> 1) + prtrc.left - (cntcx >> 1);
	int Child_Y = (prtcy >> 1) + prtrc.top - (cntcy >> 1);
	SetWindowPos(
		hClientWnd, NULL, 
		Child_X, Child_Y, 
		0, 0, 
		SWP_NOSIZE | SWP_NOZORDER
	);
	UpdateWindow(hClientWnd);
	LONG style = GetWindowLongW(hClientWnd, GWL_STYLE);
	style &= ~WS_POPUP;
	style |= WS_CHILD;
	SetWindowLongW(hClientWnd, GWL_STYLE, style);
	SetParent(hClientWnd, hDesktop);
	Child_X -= prtrc.left;
	Child_Y -= prtrc.top;
	SetWindowPos(
		hClientWnd, NULL,
		Child_X, Child_Y,
		0, 0,
		SWP_NOSIZE | SWP_NOZORDER
	);
	
	system("pause");
	style = GetWindowLongW(hClientWnd, GWL_STYLE);
	style &= ~WS_CHILD;
	SetWindowLongW(hClientWnd, GWL_STYLE, style);
	SetParent(hClientWnd, NULL);
	style = GetWindowLongW(hClientWnd, GWL_STYLE);
	style |= WS_POPUP;
	SetWindowLongW(hClientWnd, GWL_STYLE, style);
	system("pause");
	return 0;
}

2.2 问题二解决方案(虚拟桌面和扩展屏分辨率问题)

提示(2024.06.10):已知目前的代码没有考虑到在运行周期内,显示器分辨率发生变化的问题。这会导致我们的动态壁纸窗口的大小出现错误。解决方法就是再创建一个顶级窗口去接收 WM_DISPLAYCHANGE 消息,这个消息会在显示器分辨率变化时发送至所有顶级窗口。但是是否传递到子级取决于程序自身的设置,系统只负责顶级窗口的消息发送。经过验证,explorer 的桌面管理层窗口(如:WorkerW)不会将消息传递给子窗口。所以,我们的程序必须创建一个 0 大小的 Popup 窗口,它可以具有 WS_EX_TOOLWINDOW 样式并排除标题栏、边框和系统菜单,这使得我们的窗口不被用户看见。当接收到消息时候,通知给我们的动态壁纸窗口。(为什么不用仅消息窗口?因为有些特殊系统消息不会优先发送至仅消息窗口)

分辨率动态变化问题

如何解决窗口大小改变的问题呢?

2024.06.10 注:为动态壁纸再设置子窗口时, SetWindowPlacement 函数还是需要的,这可以保证下次还原窗口大小时,大小不需要多次调整

策略:通过监视器矩形信息计算

首先使用 MonitorFromWindow 函数检索具有与指定窗口边界矩形交集面积最大的显示监视器的句柄。即根据窗口句柄获取所在监视器的设备句柄。

该函数定义如下:

HMONITOR MonitorFromWindow(

[in] HWND hwnd,

[in] DWORD dwFlags

);

如果窗口与一个或多个显示监视器矩形相交,则返回值为显示监视器的 HMONITOR 句柄,该句柄与窗口的交集面积最大。

如果窗口不与显示监视器相交,则返回值取决于 dwFlags 的值。

此参数的取值可为下列值之一:

含义

MONITOR_DEFAULTTONEAREST

返回最靠近窗口的显示监视器的句柄。

MONITOR_DEFAULTTONULL

返回 NULL

MONITOR_DEFAULTTOPRIMARY

返回主显示监视器的句柄。

显然,我们应该将此参数设置为:MONITOR_DEFAULTTONEAREST

调用成功后获得 HMONITOR 句柄。

然后,调用 GetMonitorInfo 函数检索有关显示监视器的信息。参数二指向结构体指针。

MONITORINFOEX 结构包含有关显示监视器的信息。

GetMonitorInfo 函数将信息存储到 MONITORINFOEX 结构或 MONITORINFO 结构中

MONITORINFOEX 结构是 MONITORINFO 结构的超集。 MONITORINFOEX 结构添加字符串成员以包含显示监视器的名称。

typedef struct tagMONITORINFO {

DWORD cbSize;        // 结构大小(以字节为单位)。

RECT rcMonitor;        // 以虚拟屏幕坐标表示的显示监视器矩形

RECT rcWork;            // 指定显示监视器的工作区域矩形(虚拟屏幕坐标)

DWORD dwFlags;      // 一组表示显示监视器属性的标志。

} MONITORINFO, *LPMONITORINFO;

// 扩展结构 MONITORINFOEXW

typedef struct tagMONITORINFOEXW : tagMONITORINFO {

WCHAR szDevice[CCHDEVICENAME];        // 指定正在使用的监视器的设备名称。

} MONITORINFOEXW, *LPMONITORINFOEXW;

由结构体中的 RECT rcMonitor 可以获取监视器窗口的逻辑矩形,如果再获取到监视器窗口的物理矩形,即可相除获得缩放因子。

这里使用 EnumDisplaySettings 函数来获取物理矩形信息。 EnumDisplaySettings 函数可以检索显示设备图形模式之一的相关信息。 下面是这个函数的定义:

BOOL EnumDisplaySettingsW(

[in] LPCTSTR              lpszDeviceName,

[in] DWORD                 iModeNum,

[out] DEVMODEA        *lpDevMode

);

其中,iModeNum 参数表示要检索的信息的类型。 此值可以是图形模式索引或以下值之一。

含义

ENUM_CURRENT_SETTINGS

检索显示设备的当前设置。

ENUM_REGISTRY_SETTINGS

检索当前存储在注册表中的显示设备的设置。

显然,我们需要将该参设置为 ENUM_CURRENT_SETTINGS 。

参数设置以及处理如下:

DEVMODE dm{};
dm.dmSize = sizeof(dm);
dm.dmDriverExtra = 0;
EnumDisplaySettingsW(miex.szDevice, ENUM_CURRENT_SETTINGS, &dm);
int cxPhysical = dm.dmPelsWidth;    // x 轴标度的长度
int cyPhysical = dm.dmPelsHeight;    // y 轴标度的长度

接下来就是最简单的缩放比例计算,根据系统数据,返回的浮点数是取舍为 2 位有效数字,按向上取的方法:

// 缩放比例计算
double horzScale = ((double)cxPhysical / (double)cxLogical);
double vertScale = ((double)cyPhysical / (double)cyLogical);

// 双精度浮点数取小数位,转换为百分制数值
// 先小数有效位位四舍五入进位到个位前,然后int截断取整
int i_hScale = (int)((horzScale) * 100 + 0.5);
double m_hScale = i_hScale / 100.0;// 百分制转换为有效小数位

int i_vScale = (int)((vertScale) * 100.0 + 0.5);
double m_vScale = i_vScale / 100.0;

(最新补充)运行时检测虚拟桌面切换和同步窗口所在显示器分辨率的更改

以下代码属于实验代码,还有待优化(暂不能区分多显示器):

#include <windows.h>
#include <shobjidl.h>
#include <profileapi.h>
#include <iostream>
#include <mutex>

#pragma comment(lib, "user32.lib")

// Global variables
HINSTANCE hInst;
HWND hWnd;
UINT_PTR timerId = 0;
IVirtualDesktopManager* pDesktopManager = nullptr;
GUID currentDesktopId;
LARGE_INTEGER StartingTime = { 0 };
LARGE_INTEGER PerformFrequency = { 0 };
LONGLONG LastAdjustMonitorTime = 0;
bool IsSwitchDeskOrInitWindow = true;
// Global mutex object
std::mutex switchDesk_mtx;


const wchar_t CLASS_NAME[] = L"FullscreenWindowClass";

// Forward declarations of functions included in this code module:
ATOM                MyRegisterClass(HINSTANCE hInstance);
BOOL                InitInstance(HINSTANCE, int);
LRESULT CALLBACK    WndProc(HWND, UINT, WPARAM, LPARAM);
void                CheckDesktopSwitch();
void                AdjustWindowToMonitor();
BOOL                InitPerformanceCounter();
LONGLONG            GetPerformanceTime();

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPWSTR    lpCmdLine,
    _In_ int       nCmdShow)
{
    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    InitPerformanceCounter();

    // Initialize COM
    CoInitialize(nullptr);

    // Create VirtualDesktopManager instance
    HRESULT hr = CoCreateInstance(CLSID_VirtualDesktopManager, nullptr, CLSCTX_ALL, IID_PPV_ARGS(&pDesktopManager));
    if (FAILED(hr))
    {
        MessageBoxW(nullptr, L"Failed to create VirtualDesktopManager instance", L"Error", MB_OK | MB_ICONERROR);
        return FALSE;
    }

    // Register window class
    MyRegisterClass(hInstance);

    // Perform application initialization
    if (!InitInstance(hInstance, nCmdShow))
    {
        return FALSE;
    }

    //HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_ARROW));

    MSG msg;

    // Main message loop
    while (GetMessageW(&msg, nullptr, 0, 0))
    {
        //if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
        //{
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        //}
    }

    // Cleanup
    if (pDesktopManager)
    {
        pDesktopManager->Release();
    }
    CoUninitialize();

    return (int)msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex = { 0 };

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WndProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wcex.lpszClassName = CLASS_NAME;

    return RegisterClassExW(&wcex);
}

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
    hInst = hInstance; // Store instance handle in our global variable

    // 获取特定监视器的信息(例如第一个监视器)
    HMONITOR hMonitor = MonitorFromWindow(NULL, MONITOR_DEFAULTTOPRIMARY);
    MONITORINFO mi = { 0 };
    mi.cbSize = sizeof(mi);
    if (GetMonitorInfoW(hMonitor, &mi)) {
        hWnd = CreateWindowExW(
            WS_EX_COMPOSITED,
            CLASS_NAME,
            L"Fullscreen Window",
            WS_POPUP,
            mi.rcMonitor.left, mi.rcMonitor.top,
            mi.rcMonitor.right - mi.rcMonitor.left,
            mi.rcMonitor.bottom - mi.rcMonitor.top,
            NULL,
            NULL,
            hInstance,
            NULL
        );

        if (!hWnd)
        {
            return FALSE;
        }

        ShowWindow(hWnd, nCmdShow);
        UpdateWindow(hWnd);


        // Initialize current desktop ID
        IsSwitchDeskOrInitWindow = true;
        CheckDesktopSwitch();
        IsSwitchDeskOrInitWindow = false;

        Sleep(50);

        // Set timer to check for desktop switches
        timerId = SetTimer(hWnd, 1, 1000, nullptr);
    }
    return TRUE;
}

// Inside your CheckDesktopSwitch function:
void CheckDesktopSwitch()
{
    GUID desktopId;
    if (SUCCEEDED(pDesktopManager->GetWindowDesktopId(hWnd, &desktopId)))
    {
        if (currentDesktopId != desktopId)
        {
            currentDesktopId = desktopId;

            // Lock the mutex before showing the message box
            if (!IsSwitchDeskOrInitWindow) {
                IsSwitchDeskOrInitWindow = true;
                switchDesk_mtx.lock();
                MessageBoxW(hWnd, L"窗口被固定到新的虚拟桌面!", L"通知", MB_OK);
                switchDesk_mtx.unlock(); // Unlock the mutex after message box is closed
                IsSwitchDeskOrInitWindow = false;
            }
        }
    }
}


BOOL InitPerformanceCounter() {
    return QueryPerformanceFrequency(&PerformFrequency) && QueryPerformanceCounter(&StartingTime);
}

LONGLONG GetPerformanceTime() {

    LARGE_INTEGER EndingTime = { 0 }, ElapsedMicroseconds = { 0 };

    if (PerformFrequency.QuadPart == 0) {
        QueryPerformanceFrequency(&PerformFrequency);
    }

    // Activity to be timed
    QueryPerformanceCounter(&EndingTime);
    ElapsedMicroseconds.QuadPart = EndingTime.QuadPart - StartingTime.QuadPart;

    //
    // We now have the elapsed number of ticks, along with the
    // number of ticks-per-second. We use these values
    // to convert to the number of elapsed microseconds.
    // To guard against loss-of-precision, we convert
    // to microseconds *before* dividing by ticks-per-second.
    //

    ElapsedMicroseconds.QuadPart *= 1000000;
    ElapsedMicroseconds.QuadPart /= PerformFrequency.QuadPart;

    return ElapsedMicroseconds.QuadPart;
}

void AdjustWindowToMonitor()
{
    LONGLONG newCountTime = GetPerformanceTime();
    if (!LastAdjustMonitorTime) {
        LastAdjustMonitorTime = newCountTime;
        return;
    }

    if ((newCountTime - LastAdjustMonitorTime) >= 550000) {
        LastAdjustMonitorTime = newCountTime;
        HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
        MONITORINFO mi = { 0 };
        mi.cbSize = sizeof(mi);
        if (GetMonitorInfoW(hMonitor, &mi)) {
            SetWindowPos(hWnd, HWND_TOP, mi.rcMonitor.left, mi.rcMonitor.top,
                mi.rcMonitor.right - mi.rcMonitor.left,
                mi.rcMonitor.bottom - mi.rcMonitor.top,
                SWP_NOACTIVATE | SWP_NOZORDER | SWP_FRAMECHANGED);
            MessageBoxW(hWnd, L"监视器分辨率发生了变化,窗口大小已调整!", L"通知", MB_OK);
        }
    }
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_DISPLAYCHANGE:
    case WM_MOVE:
    case WM_SIZE:
        AdjustWindowToMonitor();
        break;
    case WM_TIMER:
        if (wParam == timerId)
        {
            CheckDesktopSwitch();
        }
        break;
    case WM_CLOSE:
    {
        if (IDYES == MessageBoxW(hWnd, 
            L"确定要关闭程序吗?", L"关闭", 
            MB_ICONINFORMATION | 
            MB_APPLMODAL | 
            MB_YESNO)
            ) {
            KillTimer(hWnd, timerId);
            PostQuitMessage(0);
        }
    }
    break;
    default:
        return DefWindowProcW(hWnd, message, wParam, lParam);
    }
    return 0;
}

同步监视器分辨率更改: 

监视器分辨率更改通知

固定桌面发生变化:

检测虚拟桌面切换(暂未使用未文档化的 COM 接口)

目前正在研究的文献和资料:

这些资料和扩展显示器(更早)和虚拟多桌面(Win10/11)有关。


(注意,以下水平线内的技术细节可能用不到:)

 一开始我以为是窗口处理的大小修改消息,然后主要有两种方法,一种是在调用 SetParent 前调用 SetWindowPlacement 函数修改窗口还原状态的历史记录,另一种则是在调用后使用 SetWindowPos 函数直接修改窗口大小。但是实际结果是窗口并没有接受到消息。这里窗口大小改变主要是由于窗口的 DPI 缩放不同导致的,只需要在 SetParent 前将 DPI 设置为相同的即可。如果你还不放心,就设置计时器或者消息机制来检查窗口大小,并在被修改时候恢复原始大小。

这里以在调用后直接修改为例:

基本用法:

int scWidth, scHeight;
// 获取屏幕宽高
scWidth = GetSystemMetrics(SM_CXSCREEN);
scHeight = GetSystemMetrics(SM_CYSCREEN);

// 设置最大化

ShowWindow(hClientWnd, SW_SHOWMAXIMIZED);
SetWindowPos(hClientWnd,NULL, 0, 0, scWidth, scHeight, SWP_NOZORDER | SWP_FRAMECHANGED);

样例函数代码:

BOOL SetDynamicDesktopWindow(DesktopWndInfoPtr lpWndInfo)
{
    if (lpWndInfo == NULL)
        return FALSE;

    RECT rc = { 0,0,0,0 };
    LONG style = 0,
         SWLRetn = 0;
    HWND
         hClientWnd = (*lpWndInfo).WallpaperWnd,
         hWorkerW = (*lpWndInfo).Workerw2,
         hProgman = (*lpWndInfo).Progman;
    HWND LastParent = NULL;
    DWORD nSPerror = 0;
    int Version = (*lpWndInfo).OSVersion;
    int scWidth, scHeight, perx, pery;
    int ctWidth, ctHeight;
    //获取屏幕宽高
    scWidth = GetSystemMetrics(SM_CXSCREEN);
    scHeight = GetSystemMetrics(SM_CYSCREEN);

    // 同步窗口的风格,否则SetParent()将出现意料之外的结果。
    style = GetWindowLongW(hClientWnd, GWL_STYLE);
    style &= ~WS_POPUP & ~WS_CAPTION & ~WS_SIZEBOX;
    style |= WS_CHILD;
    SWLRetn = 
        SetWindowLongW(hClientWnd, GWL_STYLE, style);

    if (SWLRetn == 0)
    {
        printf("[-] FatalError: SetWindowLong Error!err_code[ %d ]\n", 
            GetLastError());
        return FALSE;
    }


    // 设置父窗口
    if (Version == 2) // Win 7 / Win 8
    {
        ShowWindow(hWorkerW, SW_HIDE);
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hProgman);
    }
    else {// 高版本
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hWorkerW);
    }

    nSPerror = GetLastError();
    if (nSPerror > 0 || LastParent == NULL)
    {
        printf("[-] FatalError: SetParent Error!err_code[ %d ]\n \n", nSPerror);
        return FALSE;
    }
    
    
    // 确保动态壁纸主窗口是全屏的
    ShowWindow(hClientWnd, SW_SHOWMAXIMIZED);
    SetWindowPos(hClientWnd,
        NULL, 0, 0, scWidth, scHeight, SWP_NOZORDER | SWP_FRAMECHANGED);
    
    GetWindowRect(hClientWnd, &rc);
    ctWidth = rc.right - rc.left;
    ctHeight = rc.bottom - rc.top;

    if (ctWidth != scWidth || ctHeight != scHeight)
    {
        printf("[-] SetWindowPos failed.\n \n");
        return FALSE;
    }
    printf("[+] SetWindowPos Success.\n \n");
    printf("[+] SetWallpeperWindow Successfully!\n");
    return TRUE;
}

但是,重新编译后运行的效果却不如意:

我们发现,在 Win 11 系统上窗口依然无法全屏显示,我们首先检查壁纸窗口的矩形,下图是使用 GetSystemMetrics 函数获取的屏幕矩形,获取到的矩形数据为 960\times1707:

我们打开系统设置,找到屏幕分辨率,在我机器上是 2560 \times 1440,缩放比例(DPI 因子)是 1.5:

不难发现如下转换公式:

显示器矩形 = 原始矩形数据\times DPI缩放因子

那么,我们如何获取当前显示器的缩放比例呢?

策略:通过 DWM API 函数 DwmGetWindowAttribute

HRESULT DwmGetWindowAttribute(
        HWND  hwnd,
        DWORD dwAttribute,
  [out] PVOID pvAttribute,
        DWORD cbAttribute
);

【参数】

1. hwnd

        要从中检索属性值的窗口的句柄。

2. dwAttribute

        描述要检索的值的标志,指定为 DWMWINDOWATTRIBUTE 枚举的值。 此参数指定要检索的属性, pvAttribute 参数指向在其中检索属性值的对象。

3. [out] pvAttribute

        指向值的指针,当此函数成功返回时,该值接收特性的当前值。 检索到的值的类型取决于 dwAttribute 参数的值。 DWMWINDOWATTRIBUTE 枚举主题在每个标志的行中指示应在 pvAttribute 参数中将指针传递给的值的类型。

4. cbAttribute

        通过 pvAttribute 参数接收的属性值的大小(以字节为单位)。 检索到的值的类型及其大小(以字节为单位)取决于 dwAttribute 参数的值。

DWMWINDOWATTRIBUTE 中如果指定 DWMWA_EXTENDED_FRAME_BOUNDS 与 DwmGetWindowAttribute 一起使用。 函数将检索屏幕空间中的扩展框架边界矩形。 检索到的值的类型为 RECT

根据这个函数以及 GetWindowRect 可以计算出缩放比例,不过线程的 DPI 设置必须是无感知的,否则调用函数返回值为 0,也就无法计算:

RECT r1 = { 0 };
RECT r2 = { 0 };
double vScale = 0, hScale = 0;
DWORD cbyte = sizeof(r2);

// 获取不包含 DPI 缩放的数值
GetWindowRect(hWnd, &r1);
double width1 = r1.right - r1.left;
double height1 = r1.bottom - r1.top;

// 获取 DPI 下扩展矩形(窗口必须可见,且调用方线程 DPI 是无感知模式)
DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &r2, cbyte);
double width2 = r2.right - r2.left;
double height2 = r2.bottom - r2.top;

// 计算 DPI 因子
hScale = width2 / width1;
hScale = height2 / height1;

// 输出结果
printf("DPI Value: \n  horzScale: [%lf]\n  vertScale: [%lf]\n"
        , horzScale, vertScale);

 以上测试的完整代码:

#include <iostream>
#include <windows.h>
#include <winuser.h>
#include <dwmapi.h>
#include <ShellScalingApi.h>		// 引用头文件
#pragma comment(lib, "Shcore.lib")	// 链接库文件
#pragma comment(lib, "dwmapi.lib")

int main()
{
    // 获取窗口当前显示的监视器
    printf("\n\nGetMonitorInfo: \n\n");
    HWND hWnd = GetDesktopWindow();//根据需要可以替换成自己程序的句柄 
    HMONITOR hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);

    // 获取监视器逻辑宽度与高度
    MONITORINFOEX miex{};
    miex.cbSize = sizeof(miex);
    GetMonitorInfoW(hMonitor, &miex);
    int cxLogical = (miex.rcMonitor.right - miex.rcMonitor.left);
    int cyLogical = (miex.rcMonitor.bottom - miex.rcMonitor.top);

    // 获取监视器物理宽度与高度
    DEVMODE dm;
    dm.dmSize = sizeof(dm);
    dm.dmDriverExtra = 0;
    EnumDisplaySettingsW(miex.szDevice, ENUM_CURRENT_SETTINGS, &dm);
    int cxPhysical = dm.dmPelsWidth;
    int cyPhysical = dm.dmPelsHeight;

    // 缩放比例计算
    double horzScale = ((double)cxPhysical / (double)cxLogical);
    double vertScale = ((double)cyPhysical / (double)cyLogical);

    // 双精度浮点数取小数位,转换为百分制数值
    // 先小数有效位位四舍五入进位到个位前,然后int截断取整
    int i_hScale = (int)((horzScale) * 100 + 0.5);
    double m_hScale = i_hScale / 100.0;// 百分制转换为有效小数位
    int i_vScale = (int)((vertScale) * 100.0 + 0.5);
    double m_vScale = i_vScale / 100.0;
    printf("DPI ExactValue:\n  horzScale: [%lf]\n  vertScale: [%lf]\n\n"
        ,horzScale, vertScale);
    printf("DPI SysSetValue:\n  horzScale(percent): %d\n  vertScale(percent): %d\n\n"
        ,i_hScale, i_vScale);

    // ----------------------------------------------//
    printf("\n\nDwmGetWindowAttribute: \n\n");
    RECT r1 = { 0 };
    RECT r2 = { 0 };
    DWORD cbyte = sizeof(r2);
    double vScale = 0, hScale = 0;
    GetWindowRect(hWnd, &r1);
    double width1 = r1.right - r1.left;
    double height1 = r1.bottom - r1.top;

    
    DwmGetWindowAttribute(hWnd, DWMWA_EXTENDED_FRAME_BOUNDS, &r2, cbyte);
    double width2 = r2.right - r2.left;
    double height2 = r2.bottom - r2.top;

    
    hScale = width2 / width1;
    hScale = height2 / height1;

    printf("DPI Value: \n  horzScale: [%lf]\n  vertScale: [%lf]\n"
        , horzScale, vertScale);

    system("pause");
    return 0;
}

程序执行的结果如下:

成功获取到 DPI 缩放因子。根据缩放因子可以计算我们窗口需要的大小。

但是在实践中,发现这样子的方法比较麻烦,因为每个控件窗口都要重新计算大小,并且 DPI发生变化时候不能及时响应。


(有效)微软提供了一套 API 用于修改 执行进程/线程的 DPI 感知模式。

SetProcessDpiAwarenessContext、SetProcessDpiAwareness、SetProcessDPIAware、SetThreadDpiAwarenessContext、SetThreadDpiHostingBehavior、GetThreadDpiAwarenessContext、GetProcessDpiAwarenessContext、GetWindowDpiAwarenessContext、GetProcessDpiAwareness。

但是在实际测试过程中发现,这些函数会导致意外的行为,并且适用的版本受系统和编译器版本影响较为严重,比如最典型的开启了进程的监视器 DPI 感知,在 Win 10 和 Win 11 上基本不会出现差错,但是在 Win 8 上却没有实现正确的获取窗口缩放矩形。另外一点就是这些函数有些只能在 Win 8.1 以上调用,而在更早的系统中却没有。我是我迫切的需要一个能够简化这些操作的方法。

然后,在查阅 MSDN 时候我看到了这一句警告,给了我很大帮助:

建议通过应用程序清单(而不是 API 调用)设置进程默认 DPI 感知。 有关详细信息 ,请参阅设置进程的默认 DPI 感知 。 通过 API 调用设置进程默认 DPI 感知可能会导致意外的应用程序行为。

根据提示,我们可以在项目的属性页面中找到:在 项目属性->清单工具->输入和输出->DPI 识别功能中,将属性改为 每个显示器高DPI识别 即可。将任务交给编译器,他会为我们添加合适的代码。
 

再次编译,完美获取到正确的数值。但是,其实还有问题,Win8 好像依然无效,原因怪微软以前的接口和现在不一样,有 V1、V2 和 V3。此时,最佳的方法就是用程序清单来处理不同系统情况。

使用下面清单文件,可以彻底解决该问题。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:application>
    <asmv3:windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

Patch:此处的动画需要重写或删除,例如可以改为嵌入窗口之后再显示出来)接着,为了避免设置父窗口产生的突兀感,通过折叠展开的效果,沿对角线扩展窗口矩形,可以产生较为舒适的视觉效果:

// 计算增量
int perx = scWidth / 10;
int pery = scHeight / 10;

// 循环设置大小
for (int i = 0; i <= 10; i++)
{
    //SetWindowPos(hClientWnd,
    //    NULL, 0, 0, perx, pery, SWP_FRAMECHANGED);
    // 补丁:2024.06.16
    SetWindowPos(hClientWnd,
        NULL, 0, 0, perx, pery, SWP_NOZORDER | SWP_FRAMECHANGED);
    perx += perx;
    pery += pery;
    Sleep(35); // 延迟产生视觉暂留
}

06.16 修正参数错误:调用 SetWindowPos 时,若不需要更改 Z 序,则要指定 SWP_NOZORDER 参数。不然第二个参数 hWndlnsertAfter 为 0 时,将被解释为 HWND_TOP((HWND)0) 而导致内部尝试更改 Z 序。

详见:https://bbs.csdn.net/topics/390844480

最后,通过 GetWindowRect 检查窗口大小:

GetWindowRect(hClientWnd, &rc);
int ctWidth = rc.right - rc.left;
int ctHeight = rc.bottom - rc.top;

if (ctWidth != scWidth || ctHeight != scHeight)
{
    printf("[-] SetWindowPos failed.\n \n");
    return FALSE;
}

2.3 如何获取并判断系统版本(未来可能更改)

(这部分未来可能更改为通过具体版本号划分的检测策略)这里我们通过 RtlGetNtVersionNumbers 函数来获取 OS 版本号,并根据版本号信息来分类,主要分三类:(1)Win Vista 或者更早;(2)Win 7 和 Win 8(但是 Win7 要单独检查 Uxsms 服务);(3)Win 8.1 至今

下表列出了不同操作系统的版本号:

序号系统名称OS 版本号
1Windows Vista6.0
2Windows 76.1
3Windows 86.2
4Windows 8.16.3
5Windows 10, 11

10.0

根据要求,编写的系统分类代码如下:

int CheckOsVersion()
{
    int OsVersion = -1;
    typedef void(__stdcall* NTPROC)(DWORD*, DWORD*, DWORD*);
    HINSTANCE hinst = GetModuleHandleA("ntdll.dll");//加载DLL

    NTPROC GetNtVersionNumbers = (NTPROC)
        GetProcAddress(hinst, "RtlGetNtVersionNumbers");//获取函数地址
    if (!hinst || !GetNtVersionNumbers)
    {
        return 0;
    }

    DWORD dwMajor, dwMinor, dwBuildNumber;
    GetNtVersionNumbers(&dwMajor, &dwMinor, &dwBuildNumber);

    /* 旧版本判断
    if (dwMajor <= 6 && dwMinor <= 2)
    {

        if (dwMajor == 6 && dwMinor >= 1) return 2;// win 7, win8
        else return 1;// win XP, vista
    }
    else {
        return 3;// win8.1,10,11
    }
    */
    // 增加对 Win 7 的特殊分类处理
    switch (dwMajor)
    {
    case 6:
        if (dwMinor == 0) // Vista
        {
            OsVersion = 1;
        }
        else if (dwMinor == 1)  // Win 7
        {
            OsVersion = 2;
        }
        else if (dwMinor == 2)  // Win 8
        {
            OsVersion = 3;
        }
        else if (dwMinor == 3)  // Win 8.1
        {
            OsVersion = 4;
        }
        break;
    case 10:
        OsVersion = 4; // Win 10, 11
        break;
    default:
        if(dwMajor <= 5)  // Win XP
            OsVersion = 1;
        else
            OsVersion = 5; //  Future
        break;
    }
    
    return OsVersion;
}

2.4 解决退出程序时窗口残影问题

(这部分补充内容创建于 2024.03.19,更新于 2024.06.25)

完成了上面的所有操作后,我们可以将一个窗口插入到桌面窗口层之中,并成功显示出我们的窗口。但是,当我们的动态壁纸窗口关闭后,桌面图标层依然显示窗口关闭前的绘图。这是由桌面窗口层的延迟渲染导致的,桌面图标层不会在背景层更新时立即更新自身的背景副本。

解决方法就是通知桌面窗口 “桌面壁纸层已经更新” 的消息。我们使用 SystemParametersInfo 函数来实现发送特定于系统的通知消息。

SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, nullptr, SPIF_UPDATEINIFILE);

当执行该函数后,消息发送会使得桌面窗口层重新创建能够显示桌面壁纸(副本)的 WorkerW 窗口。

注释:若以手动方式点击任务栏上的 “任务视图” 窗口,也会触发刷新过程,只不过此时一般不重建 WorkerW。而通过该方式完成的操作则会重建窗口。

注意:此刷新桌面壁纸窗口的 API 是异步的,当在初始化动态壁纸时尝试使用该操作刷新桌面,有可能导致后面获取到错误的 WorkerW 窗口,如需使用请设置延迟或检查。

你现在得到了一个新的 WorkerW 窗口(部分系统上不会重建窗口),并在内部处理机制的协助下刷新了桌面图标层的绘图。所以,你将得到一份干净的原始桌面。

2.5 上文注意事项的一些补充

注意:以下补充内容仅适用于 Win11 23H2 以及更早期系统的设置,对于 24H2 以及更高版本将不适用,未来将重写这里的窗口遍历代码逻辑。[截至目前更新未完全解决问题]

第一点:在枚举窗口的时候,我们采用的是先找到 ShellDefView,再去获取其他窗口句柄,默认了 DefView 的父窗口为 WorkerW 1,这实际上是存在问题的,在窗口不存在或者 DWM 异常的时候,代码继续执行,后面的行为不可预知,所以我们要对 EnumWindow 获取到的句柄进行简单的验证:

// 验证方法

if (hWorkerW1 != NULL && hWorkerW1 != hwnd
    && hParent == hWorkerW1) 

(*lpWndInfo).Progman = hwnd;
printf("[*] EnumDesktopWorkerWindows...\n");

//枚举窗口
SetLastError(0);
if (!EnumWindows(EnumWindowsProc, (LPARAM)lpWndInfo) || GetLastError() > 0)
{
    printf("[-] EnumWindows failed.\n");
    return FALSE;
}
printf("[+] EnumWindows Finished.\n");

// 校验 WorkerW1 是否是窗口是桌面。
hWorkerW1 = (*lpWndInfo).Workerw1;
hWorkerW2 = (*lpWndInfo).Workerw2;
hDefView = (*lpWndInfo).ShellDefView;

HWND hParent = GetParent(hDefView);

if (hWorkerW1 != NULL && hWorkerW1 != hwnd
    && hParent == hWorkerW1) // DWM 正常时,窗口不应该是 Progman 应该是 WorkerW
{
    printf("[+] EnumWindows successfully.\n \n");
    printf("[+] WorkerW 1: [ 0x%I64X ] | WorkerW 2: [ 0x%I64X ]\n", 
        (long long)hWorkerW1,
        (long long)hWorkerW2);
}
else {
    printf("[-] EnumWindows failed.\n \n");
    return FALSE;
}

第二点:对于主窗口处理线程,我们需要给其设置一定的延迟和判断其完成进度、超时限制等等,不能简单设置 Sleep 啥的。等待是使用 WaitForSingleObject 和 GetExitCodeThread 函数完成的。

start = clock();// 开始计时
    HANDLE hThread = CreateThread(NULL, 0, mainWindowThread, lpWndInfo, 0, NULL);
    if (hThread == NULL)
    {
        printf("[*] CreateThread failed.err_code[ %d ]\n", GetLastError());
        return FALSE;
    }
    DWORD dwExitCode = 0;
    // 等待窗口加载完成
    do {
        IsGetExitCode = GetExitCodeThread(hThread, &dwExitCode);
        WaitForSingleObject(hThread, 0);// WAIT_TIMEOUT
        CreateFlag = (*lpWndInfo).IsCreatedWindow;
        finish = clock();
        duration = (double)(finish - start) / CLOCKS_PER_SEC;
    } while (!CreateFlag && // 判断窗口是否已经创建
        dwExitCode == STILL_ACTIVE // 判断线程是否还存活
        && IsGetExitCode != FALSE // 如果崩溃退出,则结束等待
        && fabs(duration) <= eps);// 等待超时时间
    if (!CreateFlag)// 错误日志
    {
        printf("[*] CreateThread failed. ERROR_TIMEOUT\n \n");
        return FALSE;
    }
    else {
        printf("[+] Thread Handle: [ 0x%I64X ]\n \n", (long long)hThread);
        return TRUE;
    }

 第三点:SetParent 设置父窗口需要检查,父窗口不可是 NULL,这样子可以在最后一步前也能够防止异常值。

printf("[*] SetParent: SetWallpaperWindow.\n");
    
    if (Version == 2 || Version == 3) //  Win 7, Win 8
    {
        if (hProgman == NULL)// TODO:父窗口不可以为 NULL
        {
            PostMessageW(hClientWnd, SC_CLOSE, 0,0);
            SetLastError(ERROR_INVALID_WINDOW_HANDLE);
            printf("[-] SetParent Fatal error: hParent must not be NULL.\n");
            return FALSE;
        }
        ShowWindow(hWorkerW, SW_HIDE);
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hProgman);
    }
    else {
        if (hWorkerW == NULL)
        {
            PostMessageW(hClientWnd, SC_CLOSE, 0, 0);
            SetLastError(ERROR_INVALID_WINDOW_HANDLE);
            printf("[-] SetParent Fatal error: hParent must not be NULL.\n");
            return FALSE;
        }
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hWorkerW);
    }

    nSPerror = GetLastError();
    if (nSPerror > 0 || LastParent == NULL)
    {
        printf("[-] FatalError: SetParent Error!err_code[ %d ]\n \n", nSPerror);
        return FALSE;
    }
    printf("[+] SetParent Success, hChild[ 0x%I64X ] | hParent[ 0x%I64X ].\n \n",
        (long long)hClientWnd, 
        Version == 2 ? (long long)hProgman : (long long)hWorkerW);

3 关于 0x052C 私有消息的兼容性问题(重要)

(此部分于 2024.06.04 补充)

本文介绍的设置动态壁纸图层的方法是利用了从 Win 7 SP1(再往 Win7 早期版本推暂时不确定是否支持,有资料显示打 SP1 补丁之前部分版本上面不支持)开始的桌面壁纸平滑切换机制,在该过程中,explorer 的 PM (Program Manager) 壁纸层接受私有消息 0x052C (大于 0x400 的属于用户自定义消息),这会使得在 Defview( SHELL_DefView) 图标层和 PM 壁纸层之间建立两个缓冲图层(绘制在窗口上),这两个窗口具有 "WorkerW" 类名。

发送私有消息后的窗口层次(一般情况)

3.1 0x052C 处理机制的潜在兼容性问题

我们需要明白的是,两个 WorkerW 窗口并不是没有作用的。它们是为了在发生活动桌面状态切换时,绘制出动画效果(可以理解为在蒙版上面绘制出动画过渡)而被创建的(通过 SHCreateWorkerWindowW 函数创建,随后 SetWindowLongPtrW 设置窗口回调函数)。

研究发现,0x052C 消息的 wParam 和 lParam 不是没有用的,也不是一直设置为 0。

当 wParam 设置为 1 时,会销毁 WorkerW 窗口,为 0 是默认的创建 WorkerW 窗口。此外,一些其他特殊值会带来细节上不同的结果。(分析来自文章:《如何将窗口绘制到桌面图标下》) 

对 WorkerW 的机制研究不够精细,这会导致当我们只是粗心地将窗口放置在第二个 WorkerW 窗口下或者 PM 窗口下就去吃饭时,有可能出现一些意外的情况。具体情况有(部分提示来自 @ CLeDen 的 评论):

  • 据报道部分 Win 11 系统上对默认的 0 参数消息没有动作,必须发送带有有效参数的 0x052C 消息:第一次 wParam 为 0xD,lParam 为 1。但 wParam 为 0xD 将至少导致在 Win7 上返回错误代码;
  • 如果在系统设置中指定桌面壁纸模式为 “幻灯片放映”,则当壁纸切换超时时间到达时,系统会在 WorkerW 2 的前面创建一个新 WorkerW 窗口用来绘制动画。导致播放动画时,在 WorkerW 3 上的过渡动画将覆盖 WorkerW 2 上的画面,在动画播放结束后才能恢复 Worker 2 的显示(当不透明度达到 100% 后 WorkerW 3 才被关闭);
  • 如果用户或其他应用尝试手动设置系统壁纸,则会默认播放切换动画,这会导致新生成的 WorkerW 3 的画面短暂覆盖 WorkerW 2 上的画面;
  • 在切换虚拟桌面时,会出现动态壁纸的画面撕裂感  [ 这是因为绘制的系统壁纸的滑动动画在 Z 轴上覆盖我们的窗口。 Program 窗口会被设置到 WorkerW 2 窗口的前面;随后将包含桌面图标窗口的 WorkerW 1 窗口移动到 Desktop Band 中,并先设置 WorkerW 1 窗口的 Z 序到类名为 tooltips_class32 的窗口的前面,再设置 WorkerW 1 窗口为 Desktop 窗口的最底部窗口。这将使得桌面图标窗口在桌面用户窗口带(Desktop Band)的后面导致不可见,于是出现动画时候我们看到的是 PM 窗口或者镜像在水平滑动,同时将观察到桌面图标短暂消失的现象];
  • 鼠标悬停在非活动桌面的 DWM 缩略图预览窗口(ThumbnailToolsWindow)上面时,有机会播放壁纸切换动画,当其中一个桌面的壁纸已经更新时。此时,会出现画面覆盖;
  • 虚拟多桌面的 DWM 缩略图预览窗口上,可能无法正常显示覆盖后的桌面动态壁纸的图像;
  • 在 Win8.1 以及更早系统上,在关闭视觉动画优化后,会立即销毁已经存在的 WorkerW 窗口(这会一并结束子窗口),之后 0x052C 消息将无效化,导致不能生成有效的 WorkerW 窗口;
  • 在 Win7 上,更改系统主题时,会重绘 WorkerW 窗口并使其再次可见(之前被我们手动隐藏),导致我们的壁纸窗口的绘制被覆盖;
  • 在 Win11 23H2 上切换系统主题时,会绘制淡入淡出,短暂覆盖动态壁纸窗口;
  • 在 Win11 24H2 预览版上面切换壁纸或者主题将会导致 WorkerW 窗口被关闭(已解决,将在后期更新到文章);

如下图所示为 Win11 的桌面背景播放设置: 

设置为 "幻灯片放映" 并且超时时间较短时容易观察到覆盖现象

当切换播放模式为 “幻灯片播放” 时,会触发淡入淡出动画:

幻灯片模式存在兼容性问题

切换主题时,会播放淡入淡出动画:

切换主题时存在兼容性问题

通过一个简单的代码可以验证窗口被覆盖的原因:

动画播放过程中有新窗口创建

备注(这可能是设计 BUG):当主题中背景播放设置为"图片"时,切换到此主题时会尝试关闭所有没有子窗口且 Z 序低于 WorkerW 1 的 WorkerW 窗口(估计是只关闭一个),并且不播放淡入淡出动画;当不切换主题的前提条件下,将背景播放为"图片"的设定换成"幻灯片"再换回"图片",不会额外关闭 WorkerW(也就是说只会关一次),这使得后面切换背景图片也会正常播放动画。由此可见,目前 23H2 在关闭动画窗口的设计上是存在 BUG 的。

淡入淡出动画的 BUG

代码:

#include <windows.h>
#include <vector>
#include <iostream>
#include <algorithm>
#include <string>

// 结构体用于存储窗口信息
struct WindowInfo {
    HWND hwnd;
    HWND shellViewHwnd;
};

// 全局变量
std::vector<WindowInfo> trackedWindows;
std::vector<HWND> prevZOrder;

// 获取窗口的类名和标题
std::string GetWindowClassAndTitle(HWND hwnd) {
    char className[256];
    char windowTitle[256];

    GetClassNameA(hwnd, className, sizeof(className));
    GetWindowTextA(hwnd, windowTitle, sizeof(windowTitle));

    std::string result = std::string(className) + " - " + std::string(windowTitle);
    return result;
}

// 枚举子窗口,寻找 SHELLDLL_DefView
BOOL CALLBACK EnumChildProc(HWND hwnd, LPARAM lParam) {
    char className[256];
    GetClassNameA(hwnd, className, sizeof(className));

    if (strcmp(className, "SHELLDLL_DefView") == 0) {
        WindowInfo* info = reinterpret_cast<WindowInfo*>(lParam);
        info->shellViewHwnd = hwnd;
        return FALSE;  // 找到目标窗口,停止枚举
    }
    return TRUE;  // 继续枚举
}

// 枚举顶层窗口,寻找 WorkerW
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
    char className[256];
    GetClassNameA(hwnd, className, sizeof(className));

    if (strcmp(className, "WorkerW") == 0) {
        WindowInfo info;
        info.hwnd = hwnd;
        info.shellViewHwnd = nullptr;

        // 枚举 WorkerW 的子窗口
        EnumChildWindows(hwnd, EnumChildProc, reinterpret_cast<LPARAM>(&info));

        if (info.shellViewHwnd) {
            // 找到包含 SHELLDLL_DefView 的 WorkerW 窗口
            trackedWindows.push_back(info);
        }
    }
    return TRUE;
}

// 获取目标窗口下的 Z 序顺序
std::vector<HWND> GetZOrder(HWND target) {
    std::vector<HWND> zOrder;
    HWND hwnd = target;
    while ((hwnd = GetNextWindow(hwnd, GW_HWNDNEXT)) != nullptr) {
        zOrder.push_back(hwnd);
    }
    return zOrder;
}

// 打印 Z 序和窗口信息
void PrintZOrder(const std::vector<HWND>& zOrder) {
    for (HWND hwnd : zOrder) {
        std::cout << "HWND: " << hwnd << " - " << GetWindowClassAndTitle(hwnd) << std::endl;
    }
}

// 监控 Z 序变化
void MonitorZOrder() {
    while (true) {
        for (const auto& winInfo : trackedWindows) {
            std::vector<HWND> currentZOrder = GetZOrder(winInfo.hwnd);
            if (currentZOrder != prevZOrder) {
                std::cout << "Z-order has changed!" << std::endl;
                PrintZOrder(currentZOrder);
                prevZOrder = currentZOrder;
            }
        }
        Sleep(100);  // 每100ms检查一次
    }
}

int main() {
    // 第一步:枚举 WorkerW 窗口,找到包含 SHELLDLL_DefView 的窗口
    EnumWindows(EnumWindowsProc, 0);

    if (trackedWindows.empty()) {
        std::cout << "No WorkerW windows with SHELLDLL_DefView found." << std::endl;
        return 0;
    }

    // 第二步:获取初始 Z 序顺序并打印窗口信息
    for (const auto& winInfo : trackedWindows) {
        prevZOrder = GetZOrder(winInfo.hwnd);
        PrintZOrder(prevZOrder);
    }

    // 第三步:监控 Z 序变化
    MonitorZOrder();

    return 0;
}

 而在 Windows 7 上,切换主题时候,会通过在 WorkerW 窗口的设备上下文重绘 PM 窗口的图像(此时被隐藏的 WorkerW 窗口会显示出来)。导致我们的壁纸窗口无法显现:

Win 7 嵌入动态壁纸状态

下图是监视该 WorkerW 窗口的消息日志,关注红色框出的部分:

主题发生切换时接收到的消息

观察下图所示的主题切换时, Win7 动态壁纸显示被覆盖的状态:

Win 7 切换主题时,壁纸被覆盖掉

下图是动画设置选项:

Win 8.1 动画优化设置

如下面动画所示,当关闭动画优化后,将导致无法设置动态壁纸(不知道调过什么设置后又可以使用了):

因动画优化导致的动态壁纸无效的情况

Windows 性能选项:

有些机器上为了提高性能会被设置为取消选中:窗口内的动画控件和元素、在最大化和最小化时显示窗口动画、任务栏中的动画选项。

Windows 性能选项

此外,已知 0x052C 消息的处理在不同系统上表现不一样:

  • 在 Vista 以及更早期的操作系统上不受支持
  • 在部分 Win7 上不支持通过 0x052C 消息使得桌面变为活动桌面(产生两个 WorkerW);
  • 已知 Win7 家庭普通版和家庭基础版的 Aero 优化是被屏蔽的,暂未确定是否会导致对动态壁纸设计不支持;
  • 在部分 Win 10 系统上 0x052C 消息只能产生一个 WorkerW 窗口或者无动作,如 Win10 1903 不响应 0x052C 消息 (2023.10.19);
  • 已知外部测试渠道上的 Win11 24H2 (计划于 24 年 9 月份全面推广)已经开始取消 0x052C 动画机制(微软改用了文本模糊效果,所以不再需要两个 WorkerW 了,可能是出于减少系统资源开销考虑)。

已知 Win7 上不支持部分类型 0x052C 消息,如 wParam 为 0xD 将会得到下图所示的错误代码,但支持 0x052C wParam 和 lParam 都缺省为 0(可以插入动态壁纸)。

Win 7 家庭版不支持 0x052C 消息的扩展功能

3.2 对已知兼容性问题的解决方案

解决方案:

(等待研究后更新)

1)Win 7 在使用 API 关闭 DWM 窗口合成功能时,系统会向所有顶级窗口发送 WM_DWMCOMPOSITIONCHANGED 消息,当 WorkerW 2 接收到此消息时,会销毁自身,并最终恢复传统的桌面窗口层次,导致壁纸窗口突出且失效;

检测的消息日志

当注册表设置 HKEY-Current-User\Software\Microsoft\Windows\DWM\Composition 为 0 时,DWM 的窗口合成也会禁用。

目前已经初步实现动态监测 DWM 窗口合成状态的代码,效果如下所示:

动态监测 Dwm 窗口合成是否启用并尝试恢复的工具效果

2)Win11 24H2 在切换背景壁纸或者主题时,系统会重建 WorkerW 窗口,导致作为子窗口的动态壁纸窗口被一并关闭,这会导致程序异常,严重时 DWM 可能会绿屏报错。经过研究,发现主要是WorkerW 接收到了 WM_SETTINGCHANGE(0x1A)且 wParam == SPI_SETDESKTOPWALLPAPER(0x14),lParam 为 0 的消息,这一般是由 SystemParametersInfo 函数调用引起的。随后发送 0x90 私有消息给自身,最终由窗口调用 WM_DESTROY 和 PostQuitMessage(0) 退出窗口和 GUI 线程。

Win11 24H2 窗口重建问题

目前想到的方案是窗口子类化或者 HOOK 拦截这两个特殊消息,直接返回 0 来绕过。

3)在修改窗口壁纸时候,会发送 WM_ERASEBKGND 和 WM_PAINT 消息给 WorkerW 窗口,需要通过窗口子类化拦截此消息,来避免图象被短暂的动画(有时候时间很长)覆盖。

已知 WH_CALLWNDPROC 、WH_CALLWNDPROCRET 和 WH_GETMESSAGE 钩子均不起作用。所以,最佳方案就是窗口子类化了。

WH_CALLWNDPROC 只能在窗口回调接受消息之前获取消息但不能修改消息传递链;

WH_CALLWNDPROCRET 窗口处理完消息后将处理结果通知给注册此钩子的线程,不能修改消息传递链;

WH_GETMESSAGE 只能拦截和修改通过 GetMessage 或 PeekMessage 处理的消息队列消息。WM_CREATE、WM_PAINT、WM_COPYDATA、WM_ERASEBKGND 等直接通过同步分派发送的消息是直接发送给窗口过程的,而不是通过消息队列传递。

所以自然没有合适的 Win32 Hook 可以拦截并修改我们需要的消息,由此看来只有两种方法:

(1)通过窗口子类化修改特定窗口对应的窗口回调(容易实现);

(2)通过逆向工程对未导出的窗口回调函数进行 Inline Hook,修改窗口过程。

相关文献参考:

为什么 GetMessage 不处理WM_POWERBROADCAST消息?- stackoverflow

2024.07.10 更新——目前已经编写了一个窗口子类化的示例代码,未来将会把窗口子类化技术运用于此检测。示例代码见文章:https://blog.csdn.net/qq_59075481/article/details/140334106。 

4)窗口覆盖检测:当全屏窗口遮挡桌面时候,暂停动态壁纸。该检测通过多个检测逻辑和逃逸规则来完成。现已更新在个人博客:

全屏窗口检测研究

5)检测特殊系统状态

睡眠时暂停动态壁纸:

检测系统睡眠状态的方法是注册系统通知回调,在 Win8 以上使用 PowerRegisterSuspendResumeNotification 通知线程;在 Win7 上处理 WM_POWERBROADCAST 消息。

适用于 Win 8 及以上系统的睡眠状态检测:

#include <windows.h>
#include <iostream>
#include <powrprof.h>

#pragma comment(lib, "Powrprof.lib")


std::wstring GetDayOfWeek(int dayOfWeek) {
    switch (dayOfWeek) {
    case 0: return L"Sunday";
    case 1: return L"Monday";
    case 2: return L"Tuesday";
    case 3: return L"Wednesday";
    case 4: return L"Thursday";
    case 5: return L"Friday";
    case 6: return L"Saturday";
    default: return L"";
    }
}

std::wstring GetCurrentDateTime() {
    SYSTEMTIME systemTime;
    GetLocalTime(&systemTime);

    wchar_t dateTimeBuffer[150] = { 0 };
    std::wstring dayOfWeek = GetDayOfWeek(systemTime.wDayOfWeek);
    swprintf_s(dateTimeBuffer, L"%04d-%02d-%02d %s %02d:%02d:%02d",
        systemTime.wYear, systemTime.wMonth, systemTime.wDay,
        dayOfWeek.c_str(),
        systemTime.wHour, systemTime.wMinute, systemTime.wSecond);

    return std::wstring(dateTimeBuffer);
}


ULONG CALLBACK DeviceCallback(PVOID Context, ULONG Type, PVOID Setting)
{
    if (Type == PBT_APMSUSPEND)
    {
        std::wstring currentDateTime = GetCurrentDateTime();
        std::wcout << "Sleeping at time: " << currentDateTime << std::endl;
    }
    if (Type == PBT_APMRESUMESUSPEND)
    {
        std::wstring currentDateTime = GetCurrentDateTime();
        std::wcout << "Awaking at time: " << currentDateTime << std::endl;
    }
    return ERROR_SUCCESS;
}

int main()
{
    HPOWERNOTIFY g_power_notify_handle = NULL;
    DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS params = { 0 };
    params.Callback = DeviceCallback;
    params.Context = 0;
    PowerRegisterSuspendResumeNotification(DEVICE_NOTIFY_CALLBACK, &params, &g_power_notify_handle);
    MSG msg;
    while (GetMessageW(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
    PowerUnregisterSuspendResumeNotification(g_power_notify_handle);
    return 0;
}

检测休眠的方法(休眠检测方法没有验证过是否可行):

使用 CallNtPowerInformation 并指定 InformationLevel 为 SystemPowerCapabilities,lpOutputBuffer 缓冲区接收包含当前系统电源功能的 SYSTEM_POWER_CAPABILITIES 结构。从 SYSTEM_POWER_CAPABILITIES 数据结构中检查成员 HiberFilePresent 的值如果此成员为TRUE,则存在系统休眠文件。

检测屏幕保护程序是否已经启用:通过注册表路径 [HKEY_CURRENT_USER\Control Panel\Desktop] 检查 SCRNSAVE.EXE 值项以便于获取屏幕保护程序的路径,然后检测当前活动进程是否有屏幕保护程序。但要注意的是,有两种情况下检测不准确一是正在打开屏幕保护程序设置,此时应该检查 Rundll32.exe 是否正在启动,如果其命令行参数为 " shell32.dll,Control_RunDLL desk.cpl,ScreenSaver,@ScreenSaver" 则认为当前在调整设置。另外一种情况就是需要检测用户空闲时间,检查 ScreenSaveTimeOut 和 ScreenSaveActive (为 1)值项, ScreenSaveTimeOut 是按照秒来记录的超时时间(系统设置中以分钟来显示,比如 5 分钟就是 300 秒)。然后通过以下算法检测空闲时间:

为了确定用户空闲的时间,系统提供 GetLastInputInfo API调用。要在用户空闲了指定的时间后得到通知,应用程序通常会执行以下操作:

  1. 设置具有指定超时的计时器(SetTimer);
  2. 当计时器超时时,计算 GetTickCount 和 GetLastInputInfo 之间的差值(dwCurrent-dwLastInput 可靠地计算自上次用户输入以来经过的时间,即使 GetTickCount 循环回到 0;无符号整数溢出在 C 和 C++ 中有很好的定义),并检查 powercfg -requests 返回值是否为空,来判断是否将进入屏幕保护或睡眠;
  3. 如果差值小于指定的超时值,请从步骤 1 开始,使用剩余的超时时间设置计时器;否则,在没有用户输入的情况下超时已经过去,所以在用户空闲指定的时间后,请执行程序需要执行的任何操作,例如停止动态壁纸(释放资源)。

判断显示器是否已经关闭:

当显示器关闭时,暂停播放动态壁纸。通过注册 PBT_POWERSETTINGCHANGE 事件来获取相应的信息:

#include  <windows.h>
#include <iostream>
using namespace std;
 
LRESULT CALLBACK WindowProc(
	_In_  HWND hwnd,
	_In_  UINT uMsg,
	_In_  WPARAM wParam,
	_In_  LPARAM lParam
);
HWND createMsgWin() {
	HINSTANCE lvhInstance;
	lvhInstance = GetModuleHandle(NULL);  //获取一个应用程序或动态链接库的模块句柄  
	WNDCLASS lvwcCls;
	lvwcCls.cbClsExtra = 0;
	lvwcCls.cbWndExtra = 0;
	lvwcCls.hCursor = LoadCursor(lvhInstance, IDC_ARROW);  //鼠标风格  
	lvwcCls.hIcon = LoadIcon(lvhInstance, IDI_APPLICATION);    //图标风格  
	lvwcCls.lpszMenuName = NULL; //菜单名  
	lvwcCls.style = CS_HREDRAW | CS_VREDRAW; //窗口的风格  
	lvwcCls.hbrBackground = (HBRUSH)COLOR_WINDOW;    //背景色  
	lvwcCls.lpfnWndProc = WindowProc;    //【关键】采用自定义消息处理函数,也可以用默认的DefWindowProc  
	lvwcCls.lpszClassName = _T("RenderWindow");  //【关键】该窗口类的名称  
	lvwcCls.hInstance = lvhInstance;   //【关键】表示创建该窗口的程序的运行实体代号  
 
	RegisterClass(&lvwcCls);
 
	HWND lvhwndWin = CreateWindow(
		_T("RenderWindow"),           //【关键】上面注册的类名lpszClassName,要完全一致    
		L"Zombie",  //窗口标题文字    
		WS_OVERLAPPEDWINDOW, //窗口外观样式    
		0,             //窗口相对于父级的X坐标    
		0,             //窗口相对于父级的Y坐标    
		30,                //窗口的宽度    
		20,                //窗口的高度    
		NULL,               //没有父窗口,为NULL    
		NULL,               //没有菜单,为NULL    
		lvhInstance,          //当前应用程序的实例句柄    
		NULL);              //没有附加数据,为NULL    
 
							//去标题栏  
	
	return lvhwndWin;
}
 
LRESULT CALLBACK WindowProc(
	_In_  HWND hwnd,
	_In_  UINT uMsg,
	_In_  WPARAM wParam,
	_In_  LPARAM lParam
)
{
	//cout << "MSG:" << uMsg << ",wParam:" << wParam << ",lParam:" << lParam << endl;
	switch (uMsg)
	{
		
		case WM_POWERBROADCAST:
		{
			if (wParam == PBT_POWERSETTINGCHANGE) {
				POWERBROADCAST_SETTING* lvpsSetting = (POWERBROADCAST_SETTING*)lParam;
				byte lvStatus = *(lvpsSetting->Data);
				if (lvStatus != 0) {
					cout << "Monitor is turn on" << endl;
				}
				else {
					cout << "Monitor is turn off" << endl;
				}
				//cout << (int)lvStatus << endl;
				
			}			
			break;
		}		
	}
	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
HWND mhMsgRec;
 
BOOL WINAPI ConsoleHandler(DWORD pvdwMsgType)
{
	if (pvdwMsgType == CTRL_C_EVENT)
	{
		PostMessage(mhMsgRec, WM_DESTROY, 0, 0);
		return TRUE;
	}
	else if (pvdwMsgType == CTRL_CLOSE_EVENT)
	{
		PostMessage(mhMsgRec, WM_DESTROY, 0, 0);
		return TRUE;
	}
	return FALSE;
}
 
int main()
{
 
	mhMsgRec = createMsgWin();//这个函数也是dll里的,得到控制台的句柄
	HPOWERNOTIFY lvhpNotify = RegisterPowerSettingNotification(mhMsgRec, &GUID_CONSOLE_DISPLAY_STATE, DEVICE_NOTIFY_WINDOW_HANDLE);
	SetConsoleCtrlHandler(ConsoleHandler, TRUE);
	bool lvbRet;
	MSG lvMSG;
	while ((lvbRet = GetMessage(&lvMSG, NULL, 0, 0)) != 0)
	{		
		TranslateMessage(&lvMSG);
		DispatchMessage(&lvMSG);
		if (lvMSG.message == WM_DESTROY) {
			break;
		}
	}
	UnregisterPowerSettingNotification(lvhpNotify);
	CloseWindow(mhMsgRec);
	return 0;
}
 

相关文献:

其他部分的解决方案:

即将更新相关子方案

4. 完整代码以及测试截图

4.1 代码

提示

(1)有关退出时图标窗口上可能存在残影问题的解决方案没有写入到下面的代码中,具体说明请参考 “2.3  解决退出程序时窗口残影问题”;

(2)测试代码仅仅是为了演示嵌入窗口,而使用 Win32 API 直接创建了一个空白窗口示例 。实际开发中建议直接以 WebView2 窗口或者 WebGL 窗口作为嵌入桌面的窗口;

(3)检查并重启服务的部分需要提升进程权限至管理员权限,可以在程序清单中设置该选项;

(4)目前检查并重启服务的代码为了方便解释,并未采用 SCM 的 API 而是直接用了命令行 sc 命令,这样子的检测不太稳定,实际过程需要修改完善(正文已更新,但没有整合到下面代码中);

(5)代码中暂不能响应显示器更改和分辨率更改情况,不能跟随系统桌面矩形尺寸的变化及时同步修改尺寸;

(6)以下代码已经存在 Windows 系统依赖性和兼容问题。由于时间问题将在未来修复,需要了解具体情况的,请关注 3.1 (1.1), 3.2 (2.3), 3.3 (3.1) 和 3.3 (3.2) 小节;(所以不要直接复制或者转载下面代码了,如有情况请评论区交流或私信)

完整代码使用 Visual Studio 2022 编译,在 Win 11 23H2 以及 Win 7 ~ Win 10 部分版本上测试通过,但可能仍然不支持部分系统,具体见本文中的 兼容性疑难解答 内容。

#include <iostream>
#include <windows.h>
#include <string>
#include <tchar.h>
#include <time.h>
#include <shellscalingapi.h>
#include <tlhelp32.h>

#pragma comment(lib, "Shcore.lib")

//隐藏DOS黑窗口
//#pragma comment(linker,"/subsystem:\"windows\" /entry:\"mainCRTStartup\"")

typedef struct _DesktopWndInfo
{
    int  OSVersion = 1;               // 操作系统分类号
    bool IsCreatedWindow = false;     // 动态壁纸窗口是否创建完成
    HWND WallpaperWnd = NULL;         // 动态壁纸程序主窗口句柄
    HWND Workerw1 = NULL;             // 第一个WorkerW窗口句柄
    HWND Workerw2 = NULL;             // 第二个WorkerW窗口句柄
    HWND ShellDefView = NULL;         // ShellDefView窗口句柄
    HWND Progman = NULL;              // 总窗口句柄
}DesktopWndInfo, * DesktopWndInfoPtr;

typedef BOOL(WINAPI* PSWITCHTOTHISWINDOW) (HWND, BOOL);
inline BOOL CALLBACK EnumWindowsProc(HWND handle, LPARAM lparam);
LRESULT CALLBACK __WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

int CheckOsVersion();
DWORD mainWindowThread(void* argv);
DWORD FindProcessIDByName(LPCWSTR processName);
BOOL QueryEnableDwmComposition();
BOOL PreDesktopEnvironmentInit(DesktopWndInfoPtr lpWndInfo);
BOOL SetDynamicDesktopWindow(DesktopWndInfoPtr lpWndInfo);


const static wchar_t* szClass = L"WallpaerWindowClass";
const static wchar_t* szTitle = L"WallpaerWindow";
const static wchar_t* szConTitle = L"DynamicWallpaperTool";

#define CRT_CONSOLE_TITLE L"CASCADIA_HOSTING_WINDOW_CLASS"
#define OLD_CONSOLE_TITLE L"ConsoleWindowClass"

int main(int argc, char argv[])
{
    DesktopWndInfo WndInfo{};
    HWND hControlwnd = NULL;
    SetConsoleTitleW(szConTitle);

    // 检验系统版本,程序不能够在Win XP以及更早的系统环境下运行。
    WndInfo.OSVersion = CheckOsVersion();
    if (WndInfo.OSVersion <= 1)
    {
        printf("[-] This operating system is not supported.");
        printf("The program must run in a Windows 7 or higher system environment.\n");
        getchar();
        return 1;
    }

    if (WndInfo.OSVersion == 2) // Win 7 检查 DWM 设置
    {
        printf("[*] Check if DWM is enabled.\n");
        if (QueryEnableDwmComposition() == FALSE)
        {
            printf("[-] DWM was Disabled.\n");
            getchar();
            return 1;
        }
        else {
            printf("[+] DWM is Enabled.\n");
        }
    }

    // 获取控制台窗口句柄
    do {
        hControlwnd = FindWindowW(CRT_CONSOLE_TITLE, szConTitle);
        if (!hControlwnd)
            hControlwnd = FindWindowW(OLD_CONSOLE_TITLE, szConTitle);
    } while (!hControlwnd);

    // 将窗口最小化
    ShowWindow(hControlwnd, SW_SHOWMINIMIZED);

    // 构建动态壁纸运行环境,以及相关量的获取
    printf("[*] Init Desktop Environment.\n");
    if (!PreDesktopEnvironmentInit(&WndInfo))
    {
        printf("[-] Init Desktop Environment failed.\n");
        ShowWindow(hControlwnd, SW_SHOWNORMAL);// 显示控制器窗口
        getchar();
        return 1;
    }
    printf("[+] Init Desktop Environment Success.\n \n");
    // 打印壁纸程序主窗口的句柄
    printf("[+] CefWallpeperWindow: 0x%I64X\n", (long long)WndInfo.WallpaperWnd);
    if (WndInfo.WallpaperWnd == NULL)
    {
        ShowWindow(hControlwnd, SW_SHOWNORMAL);
        printf("[-] Handle Value = (null). Restart this Program!\n");
    }
    else
    {
        printf("[*] SetDynamicDesktopWindow.\n");
        if(!SetDynamicDesktopWindow(&WndInfo))
            printf("[-] SetDynamicDesktopWindow failed.\n \n");// 设置壁纸窗口
        else
            printf("[+] Program Produced!\n \n");
        ShowWindow(hControlwnd, SW_SHOWNORMAL);// 显示控制器窗口
    }
    printf("[*] Press any key to continue.");
    getchar();
    return 0;
}

int CheckOsVersion()
{
    int OsVersion = -1;
    typedef void(__stdcall* NTPROC)(DWORD*, DWORD*, DWORD*);
    HINSTANCE hinst = GetModuleHandleA("ntdll.dll");//加载DLL

    NTPROC GetNtVersionNumbers = (NTPROC)
        GetProcAddress(hinst, "RtlGetNtVersionNumbers");//获取函数地址
    if (!hinst || !GetNtVersionNumbers)
    {
        return 0;
    }

    DWORD dwMajor, dwMinor, dwBuildNumber;
    GetNtVersionNumbers(&dwMajor, &dwMinor, &dwBuildNumber);

    /*
    if (dwMajor <= 6 && dwMinor <= 2)
    {

        if (dwMajor == 6 && dwMinor >= 1) return 2;// win 7, win8
        else return 1;// win XP, vista
    }
    else {
        return 3;// win8.1,10,11
    }
    */

    switch (dwMajor)
    {
    case 6:
        if (dwMinor == 0) // Vista
        {
            OsVersion = 1;
        }
        else if (dwMinor == 1)  // Win 7
        {
            OsVersion = 2;
        }
        else if (dwMinor == 2)  // Win 8
        {
            OsVersion = 3;
        }
        else if (dwMinor == 3)  // Win 8.1
        {
            OsVersion = 4;
        }
        break;
    case 10:
        OsVersion = 4; // Win 10, 11
        break;
    default:
        if(dwMajor <= 5)  // Win XP
            OsVersion = 1;
        else
            OsVersion = 5; //  Future
        break;
    }
    
    return OsVersion;
}

// TODO: 改为使用 SCM 查询服务状态和启用服务
BOOL RunaDwmCompositionService()
{
    BOOL bEnabled = FALSE;
    
    return bEnabled;
}

//0 not found ; other found; processName "processName.exe"
DWORD FindProcessIDByName(LPCWSTR processName)
{
    DWORD processId = 0;
    HANDLE hProcessSnap;
    PROCESSENTRY32W pe32{};
    pe32.dwSize = sizeof(PROCESSENTRY32W);
    

    hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hProcessSnap == INVALID_HANDLE_VALUE)
    {
        return(0);
    }
    
    if (!Process32FirstW(hProcessSnap, &pe32))
    {
        CloseHandle(hProcessSnap);// Clean the snapshot object.
        return(0);
    }
    
    do
    {
        if (!wcscmp(pe32.szExeFile, processName)) // 进程名称
        {
            processId = pe32.th32ProcessID; // 进程 ID
            break;
        }
    } while (Process32NextW(hProcessSnap, &pe32));

    CloseHandle(hProcessSnap);

    return processId;
}

BOOL QueryEnableDwmComposition()
{
    // 注意 DWM API 在 Vista/Win7 系统以上才有
    // win8 / win10 是不需要判断的会一直返回 TRUE

    BOOL bEnabled = FALSE;
    typedef HRESULT(__stdcall* fnDwmIsCompositionEnabled)(BOOL* pfEnabled);
    typedef HRESULT(__stdcall* fnDwmEnableComposition)(UINT uCompositionAction);

    HMODULE hModuleDwm = LoadLibraryA("dwmapi.dll");
    if (hModuleDwm != 0)
    {
        auto pFuncIsEnabled =
            (fnDwmIsCompositionEnabled)GetProcAddress(
                hModuleDwm, "DwmIsCompositionEnabled");
        auto pFuncEnableDwm =
            (fnDwmEnableComposition)GetProcAddress(
            hModuleDwm, "DwmEnableComposition");

        if (pFuncIsEnabled != 0)
        {
            BOOL result = FALSE;
            if (pFuncIsEnabled(&result) == S_OK)
            {
                // 没有启动就启动一下
                if(result == TRUE)
                    bEnabled = TRUE;
                else if (pFuncEnableDwm != 0)
                {
                    printf("[*] Attempt to start Dwm Service.\n");
                    if (!FindProcessIDByName(L"dwm.exe"))
                    {
                        system("SC stop UxSms");
                        WaitForSingleObject(GetCurrentProcess(), 1500);
                    }
                    system("SC start UxSms");
                    WaitForSingleObject(GetCurrentProcess(), 500);
                    // #define DWM_EC_ENABLECOMPOSITION 1
                    if (pFuncEnableDwm(TRUE) == S_OK)
                    {
                        bEnabled = TRUE;// 判断启动是否成功
                    }
                    else {
                        SetLastError(ERROR_INTERNAL_ERROR);
                    }
                }
            }
        }
        else {
            SetLastError(ERROR_ACCESS_DENIED);
            bEnabled = TRUE;
        }

        FreeLibrary(hModuleDwm);
        hModuleDwm = 0;
    }
    return bEnabled;
}


BOOL PreDesktopEnvironmentInit(DesktopWndInfoPtr lpWndInfo)
{
    HWND hwnd = NULL;
    HWND hWorkerW1 = NULL,
         hWorkerW2 = NULL,
         hDefView  = NULL;
    LRESULT MsgRact = NULL;
    DWORD_PTR result;
    BOOL IsGetExitCode = FALSE;
    clock_t start = 0, finish = 0;
    double  duration = 0;
    const double eps = 0x2D;// 45秒
    int CountCircle = 0;
    bool CreateFlag = false;
    
    if (lpWndInfo == NULL)
        return FALSE;

    SetLastError(0);
    hwnd = FindWindowW(L"Progman", L"Program Manager");  // 根据 https://github.com/valinet/ExplorerPatcher/issues/525,可能使用 HWND progman = GetShellWindow(); 更加妥当

    if (hwnd == NULL
        || GetLastError() == ERROR_INVALID_WINDOW_HANDLE)
    {
        printf("[-] Fatal Error: No found Desktop Manager Window.\n");
        return FALSE;
    }

    printf("[*] SendMessage To Program Manager, message: [0x052C], Timeout: [0x03E8]\n");
    // 向Program Manager窗口发送消息
    SetLastError(0);
    MsgRact = SendMessageTimeoutW(hwnd, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &result);

    // 可能存在电脑性能问题
    while(0 == MsgRact && CountCircle <= 5)
    {
        CountCircle++;
        printf("[-] Error SendMessageTimeout, err_code: [%d].\n", 
            GetLastError());
        // 重新尝试
        WaitForSingleObject(GetCurrentProcess(), 300);
        if (ERROR_TIMEOUT == GetLastError())
            MsgRact =
            SendMessageTimeoutW(hwnd, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &result);
        else
            return FALSE;
        SetLastError(0);
    }

    printf("[+] Result[0]: %I64d. Progman Handle: [ 0x%I64X ]\n",
        result, (long long)hwnd);

    if (hwnd == NULL || result != 0u)
    {
        printf("[-] Handle Value = (null). SendMessage failed! err_code: [ %d ]\n",
            GetLastError());
        return FALSE;
    }
    printf("[+] SendMessage successfully.\n \n");

    (*lpWndInfo).Progman = hwnd;
    printf("[*] EnumDesktopWorkerWindows...\n");
    //枚举窗口
    SetLastError(0);
    EnumWindows(EnumWindowsProc, (LPARAM)lpWndInfo);
    // TODO: 这里修改不对 EnumWindows 返回值直接进行判断
    if ((*lpWndInfo).Workerw1 == nullptr || GetLastError() > 0)
    {
        printf("[-] EnumWindows failed.\n");
        return FALSE;
    }
    printf("[+] EnumWindows Finished.\n");
    // 校验 WorkerW1 是否是窗口是桌面。
    hWorkerW1 = (*lpWndInfo).Workerw1;
    hWorkerW2 = (*lpWndInfo).Workerw2;
    hDefView = (*lpWndInfo).ShellDefView;
    HWND hParent = GetParent(hDefView);

    if (hWorkerW1 != NULL && hWorkerW1 != hwnd
        && hParent == hWorkerW1) // DWM 正常时,窗口不应该是 Progman 应该是 WorkerW
    {
        printf("[+] EnumWindows successfully.\n \n");
        printf("[+] WorkerW 1: [ 0x%I64X ] | WorkerW 2: [ 0x%I64X ]\n", 
            (long long)hWorkerW1,
            (long long)hWorkerW2);
    }
    else {
        printf("[-] EnumWindows failed.\n \n");
        return FALSE;
    }
        

    printf("[*] CreateThread To Handle MainWindowProc.\n");
    // 在线程中创建窗口
    
    start = clock();// 开始计时
    HANDLE hThread = CreateThread(NULL, 0, mainWindowThread, lpWndInfo, 0, NULL);
    if (hThread == NULL)
    {
        printf("[*] CreateThread failed.err_code[ %d ]\n", GetLastError());
        return FALSE;
    }
    DWORD dwExitCode = 0;
    // 等待窗口加载完成
    do {
        IsGetExitCode = GetExitCodeThread(hThread, &dwExitCode);
        WaitForSingleObject(hThread, 0);// WAIT_TIMEOUT
        CreateFlag = (*lpWndInfo).IsCreatedWindow;
        finish = clock();
        duration = (double)(finish - start) / CLOCKS_PER_SEC;
    } while (!CreateFlag && 
        dwExitCode == STILL_ACTIVE 
        && IsGetExitCode != FALSE 
        && fabs(duration) <= eps);
    if (!CreateFlag)
    {
        printf("[*] CreateThread failed. ERROR_TIMEOUT\n \n");
        return FALSE;
    }
    else {
        printf("[+] Thread Handle: [ 0x%I64X ]\n \n", (long long)hThread);
        return TRUE;
    }
}

inline BOOL CALLBACK EnumWindowsProc(HWND handle, LPARAM lparam)
{
    if ((void*)lparam == nullptr)
        return FALSE;
    auto WndInfo = (DesktopWndInfoPtr)lparam;
    DWORD_PTR result = 0;
    // 获取第一个WorkerW窗口
    HWND DefView = FindWindowExW(handle, 0, L"SHELLDLL_DefView", NULL);

    if (DefView != NULL)// 找到第一个WorkerW窗口
    {
        (*WndInfo).Workerw1 = handle;
        (*WndInfo).ShellDefView = DefView;
        // 获取第二个WorkerW窗口的窗口句柄
        (*WndInfo).Workerw2 = FindWindowExW(0, handle, L"WorkerW", 0);
        return FALSE;
    }   // Patch:可能降低性能或导致严重超时
//    else {// 如果不能找到第一个WorkerW, 则重新发送消息
//        HWND hProgman = (*WndInfo).Progman;
//        SendMessageTimeoutW(hProgman, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &result);
//    }
    return TRUE;
}

// 参数hWallpaperwnd为你开发的窗口程序的窗口句柄
// DesktopWndInfoPtr传入结构体指针
BOOL SetDynamicDesktopWindow(DesktopWndInfoPtr lpWndInfo)
{
    if (lpWndInfo == NULL)
        return FALSE;

    RECT rc = { 0,0,0,0 };
    LONG style = 0,
         SWLRetn = 0;
    HWND
         hClientWnd = (*lpWndInfo).WallpaperWnd,
         hWorkerW = (*lpWndInfo).Workerw2,
         hProgman = (*lpWndInfo).Progman;
    HWND LastParent = NULL;
    DWORD nSPerror = 0;
    int Version = (*lpWndInfo).OSVersion;
    int scWidth, scHeight, perx, pery;
    int ctWidth, ctHeight;

    if (hClientWnd == NULL)
    {
        SetLastError(ERROR_INVALID_WINDOW_HANDLE);
        printf("[-] Fatal error: NoFoundWindow.\n");
        return FALSE;
    }

    //获取屏幕宽高
    scWidth = GetSystemMetrics(SM_CXSCREEN);
    scHeight = GetSystemMetrics(SM_CYSCREEN);
    printf("[+] ScreenRectInformation: [ %d x %d ]\n", scWidth, scHeight);

    // 同步窗口的风格,否则SetParent()将出现意料之外的结果。
    style = GetWindowLongW(hClientWnd, GWL_STYLE);
    style &= ~WS_POPUP & ~WS_CAPTION & ~WS_SIZEBOX;
    style |= WS_CHILD;
    SWLRetn = 
        SetWindowLongW(hClientWnd, GWL_STYLE, style);

    if (SWLRetn == 0)
    {
        printf("[-] FatalError: SetWindowLong Error!err_code[ %d ]\n", 
            GetLastError());
        return FALSE;
    }


    // 设置父窗口
    printf("[*] SetParent: SetWallpaperWindow.\n");
    
    if (Version == 2 || Version == 3) //  Win 7, Win 8
    {
        if (hProgman == NULL)// TODO
        {
            PostMessageW(hClientWnd, SC_CLOSE, 0,0);
            SetLastError(ERROR_INVALID_WINDOW_HANDLE);
            printf("[-] SetParent Fatal error: hParent must not be NULL.\n");
            return FALSE;
        }
        ShowWindow(hWorkerW, SW_HIDE);
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hProgman);
    }
    else {
        if (hWorkerW == NULL)
        {
            PostMessageW(hClientWnd, SC_CLOSE, 0, 0);
            SetLastError(ERROR_INVALID_WINDOW_HANDLE);
            printf("[-] SetParent Fatal error: hParent must not be NULL.\n");
            return FALSE;
        }
        SetLastError(0);
        LastParent = SetParent(hClientWnd, hWorkerW);
    }

    nSPerror = GetLastError();
    if (nSPerror > 0 || LastParent == NULL)
    {
        printf("[-] FatalError: SetParent Error!err_code[ %d ]\n \n", nSPerror);
        return FALSE;
    }
    printf("[+] SetParent Success, hChild[ 0x%I64X ] | hParent[ 0x%I64X ].\n \n",
        (long long)hClientWnd, 
        Version == 2 ? (long long)hProgman : (long long)hWorkerW);

    printf("[*] DisplayWindowsAnimation using SetWindowPos.\n");
    // 动画:沿对角线拉伸效果
    perx = scWidth / 10;
    pery = scHeight / 10;
    for (int i = 0; i <= 10; i++)
    {
        //SetWindowPos(hClientWnd,
        //    NULL, 0, 0, perx, pery, SWP_FRAMECHANGED);
        // 补丁:2024.06.16
        SetWindowPos(hClientWnd,
            NULL, 0, 0, perx, pery, SWP_NOZORDER | SWP_FRAMECHANGED);
        perx += perx;
        pery += pery;
        Sleep(35);
    }
    // 确保动态壁纸主窗口是全屏的
    ShowWindow(hClientWnd, SW_SHOWMAXIMIZED);
    SetWindowPos(hClientWnd,
        NULL, 0, 0, scWidth, scHeight, SWP_NOZORDER | SWP_FRAMECHANGED);
    
    GetWindowRect(hClientWnd, &rc);
    ctWidth = rc.right - rc.left;
    ctHeight = rc.bottom - rc.top;

    if (ctWidth != scWidth || ctHeight != scHeight)
    {
        printf("[-] SetWindowPos failed.\n \n");
        return FALSE;
    }
    printf("[+] SetWindowPos Success.\n \n");
    printf("[+] SetWallpeperWindow Successfully!\n");
    return TRUE;
}


DWORD mainWindowThread(void* argv)
{
    if (argv == nullptr)
        return 1;

    auto WndInfo = (DesktopWndInfoPtr)argv;
    HINSTANCE hIns = GetModuleHandle(0);
    WNDCLASSEXW wc{};
    wc.cbSize = sizeof(wc);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hIns;
    wc.hIcon = LoadIcon(0, IDI_APPLICATION);
    wc.hIconSm = 0;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hCursor = LoadCursor(0, IDC_ARROW);
    wc.lpfnWndProc = __WndProc;
    wc.lpszMenuName = NULL;
    wc.lpszClassName = szClass;

    if (!RegisterClassExW(&wc))
    {
        printf("[-] FatalError: RegisterClassEx failed! err_code[ %d ]\n", 
            GetLastError());
        return 1;
    }

    DWORD style = WS_OVERLAPPEDWINDOW;
    DWORD styleEx = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;

    //计算客户区域为宽800,高600的窗口尺寸
    RECT rect = { 0, 0, 800, 600 };
    AdjustWindowRectEx(&rect, style, false, styleEx);

    HWND hwnd = CreateWindowExW(styleEx, szClass, szTitle, style, 0, 0,
        rect.right - rect.left, rect.bottom - rect.top, 0, 0, hIns, 0);
    // 检查窗口创建是否成功
    if (hwnd == 0)
    {
        printf("[-] FatalError: CreateWindowEx failed! err_code[ %d ]\n",
            GetLastError());
        return 1;
    }

    UpdateWindow(hwnd);
    ShowWindow(hwnd, SW_SHOW);
    // 记录窗口句柄信息,标记窗口已经创建
    (*WndInfo).WallpaperWnd = hwnd;
    (*WndInfo).IsCreatedWindow = true;
    //TODO, init this

    MSG msg = { 0 };
    while (msg.message != WM_QUIT) {
        if (PeekMessageW(&msg, 0, 0, 0, PM_REMOVE)) {
            //printf("hwnd: [ 0x%I64X ] | Message: [ %X ] | wParam: [ 0x%I64X] | lParam: [0x%I64X]\n", 
                //(long long)msg.hwnd, msg.message, msg.wParam, msg.lParam);
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    // 窗口线程结束,标记窗口被关闭
    (*WndInfo).IsCreatedWindow = false;
    return 0;
}

LRESULT CALLBACK __WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    PAINTSTRUCT ps;
    HDC hdc;
    switch (uMsg) {
    case WM_CLOSE:
        DestroyWindow(hWnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        EndPaint(hWnd, &ps);
        break;
    default:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return 0;
}

4.2 运行结果展示

注意:代码必须设置 DPI 感知清单,以便于同步桌面窗口的缩放显示,避免显示的元素出现错位等混乱现象。

嵌入目标窗口到桌面管理层

打开“任务视图”

Win7 运行结果

5. 关于后续工作

当完成了嵌入窗口部分的实现后,后续需要我们掌握窗口穿透交互的实现和动态壁纸程序的实现。

由于我们的窗口无法获得焦点(窗口的 Z 序低于 SHELL_DefView),所以消息是无法直接通过人体学设备和系统传递给窗口的。要想用户能够通过鼠标和该窗口上面的元素进行交互,就必须能够将消息传递给这个窗口。目前主要有两种主流的实现方法:(1)当完全在桌面操作时,将前台窗口(ShellDefView)的窗口消息截获并转发到动态壁纸窗口,这需要设置消息过滤器并解决消息阻塞问题;(2)[可行性更高]当完全在桌面操作并且设置允许时,通过低级鼠标钩子捕获鼠标信息,并将模拟消息发送到指定的动态壁纸窗口,也需要适当处理消息问题。

动态壁纸程序主要为三种类型:内置视频播放器式动态壁纸、内置 Web 浏览器式动态壁纸以及图片切换式动态壁纸。第一种实现相对简单,往往使用 ffmpeg 或 mpv 等视频解码和播放器;中间这种实现相对复杂,但具有很强的扩展性能,Steam 的 WallpaperEngine 就是典型的 Web 浏览器式动态壁纸程序。有些动态壁纸程序提供了集成开发环境 (IDE) 或者命令解释器终端 (Terminal),以便于壁纸开发人员可以轻松扩展他们的动态壁纸,例如 WallpaperEngine (商业性) 和 Live Wallpaper (开源免费) 就分别提供了编辑器 (WallpaperEngine) 和 Python 命令解释器 (Live Wallpaper)。此外,Live Wallpaper 还拓展了幻灯片播放壁纸和 GIF 动图壁纸的支持。图片切换式动态壁纸也有应用的场景,如 DynamicDesktop 开源动态壁纸软件模仿 Mac 系统的动态壁纸,在特定气候或者时间节点通过系统支持的 API 设置标准的系统壁纸和切换壁纸(没有采用 0x052C 消息等复杂机制),这一点看上去还是挺有创意的。

后续的工作将在新的文章中开展 —— 自认为这一篇文章已经十分凌乱不堪了(混合了太多内容和细节),导致可能并不适用于初学者和个人阅读使用。

四、处理向后兼容性问题(6 月 25 日更新)

4.1 问题提出

微软于 2024 年 3 月 13 日向 Canary 和 Dev 频道发布了 Windows 11 Insider Preview Build 26080。26080 更新包为 24H2 的 Preview 版本。微软计划将在 2024 年 9 月在正式发布渠道全面推出 24H2 版本更新。

15 号上午(2 天后),微软开始向开发人员频道中的 Windows 预览体验成员推出累积更新内部版本 26080.1201(KB5037135)。微软声称 KB5037135 更新不包含任何新内容,旨在测试服务管道。

@小彬先生 的反映,微软似乎对资源管理器的一些细节进行了改动,现在我们只能形成一个 WorkerW 窗口。目前没有任何文献介绍解释了这种情况,类似的情况可能曾经发生过,有些人也许注意到,但大多数人并未意识到这些变化可能带来的严重后果。使用了类似本文的实现逻辑(总结自目前大多数成功的案例)的动态壁纸软件可能因为搜索窗口错误而导致动态壁纸无法显示的问题。下面我将在目前配置的虚拟环境下,对该问题进行复现。

异常:搜索窗口失败

在接收到相应的反馈后,我根据外部测试中心的 ISO 文件在虚拟机部署了 24H2 的 Dev 渠道版本。

外部测试版中心 - Windows Insider Program | Microsoft Learn

外部测试中心更新日志

测试版本号如下图所示。

测试版本标签

现在,在发送 0x052C 消息后,桌面图标窗口的层次结构类似于 XP 系统,这是一个很大的变化,似乎有点“返祖”现象。我们现在只有一个 WorkerW 窗口,而不是产生两个不同的 WorkerW 窗口。

Win11 24H2 发送消息后的窗口层次

由此可见,窗口结构的巨大变化是导致动态壁纸程序无法正常运行的原因。

我们现在需要一个可靠的方法,在唯一的 WorkerW 下嵌入我们的窗口。首先,我们需要修改查找窗口的逻辑,修改 EnumWindowsProc 函数,添加对这种窗口层次的支持。

其次,SHELL_DefView 窗口具有新增的 WS_EX_LAYERED 分层窗口样式,这说明微软很有可能把第一个 WorkerW 的功能转移到 SHELL_DefView 上了。

解决因更新而导致的问题需要额外的操作,下面列举几种可能的方案(实验中):

  1. DWM 缩略图技术:尝试利用 DWM 缩略图技术将动态壁纸窗口的图形绘制在 WorkerW 窗口的背景上。
  2. 外部/托管渲染:考虑是否可以调整 SHELL_DefView 和 WorkerW 的渲染路径来在图标层和静态壁纸层之间绘制动态壁纸。关于修改渲染路径的方法涉及到窗口子类化、GDI Alpha 过滤以及处理 WM_ERASEBKGND 消息。此类动态壁纸将不受窗口层次和透明穿透的影响,可以直接将窗口渲染到桌面。该思路就是在 XP 系统上实现动态壁纸的相关开源软件所曾采用的。(原本我将在第二篇中针对 XP 系统的适配性讲解该方法)
  3. 使用自定义窗口层次(修改窗口层次):构建新的 WorkerW,并调整窗口层次来实现双层 WorkerW。因为创建 WorkerW 窗口有未被公开的 API。(这在我以前发过的文章中尝试过,不过比较困难,需要处理子窗口消息重新转发,消息风暴问题,该思路违反了微软的 UI 设计规范。初步实现了一部分功能,但因为有些问题没有得到有效的解决,所以那篇文章就被我临时撤回防止误人子弟)
  4. 覆盖系统的桌面图标层:使用自己实现的桌面图标层(类似于桌面文件夹盒子的实现原理),可以选择同时隐藏系统原始的桌面图标层。这也是延申自 Start11 等商业化美化工具的一贯思路(覆盖微软的开始菜单任务栏等)。这可以避免因微软频繁对 UI 设计的修改带来的 “不兼容-修复兼容-再次不兼容” 死循环问题。
  5. Patch Explorer 静态补丁/热补丁:拦截 WorkerW 和 SHELL_DefView 窗口创建的过程,替换为自己的窗口。或者逆向通过 BinDiff 对比新旧版本 Explorer 在实现窗口切换效果机制(WorkerW)的变化,通过打补丁修复这部分功能。该思路受开源工具 ExplorerPatcher 的一贯的思想启发而来。
  6. ......(其他)

这些思路目前处于缓慢实验阶段,因为我还有其他的事情。相关更新将间断地发布于我的博客。

——写于 2024 年 3 月 17 日晚

4.2 窗口显示解决方案(3 月 24 日)

经过测试,我们发现,普通 Win32 窗口嵌入唯一的 WorkerW 时,背景会被完全擦除,对于它的子窗口也是如此。使用 WebView2 窗口、mpv 播放器窗口直接置父,则不会出现完全不可见的情况。这两款软件使用特殊的渲染方式,而不采用传统的 Win32 窗口绘制方式。WebView2 窗口使用 Windows 的 Direct Composition 等技术将图像合成到屏幕。而 mpv 则使用 DirectX 来渲染图像。

所以,目前的解决方案是直接将 WebView2Browser 嵌入到 WorkerW 下,并修正窗口全屏尺寸,而不间接经过传统的 Win32 窗口

使用下面的模板代码使用 WebView2:Win32 示例 WebView2Browser

WebView2Browser 界面

需要直接将 D3D Window 的父窗口作为背板嵌入 WorkerW 窗口。

Intermediate D3D Window 是一种 D3D Window 的窗口类名。

需要嵌入的窗口

关于传统 Win32 窗口的背景被擦除的原因,以及如何修复问题,有待未来解决。临时先采用该方法。

——写于 2024 年 3 月 24 日晚

4.3 最新的窗口显示解决方案(6 月 9 日)

最新消息(6.25):经验证和修复,该方法在 Windows 11 24H2 专业版 Dev 渠道 Build 26120.670(Feature Experience Pack 1000.26100.6.0)和 24H2 Canary 渠道(Build 26080)版本上均正常工作。(目前持续更新的 24H2 上,此方法仍然有效。但需要注意的是,此方法利用了遗留的特性或者称之为 BUG 也不为过。所以在未来版本, MS 的开发者可能意识到这里而进一步修补;老实说,双层 WorkerW 的时代已经过去)

之所以 Intermediate D3D Window 窗口和 MPV 窗口够正常显示,是因为他们的窗口具有特殊的窗口样式。

首先,如果为 WorkerW 窗口设置多个子窗口,则在重叠兄弟窗口时,需要设置 WS_CLIPSIBLINGS 。只有正确设置该窗口样式,才能使得 Z 序低于当前窗口的兄弟窗口(同级窗口)剪切重叠部分的绘制,否则兄弟窗口可能覆盖此窗口。由于目前版本中的 WorkerW 窗口默认具有 WS_CLIPSIBLINGS 和 WS_CLIPCHILDREN 窗口样式(关于这两个窗口样式的解释 见文章1 和文章 2),所以这一设置我们可以暂时忽略。

注意:

(1)当某个子窗口收到 WM_PAINT 消息时,WS_CLIPSIBLINGS 样式会将所有其他重叠子窗口剪切出要更新的子窗口区域。如果未指定 WS_CLIPSIBLINGS 且子窗口重叠,则在子窗口的客户区内绘制时,可能会在相邻子窗口的客户区内绘制(出现覆盖现象)。

(2)所有重叠式和弹出式窗口都是 默认具有 WS_CLIPSIBLINGS 窗口样式的。也就是说,窗口不能摆脱 WS_CLIPSIBLINGS 窗口样式并在其重叠式兄弟窗口中进行绘制。只有子窗口的 WS_CLIPSIBLINGS 样式是可选的

如果要修改窗口样式的窗口只是一个子窗口,你可以通过 SetWindowLongPtr 和 SetWindowPos 来添加/删除  WS_CLIPSIBLINGS 窗口样式。

添加的示例:

1
添加 WS_CLIPSIBLINGS 样式

然后尝试重绘这两个子窗口:

2
重绘后的效果

可以看到同级子窗口的客户区中重叠区域被剪切了。

参考文献:WS_CLIPSIBLINGS style automatically added - Stack Overflow

其次,需要给窗口添加分层窗口扩展样式,即 WS_EX_LAYERED 窗口扩展样式,然后在显示窗口之前调用 SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA) 将窗口显示出来(此处简单设置为不透明)。

注意:必须在设置 WS_EX_LAYERED 后立即调用 SetLayeredWindowAttributes 分层窗口或者 UpdateLayeredWindow 以便于使得分层效果生效,否则窗口将无法显示。

然后,再嵌入唯一的那个 WorkerW(Z 序低于 SHELL_DefView 的同级窗口),我们的窗口就可以被正常显示出来了。所以说问题并不是窗口是否采用了 GPU 加速渲染,因为 Intermediate D3D Window 窗口也是普通窗口,只不过采用了特殊的窗口样式就可以在嵌入桌面时正常处理 BufferRender。关于这个窗口存在的技术内容见文章:Chrome 窗口 - CSDN博客 和 Chrome 中的 GPU 加速渲染技术

注意: UWP 应用(尤其是应用商店应用)的 FrameWindow、CoreWindow 类窗口不能作为子窗口嵌入 Win32 窗口(这里为 WorkerW 窗口),SetParent 函数会获得错误代码 87-参数错误。必要时需要采用 WinRT/Xaml 托管 API 才可能实现,而不是直接使用 SetParent 函数设置父级,尽管这些窗口并不是幻影窗口。

有关失败的原因可以参考文章:display a UWP app - Stack OverflowEmbed Win32 App inside a UWP App - Stack Overflow

具体修改 DWORD mainWindowThread(void* argv) 函数如下位置:

设置分层窗口和窗口剪切渲染样式以便于兼容 24H2

最终效果:

演示 24H2 嵌入窗口部分(片段 1)
演示 24H2 嵌入窗口部分(片段 2)
演示 24H2 嵌入窗口部分(片段 3)

——写于 2024 年 6 月 9 日晚

4.4 窗口显示解决方案(6 月 11 日 | 实验性)

实验性方案提示:此方案处于实验状态(但是,6.9 版本是目前比较稳定的方案),不保证始终有效,且可能存在已知的技术难题或者潜在的 BUG 未能够得到有效解决。

已知使用设置父窗口并依赖 0x052C 消息的动态壁纸方案有很大的局限性,是否可以不过多地依赖于窗口层次而显示出动态壁纸?答案是肯定的,并且微软以前的 DreamScene 动态壁纸方案就是这么做的。已知传统的桌面刷新通过 WM_ERASEBKGND 消息来通知图标层先绘制壁纸层图形缓冲,再绘制图标层的图形缓冲区。所以,我们可以通过窗口子类化接管桌面窗口的绘制,利用 GDI Alpha 通道的过滤和 BitBlt 绘制遮罩和适当剪裁图像,达到绘制动态壁纸窗口图像的目的。但在多数处理 0x052C 消息的系统上,父窗口变为 WorkerW 时从 SHELL_DefView 窗口接收不到 WM_ERASEBKGND 消息,导致图形绘制失败。但是,依然可以接收到 WM_NOTIFY 并包含 Code:NM_CUSTOMDRAW 的缓冲区绘制消息,这是 ListCtrl 可以特有的 Custom Draw 自定义绘制方法。具体可以参考:全面解读 WM_NOTIFY - findumars - 博客园 和 NM_CUSTOMDRAW 消息 - CSDN博客,但暂未弄清楚其是否和图标层的缓冲区有关。

目前我的 “绕道” 方法就是检查是否已经开启了 WorkerW 窗口分层,如果开启则重启 explorer 应用(使用 RM 重启管理器以便于保存用户操作进行可恢复的重启),然后过滤发送给 Program Manager 窗口的 0x052C 消息,来禁用此窗口层次。此问题有待未来解决,如果你有任何建议请联系我。

实现效果展示

——写于 2024 年 6 月 13 日


五、后记

关于如何在 XP 或者 Win 7 关闭 DWM 情况下嵌入窗口,将在后续文章中讲解,包括制作视频动态壁纸以及浏览器动态壁纸前端程序的讲解。( Vista 有点特殊,嵌入窗口比较麻烦,具体方法会在以后的系列中讲解 )


转载请注明出处:从零实现桌面动态壁纸 - 涟幽516

发布于:2023.10.09,更新于:2024.03.24, 2024.05.11, 2024.06.04 - 06.21 / 06.25, 2024.07.10, 2024.08.17, 2024.09.16.

评论 25
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

涟幽516

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值