本文原创,最早发表于公司内部博客, 禁止转载

一. 前言

本文转自我的KM空间,重新整理了一下,原文链接http://km.oa.com/articles/show/451493。

Steam上一款很火的Windows壁纸软件WallPaper Engine,它功能很强大,除了普通的静态壁纸和动态壁纸之外, 还可以设置互动壁纸。互动壁纸就是可以跟你鼠标交互的桌面动态壁纸。最近正好也做了一个动态桌面功能,研究了一下WallPaper Engine。此处分享下Windows桌面美化的一些技术细节。阅读本文需要了解Windows编程。

桌面壁纸软件早在windows XP系统上都已经出现, 不过那时候的壁纸都是静态的壁纸,动态壁纸这几年才开始流行。实现windows自定义壁纸的方法有两种:(1)直接在Windows桌面窗口背景上自绘;(2)将自己窗口嵌入到桌面(Vista/Win7及之后系统大部分用这种方法)。

二. Windows桌面壁纸原理

1. 桌面窗口层次

首先来看下Windows桌面窗口层次,这是用Spy++抓取的Win7开机后正常的一个桌面窗口层次:
在这里插入图片描述
父窗口Progman窗口, 其子窗口SHELLDLL_DefView窗口,SHELLDLL_DefView又有一个窗口类为SysListView32的子窗口,SysHeader32是不可见的。显而易见,桌面上的图标都是在这个SysListView32列表窗口中的。如果熟悉MFC,看到SysListView32会很眼熟,MFC中的CListCtrl控件窗口类也是SysListView32。

这种窗口层次的话, 不管是往Progman窗口中嵌入一个WM_CHILDWINDOW属性的窗口还是一个WS_POPUP窗口,要么覆盖在桌面SysLisView32窗口上,要么作为子窗口被挡住,简而言之,无法通过嵌入窗口的方式实现类似WallPaper Engine那样的壁纸。我们现在看下WallPaper Engine嵌入壁纸窗口时候桌面窗口层次:
在这里插入图片描述
我们发现SHELLDLL_DefView及其下面的桌面图标窗口成为一个WorkerW窗口的子窗口(我们称WorkerW1),下面还有个WorkerW窗口(我们称WorkerW2),壁纸窗口设为了WokerW2的子窗口。用Spy++分别看下WorkerW1、WorkerW2的属性,会发现WorkerW2是一个Popup窗口,其Parent是Progman窗口 ,其上一个窗口句柄是WorkerW1。 WokerW1窗口也是一个Popup窗口,其父窗口显示无,但是其下一个窗口显示的句柄正好是WokerW2。此时这三个窗口Z序很明显了:WorkerW1 > WorkerW2 > Progman窗口。既然WallPaper这样做可以在桌面图标下面实现动态壁纸,那我们反过来推断出,这个WorkerW1窗口(包括其child窗口)此时一定是个透明窗口了,否则是看不见这个CefBrowserWindow(这嵌入的是一个Web窗口,用的还是libcef)。WorkerW1、WorkerW2和Progman窗口一样一样都属于explorer.exe进程。实际上想要将自己的窗口嵌入到Windows桌面图标下方,桌面的窗口层次一定要像上图一样, 必须要有这两个WorkerW窗口, 而且WorkerW2必须Parent是Progman窗口,WorkerW1、WorkerW2和Progman窗口Z序也必须是WorkerW1 > WorkerW2 > Progman这样的。需要注意的是,用嵌入窗口的方式实现动态壁纸,只能在Vista/WIn7及其以上系统, XP不支持Aero也无法产生这种透明的窗口层次。

2. 桌面嵌入窗口实现壁纸

只要能将我们自己的窗口嵌入到Windows图标下面并且可视,我们就可以为所欲为了!

2.1. Desktop Window Manager

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

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

所以回到桌面窗口嵌入问题,在考虑窗口是否可嵌入之前, 我们要判断Desktop Compositon是否开启,如果不开启,桌面窗口层次是不可能变成上面那种透明层次的,DWM API提供DwmIsCompositionEnabled函数来判断DWM Composition 是否启用:

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

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

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

2.2. 怎样让桌面窗口层次变成透明?

我们Windows系统已经开启DWM Composition了,那么我们怎么才能让桌面窗口层次发生改变,能够让我们正常嵌入呢?上文说过桌面窗口透明必须是这样的层次,而且Z序是固定的:
在这里插入图片描述
会被WorkerW2挡住;反之,如果是嵌入到WorkerW2窗口上面,我们必确保WorkerW2窗口是Visible的,否则嵌入的窗口也是不可见的。WorkerW2窗口创建出来后并没有在其上绘制背景,所以显示的还是Progman的背景, 但是WorkerW2窗口并不是透明的,所以如果嵌入到Progman中因为Z序的原因,如果WorkerW2是Visible的话,确实会被挡住。以下这两种桌面的窗口层次就不适合嵌入:
在这里插入图片描述
在这里插入图片描述
要想让桌面窗口变成适合嵌入的透明层次,我们需要向Progman窗口发送一个0x005C的消息,这个是Windows系统保留的一个消息,它在Vista之后版本才有效。当发送这个消息后,桌面程序就会生成一个透明的WorkerW窗口(上文所说WorkerW1),Progman上的SHELLDLL_DefView以及图标SysListView32窗口都会成为WorkerW1的子窗口,同时还会产生另外一个WorkerW窗口(上文所说WorkerW2)。判断当前桌面窗口是不是透明层次, 可以用FindWindowEx查找到WorkerW2,至于WorkerW1可以EnumWindows枚举来查找,也可以FindWindowEx循环查找。如果两个都找到那就可以直接嵌入,否则需要发0x005C消息到Progman窗口,:

BOOL CALLBACK CBFindWorkerW1(HWND hWnd, LPARAM lp)
{
	if (lp == NULL) return FALSE;

	HWND hShl = NULL;
	hShl = ::FindWindowEx(hWnd, 0, _T("SHELLDLL_DefView"), 0);
	
	if (hShl)
	{
		CString strClass;
		GetClassName(hWnd, strClass.GetBuffer(32), 31);
		strClass.ReleaseBuffer();
		if (strClass == _T("WorkerW"))
		{
			*(HWND*)lp = hWnd;
			return FALSE;
		}
	}

	return TRUE;
}

//获取透明的WorkerW1窗口句柄
HWND GetDesktopWorkerW1()
{
	HWND hWork1 = NULL;
	EnumWindows(CBFindWorkerW1, (LPARAM)&hWork1);
	return hWork1;
}

//获取WorkerW2句柄
HWND GetDesktopWorkerW2()
{
	HWND hWorkerW1 = GetDesktopWorkerW1();
	if (!::IsWindow(hWorkerW1)) return NULL;

	HWND hWorkWWnd = ::FindWindowEx(0, hWorkerW1, _T("WorkerW"), NULL);
	return hWorkWWnd;
}

//发送0x052C,让windows在桌面图标后面生成一个workerw窗口
BOOL MakeDesktopTransparent()
{
	HWND hWndShlMain = ::FindWindow(_T("Progman"), NULL);
	::SendMessage(hWndShlMain, 0x052C, 0xD, 0);
	::SendMessage(hWndShlMain, 0x052C, 0xD, 1);
	HWND hWorkW = 0;
	hWorkW = GetDesktopWorkerW1();
	if (hWorkW == 0)
	{
		OutputDebugString(_T("Can't find the WorkW window, try again!"));
		::SendMessage(hWndShlMain, 0x52c, 0, 0);
		hWorkW = GetDesktopWorkerW1();
	}

	return hWorkW != NULL;
}
2.3. 嵌入自己的窗口

好的,此时桌面窗口层次变成明了,我们只需要把自己进程的窗口“嵌入”进去即可,使用SetParent,其定义如下:

HWND SetParent( HWND hWndChild, HWND hWndNewParent );

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

void CMFCD2DDrawTestDlg::OnBnClickedBtnTest()
{
	MakeDesktopTransparent();
	HWND hWorker2 = GetDesktopWorkerW2();
	::ShowWindow(hWorker2, SW_HIDE);
	
	HWND hWndShlMain = ::FindWindow(_T("Progman"), _T("Program Manager"));
	//0x00F415C8是我电脑上QQ音乐主窗口句柄, 测试直接写死
	::SetParent((HWND)0x00F415C8, hWndShlMain);
}

效果如下图:
在这里插入图片描述

3. 不能嵌入窗口的情况,如何实现桌面壁纸呢

启DWM Composition)以及XP系统都是无法使用此种方法的,因为此时系统的桌面窗口并不是透明的, 并没有上文所说的窗口类为WorkerW的WorkerW1(透明)、WokerW2窗口,考虑到此时桌面窗口层次是这样的:
在这里插入图片描述
想要做自己的桌面壁纸,会稍微麻烦些。此时桌面最上层窗口是一个类为SysListView32的Child窗口,这个窗口跟MFC的CListctrl是一样的,使用的是图标的ListCtrl。查询会发现ListrCtrl是没法设置背景片的, 我自己也在MFC里面用ListCtrl试验了一下, 想改变ListCtrl的背景图标只能通过自绘的方式,考虑到桌面列表窗口是在属于系统的explorer进程,根本无法用这种方式了。但是实际上QQ音乐的明星壁纸,我发现没开Aero的Win7、甚至xp系统都能正常显示,这是如何做到的呢?网上查了一些资料,实际上SysListView32这个类的窗口有一个扩展属性:

#define LVS_EX_TRANSPARENTBKGND 0x00400000 // Background is painted by the parent via WM_PRINTCLIENT

后面的注释,翻译的意思就是背景由父窗口在WM_PRINTCLIENT消息中绘制,但是,测试发现此处当列表发生重绘时,是给父窗口发一个WM_ERASEBKGND消息(并没有发送WM_PRINTCLIENT,与MSDN解释的不一致),所以实际上此时背景是在WM_ERASEBKGND中绘制,我尝试只在WM_PRINTCLIENT中绘制,并不起作用。还有就是MSDN中说这个扩展属性在"vista or later"才支持(实际上xp中也有效果!)。所以,我们可以给桌面列表加上0x00400000的Extended Styles,然后再在父窗口SHELLDLL_DefView的WM_ERASEBKGND中重绘背景。由于跨进程了,这种方法需要DLL注入。

在Windows中,每个进程都有自己的私有地址空间。当我们用指针来引用内存的时候,指针的值表示的是进程自己地址空间的一个内存地址。进程不能创建一个指针来引用属于其他进程的内存。独立的地址空间对开发人员和用户来说都是非常有利的。对开发人员来说,系统更有可能捕获错误的内存读\写。对用户而言, 操作系统变得更加健壮。当然这样的健壮性也是要付出代价的,因为它使我们很难编写能够与其他进程通信的应用程序或对其他进程进行操控的应用程序。《Windows核心编程》第二十二章《DLL注入和API拦截》中讲了几种机制可以将自己的DLL注入到另一个进程的地址空间中,一旦能够将自己的DLL注入另个一个进程的地址空间,那么我们就可以在那个进程中为所欲为。此处简单说下用Windows挂钩来注入自己DLL实现桌面壁纸的绘制,其关键函数:

  HHOOK WINAPI SetWindowsHookEx(
                                    __in int idHook, \\钩子类型
                                    __in HOOKPROC lpfn, \\回调函数地址
                                    __in HINSTANCE hMod, \\实例句柄
                                    __in DWORD dwThreadId); \\线程ID,0表示所有

SetWindowsHookEx会把把挂钩过滤回调函数lpfn所在的DLL映射(注入)到dwThreadID所在进程地址空间中,映射的是整个DLL模块,而不仅仅是挂钩过滤函数。要注意桌面窗口所在的explorer.exe进程是32位还是64位的(与系统是32还是64位对应),我们当前注入的dll必须是一致,32位和64位的PE文件结构是不一致的,对应的地址空间结构划分也是不一致,32位的dll是不能注入到64位程序的地址空间的,反之亦然。简单写下安装钩子伪代码:

extern "C" __declspec(dllexport) HHOOK Installhook(HWND hWnd)
{
    //hWndDesktop桌面SysListView32的列表窗口,CBWndProc钩子回调函数地址
	hhk = SetWindowsHookExW(WH_CALLWNDPROC, CBWndProc, hinst, GetWindowThreadProcessId(hWndDesktop, NULL));
	return hhk;
}

窗口给桌面窗口进程安装了一个WH_CALLWNDPROC的钩子, 这个钩子是截获SendMessage消息, 当然此处也可以用其他挂钩类型或者其他注入方式,我们要做的只是注入。好的,已经将自己DLL注入到了explorer进程了,在这DLL中可以给桌面列表加上0x00400000的Extended Styles,然后响应父窗口SHELLDLL_DefView的WM_ERASEBKGND消息来绘制背景。下面为改变桌面列表窗口扩展属性代码:

DWORD dwExtendedStyle = (DWORD) ::SendMessage(hWndDesktop, LVM_GETEXTENDEDLISTVIEWSTYLE, 0, 0);
dwExtendedStyle |= LVS_EX_DOUBLEBUFFER | LVS_EX_TRANSPARENTBKGND;//
::SendMessage(hWndDesktop, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, (LPARAM)dwExtendedStyle);

要响应桌面列表的父窗口WM_ERASEBKGND来绘制背景,捕获该窗口的消息,要用到SetWindowLongPtr修改窗口过程函数:

WNDPROC oldWindowProc = (WNDPROC) ::SetWindowLongPtrW(g_hDesktopParent, GWLP_WNDPROC, (LONG_PTR)DesktopParentWndProc)

记住结束的时候将oldWindowProc给设置回去,g_hDesktopParent其实就是SHELLDLL_DefView这个窗口。在DesktopParentWndProc函数中就能接收到桌面父窗口的消息了,在WM_ERASEBKGND就可以绘制自定义背景。此处用MFC的ListCtrl测试,设置了LVS_EX_TRANSPARENTBKGND的扩展属性,并在父窗口的WM_ERASEBKGND绘制图片背景,测试完全OK,代码就不贴了,效果如下:
在这里插入图片描述
用这种方法普通的静态壁纸是没问题的,但是想做动态壁纸,刷新是会有问题的。

三. 浅析Wallpaper Engine桌面壁纸的几种类型

WallPaper Engine的素材支持类型比较多,还可以自己定义。在其每个素材目录下面有个project.json的当前素材配置,里面json下有个type节点,里面的值就是当前素材的类型。常见的有web,scene, video,picture,exe,还有一些Combo(组合)的等等。Wallpaper Engine 提供了素材编辑器, 可以使用其提供模板去编辑各种类型素材。下面的几种都是动态壁纸,都是用嵌入窗口的方式实现的。

1. Web类型

这种类型的可以是动态的,并且可以互动(如何互动后面再说)。下面星空壁纸,星空可以跟随鼠标旋转移动的,嵌入web窗口,用了canvas绘图。一般web方式实现的,, 其素材目录如下:
在这里插入图片描述
下面为星空跟随鼠标旋转:
在这里插入图片描述
下面这个天气变化的也是web实现,实际上也是在web中用几张GIF贴图实现,可以点击下方按钮切换天气:
在这里插入图片描述

2. Scene方式

这种也可以互动,支持2D/3D的,里面有shaders(着色器)加上一个scene.pkg,这个猜测是native的, 这种方式目录结构如下图所示:
在这里插入图片描述
下面为一个type为Scene的互动壁纸:
在这里插入图片描述

3. 其他类型

还有一些其他的类型比如video、普通图片、GIF等,属于比较简单的,此处不多讲。

总结下,WallPaper Engine壁纸有静态壁纸,动态壁纸,互动壁纸。静态和动态壁纸就不多说了。互动壁纸的话目前发现一种是用web方式,web现在H5功能很强应该可以实现很多效果。 另一种一种是用scene方式,Scene类型的是native实现的,可能是直接用GPU编程或者调用DirectX(我也不清楚, 看有shaders可能直接用的GPU接口)。另外还可以直接使用exe资源, WallPaper Engine会把你exe的窗口嵌入到桌面,这种方式就可以随意发挥了,也是可以做成互动的。

四. 动态壁纸的一些其它问题

现在仍然有两个问题:互动壁纸,鼠标跟壁纸是如何互动的;腾讯桌面整理、Fence等桌面整理软件挡住了壁纸的问题。

1. 响应鼠标事件互动

上文说了,壁纸窗口是嵌入在桌面图标列表窗口下面,在桌面鼠标点击等操作,消息都是发送给桌面列表窗口, 正常情况下壁纸窗口是不会响应鼠标消息的。WallPaper Engine中有些互动壁纸,可以响应鼠标点击、移动等事件。此处用到了钩子,关键函数是上文说过的SetWindowsHookEx,这个函数功能很强大。如果只是响应桌面上的鼠标消息,可以使用WH_MOUSE_LL全局钩子。当然如果想更精致,也还是可以注入到桌面窗口所在的explorer进程,获取到桌面上的鼠标消息, 然后通过PostMessage或者SendMessae发送给我们的壁纸窗口中。

此处还有一个问题,是壁纸跟我们鼠标能互动,那么我们考虑鼠标在桌面图标上时候,此时的鼠标消息应该过滤掉,不需传送给我们的壁纸窗口。所以此处有个问题,怎么获取桌面图标的坐标呢?此处又要说到桌面使用ListView列表控件的好处了,ListView提供了很丰富的方法, 我们是可以获取到其中每个item的坐标。

由于Windows大多数原生控件, 如列表的LVM_GETITEM和LVM_GETITEMPOSITION,他是不能跨越进程的边界来运行的,原因是,LVM_GETITEM消息要求你为消息的LPARAM参数传递一个LV_ITEM的数据结构地址。由于这个内存地址只对发送消息的进程有意义,接收消息的进程无法保证能够使用它。因此我们在获取桌面列表每个Item项信息是并没有那么容易。

可以使用注入DLl的方式,来获取桌面每个图标项位置信息。《Windows核心编程》第二二章第三节中有一个DIPS的demo,讲的就是如何在分辨率发生变化时候保存桌面图标位置,待分辨率还原后,恢复之前的图标位置。其使用的就是用钩子进行注入的方法,此处不细讲了。

还有一种简单一点的方法,可以直接在桌面窗口进程地址空间分配内存。上面所说LVM_GETITEM等一些LPARAM带指针参数的,正常无法跨进程。实际上Windows可以用VirtualAllocEx在其他进程中中分配内存。所以,我们在发送列表消息获取Item信息, 可以VirtualAllocEx分配LV_ITEM内存。对应的读取使用ReadProcessMemory, 释放内存使用VirtualFreeEx。需要注意下LV_ITEM在64位和32位下的结构区别。详情可以网上搜索相关资料。

2. 被桌面整理软件挡住的问题

如果装了桌面整理软件,桌面壁纸经常会被挡住。我们先来看下桌面整理软件的实现原理。SHELLDLL_DefView为Progman子窗口时不透明层次窗口就不多讲了,此处只考虑在透明层次桌面窗口下。打开腾讯桌面整理, 用Spy++看下桌面窗口层次:
在这里插入图片描述
可以看到腾讯桌面整理的窗口,实际上就是自己做了一个窗口,覆盖在桌面列表窗口上面,让其Z序高于桌面列表窗口即可。然后再在桌面整理窗口上显示出所有桌面文件、驱动图标,并将当前桌面主题背景绘制在这个窗口上。当然真要做个桌面整理软件, 此处还有很多细节, 比如桌面图标提取、菜单、图标拖拽等等。这些操作在Windows Shell API中都有相关接口支持。

桌面整理软件原理就是自己创建一个桌面窗口覆盖掉在我们桌面窗口之上。此处我做了个简单桌面整理Demo。具体做法是创建一个Popup窗口, 调用SetParent, Parent窗口设置为WorkerW1,代码如下:

	HWND hWorkerW1 = (HWND)0x001210BC; //写死测试
	::SetParent(m_hWnd, hWorkerW1);
	::SetWindowPos(m_hWnd, hWorkerW1, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);

此时桌面整理Demo窗口确实会覆盖在桌面图标列表窗口, 同时,桌面整理窗口又不会挡住当前用户打开的其它窗口,看似完美。但是如果是开了动态壁纸,Demo窗口是会挡住壁纸窗口的。考虑到桌面的WokerW1窗口都可以变成透明的,桌面整理窗口也可以让其变成透明。我们设想,使桌面整理透明,然后隐藏真正的桌面窗口, 那么我们仍然可以在打开桌面整理的时候不遮挡桌面壁纸。我们来验证下是不是这样。

首先让桌面整理窗口透明,需使用到DWM API。DwmEnableBlurBehindWindow, 这个函数能实现我们想要的效果。看MSDN解释:Enables the blur effect on a specified window。意思就是实现窗口模糊(半透明)效果。函数定义如下:

DWMAPI DwmEnableBlurBehindWindow(
  HWND                 hWnd,
  const DWM_BLURBEHIND *pBlurBehind
);

第一个参数为窗口的句柄, 第二个参数为DWM_BLURBEHIND结构指针。DWM_BLURBEHIND结构如下:

typedef struct _DWM_BLURBEHIND {
  DWORD dwFlags;
  BOOL  fEnable;
  HRGN  hRgnBlur;
  BOOL  fTransitionOnMaximized;
} DWM_BLURBEHIND, *PDWM_BLURBEHIND;

假设我们要设置一个窗口模糊效果,这个参数应该这样设置:

CRect rcClient;
GetClientRect(&rcClient);
DWM_BLURBEHIND bb = { 0 };
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
bb.fEnable = true;
bb.hRgnBlur =  CreateRectRgnIndirect(rcClient);;
DwmEnableBlurBehindWindow(m_hWnd, &bb);
::DeleteObject(bb.hRgnBlur);

效果图如下:
在这里插入图片描述
但是我们需要的是全透明的效果,这个函数也是能实现。 DWM_BLURBEHIND中hRgnBlur,当我们调用该函数时,这个参数标识我们要需要设置半透明效果的区域,NULL表示客户区全部半透明。当这个值不为NULL时,除去这个Rgn区域外,剩余客户区是透明的,要呈现透明, 需要让背景色是纯黑色。 所以,我们有方法让桌面整理变成透明。将桌面整理的DEMO窗口设置为WorkerW1子窗口:

HWND hWorkerW1 = (HWND)0x001210BC; //写死测试
::SetParent(m_hWnd, hWorkerW1);
::SetWindowPos(m_hWnd, hWorkerW1, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);

Spy++查看窗口层次如下:
在这里插入图片描述
上文说过DwmEnableBlurBehindWindow还能使窗口透明,DWM_BLURBEHIND中hRgnBlur区域之外的客户区会变透明,需要将背景填充黑色0xFF000000(ARGB),设置代码如下:

CRect rcClient;
GetClientRect(rcClient);
DWM_BLURBEHIND bb = { 0 };
bb.dwFlags = DWM_BB_ENABLE | DWM_BB_BLURREGION;
bb.fEnable = true;
bb.hRgnBlur = CreateRectRgn(0, 0, 1, 1);//除了这一像素高区域其他地方全透明,可自行控制
DwmEnableBlurBehindWindow(m_hWnd, &bb);
::DeleteObject(bb.hRgnBlur);

上面代码只是修改了hRgnBlur参数。需要响应WM_PAINT或WM_ERASEBKGND,将背景填充为黑色,窗口就透明了。下面是桌面整理demo效果(这个MFC窗口为仿桌面整理窗口,此处测试没全屏):

此处gif太大不动, 可点击 桌面整理demo gif动图查看
在这里插入图片描述
我提取了桌面图标绘制在透明的桌面整理窗口上。这时发现一个问题,因为Demo窗口是WorkerW1子窗口,其z序是高于桌面列表窗口的, 我开启了qq音乐动态桌面,此时三个窗口z序关系是:

桌面整理Demo窗口 >桌面SysListView32>QQ音乐动态壁纸窗口

现在桌面整理demo窗口是透明的,理论上其透明区域看到的背景应该桌面窗口的, 但是实际上看上面效果视屏,我的桌面整理Demo窗口透明区域显示的仍然是下方的Q音动态壁纸,这正是我们想要的,非常完美。窗口叠加时展现的效果跟我们预想的并不一致,这也是调用DWM API后产生的效果。所以有一个结论, 当桌面整理窗口全屏时,只要按上述方法将桌面整理窗口变成透明, 那么它就不会“遮挡”桌面壁纸窗口,同样我们也并不需要再去隐藏真正的桌面窗口,这种Z序下此时它会完全透明

所以桌面整理软件都是可以做到不挡住桌面壁纸(只针对嵌入窗口实现)。只要桌面整理软件增加一种机制,可以通知使得其变成透明模式,那么就能解决桌面壁纸被桌面整理挡住的问题。实际上腾讯桌面整理确实这样做了,WallPaper Engine并不会被其挡住。

上方视屏中,我的Demo窗口上按钮和图标上文字使用了GDI绘制,透明下显示有些问题,调用DWM API中文字绘制方法会提升效果。DWM API提供了很多方法,让我们能够实现窗口叠加时透明、模糊,缩略图等等等一些高级效果,具体的可以看MSDN,网上也有能搜到不少资料。

五. 总结

本文讲述了桌面静态壁纸、动态壁纸、互动壁纸的实现原理和方法,同时针对壁纸被桌面整理软件挡住的问题进行了分析,并提出了解决法。文中讲述内容都是经过代码验证的,篇幅所限只贴了关键代码。有疑问的可以留言或者私下联系。

希望大家能把自己的所学和他人一起分享,不要去鄙视别人索取时的贪婪,因为最应该被鄙视的是不肯分享时的吝啬。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐