Windows Vista for Developers——第三部分:桌面窗口管理器

作者:Kenny Kerr

翻译:Dflying Chen

原文:http://weblogs.asp.net/kennykerr/archive/2006/08/10/Windows-Vista-for-Developers-_1320_-Part-3-_1320_-The-Desktop-Window-Manager.aspx

请同时参考《Windows Vista for Developers》系列

 

虽然从Windows 95(以及Windows NT 3.51)开始,Windows就允许开发者使用SetWindowRgn 函数创建不规则窗体,但却没有提供什么设置窗体透明度的选项。因此虽然开发者能够创建出一些奇形怪状的窗体,但却只能是不透明的。Windows 2000引入的分层窗体(依靠WS_EX_LAYERED)扩展了窗体的样式,开发者终于能够像控制窗体形状一样控制窗体的透明度了。而Windows Vista则更进一步,允许开发者控制窗体范围内部分区域的透明度。

《Windows Vista for Developers》系列的第三部分中,我将介绍桌面窗口管理器(Desktop Window Manager,DWM)相关的API。DWM负责组合桌面上的各个窗体,DWM API则允许开发者设置某个窗体在于其它窗体组合/重叠时的显示效果。文中你会看到,DWM不仅仅能够用来实现“玻璃”特效。本文还将介绍Windows Vista从Windows 2000中继承下来的现有的透明功能是如何与最新的DWM功能集成使用,并作为其有力补充的。

 

术语表

图形图像方面的术语往往比较让人迷惑。若你想在Windows平台上实现某些透明/半透明功能,那么应该了解如下的一些术语:

  1. 透明(Transparency):指能够完全穿过某一物体看到另一个物体的能力,例如干净的玻璃窗。某些程序和API也用这个术语表示从完全透明到完全不透明之间的某个状态。
  2. 半透明(Translucency):很多人在使用时并不区分“透明”和“半透明”,但实际上这两个术语的意义却完全不同。“半透明”指的是透过某个物体隐约、模糊地看到另外物体的能力。Windows Vista称其玻璃效果为“透明玻璃效果”,而单从技术角度考虑,应该为“半透明玻璃效果”。
  3. 不透明性/不透明度(Opacity):不透明度指的是一种介于完全不透明和完全透明之间的状态。某些程序和API用这个术语量化从完全透明到完全不透明之间的状态。
  4. Alpha通道(Alpha Channel):Alpha通道为图像的每一个像素都提供了透明度量化值,在图像重叠时可以将二者融合起来。
  5. 窗体区域(Window Region):窗体区域是指操作系统允许窗体在其中进行绘制的区域。虽然Windows 95就提供了窗体区域,但直到Windows XP开始,其默认的主题才带有圆角边框。虽然Windows Vista的默认主题中也用到了圆角边框,但窗体区域已经不再使用了,除非你切换回Windows Vista Basic主题。
    Glass – Glass is the catchy marketing terms that Windows Vista uses to refer to translucency.
  6. 玻璃效果(Glass):Windows Vista用来吸引眼球的半透明效果的名称。
  7. 模糊(Blur):一些DMW API使用了这个单词,它同样表示半透明。大概是这个词比较容易拼写和领会吧。
  8. 桌面合成(Desktop Composition):DWM所提供的一个功能,可以实现诸如玻璃、3D窗口变换等视觉效果。
  9. RGB:RGB是红(Red)、绿(Green)、蓝(Blue)的缩写。RGB值通常被包装在COLORREF 结构(其实就是个DWORD)中,格式如下:0x00BBGGRR。可以看到,第一个字节总是零,剩下的三个字节倒序存放了红绿蓝的值。每一种颜色的范围是0到255。若三原色均为0,即为黑色,若均为255,则为白色。例如,纯红色用0x000000FF表示。我们也可以使用RGB宏,例如:#ff0000。可以看到RGB并没有提供Alpha通道信息。
  10. ARGB:ARGB是Alpha、红(Red)、绿(Green)、蓝(Blue)的缩写,通常被包装在ARGB 结构(其实也是个DWORD)中,格式如下:0xAARRGGBB。第一个字节存放了Alpha值,剩下的三个字节存放了红绿蓝的值。注意这里颜色的顺序和RGB的相反。
  11. GDI:Windows图形设备接口(Windows Graphics Device Interface,GDI)是Windows最初用来实现2D绘图的API,除了几个较新的函数之外,GDI API并没有什么对Alpha通道的支持。GDI用RGB值表示颜色。
  12. GDI+:GDI+是Windows XP(以及Windows Server 2003)引入的一个更加强大的用来进行2D绘图、图像、文字处理的编程模型。GDI+完全支持Alpha通道,并用ARGB值表示颜色。顺便说一句,.NET Framework中的System.Drawing就建立于GDI+上。

 

启用了合成效果么?

搞定了上面这些术语之后,我们终于可以深入到桌面合成中了。当然,在使用之前必须保证该效果已经启用。出于性能考虑,用户可能禁用了桌面合成效果。启用/禁用桌面合成效果可由如下步骤完成:

  1. 用如下命令打开System Properties窗口:
    %windir%\system32\SystemPropertiesAdvanced.exe
  2. 点击Performance Settings按钮
  3. 选中/清空“Enabled desktop composition”复选框

需要注意的是桌面合成并不依赖于“玻璃效果”。虽然玻璃效果需要桌面合成,但你也可以在禁用玻璃效果的同时却启用桌面合成。

Windows Vista提供了DwmIsCompositionEnabled 函数,用来监测当前是否启用了桌面合成功能。请参考如下代码:

BOOL enabled = FALSE;
HRESULT result = ::DwmIsCompositionEnabled(&enabled);

当然,这个函数并不是十分有用,因为在一些较早版本的Windows上,这个函数会因为缺少必要的DLL而调用失败。一个解决方案就是混合使用延迟加载和运行时动态链接技术。DWM API声明在dwmapi.dll中,为了保证可以你的程序可以运行于较早版本的Windows平台上,我们可以使用Visual C++的Delay Load功能只在能够使用DWM的Vista上加载该DLL。第一步就是让连接器延迟加载该DWM DLL。步骤如下:

  1. 打开项目的属性页
  2. 导航至Linker > Input节
  3. dwmapi.dll添加至“Delay Load DLLs”。还要保证“Additional Dependencies”中包含dwmapi.lib一项,以便连接器找到将要使用的各个DWM相关的API

这样设置之后,DWM库将在你第一次调用其中函数时加载,但我们怎么知道这样调用是否安全呢?毕竟在较早版本的Windows上调用DwmIsCompositionEnabled 函数将尝试加载DMW库,进而导致你的程序崩溃。解决方案就是手工尝试加载DWM库,并小心地尝试取得DWM函数的入口地址。请参考如下封装好了的函数:

bool IsCompositionEnabled()
{
    HMODULE library = ::LoadLibrary(L"dwmapi.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;
}

IsCompositionEnabled 函数尝试加载DWM库,并尝试得到DwmIsCompositionEnabled 函数的入口地址。若二者均成功的话,我们就可以认为程序此时正运行于Windows Vista或后续版本中。然后该函数简单地调用了DwmIsCompositionEnabled ,判断桌面合成是否启用。这样若IsCompositionEnabled 返回false,我们就知道不能再调用任何DWM函数了。

还要注意的是,因为用户以及其他的应用程序都可以在任何时候启用/禁用桌面合成功能,所以你的程序需要能够合理应对这一变化并正常工作。若是桌面合成功能被禁用/启用,那么系统将发送WM_DWMCOMPOSITIONCHANGED消息来告知。不过WPARAMLPARAM 值却没有任何意义,你需要再次调用DwmIsCompositionEnabled 函数来取得当前桌面合成功能的状态。

正如我前面面说的,程序可以在其运行的生命周期中根据需要暂时禁用/启用桌面合成功能(调用DwmEnableComposition 函数)。若你在禁用之后忘记了将其启用,那么系统将在程序退出后自动启用。

下面的代码可以禁用桌面合成功能:

HRESULT result = ::DwmEnableComposition(DWM_EC_DISABLECOMPOSITION);

下面的代码将启用桌面合成功能:

HRESULT result = ::DwmEnableComposition(DWM_EC_ENABLECOMPOSITION);

若用户正使用着Windows Vista的默认主题,那么这两行代码将在“Window Vista”和“Windows Vista Basic”两套主题中切换。记住,当应用程序退出之后,无论你是否手工启用,系统都将自动启用该功能。

 

启用了半透明合成么?

前一节中我曾经提到过,启用桌面合成功能并不意味着“玻璃效果”一定是半透明的。下面这两张图是一个完全相同的窗体。左边的是在半透明玻璃效果下的样式,而右边的是在不透明玻璃效果下的样式。可以看到,半透明玻璃效果下,可以看到桌面的颜色以及隐约的回收站图标,而不透明玻璃效果下只能看到DWM提供的极光效果(aurora effect)。

用户可以按照如下步骤设置不透明度以及合成颜色(即用来呈现玻璃效果的颜色):

  1. 在桌面上右键单击,选择“Personalize”命令
  2. 点击“Window Color and Appearance”链接

应用程序也可以通过调用DwmGetColorizationColor函数检测到合成效果是半透明的还是不透明的,以及合成颜色:

Gdiplus::ARGB color = 0;
BOOL opaque = FALSE;
HRESULT result = ::DwmGetColorizationColor(&color,
                                           &opaque);

因为用户可以在任何时间更改这些设定,所以在任何设定改变之后,窗体都会受到系统发来的WM_DWMCOLORIZATIONCOLORCHANGED消息。WPARAM 提供了新的ARGB合成颜色;若更改为半透明合成,那么LPARAM为0,若更改为不透明合成,则为非0值。

 

模糊客户区域

假设桌面合成已经启用,那么DWM将把窗体的非客户区域以玻璃效果呈现。而客户区域默认为不透明,若想让客户区域完全或某部分实现玻璃效果,程序必须显式请求。使用DwmEnableBlurBehindWindow 函数即可实现这个功能该函数接受一个窗体的句柄,以及一个DWM_BLURBEHIND结构。DWM_BLURBEHIND的定义如下:

struct DWM_BLURBEHIND
{
    DWORD dwFlags;
    BOOL fEnable;
    HRGN hRgnBlur;
    BOOL fTransitionOnMaximized;
};

如下的几个标记用来设定dwFlags

  1. DWM_BB_ENABLE
  2. DWM_BB_BLURREGION
  3. DWM_BB_TRANSITIONONMAXIMIZED

DwmEnableBlurBehindWindow 用起来并不是那么容易。我们可以使用C++对其进行简单封装:

HRESULT EnableBlurBehindWindow(HWND window,
                               bool enable = 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 |= DWM_BB_BLURREGION;
        blurBehind.hRgnBlur = region;
    }
    return ::DwmEnableBlurBehindWindow(window,
                                       &blurBehind);
}

这样,启用/禁用模糊效果就变得很直观了。下面是几个例子:

启用客户区域的模糊效果:

HRESULT result = EnableBlurBehindWindow(window); 

 

禁用客户区域的模糊效果:

HRESULT result = EnableBlurBehindWindow(window,
                                        false);

仅模糊窗体中的一个区域:

CRgn rgn;
rgn.CreateEllipticRgn(30, 30, 170, 170);
HRESULT result = EnableBlurBehindWindow(window,
                                        true,
                                        rgn);

使用最大化窗口时的默认模糊效果:

HRESULT result = EnableBlurBehindWindow(window,
                                        true,
                                        0,
                                        true);

可以看到DwmEnableBlurBehindWindow 的功能非常强大,借助于一个简单的C++封装,使用起来也颇为简易。在与分层窗体共同使用时,窗体的部分模糊也非常有用。


扩展窗体框架(Window Frame)

你可能已经注意到了,前面一节中的几个窗体截图中,虽然客户区域显示出了模糊效果,但客户区域的边缘仍旧可见。若希望整个窗体能够无缝地显示出玻璃效果,那么要使用一种不同的办法。当然,若窗体本身就没有框架的话,那么DwmEnableBlurBehindWindow 也就足够用了。

若想让框架撑满整个客户区域,我们需要使用一个名为DwmExtendFrameIntoClientArea 的函数。与DwmEnableBlurBehindWindow不一样的是,DwmExtendFrameIntoClientArea 函数非常易于理解,便于使用。

DwmExtendFrameIntoClientArea 函数接受一个窗体句柄参数,以及一个指向MARGINS 结构的指针。MARGINS 结构用来指示框架将要在客户区域内扩展多少。下面这段代码就为客户区域添加了20像素的下边距:

MARGINS margins = { 0 };
margins.cyBottomHeight = 20;
HRESULT result = ::DwmExtendFrameIntoClientArea(m_hWnd,
                                                &margins);

若想恢复默认设置,只要简单地再次将四个边距设置为0即可:

MARGINS margins = { 0 };
HRESULT result = ::DwmExtendFrameIntoClientArea(m_hWnd,
                                                &margins);

DwmExtendFrameIntoClientArea 还提供了一个很有用的扩展,允许我们将整个客户区域和非客户区域作为一个无缝的整体显示出玻璃效果。只要将边距设置为-1即可:

MARGINS margins = { -1 };
HRESULT result = ::DwmExtendFrameIntoClientArea(m_hWnd,
                                                &margins);

 

绘图(Painting )

目前为止,我们主要讨论的就是DMW中用来控制模糊效果的函数,还没有提到过模糊效果的实际应用。自然,接下来我们就将看看这样可爱的玻璃效果的用处。或许你正要在上面画出点什么呢!

想要理解玻璃效果的工作原理,那么必须首先理解DWM函数和窗体之间的关系。DwmEnableBlurBehindWindow DwmExtendFrameIntoClientArea 函数能够让DWM将窗体的任何部分实现玻璃效果,这是通过使用一个带有Alpha通道的、非完全不透明的笔刷来实现的。来看看下面的这个窗体的截图,其中包含了一幅我在Photoshop中创建的PNG图像:

绘制在窗体上的这张图像包含有Alpha通道,因此DMW对其每个象素都严格地遵从了透明级别,显示出模糊的背景图像。需要意识到的就是,Windows开发者所使用的GDI函数对Alpha值并没有什么概念,也不能处理任何Alpha混合的相关操作。因此,若你想在Windows中实现透明/半透明效果,那么就必须使用GDI+(或者其他图形处理库)。在开始用GDI+之前,我们先来看看老式的GDI能够实现出什么样的效果。

碰巧,RGB的黑色(0x00000000)与ARGB的100%透明的二进制表示完全一样,因此我们就可以使用黑色的GDI画刷,让DWM理解为我们想要模糊绘制区域,结果就实现了我们期待的玻璃效果。请参考下面的ATL示例代码:

class SampleWindow : 
    public CWindowImpl<SampleWindow, CWindow, CFrameWinTraits>
{
public:
    BEGIN_MSG_MAP(SampleWindow)
        MSG_WM_ERASEBKGND(OnEraseBackground)
    END_MSG_MAP()
    SampleWindow()
    {
        VERIFY(Create(0, 0, L"Sample"));
        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 true; // Yes, I erased the background.
    }
};

在窗体创建之后,我们马上使用DwmExtendFrameIntoClientArea 函数告知DMW将整个客户区域呈现出无缝的玻璃效果。随后,窗体接收到WM_ERASEBKGND 消息,并用“黑色”填充了整个客户区域。结果正如你所期待:

这样做当然可以实现玻璃效果,但若你还想在窗体上添加一些别的什么东西的话,那么最好不要使用这种黑色画刷的方法,否则其他东西也将以半透明的形式显示出来。比如说一个包含文本框的对话框:

若我们还是使用黑色画刷的方式实现玻璃效果,那么结果或许并不是那么漂亮:

可以看到,因为该文本框使用黑色画刷来绘出文本,所以DWM将以为这部分内容也将呈现为半透明效果。一个解决方案就是手工绘制这个文本框。不知道你们是怎么想的,不过我可不想把时间花费在“手工绘制”每一个控件上。我会使用系统已经自带的那一套控件,而不是去重复发明轮子。

更实际的做法就是使用分层窗体(layered window)。分层窗体最先由Windows 2000引入,它支持Alpha混合,自然成了实现玻璃效果的理想图景。分层窗体提供了两种截然不同的编程模型。你既可以使用UpdateLayeredWindow  函数提供一个与设备无关的位图,完整定义屏幕上窗体的整体样式(比较困难),也可以直接使用SetLayeredWindowAttributes 函数(比较简单)。让我们先从简单的开始。

SetLayeredWindowAttributes 函数允许我们设置一个RGB颜色,然后所有以该颜色绘出的像素都将呈现为透明。这样黑色就不再承担两种不同的任务了,我们即可继续根据需要使用黑色绘出文本等内容。若你的窗体已经包含了WS_EX_LAYERED 窗体样式,那么即可依照如下方式调用SetLayeredWindowAttributes 函数,来定义透明颜色:

const COLORREF color = #c8c9ca;
VERIFY(::SetLayeredWindowAttributes(window, 
                                    color, 
                                    0, 
                                    LWA_COLORKEY));

如何选择一个恰当的颜色就成了最困难的事情。理论上,我们应该选择一个能够与背景混合得最好的颜色,可我们并不知道程序运行时的背景是什么样子的。字体的平滑处理也不是很让人满意,字体将与透明颜色混合起来,而不是我们期待的背景,然而我们还是有办法解决。很重要的一点就是要选择一个窗体中不会出现的颜色。SetLayeredWindowAttributes 函数同样也努力为那些使用GDI的朋友们提供一些UpdateLayeredWindow  函数的高级功能,其中一个就是创建不规则窗体。这似乎有些离题了,不过只所以我在这里提到,是因为只有你选择红、绿或蓝作为透明颜色时,SetLayeredWindowAttributes 才能提供该功能。不过这个效果与DWM配合的并不是那么好,因为虽然玻璃效果没什么问题,但用户的鼠标操作却将被其下面的窗体捕获。

了解之后,我们来看一个使用分层窗体实现半透明效果对话框的完整示例:

class SampleDialog :
    public CDialogImpl<SampleDialog>
{
public:
    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 /*control*/,
                         LPARAM /*lParam*/)
    {
        SetWindowLong(GWL_EXSTYLE,
                      GetExStyle() | WS_EX_LAYERED);
        VERIFY(::SetLayeredWindowAttributes(m_hWnd, 
                                            m_transparencyKey, 
                                            0, 
                                            LWA_COLORKEY));
        const MARGINS margins = { -1 };
        COM_VERIFY(::DwmExtendFrameIntoClientArea(m_hWnd,
                                                  &margins));
        return TRUE; // Yes, go ahead and set the keyboard focus.
    }
    bool OnEraseBackground(CDCHandle dc)
    {
        CRect rect;
        VERIFY(GetClientRect(&rect));
        dc.FillSolidRect(&rect, 
                         m_transparencyKey);
        return true; // Yes, I erased the background.
    }
    LRESULT OnCancel(WORD /*notifyCode*/, 
                     WORD identifier, 
                     HWND /*window*/, 
                     BOOL& /*handled*/)
    {
        VERIFY(EndDialog(identifier));
        return 0;
    }
    const COLORREF m_transparencyKey;
};

结果比前面强了不少,还算不错:

这个SampleDialog 类和前面的使用黑色GDI画刷的SampleWindow 类得最主要区别就是,在SampleDialog 示例中我们使用SetLayeredWindowAttributes 设定了透明颜色,且在WM_ERASEBKGND消息的处理函数中使用该透明颜色(而不是前面的黑色)填充了整个客户区域。

 

高级分层窗体

在前面一节中,我们讨论了使用SetLayeredWindowAttributes 函数实现分层窗体来简单地实现半透明效果。若想在效果上更上一层楼,我们要自己准备分层窗体的位图,而不是仅仅依赖于系统自动生成的。虽然使用GDI+似乎是个理想的方法,但重复地绘制GDI+位图然后转为能在屏幕中显示的位图却非常耗时。幸运的是,ATL中的CImage 也提供了恰好够用的功能,因此我们无须复杂的GDI+位图即可使用其强大的功能。在创建好图像之后,我们可以使用UpdateLayeredWindow 函数将其拷贝到屏幕上。UpdateLayeredWindow 函数带来的好处就是,它完整地保留了Alpha通道信息,而不仅仅是一个透明颜色。这样,在窗体中使用不同级别的透明度就变得极为简单。

让我们通过一个简单的示例程序熟悉一下:

首先,我们要用CImage 类创建一幅带有Alpha通道的图像,然后用GDI+对其进行梯度填充:从左上角的透明到右下角的不透明。下面的代码就创建了该位图:

CImage image;
VERIFY(image.Create(300, // width
                    300, // height
                    32, // bits per pixel
                    CImage::createAlphaChannel));

这段代码创建了一幅300*300的、带有Alpha通道的设备无关(device-independent bitmap,DIB)位图。然后即可使用GDI+的Graphics 类在该位图上开始绘画。CImageGetDC 方法用来将设备上下文告知该Graphics 对象。一旦Graphics 对象被销毁,那么必须调用CImageReleaseDC 方法。然后Graphics 对象即可用来进行任何绘画。接下来的这段代码对其进行了梯度填充:

{
    Gdiplus::Graphics graphics(image.GetDC());
    GDIPLUS_VERIFY(graphics);
    Gdiplus::Rect rect(0, 
                       0,
                       image.GetWidth(),
                       image.GetHeight());
    Gdiplus::LinearGradientBrush brush(rect,
                                       Gdiplus::Color(0, 0, 0, 0),
                                       Gdiplus::Color(255, 0, 0, 0),
                                       Gdiplus::LinearGradientModeForwardDiagonal);
    GDIPLUS_VERIFY(graphics.FillRectangle(&brush,
                                          rect));
}
image.ReleaseDC();

大括号用来保证在调用CImage 对象的ReleaseDC 方法之前,Graphics 就已经被销毁。接下来的代码都是些GDI+相关的,已经超出了本文的范围不过却不难理解。当图像准备好之后,我们即可调用UpdateLayeredWindow 并基于该位图更新分层窗体。UpdateLayeredWindow 需要一个源DC,因此我们要再次使用GetDC ReleaseDC 方法:

CPoint windowPosition(0, 0);
CPoint layerPosition(0, 0);
CSize size(image.GetWidth(),
           image.GetHeight());
BLENDFUNCTION blendFunction = { 0 };
blendFunction.BlendOp = AC_SRC_OVER;
blendFunction.BlendFlags = 0;
blendFunction.SourceConstantAlpha = 255;
blendFunction.AlphaFormat = AC_SRC_ALPHA;
VERIFY(::UpdateLayeredWindow(m_hWnd,
                             0, // obtain screen DC
                             0,
                             &size,
                             image.GetDC(),
                             &layerPosition,
                             0, // no color key
                             &blendFunction,
                             ULW_ALPHA));
image.ReleaseDC();

结果正如你所愿:

搞定了分层窗体之后,接下来就是将完全透明变为半透明,以期实现玻璃效果。只要调用在“模糊客户区域”一节中给出的那个EnableBlurBehindWindow 辅助函数即可:

本文的下载代码中提供了一个更为完善的示例,你可以加载自定义的PNG文件并将其呈现在普通窗体或分层窗体中,还可以控制是否显示出玻璃效果。

 

DWM的窗体属性

DWM还为DwmSetWindowAttribute 函数提供了一大堆的窗体属性。有一些非常有意思:

  1. DWMWA_TRANSITIONS_FORCEDISABLED属性允许你禁用窗口在恢复/最大华/最小化时的动画效果。
  2. DWMWA_ALLOW_NCPAINT可以让我们在桌面合成启用时,在窗体的非客户区域进行绘画。请小心使用该属性,若与玻璃效果冲突的话,那么将导致非常丑陋的界面。Word 2007的Beta 2就出现了这个问题(幸运的是,在后续版本中Word 2007修复了该问题)。
  3. DWMWA_FLIP3D_POLICY 属性允许你控制Vista 的Flip3D功能(Windows key + Tab)处理窗口的样式。我们可以使用DWMFLIP3D_EXCLUDEBELOW 属性让Flip3D效果将你的窗口排除在外,并将其放在3D效果时所有窗口的最后面。

下载代码中的示例程序包含了这些窗体属性的演示,你可以逐一进行试验。

 

缩略图

我要提到的最后一个DMW属性就是自行处理缩略图的能力。你可能已经注意到了Windows Vista任务栏上漂亮的缩略图功能。只要将鼠标悬停于任务栏中某个程序项上,就会弹出一个小小的缩略图,让用户无须切换至该窗口即可了解其中的内容。

你知道么?这个缩略图能够保持时时更新。要是不明白我再说什么,可以试试看下述操作:

  1. 右键单击任务栏上的时间/日期,选择“调整日期/时间”命令。这时一个包含着钟表的窗口将弹出来。
  2. 将鼠标悬停于该窗口的任务栏项上,等待缩略图出现。

仔细看,你会发现这个钟表的秒针不停地移动,保持与主窗口同步。DWM也提供了这个功能,方便我们在程序和缩略图之间保持同步!

缩略图工作的原理就是,在DWM中作为桌面合成的一部分“注册”一对源/目的窗体,然后DWM即可自动更新目的窗体并实时反映出源窗体的变化。考虑到如此简单的代码,这真是个令人惊讶的功能。因为注册缩略图需要保留一个句柄,以便随后传递给DWM以更新缩略图属性,并最终取消注册该缩略图,所以用C++封装一下或许会更简单一些:

class Thumbnail
{
public:
    explicit Thumbnail(HTHUMBNAIL handle = 0) :
        m_handle(handle)
    {
        // Do nothing
    }
    ~Thumbnail()
    {
        if (0 != m_handle)
        {
            COM_VERIFY(Unregister());
        }
    }
    bool IsRegistered() const
    {
        return 0 != m_handle;
    }
    HRESULT Register(HWND destination,
                     HWND source)
    {
        ASSERT(0 == m_handle);
        CSize reserved(0, 0);
        return ::DwmRegisterThumbnail(destination,
                                      source,
                                      &reserved,
                                      &m_handle);
    }
    HRESULT Unregister()
    {
        ASSERT(0 != m_handle);
        HRESULT result = ::DwmUnregisterThumbnail(m_handle);
        m_handle = 0;
        return result;
    }
    HRESULT QuerySourceSize(CSize& size)
    {
        ASSERT(0 != m_handle);
        return ::DwmQueryThumbnailSourceSize(m_handle,
                                             &size);
    }
    HRESULT UpdateProperties(const DWM_THUMBNAIL_PROPERTIES& properties)
    {
        ASSERT(0 != m_handle);
        return ::DwmUpdateThumbnailProperties(m_handle,
                                              &properties);
    }
private:
    HTHUMBNAIL m_handle;
};

注册/取消注册缩略图很简单。QuerySourceSize 允许我们检测源窗体的大小,进而控制缩略图的属性。我们还要至少调用一次UpdateProperties 方法,通知DWM开始实时更新缩略图。DWM_THUMBNAIL_PROPERTIES结构提供了一些控制缩略图行为的属性。

struct DWM_THUMBNAIL_PROPERTIES
{
    DWORD dwFlags;
    RECT rcDestination;
    RECT rcSource;
    BYTE opacity;
    BOOL fVisible;
    BOOL fSourceClientAreaOnly;
};
  1. dwFlags 给出了接下来的那个字段是有效的。
  2. rcDestination 字段表示目的窗体用来显示缩略图的客户区域的边界范围。若想使用该字段,请在dwFlags 中包含DWM_TNP_RECTDESTINATION标记。
  3. rcSource 字段表示窗体客户区域的边界,该范围的内容将显示到缩略图中。若想使用该字段,请在dwFlags 中包含DWM_TNP_RECTSOURCE标记。
  4. opacity 字段表示缩略图的不透明度。若源窗体是半透明的,那么缩略图也将是半透明的,提供这个选项仅仅为了需要时得更全面控制而已。若想使用该字段,请在dwFlags 中包含DWM_TNP_OPACITY标记。
  5. fVisible 允许我们暂时打开/关闭缩略图功能,而无须注册/取消注册。若想使用该字段,请在dwFlags 中包含DWM_TNP_VISIBLE标记。
  6. fSourceClientAreaOnly 用来让缩略图只显示源窗口的客户区域,而并不包含其边框内容。若想使用该字段,请在dwFlags 中包含DWM_TNP_SOURCECLIENTAREAONLY标记。

下载代码中通过一个可以缩放的缩略图窗口演示了实时缩略图的相关功能。

 

示例程序

本文所附的示例程序(http://www.kennyandkarin.com/kenny/vista/dwmsample.zip)演示了文中提到的很多功能。大多数的设定改变都可以立即生效,所以你可以直接尝试不同的选项组合。

这个示例程序包含了我对Windows Vista中透明/半透明功能的总结。

Daniel Moth 也在这篇文章中用C#讨论了一些玻璃效果的基础知识。

(又是一片长文章,断断续续两天才完成,希望对朋友们有所帮助。)

posted on 2007-03-20 11:50  Dflying Chen  阅读(20762)  评论(24编辑  收藏  举报