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

Vista 中的 C++ 实用功能:在 UI 中使用 Glass

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (91投票s)

2006 年 10 月 2 日

11分钟阅读

viewsIcon

311461

downloadIcon

4167

如何为应用程序的主窗口添加 glass 效果。

目录

引言

这是本系列文章的第一篇,将演示如何使用原生 C++ 代码来利用 Vista 的各种新特性。示例代码使用 Visual Studio 2005、WTL 7.5Windows SDK 构建。我将这些文章归类为中级,因为我不会涵盖 Win32 API 和 WTL 的基础知识。如果您需要快速掌握 WTL,请参阅我关于 WTL 的系列文章。我也不会一步一步地讲解 Visual Studio 向导,这在 WTL 系列中也已经讲过了。(那些文章展示的是 VC 2003 中的向导,但 2005 中的向导是类似的。)

Aero 主题和 glass 效果,以及桌面窗口管理器 (DWM),是 Vista 中微软大力推广的几个主要新特性。在这第一篇文章中,我将演示如何在基于框架窗口的应用程序和基于对话框的应用程序中使用 Aero glass。将 glass 集成到您的应用程序中,是在启用 Aero 主题时使其与众不同(而且,坦白说,看起来很酷)的一种方式。

Aero 主题中的 Glass

当 Aero 是活动主题,并且 Vista 判断您的显卡能够支持时,桌面将使用 DWM 进行绘制。DWM 使用一种称为**合成 (composition)** 的过程来渲染桌面。DWM 会自动在顶级窗口的非客户端区域使用 Aero 主题元素。(这与 XP 自动为顶级窗口设置主题的方式类似。)但这并不总是添加 glass 效果;如果计算机正在使用电池运行,或者用户决定关闭透明度,那么非客户端区域将不会是 glass。

如果您在“**个性化 | 外观**”控制面板小程序中启用了透明 glass,那么非客户端区域就会是透明的。

请注意框架是如何呈现出绿色调(那是壁纸透过来的效果),以及在标题栏中可以看到几个桌面图标。

需要记住的关键是,您的代码只需要关心**合成**是否已启用,而无需关心 glass 的设置,因为 DWM 会自行处理 glass 的绘制。

启动项目

第一个示例程序是一个 SDI 应用程序,没有视图窗口、工具栏或状态栏。运行 WTL AppWizard 后,我们需要做的第一件事是在 *stdafx.h* 中设置 #define,以便使用 Vista 的特性。Vista 是 Windows 版本 6,Vista 中的 IE 版本是 7,因此 *stdafx.h* 的开头应该如下所示:

#define WINVER         0x0600
#define _WIN32_WINNT   0x0600
#define _WIN32_IE      0x0700

然后我们包含 ATL 和 WTL 头文件。

#define _WTL_NO_WTYPES // Don't define CRect/CPoint/CSize in WTL headers
 
#include <atlbase.h>
#include <atltypes.h>   // shared CRect/CPoint/CSize
#include <atlapp.h>
extern CAppModule _Module;
#include <atlwin.h>
#include <atlframe.h>
#include <atlmisc.h>
#include <atlcrack.h>
#include <atltheme.h>   // XP/Vista theme support
#include <dwmapi.h>     // DWM APIs

如果您进行这些更改并立即编译,您将在 *atltheme.h* 中遇到四个错误。例如,这是 CTheme::GetThemeTextMetrics() 的代码,它无法编译:

HRESULT GetThemeTextMetrics(..., PTEXTMETRICW pTextMetric)
{
  ATLASSERT(m_hTheme != NULL);
 
  // Note: The cast to PTEXTMETRIC is because uxtheme.h
  // incorrectly uses it instead of PTEXTMETRICW
  return ::GetThemeTextMetrics(m_hTheme, ..., (PTEXTMETRIC) pTextMetric);
}    

GetThemeTextMetrics() API 的调用中的类型转换是为了绕过 Platform SDK 的 *uxtheme.h* 中的一个错误。然而,Windows SDK 没有这个错误,因此类型转换会导致错误。您可以移除该函数中以及其他三个具有相同绕过方法的类型转换。

为框架添加 Glass

添加 glass 是通过将 glass 效果从非客户端区域扩展到客户端区域来实现的。执行此操作的 API 是 DwmExtendFrameIntoClientArea()DwmExtendFrameIntoClientArea() 接受两个参数:我们的框架窗口的 HWND,以及一个 MARGINS 结构,该结构指定 glass 在窗口的四个边缘的扩展距离。我们可以在 OnCreate() 中调用此 API:

LRESULT CMainFrame::OnCreate(LPCREATESTRUCT lpcs)
{
  // frame initialization here...
 
  // Add glass to the bottom of the frame.
MARGINS mar = {0};
 
  mar.cyBottomHeight = 100;
  DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
 
  return 0;
}

如果您运行此代码,您将不会注意到任何区别。

这种情况发生的原因是 glass 效果依赖于窗口的正确透明度。为了使 glass 出现,区域中的像素(在本例中是客户端区域底部的 100 像素)必须将其 alpha 值设置为 0。最简单的方法是用黑色画刷绘制该区域,这会将像素的颜色值(红、绿、蓝**以及** alpha)设置为 0。我们可以在 OnEraseBkgnd() 中做到这一点:

BOOL CMainFrame::OnEraseBkgnd ( HDC hdc )    
{
CDCHandle dc = hdc;
CRect rcClient;
 
  GetClientRect(rcClient);
  dc.FillSolidRect(rcClient, RGB(0,0,0));
 
  return true;
}    

有了这个更改,框架窗口看起来就像这样:

底部的 100 像素现在是 glass 了!

在 Glass 区域添加文本

为窗口添加 glass 是容易的部分,在 glass 上添加您自己的 UI 则有点棘手。由于像素的 alpha 值必须正确维护,因此我们必须使用理解 alpha 并正确设置 alpha 值的绘图 API。坏消息是 GDI 几乎完全忽略 alpha - 唯一保持 alpha 的 API 是带有 SRCCOPY 光栅操作的 BitBlt()。因此,应用程序必须使用 GDI+ 或主题 API 进行绘制,因为这些 API 是考虑到 alpha 而设计的。

Vista 附带的应用程序中 glass 的一个常见用途是作为状态区域(取代状态栏常用控件)。例如,Windows Media Player 11 在窗口底部的 glass 区域显示播放控件和当前曲目信息。

在本节中,我将演示如何在 glass 区域绘制文本,以及如何添加发光效果,以便文本在任何背景下都清晰可读。

使用正确的字体

Vista 已经摆脱了 MS Sans Serif 和 Tahoma 的旧外观,现在使用 *Segoe UI* 作为默认 UI 字体。我们的应用程序也应该使用 Segoe UI(或者未来可能出现的任何其他字体),因此我们创建一个基于当前主题的字体。如果禁用了主题(例如,用户正在运行 Windows Classic 颜色方案),那么我们将回退到使用 SystemParametersInfo() API。

我们首先需要向 CMainFrame 添加主题支持。这非常简单,因为 WTL 有一个处理主题的类:CThemeImpl。我们将 CThemeImpl 添加到继承列表中,并将消息链接到 CThemeImpl,以便代码可以处理活动主题更改时的通知。

class CMainFrame :
  public CFrameWindowImpl<CMainFrame>,
  public CMessageFilter,
  public CThemeImpl<CMainFrame>
{
  // ...
 
  BEGIN_MSG_MAP(CMainFrame)
    CHAIN_MSG_MAP(CThemeImpl<CMainFrame>)
    // ...
  END_MSG_MAP()
 
protected:
  CFont m_font;  // font we'll use to draw text
};    

CMainFrame 的构造函数中,我们调用 CThemeImpl::SetThemeClassList(),它指定我们将使用其主题的窗口类。对于普通窗口(即,不是常用控件的窗口),请使用名称“globals”。

CMainFrame::CMainFrame()
{
  SetThemeClassList ( L"globals" );
}    

最后,在 OnCreate() 中,我们可以从主题读取字体信息并创建我们自己的字体。

LRESULT CMainFrame::OnCreate ( LPCREATESTRUCT lpcs )
{
  // ...
 
  // Determine what font to use for the text.
LOGFONT lf = {0};
 
  if ( !IsThemeNull() )
    GetThemeSysFont ( TMT_MSGBOXFONT, &lf );
  else
    {
    NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) };
  
    SystemParametersInfo (
        SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS),
        &ncm, false );
 
    lf = ncm.lfMessageFont;
    }
 
  m_font.CreateFontIndirect ( &lf );
 
  return 0;
}

绘制文本

在 glass 上绘制文本涉及以下步骤:

  1. 像双缓冲绘图一样创建一个内存 DC。
  2. 创建一个 32 bpp 的 DIB 并将其选择到 DC 中。
  3. 使用 DrawThemeTextEx() 在内存 DIB 上绘制文本。
  4. 使用 BitBit() 将文本复制到屏幕。

由于我们的绘图代码将根据是否启用了合成而有所不同,因此我们需要在绘图过程中检查合成状态。检查状态的 API 是 DwmIsCompositionEnabled()。由于该 API 可能会失败,并且启用状态未在返回值中指示,因此 CMainFrame 有一个更易于使用的包装器 IsCompositionEnabled()

bool CMainFrame::IsCompositionEnabled() const
{
HRESULT hr;
BOOL bEnabled;
 
  hr = DwmIsCompositionEnabled(&bEnabled);
  return SUCCEEDED(hr) && bEnabled;
}    

现在让我们看看 OnEraseBkgnd(),了解每个步骤是如何完成的。由于此应用程序是一个时钟,我们首先使用 GetTimeFormat() 获取当前时间。

BOOL CMainFrame::OnEraseBkgnd(HDC hdc)
{
CDCHandle dc = hdc;
CRect rcClient, rcText;
 
  GetClientRect ( rcClient );
  dc.FillSolidRect ( rcClient, RGB(0,0,0) );
 
  rcText = rcClient;
  rcText.top = rcText.bottom - 100;
 
  // Get the current time.
TCHAR szTime[64];
 
  GetTimeFormat ( LOCALE_USER_DEFAULT, 0, NULL, NULL,
                  szTime, _countof(szTime) );

如果启用了合成,那么我们将执行合成绘图步骤。我们首先设置一个内存 DC:

  if ( IsCompositionEnabled() )
    {
    // Set up a memory DC and bitmap that we'll draw into
    CDC dcMem;
    CBitmap bmp;
    BITMAPINFO dib = {0};
 
    dcMem.CreateCompatibleDC ( dc );

接下来,我们填写 BITMAPINFO 结构,以创建一个 32 bpp 的位图,其宽度和高度与 glass 区域相同。需要注意的一个重要事项是,位图的高度(BITMAPINFOHEADERbiHeight 成员)为负数。这样做是因为通常,BMP 在内存中存储的顺序是自下而上;但是,DrawThemeTextEx() 需要位图的顺序是自上而下。将高度设置为负值即可实现此目的。

    dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    dib.bmiHeader.biWidth = rcText.Width();
    dib.bmiHeader.biHeight = -rcText.Height();
    dib.bmiHeader.biPlanes = 1;
    dib.bmiHeader.biBitCount = 32;
    dib.bmiHeader.biCompression = BI_RGB;
 
    bmp.CreateDIBSection ( dc, &dib, DIB_RGB_COLORS,
                           NULL, NULL, 0 );

现在我们的图形对象已经创建,我们可以绘制文本了。

    // Set up the DC
    dcMem.SelectBitmap ( bmp );
    dcMem.SelectFont ( m_font );
 
    // Draw the text!
    DTTOPTS dto = { sizeof(DTTOPTS) };
    const UINT uFormat = DT_SINGLELINE|DT_CENTER|DT_VCENTER|DT_NOPREFIX;
    CRect rcText2 = rcText;
 
    dto.dwFlags = DTT_COMPOSITED|DTT_GLOWSIZE;
    dto.iGlowSize = 10;
    rcText2 -= rcText2.TopLeft(); // same rect but with (0,0) as the top-left
 
    DrawThemeTextEx ( m_hTheme, dcMem, 0, 0, CT2CW(szTime), -1,
                      uFormat, rcText2, &dto );

DTTOPTS 结构控制文本的绘制方式。这些标志表明我们正在绘制合成文本,并且我们希望为文本添加发光效果。最后,我们将内存位图的内容位图复制到屏幕:

    // Blit the text to the screen.
    BitBlt ( dc, rcText.left, rcText.top, rcText.Width(), rcText.Height(),
             dcMem, 0, 0, SRCCOPY );
    }  // end if (IsCompositionEnabled())

如果未启用合成,我们将使用 GDI 调用绘制文本:

  else
    {
    const UINT uFormat = DT_SINGLELINE|DT_CENTER|DT_VCENTER|DT_NOPREFIX;
 
    // Set up the DC
    dc.SetTextColor ( RGB(255,255,255) );
    dc.SelectFont ( m_font );
    dc.SetBkMode ( TRANSPARENT );
 
    // Draw the text!
    dc.DrawText ( szTime, -1, rcText, uFormat );
    }
 
  return true;  // we drew the entire background
}

合成文本看起来像这样:

为了说明发光效果的有用性,这里是文本在相同背景下的显示效果,但没有发光:

处理与合成相关的通知

当 DWM 合成启用或禁用时,系统会向所有顶级窗口广播一个 WM_DWMCOMPOSITIONCHANGED 消息。如果正在开启合成,我们需要再次调用 DwmExtendFrameIntoClientArea() 来告知 DWM 窗口的哪个部分应该成为 glass。

LRESULT CMainFrame::OnCompositionChanged(...)
{
  if ( IsCompositionEnabled() )
    {
    MARGINS mar = {0};
 
    mar.cyBottomHeight = 100;
    DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
    }
 
  return 0;
}

在基于对话框的应用程序中使用 Glass

为对话框添加 glass 的过程与框架窗口类似,但有一些区别需要一些略有不同的代码。示例对话框应用程序在窗口顶部添加了 glass;在下面的文本中,与前一个示例相比,已更改或添加的代码以粗体显示。

设置对话框

与之前一样,我们告诉 CThemeImpl 使用哪个窗口类主题,并调用 DwmExtendFrameIntoClientArea() 来为窗口框架添加 glass。

CMainDlg::CMainDlg()
{
  SetThemeClassList ( L"globals" );
}
 
BOOL CMainDlg::OnInitDialog ( HWND hwndFocus, LPARAM lParam )
{
  // (wizard-generared init code omitted)
 
  // Add glass to the top of the window.
  if ( IsCompositionEnabled() )
    {
    MARGINS mar = {0};
 
    mar.cyTopHeight = 150;
    DwmExtendFrameIntoClientArea ( m_hWnd, &mar );
    }

请注意,我们需要显式调用 OpenThemeData()。在框架窗口示例中,我们无需调用它,因为 CThemeImpl 在其 WM_CREATE 处理程序中调用了它。由于对话框接收 WM_INITDIALOG 而不是 WM_CREATE,并且 CThemeImpl 不处理 WM_INITDIALOG,因此我们需要自己调用 OpenThemeData()

接下来,我们构建要用于文本的字体。我们还将字体设置得更大一些,只是为了展示发光效果在较大文本上的外观。

  // Determine what font to use for the text.
LOGFONT lf = {0};
 
  OpenThemeData();
 
  if ( !IsThemeNull() )
    GetThemeSysFont ( TMT_MSGBOXFONT, &lf );
  else
    {
    NONCLIENTMETRICS ncm = { sizeof(NONCLIENTMETRICS) };
 
    SystemParametersInfo (
        SPI_GETNONCLIENTMETRICS, sizeof(NONCLIENTMETRICS),
        &ncm, false );
 
    lf = ncm.lfMessageFont;
    }
 
  lf.lfHeight *= 3;
  m_font.CreateFontIndirect ( &lf );

对话框顶部有一个大型静态文本控件,我们将在此绘制时间。此代码在控件上设置了 owner-draw 样式,以便我们可以将所有文本绘制代码放在 OnDrawItem() 中。

  // Set up the owner-draw static control
  m_wndTimeLabel.Attach ( GetDlgItem(IDC_CLOCK) );
  m_wndTimeLabel.ModifyStyle ( SS_TYPEMASK, SS_OWNERDRAW );

最后,我们调用 EnableThemeDialogTexture(),以便对话框的背景使用当前主题进行绘制。

  // Other initialization
  EnableThemeDialogTexture ( ETDT_ENABLE );
 
  // Start a 1-second timer so we update the clock every second.
  SetTimer ( 1, 1000 );
 
  return TRUE;
}

启用 Glass

与之前一样,我们需要用黑色画刷填充 glass 区域,以便 glass 可以透出来。由于内置的对话框窗口过程会在响应 WM_ERASEBKGND 时绘制对话框的背景,并处理非方形或半透明控件等细节,因此我们需要在 OnPaint() 中而不是在 OnEraseBkgnd() 中进行绘制。

void CMainDlg::OnPaint ( HDC hdc )
{
CPaintDC dc(m_hWnd);
CRect rcGlassArea;
 
  if ( IsCompositionEnabled() )
    {
    GetClientRect ( rcGlassArea );
 
    rcGlassArea.bottom = 150;
    dc.FillSolidRect(rcGlassArea, RGB(0,0,0));
    }
}

绘制文本

OnTimer() 中,我们获取当前时间,然后将静态控件的文本设置为该字符串。

void CMainDlg::OnTimer ( UINT uID, TIMERPROC pProc )
{
  // Get the current time.
TCHAR szTime[64];
 
  GetTimeFormat ( LOCALE_USER_DEFAULT, 0, NULL, NULL,
                  szTime, _countof(szTime) );
 
  m_wndTimeLabel.SetWindowText ( szTime )
}

SetWindowText() 调用会使静态控件重绘,从而导致调用 OnDrawItem()OnDrawItem() 中的代码与框架窗口示例完全相同,因此我在此不再重复。时钟看起来是这样的:

在 Glass 上绘制图形

如前所述,在 glass 区域进行的任何绘图都需要使用 GDI+ 等 alpha 感知 API。示例项目使用 GDI+ Image 类在对话框的左上角绘制一个徽标,如下所示:

徽标从与 EXE 相同目录下的 *mylogo.png* 文件中读取。请注意,徽标周围的 alpha 透明度得到了保留,因为代码使用 GDI+ 来绘制徽标。

使整个窗口 Glass 化

另一种选择是使**整个**窗口 glass 化。有一个快捷方式可以实现这一点,只需将 MARGINS 结构体的第一个成员设置为 -1 即可。

MARGINS mar = {-1};
 
  DwmExtendFrameIntoClientArea ( m_hWnd, &mar );    

如果我们在对话框中这样做,效果不会很好。

请注意,四个按钮中的文本颜色不正确,并且每个按钮周围都有一个不透明的矩形。总的来说,透明度和子窗口混合得不太好。如果您确实想要一个全 glass 对话框,那么包含控件的部分应该使用不透明的背景绘制,就像在 Mobility Center 应用程序中一样。

结论

为您的应用程序添加 glass 是使其在视觉上与众不同的好方法,它可以提供比状态栏常用控件更丰富的状态区域。本文应该为您提供一个良好的起点,并帮助您理解在为原生 C++ 应用程序添加 glass 时将使用的 DWM API。

参考文献

本信息很大程度上来源于 PDC 2005 的 PRS319 会议(“构建在 Windows Vista 中外观出色的应用程序”)。

我直到写完文章才发现,但 Kenny Kerr 有一篇巨大的博客文章涵盖了许多 glass 主题。也请查看他的整个“面向开发者的 Windows Vista”系列,它们非常值得花时间阅读。

版权和许可

本文为版权材料,©2006 Michael Dunn。我知道这无法阻止人们在网上到处复制,但我还是要说。如果您有兴趣翻译本文,请给我发送电子邮件告知。我预计不会拒绝任何人翻译的请求,只是想知道翻译情况,以便在此处发布链接。

本文附带的演示代码已发布到公共领域。我以这种方式发布它是为了让代码能够造福所有人。(我不将文章本身设为公共领域,因为文章只在 CodeProject 上提供有助于提高我的知名度和 CodeProject 网站。)如果您在自己的应用程序中使用演示代码,提供一封电子邮件让我知道将受到赞赏(仅为了满足我是否有人受益于我的代码的好奇心),但不是必需的。在您的源代码中注明出处也受到赞赏,但不是必需的。

修订历史

  • 2006 年 10 月 2 日:文章首次发布。
  • 2006 年 10 月 6 日:添加了 Kenny Kerr 关于使用 glass 的博客文章链接。
  • 2006 年 12 月 26 日:代码已在 RTM Vista 版本上进行了测试,并相应更新了介绍。对措辞进行了细微的修改以提高清晰度。

系列导航:监视计算机电源状态 »

© . All rights reserved.