65.9K
CodeProject 正在变化。 阅读更多。
Home

为框架窗口添加阴影

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (55投票s)

2006年11月13日

Ms-PL

8分钟阅读

viewsIcon

5153235

downloadIcon

12525

一篇关于如何为应用程序框架窗口添加阴影的文章。不需要MFC/ATL。

Screenshot - FrameShadow.jpg

引言

在即将推出的 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 应用程序。

  1. WndShadow.hWndShadow.cpp 添加到你的项目中。在任何使用 CWndShadow 的地方,添加:
    #include "WndShadow.h"
  2. 在创建任何阴影之前调用 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...
    }
  3. 为你想要添加阴影的每个框架窗口声明一个 CWndShadow 实例。
    CWndShadow WndShadow;
    

    请记住,在框架窗口被销毁之前,这个对象不应该被销毁。例如,你可以将其设为全局变量,或者,如果使用 MFC,可以将其设为该窗口对应 MFC 类的成员。

  4. 通过传入框架窗口的句柄调用 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 启用时,我的代码生成的阴影将自动禁用。

Screenshot - Aero.gif

阴影的高级控制

可以设置一些参数来控制阴影的外观。以下是这些参数的完整列表。我必须强调,这些参数的范围未经严格测试。如果这些参数没有设置在“常规”值附近,阴影可能无法正常工作。

  • 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 中工作。两个要点如下:

  1. UpdateLayeredWindow() Windows API 和一些相关的常量在旧版 SDK 中没有定义。在这里,我使用了类似于 Quick and Dirty Window Transparency 中描述的技巧。
  2. 我将阴影参数变量声明为 INT8UINT8,它们与 charunsigned char 相同,但看起来更像整数值。不幸的是,它们在旧版 SDK 中也没有定义。所以最后,我不得不将它们声明为 charunsigned char,这有时可能看起来令人困惑。

我的代码还涉及一些 Vista 特有的 API 和窗口消息,以使我的代码支持 Aero。这不幸地需要安装 Vista SDK 才能编译。更糟糕的是,如果这些 API 硬编码在代码中,应用程序将在 Vista 之前的 Windows 版本上崩溃。因此,我也使用了与第 1 点类似的技巧将这些 Vista 特有内容集成到我的代码中。

未来工作

使用当前的 CWndShadow,在 Windows Vista 中模拟窗口阴影似乎很困难。主要有两个原因:

  1. Windows Vista——以及 WLM——在阴影边框周围使用非线性渐变函数。
  2. 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
    • 首次发布。
© . All rights reserved.