1、Windows Vista for Developers第三部分桌面窗口管理器虽然从Windows 95(以及Windows NT 3.51)开始,Windows就允许开发者使用SetWindowRgn 函数创建不规则窗体,但却没有提供什么设置窗体透明度的选项。因此虽然开发者能够创建出一些奇形怪状的窗体,但却只能是不透明的。Windows 2000引入的分层窗体(依靠WS_EX_LAYERED)扩展了窗体的样式,开发者终于能够像控制窗体形状一样控制窗体的透明度了。而Windows Vista则更进一步,允许开发者控制窗体范围内部分区域的透明度。在Windows Vista for Develo
2、pers系列的第三部分中,我将介绍桌面窗口管理器(Desktop Window Manager,DWM)相关的API。DWM负责组合桌面上的各个窗体,DWM API则允许开发者设置某个窗体在于其它窗体组合/重叠时的显示效果。文中你会看到,DWM不仅仅能够用来实现“玻璃”特效。本文还将介绍Windows Vista从Windows 2000中继承下来的现有的透明功能是如何与最新的DWM功能集成使用,并作为其有力补充的。术语表图形图像方面的术语往往比较让人迷惑。若你想在Windows平台上实现某些透明/半透明功能,那么应该了解如下的一些术语:1. 透明(Transparency):指能够完全穿过某
3、一物体看到另一个物体的能力,例如干净的玻璃窗。某些程序和API也用这个术语表示从完全透明到完全不透明之间的某个状态。 2. 半透明(Translucency):很多人在使用时并不区分“透明”和“半透明”,但实际上这两个术语的意义却完全不同。“半透明”指的是透过某个物体隐约、模糊地看到另外物体的能力。Windows Vista称其玻璃效果为“透明玻璃效果”,而单从技术角度考虑,应该为“半透明玻璃效果”。 3. 不透明性/不透明度(Opacity):不透明度指的是一种介于完全不透明和完全透明之间的状态。某些程序和API用这个术语量化从完全透明到完全不透明之间的状态。 4. Alpha通道(Alph
4、a Channel):Alpha通道为图像的每一个像素都提供了透明度量化值,在图像重叠时可以将二者融合起来。 5. 窗体区域(Window Region):窗体区域是指操作系统允许窗体在其中进行绘制的区域。虽然Windows 95就提供了窗体区域,但直到Windows XP开始,其默认的主题才带有圆角边框。虽然Windows Vista的默认主题中也用到了圆角边框,但窗体区域已经不再使用了,除非你切换回Windows Vista Basic主题。6. Glass Glass is the catchy marketing terms that Windows Vista uses to ref
5、er to translucency. 7. 玻璃效果(Glass):Windows Vista用来吸引眼球的半透明效果的名称。 8. 模糊(Blur):一些DMW API使用了这个单词,它同样表示半透明。大概是这个词比较容易拼写和领会吧。 9. 桌面合成(Desktop Composition):DWM所提供的一个功能,可以实现诸如玻璃、3D窗口变换等视觉效果。 10. RGB:RGB是红(Red)、绿(Green)、蓝(Blue)的缩写。RGB值通常被包装在COLORREF 结构(其实就是个DWORD)中,格式如下:0x00BBGGRR。可以看到,第一个字节总是零,剩下的三个字节倒序存放了
6、红绿蓝的值。每一种颜色的范围是0到255。若三原色均为0,即为黑色,若均为255,则为白色。例如,纯红色用0x000000FF表示。我们也可以使用RGB宏,例如:#ff0000。可以看到RGB并没有提供Alpha通道信息。 11. ARGB:ARGB是Alpha、红(Red)、绿(Green)、蓝(Blue)的缩写,通常被包装在ARGB 结构(其实也是个DWORD)中,格式如下:0xAARRGGBB。第一个字节存放了Alpha值,剩下的三个字节存放了红绿蓝的值。注意这里颜色的顺序和RGB的相反。 12. GDI:Windows图形设备接口(Windows Graphics Device Int
7、erface,GDI)是Windows最初用来实现2D绘图的API,除了几个较新的函数之外,GDI API并没有什么对Alpha通道的支持。GDI用RGB值表示颜色。 13. GDI+:GDI+是Windows XP(以及Windows Server 2003)引入的一个更加强大的用来进行2D绘图、图像、文字处理的编程模型。GDI+完全支持Alpha通道,并用ARGB值表示颜色。顺便说一句,.NET Framework中的System.Drawing就建立于GDI+上。启用了合成效果么?搞定了上面这些术语之后,我们终于可以深入到桌面合成中了。当然,在使用之前必须保证该效果已经启用。出于性能考虑
8、,用户可能禁用了桌面合成效果。启用/禁用桌面合成效果可由如下步骤完成:1. 用如下命令打开System Properties窗口:2. %windir%system32SystemPropertiesAdvanced.exe 3. 点击Performance Settings按钮 4. 选中/清空“Enabled desktop composition”复选框需要注意的是桌面合成并不依赖于“玻璃效果”。虽然玻璃效果需要桌面合成,但你也可以在禁用玻璃效果的同时却启用桌面合成。Windows Vista提供了DwmIsCompositionEnabled 函数,用来监测当前是否启用了桌面合成功能。
9、请参考如下代码:BOOL enabled = FALSE;HRESULT result = :DwmIsCompositionEnabled(&enabled); 当然,这个函数并不是十分有用,因为在一些较早版本的Windows上,这个函数会因为缺少必要的DLL而调用失败。一个解决方案就是混合使用延迟加载和运行时动态链接技术。DWM API声明在dwmapi.dll中,为了保证可以你的程序可以运行于较早版本的Windows平台上,我们可以使用Visual C+的Delay Load功能只在能够使用DWM的Vista上加载该DLL。第一步就是让连接器延迟加载该DWM DLL。步骤如下:1. 打开
10、项目的属性页 2. 导航至Linker Input节 3. 将dwmapi.dll添加至“Delay Load DLLs”。还要保证“Additional Dependencies”中包含dwmapi.lib一项,以便连接器找到将要使用的各个DWM相关的API这样设置之后,DWM库将在你第一次调用其中函数时加载,但我们怎么知道这样调用是否安全呢?毕竟在较早版本的Windows上调用DwmIsCompositionEnabled 函数将尝试加载DMW库,进而导致你的程序崩溃。解决方案就是手工尝试加载DWM库,并小心地尝试取得DWM函数的入口地址。请参考如下封装好了的函数:bool IsCompo
11、sitionEnabled() HMODULE library = :LoadLibrary(Ldwmapi.dll); bool result = false; if (0 != library) if (0 != :GetProcAddress(library, DwmIsCompositionEnabled) BOOL enabled = FALSE; result = SUCCEEDED(:DwmIsCompositionEnabled(&enabled) & enabled; VERIFY(:FreeLibrary(library); return result;IsComposit
12、ionEnabled 函数尝试加载DWM库,并尝试得到DwmIsCompositionEnabled 函数的入口地址。若二者均成功的话,我们就可以认为程序此时正运行于Windows Vista或后续版本中。然后该函数简单地调用了DwmIsCompositionEnabled ,判断桌面合成是否启用。这样若IsCompositionEnabled 返回false,我们就知道不能再调用任何DWM函数了。还要注意的是,因为用户以及其他的应用程序都可以在任何时候启用/禁用桌面合成功能,所以你的程序需要能够合理应对这一变化并正常工作。若是桌面合成功能被禁用/启用,那么系统将发送WM_DWMCOMPOSI
13、TIONCHANGED消息来告知。不过WPARAM 和LPARAM 值却没有任何意义,你需要再次调用DwmIsCompositionEnabled 函数来取得当前桌面合成功能的状态。正如我前面面说的,程序可以在其运行的生命周期中根据需要暂时禁用/启用桌面合成功能(调用DwmEnableComposition 函数)。若你在禁用之后忘记了将其启用,那么系统将在程序退出后自动启用。下面的代码可以禁用桌面合成功能:HRESULT result = :DwmEnableComposition(DWM_EC_DISABLECOMPOSITION);下面的代码将启用桌面合成功能:HRESULT resul
14、t = :DwmEnableComposition(DWM_EC_ENABLECOMPOSITION);若用户正使用着Windows Vista的默认主题,那么这两行代码将在“Window Vista”和“Windows Vista Basic”两套主题中切换。记住,当应用程序退出之后,无论你是否手工启用,系统都将自动启用该功能。启用了半透明合成么?前一节中我曾经提到过,启用桌面合成功能并不意味着“玻璃效果”一定是半透明的。下面这两张图是一个完全相同的窗体。左边的是在半透明玻璃效果下的样式,而右边的是在不透明玻璃效果下的样式。可以看到,半透明玻璃效果下,可以看到桌面的颜色以及隐约的回收站图标,
15、而不透明玻璃效果下只能看到DWM提供的极光效果(aurora effect)。 用户可以按照如下步骤设置不透明度以及合成颜色(即用来呈现玻璃效果的颜色):1. 在桌面上右键单击,选择“Personalize”命令 2. 点击“Window Color and Appearance”链接应用程序也可以通过调用DwmGetColorizationColor函数检测到合成效果是半透明的还是不透明的,以及合成颜色:Gdiplus:ARGB color = 0;BOOL opaque = FALSE;HRESULT result = :DwmGetColorizationColor(&color, &o
16、paque);因为用户可以在任何时间更改这些设定,所以在任何设定改变之后,窗体都会受到系统发来的WM_DWMCOLORIZATIONCOLORCHANGED消息。WPARAM 提供了新的ARGB合成颜色;若更改为半透明合成,那么LPARAM为0,若更改为不透明合成,则为非0值。模糊客户区域假设桌面合成已经启用,那么DWM将把窗体的非客户区域以玻璃效果呈现。而客户区域默认为不透明,若想让客户区域完全或某部分实现玻璃效果,程序必须显式请求。使用DwmEnableBlurBehindWindow 函数即可实现这个功能该函数接受一个窗体的句柄,以及一个DWM_BLURBEHIND结构。DWM_BLUR
17、BEHIND的定义如下:struct DWM_BLURBEHIND DWORD dwFlags; BOOL fEnable; HRGN hRgnBlur; BOOL fTransitionOnMaximized;如下的几个标记用来设定dwFlags:1. DWM_BB_ENABLE 2. DWM_BB_BLURREGION 3. DWM_BB_TRANSITIONONMAXIMIZEDDwmEnableBlurBehindWindow 用起来并不是那么容易。我们可以使用C+对其进行简单封装:HRESULT EnableBlurBehindWindow(HWND window, bool ena
18、ble = true, HRGN region = 0, bool transitionOnMaximized = false) DWM_BLURBEHIND blurBehind = 0 ; blurBehind.dwFlags = DWM_BB_ENABLE | DWM_BB_TRANSITIONONMAXIMIZED; blurBehind.fEnable = enable; blurBehind.fTransitionOnMaximized = transitionOnMaximized; if (enable & 0 != region) blurBehind.dwFlags |=
19、DWM_BB_BLURREGION; blurBehind.hRgnBlur = region; return :DwmEnableBlurBehindWindow(window, &blurBehind);这样,启用/禁用模糊效果就变得很直观了。下面是几个例子:启用客户区域的模糊效果:HRESULT result = EnableBlurBehindWindow(window); 禁用客户区域的模糊效果:HRESULT result = EnableBlurBehindWindow(window, false);仅模糊窗体中的一个区域:CRgn rgn;rgn.CreateEllipticR
20、gn(30, 30, 170, 170);HRESULT result = EnableBlurBehindWindow(window, true, rgn);使用最大化窗口时的默认模糊效果:HRESULT result = EnableBlurBehindWindow(window, true, 0, true);可以看到DwmEnableBlurBehindWindow 的功能非常强大,借助于一个简单的C+封装,使用起来也颇为简易。在与分层窗体共同使用时,窗体的部分模糊也非常有用。扩展窗体框架(Window Frame)你可能已经注意到了,前面一节中的几个窗体截图中,虽然客户区域显示出了模
21、糊效果,但客户区域的边缘仍旧可见。若希望整个窗体能够无缝地显示出玻璃效果,那么要使用一种不同的办法。当然,若窗体本身就没有框架的话,那么DwmEnableBlurBehindWindow 也就足够用了。若想让框架撑满整个客户区域,我们需要使用一个名为DwmExtendFrameIntoClientArea 的函数。与DwmEnableBlurBehindWindow不一样的是,DwmExtendFrameIntoClientArea 函数非常易于理解,便于使用。DwmExtendFrameIntoClientArea 函数接受一个窗体句柄参数,以及一个指向MARGINS 结构的指针。MARGI
22、NS 结构用来指示框架将要在客户区域内扩展多少。下面这段代码就为客户区域添加了20像素的下边距:MARGINS margins = 0 ;margins.cyBottomHeight = 20;HRESULT result = :DwmExtendFrameIntoClientArea(m_hWnd, &margins);若想恢复默认设置,只要简单地再次将四个边距设置为0即可:MARGINS margins = 0 ;HRESULT result = :DwmExtendFrameIntoClientArea(m_hWnd, &margins);DwmExtendFrameIntoClient
23、Area 还提供了一个很有用的扩展,允许我们将整个客户区域和非客户区域作为一个无缝的整体显示出玻璃效果。只要将边距设置为-1即可:MARGINS margins = -1 ;HRESULT result = :DwmExtendFrameIntoClientArea(m_hWnd, &margins);绘图(Painting )目前为止,我们主要讨论的就是DMW中用来控制模糊效果的函数,还没有提到过模糊效果的实际应用。自然,接下来我们就将看看这样可爱的玻璃效果的用处。或许你正要在上面画出点什么呢!想要理解玻璃效果的工作原理,那么必须首先理解DWM函数和窗体之间的关系。DwmEnableBlur
24、BehindWindow 和DwmExtendFrameIntoClientArea 函数能够让DWM将窗体的任何部分实现玻璃效果,这是通过使用一个带有Alpha通道的、非完全不透明的笔刷来实现的。来看看下面的这个窗体的截图,其中包含了一幅我在Photoshop中创建的PNG图像:绘制在窗体上的这张图像包含有Alpha通道,因此DMW对其每个象素都严格地遵从了透明级别,显示出模糊的背景图像。需要意识到的就是,Windows开发者所使用的GDI函数对Alpha值并没有什么概念,也不能处理任何Alpha混合的相关操作。因此,若你想在Windows中实现透明/半透明效果,那么就必须使用GDI+(或者
25、其他图形处理库)。在开始用GDI+之前,我们先来看看老式的GDI能够实现出什么样的效果。碰巧,RGB的黑色(0x00000000)与ARGB的100%透明的二进制表示完全一样,因此我们就可以使用黑色的GDI画刷,让DWM理解为我们想要模糊绘制区域,结果就实现了我们期待的玻璃效果。请参考下面的ATL示例代码:class SampleWindow : public CWindowImplpublic: BEGIN_MSG_MAP(SampleWindow) MSG_WM_ERASEBKGND(OnEraseBackground) END_MSG_MAP() SampleWindow() VERIF
26、Y(Create(0, 0, LSample); const MARGINS margins = -1 ; COM_VERIFY(:DwmExtendFrameIntoClientArea(m_hWnd, &margins); private: virtual void OnFinalMessage(HWND) :PostQuitMessage(0); bool OnEraseBackground(CDCHandle dc) CRect rect; VERIFY(GetClientRect(&rect); dc.FillSolidRect(&rect, #000000); return tru
27、e; / Yes, I erased the background. ;在窗体创建之后,我们马上使用DwmExtendFrameIntoClientArea 函数告知DMW将整个客户区域呈现出无缝的玻璃效果。随后,窗体接收到WM_ERASEBKGND 消息,并用“黑色”填充了整个客户区域。结果正如你所期待:这样做当然可以实现玻璃效果,但若你还想在窗体上添加一些别的什么东西的话,那么最好不要使用这种黑色画刷的方法,否则其他东西也将以半透明的形式显示出来。比如说一个包含文本框的对话框:若我们还是使用黑色画刷的方式实现玻璃效果,那么结果或许并不是那么漂亮:可以看到,因为该文本框使用黑色画刷来绘出文本
28、,所以DWM将以为这部分内容也将呈现为半透明效果。一个解决方案就是手工绘制这个文本框。不知道你们是怎么想的,不过我可不想把时间花费在“手工绘制”每一个控件上。我会使用系统已经自带的那一套控件,而不是去重复发明轮子。更实际的做法就是使用分层窗体(layered window)。分层窗体最先由Windows 2000引入,它支持Alpha混合,自然成了实现玻璃效果的理想图景。分层窗体提供了两种截然不同的编程模型。你既可以使用UpdateLayeredWindow 函数提供一个与设备无关的位图,完整定义屏幕上窗体的整体样式(比较困难),也可以直接使用SetLayeredWindowAttribute
29、s 函数(比较简单)。让我们先从简单的开始。SetLayeredWindowAttributes 函数允许我们设置一个RGB颜色,然后所有以该颜色绘出的像素都将呈现为透明。这样黑色就不再承担两种不同的任务了,我们即可继续根据需要使用黑色绘出文本等内容。若你的窗体已经包含了WS_EX_LAYERED 窗体样式,那么即可依照如下方式调用SetLayeredWindowAttributes 函数,来定义透明颜色:const COLORREF color = #c8c9ca;VERIFY(:SetLayeredWindowAttributes(window, color, 0, LWA_COLORKE
30、Y);如何选择一个恰当的颜色就成了最困难的事情。理论上,我们应该选择一个能够与背景混合得最好的颜色,可我们并不知道程序运行时的背景是什么样子的。字体的平滑处理也不是很让人满意,字体将与透明颜色混合起来,而不是我们期待的背景,然而我们还是有办法解决。很重要的一点就是要选择一个窗体中不会出现的颜色。SetLayeredWindowAttributes 函数同样也努力为那些使用GDI的朋友们提供一些UpdateLayeredWindow 函数的高级功能,其中一个就是创建不规则窗体。这似乎有些离题了,不过只所以我在这里提到,是因为只有你选择红、绿或蓝作为透明颜色时,SetLayeredWindowAt
31、tributes 才能提供该功能。不过这个效果与DWM配合的并不是那么好,因为虽然玻璃效果没什么问题,但用户的鼠标操作却将被其下面的窗体捕获。了解之后,我们来看一个使用分层窗体实现半透明效果对话框的完整示例:class SampleDialog : public CDialogImplpublic: enum IDD = IDD_SAMPLE ; BEGIN_MSG_MAP(MainWindow) MSG_WM_INITDIALOG(OnInitDialog) MSG_WM_ERASEBKGND(OnEraseBackground) COMMAND_ID_HANDLER(IDCANCEL, OnCancel) END_MSG_MAP() SampleDialog() : m_transparencyKey(#c8c9ca) / Do nothing private: LRESULT OnInitDialog(HWND /*co
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1