为框架窗口添加阴影






4.82/5 (55投票s)
一篇关于如何为应用程序框架窗口添加阴影的文章。不需要MFC/ATL。
引言
在即将推出的 Windows Vista 中,每个框架窗口默认都有一个炫酷的阴影。微软最近发布的 Windows Live Messenger,其中有一个选项可以使它的框架窗口带有阴影。这强烈表明,在 Windows Vista 以外的操作系统上,有一种方法可以为应用程序窗口添加阴影。在本文中,我们将尝试用一种类似于 WLM 的方法来实现这一点。
背景
当我遇到这个问题时,我首先想到的是在网上搜索,看看是否有人已经做过。不幸的是,我找到的几乎所有内容——其中一些也在 CodeProject 上——都是关于如何为子窗口(如控件)添加阴影的。主要思想是在父窗口上绘制阴影。然而,关键在于,如果你想为一个最顶层窗口添加阴影,阴影应该绘制在不属于你的应用程序的区域。绘制在其他应用程序的窗口上可能会弄乱它们的屏幕显示。更糟糕的是,如果那些窗口被改变或移动了呢?
然后我找到了这篇用中文写的 文章,它提出了一个解决绘制问题的绝妙方法。作者为框架窗口创建了三个小的子窗口:分别在右侧、底部和右下角,用于绘制阴影。阴影是通过对底层窗口图像进行 alpha 混合绘制的。当框架窗口移动或调整大小时,子阴影窗口也会相应地移动、调整大小并重绘。
然而,这个解决方案有两个主要缺点:
- 如果你点击阴影,得到焦点的是阴影窗口而不是底层窗口。通常人们期望这些区域实际上属于底层窗口,只是上面叠加了阴影。
- 当底层窗口移动或改变时,阴影看起来会出错。作者设置了一个定时器,让阴影周期性地用底层窗口的最新图像重绘,这使得它看起来更好,但问题并没有解决。
然后,幸运的是,我发现从 Windows 2000 开始支持的层叠窗口(layered windows)可以解决上述两个问题。层叠窗口可以“透明”地响应鼠标操作,并且可以自动与底层窗口的实际图像进行 alpha 混合。这比上面提到的方法更容易,因为你实际上不必担心如何进行 alpha 混合绘制。你只需要为每个像素设置颜色——当然是预乘的——以及 alpha 值。
有了这些启发,我用 Spy++ 观察了 WLM 窗口。我发现 WLM 用这种方法来使其窗口带阴影。每个 WLM 带阴影的窗口都有一个类名为 SysShadow 的层叠窗口。SysShadow 实际上由 Windows 系统提供,用于为某些窗口(如菜单)添加阴影。通过为窗口类添加 CS_DROPSHADOW
类样式,可以轻松地为你的框架窗口添加 SysShadow 阴影窗口。然而,这个解决方案仍然有两个缺点:
- 该阴影的外观不可控,或者至少我没有找到任何方法可以做到。
- 该阴影由系统选项控制。尝试在系统属性的“视觉效果”中取消选中“显示菜单下方的阴影”选项;然后 WLM 的阴影就会消失!
因此,我决定编写自己的代码。
使用代码
使用我的代码,为框架窗口添加阴影很容易。CWndShadow
可以用于 MFC/ATL 和非 MFC/ATL 应用程序。
- 将 WndShadow.h 和 WndShadow.cpp 添加到你的项目中。在任何使用
CWndShadow
的地方,添加:#include "WndShadow.h"
- 在创建任何阴影之前调用
CWndShadow::Initialize()
。此函数需要应用程序实例的句柄作为参数。这通过 `WinMain()` 的参数传递给应用程序。如果使用 MFC,可以通过调用AfxGetInstanceHandle()
来获取。MFC 的示例代码如下:BOOL CYourApp::InitInstance() { // CWndShadow::Initialize() could be also called anywhere else, just // before any call to CWndShadow::Create(). // Here is only an example CWndShadow::Initialize(AfxGetInstanceHandle()); // Other codes... }
- 为你想要添加阴影的每个框架窗口声明一个
CWndShadow
实例。CWndShadow WndShadow;
请记住,在框架窗口被销毁之前,这个对象不应该被销毁。例如,你可以将其设为全局变量,或者,如果使用 MFC,可以将其设为该窗口对应 MFC 类的成员。
- 通过传入框架窗口的句柄调用
CWndShadow
对象的Create()
来创建阴影。如果使用 MFC,请记住Create()
必须在框架窗口实际创建之后调用。例如,对于常规窗口,在OnCreate()
中;对于对话框,在OnInitDialog()
中。MFC 的代码可能如下所示:int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { // Other code of CMainFrame::OnCreate() // assume CWndShadow CMainFrame::m_Shadow; m_Shadow.Create(GetSafeHwnd()); return 0; }
现在你的框架窗口应该有阴影了。尽情享受吧。
Vista Aero 感知
在 Windows Vista 上,当启用 Aero 时,或者更准确地说,当 桌面合成 启用时,框架窗口应该已经有了下拉阴影。桌面合成负责 Vista 的多项视觉效果。这包括但不限于:玻璃窗口框架——无论是半透明还是不透明——以及下拉阴影。当已经存在阴影时,我的阴影应该被隐藏。因此,我借助 DwmIsCompositionEnabled()
API 和 WM_DWMCOMPOSITIONCHANGED
窗口消息,使我的阴影支持 Aero 感知。当 Vista Aero 启用时,我的代码生成的阴影将自动禁用。
阴影的高级控制
可以设置一些参数来控制阴影的外观。以下是这些参数的完整列表。我必须强调,这些参数的范围未经严格测试。如果这些参数没有设置在“常规”值附近,阴影可能无法正常工作。
bool CWndShadow::SetColor(COLORREF NewColor);
设置阴影的颜色。bool CWndShadow::SetSize(int NewSize);
设置阴影窗口的相对大小。该大小相对于父窗口,而不是绝对值。大小值会添加到父窗口的四个边。也就是说,如果父窗口的大小为 100*100,并且调用SetSize()
并传入值 2,那么阴影窗口的大小将为 104*104。此参数可以设置为负值。bool CWndShadow::SetSharpness(unsigned int NewSharpness);
,锐度值控制阴影边框周围的渐变半透明区域的宽度。如果此值设置为 0,则阴影将具有最锐利的边框。bool CWndShadow::SetHardness(unsigned int NewHardness);
设置阴影的黑暗度。此值越大,阴影越暗。bool CWndShadow::SetPosition(int NewXOffset, int NewYOffset);
设置阴影窗口的相对位置。原点是父窗口的中心和阴影窗口的中心。如果XOffset
设置为正值,则阴影会向右移动。如果YOffset
设置为正值,则阴影会向下移动。
关注点
我的代码最初是用 Visual C++ 2005 编写的。当我尝试在 Visual C++ 6.0 中测试它时,遇到了几个问题。其中大部分是由于 VC++ 6 附带的旧版 Platform SDK,有些是因为它与 C++ 标准不兼容。由于我从微软下载的最新 SDK 版本不支持 VC++ 6,我决定修改原始代码,使其能在 VC6 中工作。两个要点如下:
UpdateLayeredWindow()
Windows API 和一些相关的常量在旧版 SDK 中没有定义。在这里,我使用了类似于 Quick and Dirty Window Transparency 中描述的技巧。- 我将阴影参数变量声明为
INT8
和UINT8
,它们与char
和unsigned char
相同,但看起来更像整数值。不幸的是,它们在旧版 SDK 中也没有定义。所以最后,我不得不将它们声明为char
和unsigned char
,这有时可能看起来令人困惑。
我的代码还涉及一些 Vista 特有的 API 和窗口消息,以使我的代码支持 Aero。这不幸地需要安装 Vista SDK 才能编译。更糟糕的是,如果这些 API 硬编码在代码中,应用程序将在 Vista 之前的 Windows 版本上崩溃。因此,我也使用了与第 1 点类似的技巧将这些 Vista 特有内容集成到我的代码中。
未来工作
使用当前的 CWndShadow
,在 Windows Vista 中模拟窗口阴影似乎很困难。主要有两个原因:
- Windows Vista——以及 WLM——在阴影边框周围使用非线性渐变函数。
- Windows Vista 使用不同的模型来生成阴影,而我的模型类似于 WLM。并且由 Vista 模型生成的阴影似乎必须比父窗口大。
我正在考虑在 CWndShadow
的未来版本中添加对非渐变边框函数以及这两种阴影模型(Vista 模型和 WLM 模型)的支持。
已知问题
- 我的
CWndShadow
使用父窗口区域来计算阴影的形状。似乎在某些窗口大小调整的情况下,父窗口区域可能没有正确更新,因此直到大小调整完成(即鼠标按钮抬起),阴影的某些部分才可能看起来不正确。 - 如果使用 VC++ 6 编译,我的代码将因为使用
std::map
而收到几个 C4786 警告。这是 VC++ 6 编译器的一个已知问题,不应对代码的执行造成任何影响。
历史
- 版本 0.3, 2007-06-14
- 阴影已实现 Windows Vista Aero 感知。
- 修复了一个导致阴影在 Windows Vista 上显示异常的 bug。
- 修复了一个导致父窗口初始最小化或最大化时阴影显示异常的 bug。
- 版本 0.2, 2006-11-23
- 修复了一个关键问题,该问题可能导致阴影在某些条件下无法工作,例如,在 Win2K 上,在没有启用视觉主题的 Win XP/2003 上,或者当框架窗口没有标题栏时。
- 版本 0.1, 2006-11-10
- 首次发布。