实现桌面动态壁纸(一)

编程入门 行业动态 更新时间:2024-10-22 15:31:37

实现<a href=https://www.elefans.com/category/jswz/34/1767000.html style=桌面动态壁纸(一)"/>

实现桌面动态壁纸(一)

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

目录

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

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 补充注意事项

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

3.1 代码

3.2 运行结果展示

后记


前言

Wallpaper Engine 是由 Kristjan Skutta 所开发的一款动态壁纸软件,区别于其他形式的壁纸软件,Wallpaper Engine 可以让用户通过其引擎深度的自定义或编辑来创作出符合个人需求的壁纸样式。如果你惊艳于 WallpaperEngine 的效果,并且自己也想制作个性化的桌面美化程序,路漫漫其修远兮,我们先从认识桌面窗口管理器开始。


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

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 DWM

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

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

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

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

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

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

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

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

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

支持Aero配色方案的系统:(内核版本NT 6.0及以上基本可以)

Windows VistaHome Premium(Vista家庭高级版)
Windows Vista Business(Vista商业版)
Windows Vista Enterprise(Vista企业版)
Windows Vista Ultimate(Vista旗舰版)
Windows 7Home Premium(Win7家庭高级版)
Windows 7 Professional(Win7专业版)
Windows 7 Enterprise(Win7企业版)
Windows 7 Ultimate(Win7旗舰版)
Windows 8Developer Preview(Win8开发者预览版)
Windows 8 Consumer Preview(Win8消费者预览版)
Windows8 Release Preview (Win8发行预览版)
Windows7 Home Basic、Windows Vista Home Basic 隐藏了 Aero 特效,但可以通过修改注册表强行开启。Windows8正式版中取消了 Aero Glass 特效。

1.2 桌面窗口管理器(DWM)

​桌面窗口管理器(Desktop Window Manager, DWM)是 Windows Vista 操作系统中的组件。

建立在 WPF 核心图形层组件基础之上。DWM 的桌面合成是建立在 Composition 引擎基础之上的新特征。它的出现几乎改变了 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 视觉效果消失。你可以在控制面板\管理工具\服务中查看服务状态:

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

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

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

 可以通过控制台等来终止\启动 dwm.exe:

方法一:通过 NET 命令操作 DWM 服务

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

1.启动 DWM 服务:net start UxSms

2.终止 DWM 服务:net stop UxSms

方法二:使用 Rundll32 直接加载 dwmapi.dll 的符号

 Win + R键 打开“运行”窗口(或者在开始菜单的运行窗口里面输入),输入以下指令:

  开启毛玻璃效果: Rundll32 dwmapi #102(只能关闭毛玻璃效果,Aero还在)

  关闭毛玻璃效果: Rundll32 dwmapi #104(只能关闭毛玻璃效果,Aero还在)

       任务视图(3D桌面): Rundll32 dwmapi #105

       显示桌面:Rundll32 dwmapi #113

题外话:Themes 服务为用户提供使用主题管理的体验。当系统登录程序 Winlogon.exe 终止后,csrss.exe 会结束 UxSms 和 Themes 服务,最后强制重启用户会话。类似地很多视频会议系统的窗口锁定流程是:先关闭 DWM 服务(Uxsms,或者VC中停用桌面合成 Composition)然后使用SwitchToThisWindow 和 ShowWindow 组合实现让前景窗口一直是会议窗口。 

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

Windows 7 以及更早的系统版本下,支持编程禁用 DWM。

由于 DWM 使用图形处理单元 (GPU) 进行桌面组合,因此部分应用程序可能必须禁用 DWM 才能实现兼容性。 完全控制桌面的应用程序(例如在全屏模式下运行的游戏)必须确定是否已启用 DWM,如果是,则禁用它。 为此,需要两个函数:

  • DwmIsCompositionEnabled
  • DwmEnableComposition

在 fEnable 设置为 DWM_EC_DISABLECOMPOSITION 的情况下调用 DwmEnableComposition 会禁用 DWM 合成,直到调用进程已关闭,或者通过将 fEnable 设置为 DWM_EC_ENABLECOMPOSITION 调用 DwmEnableComposition 来重新启用合成。 禁用组合的所有应用程序关闭或通过调用 DwmEnableComposition 手动重新启用组合后,DWM 组合会自动重启。

1.5 DWM 系统关键进程

从 Windows 8 开始,当应用程序尝试直接绘制到主要显示图面时,DWM 会自动禁用合成。 组合将被禁用,直到该应用程序释放主设备图面。微软将该进程归为由 Winlogon.exe 进程启动和监视的关键进程,dwm.exe 不再是服务进程,不可以被编程终止,否则会黑屏卡死。

但是,有很多通过钩子或者修改链接库实现禁用 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 上的行为和更高版本系统不一样,首先是 Win 8 及以上操作系统:

"" 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 的浏览器窗口)

我们发现 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 并没有太大意义,只要发送 0x052C 消息,窗口层次就自动变为透明层次。

通过很多发送消息函数都可以实现,比如具有超时发送的 SendMessageTimeout 函数:

SendMessageTimeoutW(hProgmanWnd, 0x052C, 0, 0, SMTO_NORMAL, 0x03E8, &result);

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

二、实战&编写代码

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

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

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

想让自己的窗口嵌入到桌面管理层窗口下面不被遮挡,你必须让桌面管理层窗口变成透明。说到窗口透明,做过客户端的程序员可能会想到 Windows 的 Layerd 窗口,但是实际上用 Spy ++ 去看 WorkerW 1 窗口样式, 他并不包含 WS_EX_LAYERD 风格。而且有个重要的问题,Layerd 窗口是无法拥有 Child 属性子窗口的(会显示不可见),并且用 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是不需要判断的会一直返回TRUEBOOL 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 objectreturn(0);}do{if (!wcscmp(pe32.szExeFile, processName))//进程名称{processId = pe32.th32ProcessID;//进程IDbreak;}} while (Process32NextW(hProcessSnap, &pe32));CloseHandle(hProcessSnap);return processId;
}BOOL QueryEnableDwmComposition()
{// 注意 DWM API 在 Vista/Win7 系统以上才有// win8 / win10 是不需要判断的会一直返回 TRUEBOOL 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 1if (pFuncEnableDwm(TRUE) == S_OK){bEnabled = TRUE;// 判断启动是否成功}else {SetLastError(ERROR_INTERNAL_ERROR);}}}}else {SetLastError(ERROR_ACCESS_DENIED);bEnabled = TRUE;}FreeLibrary(hModuleDwm);hModuleDwm = 0;}return bEnabled;
}

更好的方法是使用 SCManager 接口监视 DWM 服务进程状态,这里懒,就用 sc 命令代替了。

测试效果:

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 为父窗口,否则无法显示窗口;在高版本(不低于Win 8.1)的桌面嵌入窗口必须以 WorkerW 2 为父窗口。

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

在不低于 Win 8.1 上,如果以 Progman 作为父窗口嵌入壁纸窗口,虽然看似嵌入了,但是存在一个问题,那就是当再次发生 0X052C 消息的时候,我们窗口的文本会被强制同步,由 RGB 配色方案转为 ARGB,这会导致将黑色识别为透明色,导致窗口字体透明:

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

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

辅助资料:以下内容引用自文章:《DWM 窗体玻璃效果实现》

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

图 10 透明对话框(图片源自于问答) 

令人欣慰的是,可以通过多种方法解决此问题。其中一种方法是使用所有者描述的控件。另一种方法是呈现到具有 Alpha 管道的位图。但控制文本的最简单方法是让 .NET Framework 2.0 为您使用 GDI+。通过在您的控件上设置 UseCompatibleTextRendering 属性可以很容易地做到这一点。默认情况下,此属性设置为 false,这样,为 .NET Framework 的以前版本编写的控件将以相同的方式呈现。但是,如果将其设置为 true,则文本将正确呈现。您可以使用 Application.SetUseCompatibleTextRenderingDefault 方法在全局设置该属性。如果您使用的是 Visual Studio® 2005,则模板代码将包括一个调用,以便在创建窗体之前在主例程中将兼容文本呈现设置为 false。您可以编辑它,将其设置为 true(如下所示),这时在玻璃窗口中进行编写时,所有控件看上去都会是正确的。

static void Main(){Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(true);Application.Run(new GlassForm());}

您可以在 Miguel A. Lacouture 在 2006 年 3 月份的《MSDN 杂志》上发表的文章“Build World-Ready Apps Using Complex Scripts In Windows Forms Controls”中找到有关此内容和使用 TextRenderer 类的详细信息。
在开始呈现窗口之前应启用玻璃效果。组合引擎将查看您窗口的 Alpha 值,并将模糊效果应用到透明区域。这在使用某些 GDI 函数时可能会出现问题,因为这些函数不保留 Alpha 值。您可以在需要时使用 GDI+,但应谨慎使用,因为 GDI+ 调用在软件中呈现,而不是在硬件上呈现,因此窗口刷新频率较高时使用 GDI+ 调用可能导致耗费大量的系统资源。
可以通过相同方法在 DirectX 应用程序中获得玻璃效果。您需要做的只是控制呈现目标的 Alpha 值和使用两个启用玻璃效果的 DWM 函数之一。无论在何处指示 DWM 使用玻璃效果,它都将使用呈现目标的 Alpha 值。无论在其他任何地方,呈现目标均应为不透明的,否则将得到未定义的行为。


所以,多一事不如少一事,嵌入在 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);}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;

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。

那么,如何取消设置父窗口呢,我们发现即使再次调用SetParent,指定窗口仍然在最找设置的父窗口上。

我们可以通过将 SetParent 的第二个参数设置为 NULL,并在调用前去除 WS_CHILD 属性,在调用后根据记录恢复 WS_POPUP 属性。

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

// 取消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;      // 8margins.cxRightWidth = 8;    // 8margins.cyBottomHeight = 20; // 20margins.cyTopHeight = 27;       // 27hr = 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 问题二解决方案

如何解决窗口大小改变的问题呢,主要有两种方法,一种是在调用 SetParent 前调用 SetWindowPlacement 函数修改窗口还原状态的历史记录,另一种则是在调用后使用 SetWindowPos 函数直接修改窗口大小。

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

基本用法:

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;HWNDhClientWnd = (*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 函数获取的屏幕矩形,获取到的矩形数据为 9601707:

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

不难发现如下转换公式:

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

我们可以利用两种函数来实现目的:

方法一:通过监视器矩形信息计算

首先使用 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 EnumDisplaySettingsA(

[in] LPCSTR              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;

方法二:通过 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识别 即可。将任务交给编译器,他会为我们添加合适的代码。
 

再次编译,完美获取到正确的数值。


接着,为了避免设置父窗口产生的突兀感,通过折叠展开的效果,沿对角线扩展窗口矩形,可以产生较为舒适的视觉效果:

// 计算增量
int perx = scWidth / 10;
int pery = scHeight / 10;// 循环设置大小
for (int i = 0; i <= 10; i++)
{SetWindowPos(hClientWnd,NULL, 0, 0, perx, pery, SWP_FRAMECHANGED);perx += perx;pery += pery;Sleep(35); // 延迟产生视觉暂留
}

最后,通过 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;
}

3.1 如何获取并判断系统版本

这里我们通过 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");//加载DLLNTPROC 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, win8else 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, 11break;default:if(dwMajor <= 5)  // Win XPOsVersion = 1;elseOsVersion = 5; //  Futurebreak;}return OsVersion;
}

2.3 补充注意事项

第一点:在枚举窗口的时候,我们采用的是先找到 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_TIMEOUTCreateFlag = (*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. 完整代码以及测试截图

3.1 代码

完整代码使用 Visual Studio 2022 编译,在 Win 11 物理机以及 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");// 设置壁纸窗口elseprintf("[+] 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");//加载DLLNTPROC 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, win8else 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, 11break;default:if(dwMajor <= 5)  // Win XPOsVersion = 1;elseOsVersion = 5; //  Futurebreak;}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; // 进程 IDbreak;}} while (Process32NextW(hProcessSnap, &pe32));CloseHandle(hProcessSnap);return processId;
}BOOL QueryEnableDwmComposition()
{// 注意 DWM API 在 Vista/Win7 系统以上才有// win8 / win10 是不需要判断的会一直返回 TRUEBOOL 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 1if (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");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);elsereturn 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);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;}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_TIMEOUTCreateFlag = (*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);}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;HWNDhClientWnd = (*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);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 thisMSG 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;
}

3.2 运行结果展示

注意:代码必须设置 DPI 感知清单。

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

打开“任务视图”

Win7 运行结果

后记

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

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

更新于:2023.10.13

更多推荐

实现桌面动态壁纸(一)

本文发布于:2024-02-06 23:22:22,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1751464.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:桌面   壁纸   动态

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!