Windows编程之 | 您所在的位置:网站首页 › 桌面壁纸是什么软件做的 › Windows编程之 |
本文原创,最早发表于公司内部博客, 禁止转载 文章目录 一. 前言二. Windows桌面壁纸原理1. 桌面窗口层次2. 桌面嵌入窗口实现壁纸2.1. Desktop Window Manager2.2. 怎样让桌面窗口层次变成透明?2.3. 嵌入自己的窗口 3. 不能嵌入窗口的情况,如何实现桌面壁纸呢 三. 浅析Wallpaper Engine桌面壁纸的几种类型1. Web类型2. Scene方式3. 其他类型 四. 动态壁纸的一些其它问题1. 响应鼠标事件互动2. 被桌面整理软件挡住的问题 五. 总结 一. 前言本文转自我的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窗口中嵌入一个WM_CHILDWINDOW属性的窗口还是一个WS_POPUP窗口,要么覆盖在桌面SysLisView32窗口上,要么作为子窗口被挡住,简而言之,无法通过嵌入窗口的方式实现类似WallPaper Engine那样的壁纸。我们现在看下WallPaper Engine嵌入壁纸窗口时候桌面窗口层次: 只要能将我们自己的窗口嵌入到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序是固定的: 好的,此时桌面窗口层次变成明了,我们只需要把自己进程的窗口“嵌入”进去即可,使用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); }效果如下图: 启DWM Composition)以及XP系统都是无法使用此种方法的,因为此时系统的桌面窗口并不是透明的, 并没有上文所说的窗口类为WorkerW的WorkerW1(透明)、WokerW2窗口,考虑到此时桌面窗口层次是这样的: 后面的注释,翻译的意思就是背景由父窗口在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的素材支持类型比较多,还可以自己定义。在其每个素材目录下面有个project.json的当前素材配置,里面json下有个type节点,里面的值就是当前素材的类型。常见的有web,scene, video,picture,exe,还有一些Combo(组合)的等等。Wallpaper Engine 提供了素材编辑器, 可以使用其提供模板去编辑各种类型素材。下面的几种都是动态壁纸,都是用嵌入窗口的方式实现的。 1. Web类型这种类型的可以是动态的,并且可以互动(如何互动后面再说)。下面星空壁纸,星空可以跟随鼠标旋转移动的,嵌入web窗口,用了canvas绘图。一般web方式实现的,, 其素材目录如下: 这种也可以互动,支持2D/3D的,里面有shaders(着色器)加上一个scene.pkg,这个猜测是native的, 这种方式目录结构如下图所示: 还有一些其他的类型比如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++看下桌面窗口层次: 桌面整理软件原理就是自己创建一个桌面窗口覆盖掉在我们桌面窗口之上。此处我做了个简单桌面整理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);效果图如下: Spy++查看窗口层次如下: 上面代码只是修改了hRgnBlur参数。需要响应WM_PAINT或WM_ERASEBKGND,将背景填充为黑色,窗口就透明了。下面是桌面整理demo效果(这个MFC窗口为仿桌面整理窗口,此处测试没全屏): 此处gif太大不动, 可点击 桌面整理demo gif动图查看 桌面整理Demo窗口 >桌面SysListView32>QQ音乐动态壁纸窗口 现在桌面整理demo窗口是透明的,理论上其透明区域看到的背景应该桌面窗口的, 但是实际上看上面效果视屏,我的桌面整理Demo窗口透明区域显示的仍然是下方的Q音动态壁纸,这正是我们想要的,非常完美。窗口叠加时展现的效果跟我们预想的并不一致,这也是调用DWM API后产生的效果。所以有一个结论, 当桌面整理窗口全屏时,只要按上述方法将桌面整理窗口变成透明, 那么它就不会“遮挡”桌面壁纸窗口,同样我们也并不需要再去隐藏真正的桌面窗口,这种Z序下此时它会完全透明。 所以桌面整理软件都是可以做到不挡住桌面壁纸(只针对嵌入窗口实现)。只要桌面整理软件增加一种机制,可以通知使得其变成透明模式,那么就能解决桌面壁纸被桌面整理挡住的问题。实际上腾讯桌面整理确实这样做了,WallPaper Engine并不会被其挡住。 上方视屏中,我的Demo窗口上按钮和图标上文字使用了GDI绘制,透明下显示有些问题,调用DWM API中文字绘制方法会提升效果。DWM API提供了很多方法,让我们能够实现窗口叠加时透明、模糊,缩略图等等等一些高级效果,具体的可以看MSDN,网上也有能搜到不少资料。 五. 总结本文讲述了桌面静态壁纸、动态壁纸、互动壁纸的实现原理和方法,同时针对壁纸被桌面整理软件挡住的问题进行了分析,并提出了解决法。文中讲述内容都是经过代码验证的,篇幅所限只贴了关键代码。有疑问的可以留言或者私下联系。 希望大家能把自己的所学和他人一起分享,不要去鄙视别人索取时的贪婪,因为最应该被鄙视的是不肯分享时的吝啬。 |
CopyRight 2018-2019 实验室设备网 版权所有 |