Vista 主题下支持所有者绘制和完全自定义的推送/菜单/图像按钮
重现 Vista 淡入效果和动画发光默认状态效果的类。
引言
本文介绍了一组主题化的所有者绘制按钮和另一组主题化的完全自定义的推送按钮。在 Vista 上,它们提供了与标准推送按钮相似的视觉效果——淡入淡出的过渡以及微妙的发光默认状态效果。
我已经在我的应用程序中改进这些按钮一段时间了,最近将它们升级到了 Vista,并认为其他人可能会受益。这些按钮类与 Windows 98、2000、XP 和 Vista 兼容。它们作为 C++/MFC 控件实现。提供了标准的推送按钮、菜单下拉按钮以及图像(位图或图标)按钮。
背景
Windows 对主题化控件的支持非常好。创建与系统风格匹配的自定义控件很容易。Vista 更进一步,为某些控件添加了动画效果——尤其是推送按钮。Vista 推送按钮在状态之间平滑过渡,并具有微妙的发光默认状态按钮来吸引眼球。很酷!
不发光
发光
不幸的是,主题 API 并没有明显地允许自定义控件匹配这种新行为。自定义绘制的 NM_CUSTOMDRAW 技术(请参阅 Stephen Steel 的 CImageButtonWithStyle 文章)不支持发光效果,尽管其他过渡效果是可行的。然而,在 Vista 上,对于简单的imerick主题位图/图标按钮,不需要 NM_CUSTOMDRAW 技术。它们是原生支持的。
自定义绘制
但是,如果您确实需要所有者绘制或自定义绘制,请继续阅读。随附的按钮类在不同平台之间提供了统一的绘制框架,并复制了 Vista 的发光效果。例如,包括了普通的推送按钮、菜单按钮和图像+文本按钮。
需要明确的是,这些视觉效果仅在 Vista 上出现。在 98/2k/XP 上,这些按钮与其他按钮无异。没有为复选框或单选按钮提供所有者绘制支持(因为它们没有动画,所以没意义)。
由于我需要向后兼容 Win98,所以我在这里使用了 VC6(太可怕了!)。幸运的是,在 VirtualPC 或 VMWare 中运行 Win98 使这种支持变得微不足道。它也可以用 VS7/2003 或 VS8/2005 很好地编译——只需打开 DSW 项目文件。VS8 用户必须提前从 RT_MANIFEST 资源类别中删除条目。
David Zhao 的 视觉样式类 用于避免在 Win98/2k 上出现 DLL 问题。您还需要一个包含 XP 主题头文件的平台 SDK。
Alpha 混合
AlphaBlend API 用于合并渲染的按钮状态的位图。什么是 Alpha 混合?“Alpha”的引用意味着两个图像混合的程度。下面的方程可能有助于说明
输出 = (旧像素 * (255-alpha) + 新像素 * alpha) / 255
Alpha 为零意味着输出等于旧像素。Alpha 为 255 意味着输出等于新像素。其他值会产生两者的混合组合。将这个概念扩展到 RGB 颜色,添加图像缩放,您就得到了 AlphaBlend API。
Vista 按钮过渡
主题 API 支持推送按钮的五种状态:禁用、正常、热、默认和按下。Vista 以不同的速度在这些状态之间进行过渡。有些很快——比如按下按钮时。有些较慢——比如从热或禁用状态淡入。
鼠标悬停在按钮上的时间长短也有所不同。生活就是这样。我不声称能够完美地复制 Vista(包括其缺点),但这些类非常接近。现在开始编码...
首先,我们通过优先检查来确定新的按钮状态
int old_stateid = m_stateid;
if (!button_enabled) m_stateid = PBS_DISABLED;
else if (button_pressed) m_stateid = PBS_PRESSED;
else if (button_hot) m_stateid = PBS_HOT;
else if (button_default) m_stateid = PBS_DEFAULTED;
else m_stateid = PBS_NORMAL;
如果状态发生变化,那么我们设置过渡
if (UseVistaEffects() && (m_stateid != old_stateid))
{
switch (m_stateid)
{
case PBS_HOT :
m_transition_tickcount = (old_stateid==PBS_PRESSED) ? 4 : 2;
break;
case PBS_NORMAL :
m_transition_tickcount = (old_stateid==PBS_HOT) ? 20 : 4;
break;
case PBS_PRESSED :
m_transition_tickcount = 2;
break;
case PBS_DEFAULTED :
m_transition_tickcount = (old_stateid==PBS_HOT) ? 20 : 2;
break;
default : m_transition_tickcount = 4; break;
}
m_transition_tickscale = 250/m_transition_tickcount;
// Get snapshot of button in old state...
CDCBitmap tempDC(dc,m_oldstate_bitmap);
g_xpStyle.DrawThemeParentBackground(m_hWnd, tempDC.GetSafeHdc(), &rc);
g_xpStyle.DrawThemeBackground (
hTheme, tempDC.GetSafeHdc(), BP_PUSHBUTTON,
old_stateid, &rc, NULL);
}
tickcount
变量保存所需的 50ms 定时器滴答数。tickscale
变量乘以 tickcount
,提供了 0 到 250 的“alpha”范围,用于合并位图。我们还在这里记录旧按钮状态的快照。CDCBitmap
是用于在位图中绘制的辅助类。
接下来,渲染新的按钮状态背景
// Draw themed button background...
if (g_xpStyle.IsThemeBackgroundPartiallyTransparent(hTheme, BP_PUSHBUTTON,
m_stateid))
{
g_xpStyle.DrawThemeParentBackground(m_hWnd, mDC.GetSafeHdc(), &rc);
}
g_xpStyle.DrawThemeBackground (
hTheme, mDC.GetSafeHdc(), BP_PUSHBUTTON,
m_stateid, &rc, NULL);
// Get content rectangle...
CRect border(rc);
g_xpStyle.GetThemeBackgroundContentRect (
hTheme, mDC.GetSafeHdc(), BP_PUSHBUTTON, m_stateid,
&border, &border);
标准操作。没什么新东西。
最后,AlphaBlend 旧的与新的
if (UseVistaEffects() && (m_transition_tickcount>0))
{
CDCBitmap tempDC(dc,m_oldstate_bitmap);
BLENDFUNCTION bf;
bf.BlendOp = AC_SRC_OVER;
bf.BlendFlags = 0;
bf.SourceConstantAlpha = m_transition_tickcount*m_transition_tickscale;
bf.AlphaFormat = 0; // AC_SRC_ALPHA;
AlphaBlend(mDC.GetSafeHdc(), rc.left, rc.top, rc.Width(), rc.Height(),
tempDC.GetSafeHdc(), rc.left, rc.top, rc.Width(), rc.Height(), bf);
}
大功告成!大部分只是为 AlphaBlend
设置 BLENDFUNCTION
结构。最初,alpha 接近 100%,因此旧状态在视觉上占主导地位。随着定时器递减 tickcount
并强制窗口刷新,alpha 降至 0%,因此新状态占优。简单吧?
您可能会问我为什么不使用 NM_CUSTOMDRAW。它与基于定时器的更新不兼容,因为 Windows 已经提供了状态过渡。这些方法会相互冲突——真是令人头疼。
注意:上面的代码仅处理按钮背景。中心内容(文本、菜单箭头、位图等)将在之后绘制。
Vista 默认/焦点按钮
发光/脉动的默认按钮是最酷的效果。不幸的是,主题 API 对此一无所知。唉。但是,它显然只是默认状态和热状态的组合。
我们将按如下方式重现效果
- 将默认状态图像复制到临时位图中。
- 通过临时位图的 50% AlphaBlend 加粗按钮边框。
- 将热按钮状态渲染到临时位图中。
- 将内容区域从临时位图中 AlphaBlend 出来。
AlphaBlend 步骤在两秒钟内进行 alpha 缩放,从而产生脉动效果。
if (UseVistaEffects() && (m_transition_tickcount==0) &&
(m_stateid == PBS_DEFAULTED))
{
// Copy "default" button state...
CDCBitmap tempDC(dc,rc);
AlphaBlt (tempDC, rc, mDC, rc, 255); // lazy bitblt srccopy
// Compute "glow" alpha... 0->250->0 over 40 ticks.
int alpha = (int)(m_defaultbutton_tickcount*12.5);
if (m_defaultbutton_tickcount>=20) alpha = 500-alpha;
// Thicken content border...
CRect rect(border);
rect.InflateRect(1,1);
AlphaBlt (mDC, border, tempDC, rect, alpha/2);
// Render hot button state...
g_xpStyle.DrawThemeParentBackground(m_hWnd, tempDC.GetSafeHdc(), &rc);
g_xpStyle.DrawThemeBackground (
hTheme, tempDC.GetSafeHdc(), BP_PUSHBUTTON, PBS_HOT, &rc, NULL);
// Blend the hot-state content area (avoiding thick border)...
border.DeflateRect(1,1);
AlphaBlt (mDC, border, tempDC, border, alpha);
border.InflateRect(1,1);
}
在这里,AlphaBlend 函数已移至一个独立的函数 AlphaBlt
,从而简化了代码。定时器循环 tickcount
从 0 到 40 无限循环,我们从中计算 alpha
。滴答 0 到 19 变为 alpha 0 到 237,滴答 20 到 40 变为 alpha 250 到 0。
使用代码
要评估 Vista 效果,请将 CButton 的任何推送按钮实例替换或子类化为所有者绘制的 CButtonVE
。将源文件添加到您的项目中即可开始使用。提供了以下类
CButtonVE
/CButtonVE2
- 所有者绘制和完全自定义的推送按钮。
CButtonVE_Menu
/CButtonVE2_Menu
- 所有者绘制和完全自定义的菜单按钮。CButtonVE_Image
/CButtonVE2_Image
- 所有者绘制和完全自定义的图像按钮(位图或图标)。
所有这些都提供了以下内容控制函数
SetOwner
- 指定接收按钮点击(和菜单按钮命令)的窗口。默认为父窗口。SetContentHorz
- 指定按钮图像/文本内容的水平对齐方式(也可以使用 ModifyStyle)。SetContentVert
- 指定按钮图像/文本内容的垂直对齐方式(也可以使用 ModifyStyle)。SetContentMargin
- 指定按钮边框和内容之间的间距。SetBackgroundColor
- 强制设置按钮背景颜色(默认为通过 WM_CTLCOLOR 轮询父窗口)。
菜单按钮类添加了以下设置/通知函数
SetMenu
- 从资源 ID 或 CMenu 预加载菜单(可以在显示前使用以下函数更改)。AddMenuItem
- 手动添加菜单项(可以附加到加载的菜单资源)。RemoveMenuItem
- 删除菜单项(可以从加载的菜单资源中删除项)。RemoveAllMenuItems
- 删除所有菜单项。NotifyMenuPopup
- 在菜单出现之前调用,用于动态更新(默认轮询所有者进行 UI 更新)。
图像按钮类提供了这些
SetImagePosition
- 指定图像相对于文本的位置(左、右、上或下)。SetImageSpacing
- 指定图像和文本之间的间距。SetImageShadow
- 控制图像下方是否显示高斯模糊的下拉阴影。SetTransparentColor
- 指定位图背景颜色。默认为左上角像素。SetHotImage
- 指定按钮处于热状态(鼠标悬停在其上)时显示的位图或图标。SetDisabledImage
- 指定按钮禁用时显示的位图或图标。默认情况下,图像按钮会创建一个源图像的着色版本。
添加所有者绘制按钮
在对话框编辑器中添加一个标准按钮,并设置“所有者绘制”样式。添加一个控件类型成员变量(对其进行子类化),并将“CButton”头实例替换为您选择的按钮类。
添加完全自定义按钮
在对话框编辑器中添加一个自定义控件,并在属性中指定所需的“类”名称。例如:

在您的 WM_INITDIALOG 处理程序中,您需要为完全自定义控件配置字体和窗口文本(请参阅演示代码)。
自定义按钮内容
要自定义按钮绘制,请派生一个新类并替换“DrawContent
”。CButtonVE
框架负责处理背景和过渡效果。
几个参数提供了绘制的有用信息,包括三个不同的矩形坐标:按钮轮廓、安全内容边框以及推荐的文本矩形。如果 hTheme
有效,建议尽可能使用主题 API。uistate
掩码控制隐藏焦点和加速键标记。
virtual void DrawContent (
CDC &dc, // Drawing context
HTHEME hTheme, // Vista theme (if available).
// Use g_xpStyle global var for drawing.
int uistate, // Windows keyboard/mouse ui styles.
// If UISF_HIDEACCEL set, hide underscores.
CRect rclient, // Button outline rectangle.
CRect border, // Safe content rectangle.
CRect textrc, // Text rectangle.
int text_format, // DrawText API formatting.
BOOL enabled) // Set if button enabled.
例如,图像按钮会覆盖此函数,在“textrc”坐标内进行绘制。
关注点
每种按钮都提供两个版本。所有者绘制(VE)和一个完全自定义(VE2)实现的按钮。
为什么两者都有,你可能会问?所有者绘制按钮很棒!它们在许多方面简化了生活(尽管后面会提到一些麻烦)。然而,有一个重大的障碍。为了绘制自身,子窗口依赖于父窗口的配合并反射回消息。因此,“所有者绘制”中的“所有者”。这也适用于 NM_CUSTOMDRAW。
有些父窗口不反射通知消息,例如 CFileDialog (GetOpenFileName/GetSaveFileName)。对于这些不配合的父窗口,您不能使用所有者绘制或 NM_CUSTOMDRAW 方法。从 CButton 派生并替换 WM_PAINT 处理程序不起作用,因为 Windows 在按钮点击时在 WM_PAINT 之外重绘按钮控件。因此,我们提供了派生自 CWnd 的完全自定义推送按钮选项。
所有者绘制的麻烦
所有者绘制按钮有一个令人恼火的问题值得一提。在对话框中,Windows 会跟踪“默认”按钮——即按下 Enter 键时“点击”的按钮。默认按钮以粗边框绘制。
Windows 通过查询窗口的 WM_GETDLGCODE
来跟踪“默认”状态。推送按钮应返回 DLGC_DEFPUSHBUTTON
(如果是默认按钮)或 DLGC_UNDEFPUSHBUTTON
(如果不是)。未能这样做意味着在 Windows 看来,您无法成为“默认”按钮。好的,很简单。
现在是令人恼火的问题。在 WM_GETDLGCODE
中返回值有一个副作用——它们会使 Windows 删除 BS_OWNERDRAW
按钮样式!什么?Windows 发送一个 BS_SETSTYLE
消息,为默认按钮设置 BS_DEFPUSHBUTTON
样式,而 BS_DEFPUSHBUTTON
与所有者绘制样式互斥。唉。幸运的是,可以覆盖 BS_SETSTYLE
并恢复所有者绘制。但这也会导致失去默认状态。不错。
我的解决方法涉及本地跟踪默认状态。一个 BS_SETSTYLE
处理程序会在 Windows 指定 BS_DEFPUSHBUTTON
时记录下来,然后强制改为所有者绘制。记录的状态用于绘制并在 WM_GETDLGCODE
中返回正确的值。对于键盘导航和鼠标点击都效果很好!后续研究发现了 Paolo Messina 的 COddButton 文章,该文章以类似的方式解决了问题。
自定义文件对话框演示
CFileDialogVE
演示类(通过“普通推送”按钮打开)展示了自定义图像按钮的使用
OnInitDialog 处理程序会创建一个 CButtonVE2_Image
实例,并调整对话框大小使其可见
// Create & setup full-custom image button...
CButtonVE2_Image *btn = new CButtonVE2_Image(); // note: self deletes
btn->Create(_T("Custom Drawn Bitmap\n with Glow"),
WS_VISIBLE|WS_CHILD|WS_TABSTOP|BS_MULTILINE,
rve, CWnd::FromHandle(ofn_hWnd), CUSTOM_IMAGEBTN_ID);
HBITMAP hBitmap = (HBITMAP)(LoadImage(theApp.m_hInstance,
MAKEINTRESOURCE(IDB_BITMAP1), IMAGE_BITMAP, 0, 0, LR_DEFAULTCOLOR));
btn->SetBitmap(hBitmap);
// Update dialog size...
CRect rw;
::GetWindowRect(ofn_hWnd,&rw);
int adjust = rve.bottom-rcombo.bottom;
// tweak height if no places bar visible...
if (m_ofn.lStructSize == OPENFILENAME400SIZE) adjust -= rcombo.Height();
::SetWindowPos(ofn_hWnd, NULL, 0, 0, rw.Width(), rw.Height()+adjust,
SWP_NOMOVE|SWP_NOZORDER);
CRect rve
保存按钮坐标,rcombo
保存类型组合框的坐标,ofn_hWnd
保存文件对话框句柄。对话框大小的调整取决于“位置栏”是否可见(通过结构的大小检测)。
Vista 当然在其新的文件对话框中已经提供了有限的自定义支持(请参阅 Michael Dunn 的 Vista 文件对话框 文章)。
其他附加功能
代码包含各种有用的例程。CFontOccManager
(请参阅ButtonVE_demo.cpp)负责支持 Vista 的 9pt Segoe UI 字体以及 98/2k/XP 的 8pt MS Sans Serif。有人在 MSDN 论坛上发布了它,但它太好了,不应该被遗忘。如果运行 Vista,它会查询 SystemParametersInfo 并初始化 LOGFONT,然后计算 MFC CDialogTemplate 的正确缩放。
图像按钮在运行时计算禁用和正常图像,支持透明背景色。对于喜欢 BitBlt 的人,请参阅 DrawDisabledImage
、DrawTransparentImage
和 DrawBluredShadowImage
(在ButtonVE_Helper.h 中)。创建单色位图来屏蔽背景,然后将图像绘制到输出上下文。
尽情享用!
版权和许可
本文版权归 Ian E Davis 所有 © 2007。随本文提供的演示代码和源代码现已公之于众。
历史
- 2007 年 4 月 24 日 - 首次发布。我的第一篇 Code Project 文章!
- 2007 年 4 月 27 日 - 修补了 Hans Dietrich(主题化 WinXP 默认状态下的完全自定义按钮)和 Jerry Evans(uistate 未正确处理)发现的问题。还增加了完全自定义按钮更好的 Enter 处理。
- 2007 年 5 月 4 日 - 添加了对 BS_PUSHLIKE 样式(通过正常 GetCheck 查询)的支持,用于切换推送按钮,高斯模糊的图像下拉阴影,以及热图像悬停支持。还添加了 WM_CTLCOLOR 轮询以获取背景颜色。修复了罕见且难以重现的、带有动画默认按钮的视觉闪烁问题。