阅读之前:请先了解有关术语,如句柄、窗口、消息等

有一定技术含量,如果把桌面弄坏了请重启文件资源管理器。

建议改成:视频壁纸原理

鄙人不才,如有疏漏或错误请在评论指出。

该功能只能在 Windows 8 —— Windows 11 23H2 间的版本中使用

原因:Windows 11 24H2 中该窗口消息流程被修改,窗口关系变化。

开始之前

这几天,闲得慌,在这里弄视频壁纸,就搓出这么个东东

基本实现

插入窗口

总所周知,Windows 就是窗户,所以各个组成都是窗口
现在我们要弄视频壁纸,就将视频窗口设置为桌面的子窗口,然后设置前置、全屏
桌面图标其实是一个透明的控件,所以大概实现如下:

|  |  |
-------
|  |  | <= 背景壁纸
|  | <= 插入的窗口
| <= 控件

说起来似乎很简单,但实际实现却有些难
在正常情况下,
使用 Spy++ 查询桌面归窗口 Program Manager 管,大概是以下结构

|
+ Program Manager
  + (无名称) SHELLDLL_DefView
    + FolderView

你无论给设谁的子窗口,它都在控件前面或者索性不知道去了那

但 Win10 有个功能,叫任务视图,这个东西你点击去,欸,后面有壁纸,同时 Program Manager 下面没有子窗口了

那就有人问了:子窗口呢

哎,它分裂了,就像细胞分裂一样
桌面变成了下面的结构

|
+ WorkerW
  + (无名称) SHELLDLL_DefView
    + FolderView
+ WorkerW
+ Program Manager

那是什么神奇的力量驱使它分裂的呢

通过对 Program ManagerMessages 分析,发现有这么一条Messages

有一条叫 0x052C 的消息

<000012> 0000000000010116 P message: 0x052C [用户定义: WM_USER+300] wParam: 0000000D Iparam: 00000001

是不是它呢这不废话吗,都到这来了,肯定是它呀,注销一下
恢复最开始的状态

然后写个简单的 C++ 程序,作用就是发消息
快给 Program Manager 送去,告诉他,味道鲜美无比编译运行

1
2
HWND hProgman = FindWindow(_T("Progman"), NULL);
SendMessageTimeout(hProgman, 0x052C, 0, 0, SMTO_NORMAL, 1000, NULL);

这段代码将 0x052c 消息传输到 Program Manager 窗口,不等待返回,生存时间为 1 秒

实际使用应处理返回值。

部分系统在收到消息后可能不会分裂,也不知道为什么,可能是权限的问题吧(哎可能他忘记以管理员启动了)

Spy++ 一看,已经分裂了,没错,就是你 0x052C
下文就称这个 0x052C细胞分裂消息

好了,现在有 WorkerW(1) | WorkerW(2) | Program Manager 一共 3 个管桌面的窗口

|
+ WorkerW                       <= 1
  + (无名称) SHELLDLL_DefView
    + FolderView
+ WorkerW                       <= 2
+ Program Manager

插在那好呢,非常简单 => 枚举

一个一个的试,不就可以了

  1. WorkerW(1)
    窗口是有了,但是在桌面图标的前面
    所以

    1
    const WorkerW_1 = (this) => 垃圾桶;

  2. WorkerW(2)
    效果已经出来了:
    视频被渲染在桌面图标的后面
    不干扰前面的正常工作
    一键三连娘的视频在桌面后面播放

    该方法具有一定的问题,主要因为该窗口具有缓存快照,这就导致每次有窗口需要重绘时不会重绘。
    所以只有视频壁纸这种实时绘制的可以放进去毕竟刚缓存就被重绘了

    如果是某些动态壁纸(如根据鼠标位置移动窗口的)不能放进去,否则会有残影

    关于一些别的事情

    在撰写本文此段的当天(2025-06-16) 我在教室的电脑上,
    手欠把一个文件资源管理器的窗口放了进去,
    然后,
    每一节课的老师都试图关闭这个窗口,
    然鹅关不上,被桌面图标控件挡住了
    然后被班主任说了一顿

  3. Program Manager
    可以实现类似效果,但是
    由于每收到一次 细胞分裂消息 就会尝试分裂,但是已经分裂了,它会把自己的子窗口全部转给第一个 WorkerW 窗口,好,它就在桌面图标前了。

    什么,你问为啥会有消息,你要是不小心把任务视图打开了咋办

    但是由于 WorkerW(2) 的特殊性,部分动态壁纸只能放在此处,因为该窗口没有缓存快照
    这时候就是这样的

     |
     + WorkerW                       <= 1
       + (无名称) SHELLDLL_DefView
         + FolderView
     + WorkerW                       <= 2
     + Program Manager
       + FFplay
    

    这是就会发现,欸,ffplay 的窗口呢?
    哎,被 WorkerW(2) 挡住了,
    不过此事好办,调用 ShowWindow 函数把他隐藏就可以了。

窗口在里面移动的时候,如果窗口 top 的值修改了,窗口就不知道去哪了,修改 left 值,哎,他就又出来了。

好,现在窗口可以进去了,现在解决点实际的问题


显示

坐标位置

我们都知道,在多显示器情况下,窗口二维坐标系原点 基于主显示器的右上角开始计算
设每个显示器分辨率为 1366x768 ,1 显示器为主显示器

   --> X             +-------+ +-------+ +-------+
|                    |   5   | |   2   | |   4   |
Y                    +-------+ +-------+ +-------+
                        0,0 => +-------+
                               |   1   |
                               +-------+
                               +-------+
                               |   3   |
                               +-------+

然鹅桌面窗口却是用最左边显示器的左边缘和最上边显示器的上边缘的交点为二维坐标系原点

也就是说这个原点可能不在显示器上。

   --> X      0,0 => +-------+ +-------+ +-------+
|                    |   5   | |   2   | |   4   |
Y                    +-------+ +-------+ +-------+
                               +-------+
                               |   1   |
                               +-------+
                               +-------+
                               |   3   |
                               +-------+
   --> X      0,0 => + - - - - +-------+ +-------+
|                    |         |   2   | |   4   |
Y                              +-------+ +-------+
                     |         +-------+
                               |   1   |
                     |         +-------+
                     +-------+ +-------+
                     |   5   | |   3   |
                     +-------+ +-------+

一般也没人整这么奇怪的东西把。不过最少 2 显示器就可以

窗口坐标系 - Windows 应用开发文档

换句话说,以桌面窗口来看,所有的有效显示位置的坐标都为整数正值;以屏幕来看,所有的有效显示位置的坐标都是整数值。

知道了这一点,下面的东西才好说。

显示

单显示器

一个显示器好说,直接用 ffplay 创建一个无边框全屏窗口,缩放成屏幕大小,然后发送 细胞分裂消息 ,再调用 SetParent 函数 设置为子窗口,欸,完成。

多显示器

多显示器有点难搞,但也挺简单,主要原因出在桌面的坐标系和屏幕坐标系使用不同原点,但其实挺好解决的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <Windows.h>
#include <vector>
#include <iostream>

// 回调函数,用于枚举显示器
BOOL CALLBACK MonitorEnumProc(
HMONITOR hMonitor,
HDC hdcMonitor,
LPRECT lprcMonitor,
LPARAM dwData
) {
std::vector<POINT>* monitors = reinterpret_cast<std::vector<POINT>*>(dwData);
POINT topLeft = { lprcMonitor->left, lprcMonitor->top };
monitors->push_back(topLeft);
return TRUE;
}

int main() {
std::vector<POINT> monitorPositions;

// 枚举所有显示器
EnumDisplayMonitors(
NULL, // 指定整个虚拟桌面
NULL, // 无裁剪区域
MonitorEnumProc,
reinterpret_cast<LPARAM>(&monitorPositions)
);

// 输出结果
std::cout << "找到 " << monitorPositions.size() << " 个显示器:\n";
for (size_t i = 0; i < monitorPositions.size(); ++i) {
std::cout << "显示器 " << i + 1 << " 左上角位置: ("
<< monitorPositions[i].x << ", "
<< monitorPositions[i].y << ")\n";
}

return 0;
}

这段代码通过调用 EnumDusplayMonitors 函数,可以获取所有显示器的屏幕左上角位置。
但是这里的使用的是屏幕坐标系(原点在主显示器的左上角),但我们需要的是桌面窗口的位置。

所以我们使用 MapWindowPoints 来达到将屏幕坐标转化为基于桌面窗口的坐标的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <Windows.h>
#include <vector>
#include <iostream>

// 回调函数,用于枚举显示器
BOOL CALLBACK MonitorEnumProc(
HMONITOR hMonitor,
HDC hdcMonitor,
LPRECT lprcMonitor,
LPARAM dwData
) {
std::vector<POINT>* monitors = reinterpret_cast<std::vector<POINT>*>(dwData);
POINT topLeft = { lprcMonitor->left, lprcMonitor->top };
monitors->push_back(topLeft);
return TRUE;
}

HWND FindWindowA(
LPCSTR lpClassName,
LPCSTR lpWindowName
);

int main() {
std::vector<POINT> monitorPositions;

// 枚举所有显示器
EnumDisplayMonitors(
NULL, // 指定整个虚拟桌面
NULL, // 无裁剪区域
MonitorEnumProc,
reinterpret_cast<LPARAM>(&monitorPositions)
);

// 输出结果
std::cout << "找到 " << monitorPositions.size() << " 个显示器:\n";
// 查找 Program Manager 窗口的句柄
HWND hProgman = FindWindowA("Progman", NULL);
for (size_t i = 0; i < monitorPositions.size(); ++i) {
POINT pt = monitorPositions[i]; // 原始坐标

// 映射坐标到桌面窗口坐标系
MapWindowPoints(
NULL, // 从虚拟桌面映射
hProgman, // 到桌面窗口
&pt, // 要转换的点
1 // 点的数量
);

std::cout << "显示器 " << i + 1 << " 左上角位置: ("
<< pt.x << ", " << pt.y << ")\n";
}

return 0;
}

这样就可以获取到所有窗口基于桌面窗口的位置了。

再使用 ffplay 创建视频窗口,放置进桌面,调整位置,便可达到效果