风格工具包 - 使用高级图形技术创建自定义用户界面






4.97/5 (105投票s)
Style Toolkit 允许您使用渐变、透明度、PNG 图像等技术来现代化您的程序外观。
引言
如果上面的一些样式看起来很熟悉,那应该。其中许多样式源自 CodeProject 网站上的其他文章。Style Toolkit 不使用其他文章中的代码,只使用颜色、效果、边框大小等内容的定义。
Style Toolkit 不会预定义样式,就像 C++ 不预定义类一样;因此,它可以创建的样式数量是无限的。当然,一旦创建了样式,就像创建类一样,您可以重用、修改和共享它。
所有样式组件都是通过图层创建的,就像您使用 Photoshop 这样的程序一样;然而,与 Photoshop 不同的是,所有图层都是以编程方式创建的。无论是背景、指示器控件还是 aqua 按钮;它们都是通过定义一系列图形操作来创建的。
背景
Style Toolkit 使用 GDI+;然而,除了必需的初始化以及它需要正确编译和链接之外,它对用户来说是透明的。本文不介绍如何使用和集成 GDI+;如果您不熟悉它,请阅读我的 GdipButton[^] 文章以及 CodeProject 上的其他资源。
使用预览
为了演示 Style Toolkit 的强大功能和简洁性,让我们来看一下演示程序的 Teal Pane。我移除了控件,因为它们有自己的样式对象。此样式的灵感来自 Vista Info Bar[^] 文章。
上面的样式可以用九行代码创建
void CStyleDemoDlg::CreateTealPane()
{
// create the stack
Stack TealPane(rect);
// set the OuterBorder params
TealPane.SetOuterBorder(2, Black);
// set the Inner Border params
TealPane.SetInnerBorder(2, RGB(135, 192, 185), RGB(109, 165, 168));
// 3 color gradient
TealPane.FillGrad3(HORIZ, RGB(9, 74, 116), RGB(32, 121, 140), RGB(5, 111, 90));
// add a top edge effect
TealPane.FillBar(rect, TOP_EDGE, 20, Clr(180,RGB(135, 192, 185)), Clr(0,White));
// add the shield image
TealPane.AddImage(CPoint(crnr.left, crnr.top),
IDR_SHIELD, _T("PNG"), 255);
// create a display string
CString str1(_T("Progress Styles\t\t\t Indicator Styles"));
// add the string
TealPane.AddString(str1, CPoint pt1(158, 35), RGB(148, 215, 255),
14, FONT_REG, L"Swiss")
// add to style
m_Style.AddStack(TealPane);
}
如果将其与 Vista Info Bar 源代码进行比较,您会发现它要紧凑得多。此外,Style Toolkit 不使用任何外部库,它只需要 gdiplus.dll。
上面的代码实际上不执行任何图形操作,它只定义它们。后续调用 PaintStyle
()
将会绘制图形对象。
样式、堆栈和图层
Style Toolkit 使用的术语定义如下
- 图层 – 从概念上讲,Style Toolkit 中的图层与 Photoshop 等程序中的图层类似。但从技术上讲,图层是一个定义图形操作的结构。图层可以定义
FillSolid
之类的内容,但也可以定义AddImage
或CreateRgn
之类的内容。 - 堆栈 – 堆栈是图层的集合,具有底和顶的概念。堆栈还有一个由矩形边界定义的框架,该框架定义了其剪裁区域。堆栈有三个边框,默认宽度为零,它们始终位于最顶部。
Stack
类派生自Layers
类,实际上只是一个包含所有用户 API 函数的分区。 - 样式 – 样式是堆栈的集合,也具有底和顶的概念。堆栈可能重叠或不相交。
Style
类包含主要的 P API 函数,PaintStyle
。
使用 Style Toolkit
创建样式只需声明一个带有边界矩形的 Stack
,添加一些图形操作,然后将其添加到 Style
对象中进行绘制。下面显示了最简单的可能样式。
BOOL CStyleDemoDlg::OnEraseBkgnd(CDC* pDC)
{
CDialog::OnEraseBkgnd(pDevC);
CRect rect;
GetClientRect(rect);
Stack stack(rect);
Style style;
style.AddStack(stack);
stack.FillSolid(Green);
style.PaintStyle(pDC, rect);
return TRUE;
}
在实践中,您不会在 erase 函数中声明堆栈和样式;您只会调用那里的 PaintStyle
函数。
Stack
函数将图形操作添加到充当 FIFO 的数组中。操作将按照声明的顺序执行,因此请始终从底层开始,然后向顶层工作。
背景和边框
背景
我们已经看到了如何创建背景,让我们看几个。左边的背景是 JPEG 图像。您可以使用 GDI+ 支持的任何格式的图像,它们也可以是部分透明的。右边是一个 SolidFill
,您还可以使用两色或三色渐变,这些渐变可以是水平、垂直或对角的。
边框
一个 Stack
有三个边框,无论您在哪里声明它们,它们始终在顶部。它们可以有任何宽度,但默认情况下,它们的宽度为零,因此除非指定,否则不会使用它们。边框可以是 RECTANGLE
、ELLIPSE
、ROUNDRECT
或 TRANSITION
。
上图的左侧是过渡边框的示例。外边缘是矩形的,内边缘是圆角矩形。预定义边框仅为方便起见,易于声明,因为它们的相对位置是固定的,因此您只需声明宽度。
您也可以在不使用预定义边框的情况下创建边框,因为图层也可以是上述任何形状。上图右侧的边框是通过定义中心的剪裁区域并使用渐变填充边缘来创建的。
样式控件
Style Toolkit 的初始版本支持按钮、编辑框和进度控件。随着时间的推移,将添加更多控件。
StyleProgress
StyleProgress
控件至少包含两个堆栈:一个不变的底部堆栈,以及一个在控件步进时改变的顶部堆栈。底部堆栈可以只是一个透明图层,但出于连续性原因,它必须存在。
StyleProgress
可以是进度类型或指示器类型。区别在于,指示器类型代表某物的数量(如温度计),它不像进度控件那样期望达到终点。控件实际上不知道它是哪种类型,这完全取决于堆栈的定义方式。
默认样式是左上角的 Vista 外观样式。如果您不向控件添加任何堆栈,它将创建该样式。默认样式的定义来自 Xasthom 的 VistaProgressBar[^] 文章。您可以通过调用 SetBackColor
和 SetForeColor
来更改默认控件的颜色。
第二个进度条仅使用渐变图层作为底部,使用椭圆渐变图层作为顶部。此样式的灵感来自 Kochise 的 CSkinProgress[^] 文章。他的控件实际上使用了 PNG 图像,并且比我的控件多出几百个功能。
顶部的指示条不会步进彩色图层。它不能,因为它总是从绿到红,无论位置如何。它实际上是在彩色图层之上步进一个透明图层,该透明图层定义了剪裁区域。
底部的指示器受到 Hans Dietrich 的 XGradientZoneBar[^] 文章的启发,但我添加了一个额外的包裹渐变,使其具有驼峰效果。
箭头是 StyleButton
,可用于步进条。
StyleButton
StyleButton
是我的 GdipButton[^] 的扩展版本。它具有 GdipButton
的所有功能(如按下状态、悬停状态等),但它还可以使用样式对象而不是图像。StyleButton
可以为其每种状态使用不同的样式。它们通过 LoadStdStyle(style1)
和 LoadHotStyle(style2)
等函数加载。通常,您只需要一个堆栈来创建一种状态,但函数接受样式参数,以便它们可以调用绘制函数。
至少,您必须调用 LoadStdStyle()
或 LoadStdImage()
。未通过加载函数设置的状态将自动从标准状态派生。
VistaStyle1 Pane 显示了按钮可能具有的大多数不同状态。此按钮的定义大致源自 Jose Manuel Menéndez Poó 的文章 WindowsVistaRenderer[^]。只有上面两个按钮(一个普通按钮和一个复选框按钮)是完全实现的,其他四个为了说明目的而被强制设置为备用状态。
为按钮实现样式
我选择此按钮样式用于演示程序,因为它很复杂。创建这些按钮的方法是将它们分解成其独特的组件,或按工具包术语来说是堆栈。例如,大多数状态共享相同的基部,或共享相同的辉光等。在创建样式状态时,我们只需将正确的组件组合在一起。Stack
类重载了运算符“=”、“+”和“+=”,这些将在 API 部分更详细地介绍。
在定义创建每个堆栈所需的各种操作(此处未显示)之后,我们只需将每个按钮状态所需的各种组件组合在一起。
// create standard group
VBStd = VB1Base;
// create the hot group
VBHot = VB1Base2 + VB1Hover + VB1Glow;
// create the pressed group
VBPress = VB1Base2 + VB1Pressed + VB1Glow;
// create the alt group
VBAlt = VB1Base2 + VB1Checked + VB1Hover + VB1CheckedGlow;
VB1Base
和 VB1Base2
不同,因为边框不同。另请注意,在此实现中,辉光不会溢出边框。对我来说,如果玻璃后面的东西发光,它也不能在前面,这更有意义。可以通过定义不同来实现溢出样式。
创建不同的堆栈后,我们只需将它们添加到样式中,然后将其加载到 StyleButton
中。
更多按钮样式
按钮可以是矩形、椭圆形或圆角矩形,如下图所示。
Aqua 样式按钮的定义源自 The Aqualizer Strikes Back[^] 文章。
Vista Style 2 按钮的定义源自 Xasthom 的 Vista Style Button in C#[^] 文章。
箭头按钮不使用样式;它们使用 PNG 文件作为标准图像,并允许 StyleButton
类生成其他状态。
StyleEdit
StyleEdit
类派生自 CEdit
,并具有添加样式的能力。除了任何已设置的水平居中之外,它还将文本垂直居中。演示程序显示了一些带边框的渐变,我保持简单以便文本可读。此类还需要进行一些调整和功能改进,但我没时间了,它对于初始发布来说已经足够了。
Style Toolkit API
Style Toolkit API 定义在 Style.h 文件中。
所有 API 函数都将接受 GDI 或 GDI+ 类型的参数。例如,您可以将 COLORREF
、CRect
和 CPoint
传递给特定函数,也可以将 Color
、Rect
和 Point
传递给同一函数。原因是实际的 API 参数是 Clr
、SRect
和 SPoint
,它们是转换类。内部所有内容都以 GDI+ 格式存储。
重载运算符
Stack
类重载了运算符“=”、“+”和“+=”。
stack1 = stack2;
“=”运算符是复制构造函数,其行为符合预期。堆栈中的图层是 STL 向量,复制构造函数将在执行一些 housekeeping 任务的同时复制所有向量。图像、字符串和区域是单独的向量,也会被复制。
stack1 += stack2;
在此示例中,“+=”运算符将 stack2
中的所有内容添加到 stack1
中,但不包括框架。由于框架包含边框和剪裁区域,因此 stack2
的边框将被忽略,并且所有图层都将被 stack1
的边界矩形和形状裁剪。
stack1 = stack2 + stack3;
在此示例中,stack1
将包含来自 stack2
的框架以及来自两个堆栈的图层。stack2
对象将不会被修改。
模板
模板技术上不属于 Style Toolkit,因为您不需要它们来创建样式。模板只是一种用于定义、维护和共享样式的机制。
Style Toolkit 中的模板与 C++ 模板不同。C++ 模板用于将一组行为应用于许多不同的事物;Style Toolkit 模板更类似于木工模板,用于制作同一事物的许多副本。例如,如果一个程序有十个按钮,则需要一种机制来制作同一样式的多个副本,而无需定义十次。
由于样式只是一个数据集,因此有多种方法可以创建模板。样式可以存储在 ASCII 文件或 XML 文件中;然而,我选择使用类,因为它是一种相当简单的方法。
在类方法中,您只需定义一个类,添加一些堆栈或样式作为成员变量,并在构造函数中定义所有图层。请参阅 Templates.h 和 Templates.cpp 文件中的示例。
范围
您在查看代码时可能会注意到,堆栈从未用 new
运算符声明。原因在于堆栈是临时构造对象。使堆栈数据永久化的是当它被添加到样式中时。AddStack
函数会将所有数据复制到样式对象,并且构造堆栈将在超出作用域时销毁。样式对象通常声明为类的成员函数,这赋予了它们永久性。
即使堆栈声明为模板类的成员变量,模板类本身也是临时构造对象,在超出作用域时会销毁。
透明度
演示程序的底部窗格演示了透明度的使用。样式的所有组件都可以是完全不透明的,也可以是完全透明的。您可以使用 Color
类或 Clr
类来定义颜色的透明度。如果您不声明透明度,它将是完全不透明的。例如,要定义半透明的蓝色,您将使用 Clr semiblue(128,RGB(0,0,255))
。
图形效率
堆栈实际上只“绘制”一次 — 第一次使用时。一旦堆栈绘制完所有图层,它就会创建一个由其框架界定的位图。任何后续对堆栈的绘制调用都只是将位图复制到设备上下文中。例外情况是当使用堆栈的 Regenerate
函数时。Regenerate
函数将堆栈标记为“脏”,它将在下次需要时重新绘制。
调用 PaintStyle
函数时,它会将所有“干净”的堆栈位图复制到设备上下文中。它们单独复制的原因是,直到样式遍历堆栈,它才知道是否有任何堆栈是“脏”的。
图层和堆栈的数量没有实际限制。此外,使用 Style Toolkit 的速度并不比直接使用 GDI+ 慢。原因是堆栈和图层实际上只是 Style
对象定义的图形操作序列的嵌套循环计数器。在数组中定义操作与通过后续代码行调用它们不会改变所需图形操作的数量。Style Toolkit 在内存中完成所有绘制,然后将最终结果复制到屏幕。
如何高效使用 Style Toolkit
为了最小化绘制操作,请为动态内容和静态内容创建不同的堆栈。例如,进度控件的背景永远不会改变,因此它有自己的堆栈,只绘制一次。前景需要在每次步进时重新生成,因此它在单独的堆栈中。
任何位于已标记为重新生成堆栈之上的堆栈也需要由用户标记为重新生成。即使上层堆栈本身可能没有改变,但其背景已改变,因此其位图不正确。工具包不会自动执行此操作,因为它不跟踪或关心堆栈的相对位置。
Needless to say, if a layer is completely covered by another layer or stack which is not at least partially transparent, then it should be removed. The stack code does not attempt to identify and/or remove any redundant paint operations.
问题和解决方法
GDI+ 有一些问题会导致意外结果。
- 不对称性 – GDI+ 在创建圆角矩形方面存在对称性问题。详情请参阅我的 RoundRect[^]。我在
Border
类中实现了对其中一些(如果不是全部)问题的解决方法。Border
类是RoundRect
类的扩展版本。 - 伪影 – GDI+ 在使用
GradientBrush
时有时会产生伪影(不应出现的额外线条)。如果发生这种情况,请尝试以下方法之一:- 更改带有伪影的对象的尺寸。这是最简单的方法,通常有效,但可能不是可接受的解决方案。
- 尝试不同的平滑和插值模式。这有时有效,但也有将问题转移到其他地方的倾向。
- 如果在使用
AddEdgeEffect
函数时出现伪影,请尝试使用配置文件版本,并调整配置文件参数直到伪影消失。
- 延迟绘制 – 控件在接收到告知其绘制的 Windows 消息之前不会被绘制,这会引入一些延迟。如果控件很多,就像在演示程序中一样,它们往往会在背景绘制后弹出。这是一个相当不受欢迎的效果,可以通过在背景上预先绘制控件来规避。控件稍后仍会自行绘制,但您无法分辨,因为它绘制的是相同的内容。请参阅 Vista Style 2 或 Aqua Style 按钮的示例。
最终评论
演示程序使用 VC6 和 VS2005 编译,并在 Windows XP 上进行了测试。然而,Style Toolkit 应该适用于支持 GDI+ 的任何编译器和平台。
随着时间的推移,将添加更多控件和功能。如果您希望添加特定控件,请在消息部分提出请求。
修订历史
版本 1.2 - 2008 年 8 月 9 日
- 添加了
StyleDialog
类。 - 增加了对 Unicode 构建的支持。
- 添加了字符串轮廓效果。
- 将
AddAString
替换为AddAlignString
。 - 修复了 StyleEdit 中的文本更新错误。
版本 1.0 - 2008 年 7 月 15 日
- 初始发布。