创建 WPF 自定义控件,第一部分






4.76/5 (41投票s)
如何创建 WPF 自定义控件,使用 Expression Blend 和 Visual Studio。
引言
WPF 中一项更常见的任务是重新设计控件的外观。在大多数情况下,一个简单的样式,或者最多一个控件模板,就足够了。但在某些情况下,重新设计会非常广泛,以至于需要一个自定义控件。例如,创建 Outlook 风格的任务按钮就是这种情况。
过去,我使用带有 ContentPresenter
的控件模板创建了这些按钮。它完成了工作,但出于我将在下面解释的原因,我决定自定义控件会更有效。这就是这篇文章的由来。
本文分为两部分
- 第一部分: 在这一部分,我们将为按钮创建一个 WPF 控件模板。我们将主要在 Expression Blend 中完成工作,尽管模板也可以在 Visual Studio 中手动编写。
- 第二部分: 在 第二部分 中,我们将把控件模板封装到一个自定义控件中。我们将在 Visual Studio 中完成大部分工作。
一如既往,我发布这篇文章还有第二个目的:我想获得 CodeProject 社区的同行评审。如果您有任何更正或改进建议,我欢迎您的评论。
第一步:问题分析
我们想做的事情非常简单:我们想创建一个看起来和行为都像 Outlook 2010 任务按钮的按钮。那么,让我们先看看我们要模仿的按钮。
多年来,Outlook 一直在其左下角有一组按钮,用于配置程序执行其各种功能
Outlook 2010 更新了按钮的外观和感觉,尽管按钮的功能与以前的版本保持不变。关于这些按钮,首先要注意的是它们高 28 像素,包含图像和文本。图像为 24 x 24,文本为 Segoe UI 9 磅粗体。图像左边距为 4 像素,文本左边距为 6 像素。此外,按钮有四种状态
- 默认:按钮未选中,并融入背景。
- 选中:选中的按钮带有边框、阴影和一些玻璃效果。
- 鼠标悬停:当鼠标悬停在按钮上时,它看起来很像被选中。但是,玻璃效果比“选中”状态更强烈。
- 按下:按下按钮时,它会变暗,并在按钮内部出现阴影。
所以,这些是我们将在按钮中建模的四种状态。我们将通过触发器来实现它们。此外,当我们选择组中的一个按钮时,组中的所有其他按钮都应取消选中。
第二步:初步设计
现在我们对要创建的内容有了一个大致的了解,让我们开始着手实现按钮。您可能已经注意到上面,一组任务按钮的行为类似于一组单选按钮 - 选择一个,其他所有按钮都会取消选择。所以我们的第一个设计选择很简单 - 我们将基于 WPF 单选按钮来构建我们的任务按钮。但是我们如何实现我们自定义的外观和感觉呢?
最简单的选择是资源字典中的控件模板。如果它能在你的情况下工作,这始终是你的最佳选择,因此它是一个好的起点。如果您不熟悉控件模板,这是一个迂回的绝佳时机,让自己熟悉它们。从这一点开始,我们将假设您对控件模板的工作原理有一般的了解。
用于可变内容的 ContentPresenter
所以第一个问题是:我们能否仅通过控件模板完成这项工作?只要每个按钮都有自己的图标和文本,我们就可以做到。通过使用 ContentPresenter
控件设置我们的控件模板,我们可以提供这种灵活性。我们不在控件模板中添加 Image 控件和 TextBlock,而是添加一个 ContentPresenter
,它充当控件模板中的占位符。我们可以通过将 Image 控件和 TextBlock 放在模板化按钮的开始和结束标签之间来填充该位置,而不是放在控件模板中。
<RadioButton Template="TaskButton">
<StackPanel Orientation="Horizontal">
<Image Source="MyImage.png">
<TextBlock Text="My Task Button">
</StackPanel>
</ RadioButton>
StackPanel 被认为是 RadioButton
控件的内容,因为它位于控件的开始和结束标签之间。控件模板中的 ContentPresenter
将读取此内容并显示它。ContentPresenter
是使控件模板能够处理可变内容的快速简便的方法,在大多数情况下,它都能正常工作。
使用自定义控件强制执行标准
ContentPresenter 很棒,但它们的灵活性会带来问题:开发人员可以几乎将任何他们想要的东西放入 ContentPresenter。如果我们创建一个通用按钮,ContentPresenter
会很好地满足要求,但事实上,我们正在创建一个专用按钮 - Outlook 2010 任务按钮。我们的按钮将始终包含图标和文本,仅此而已。图像需要是 24x24,文本需要是 Segoe UI 9 磅粗体,并且图像和文本的边距对所有按钮都必须相同。
我们强制执行这些标准的唯一方法是将按钮的内容移到控件模板中。这将确保任何实现该模板的控件都具有相同的外观。但这又回到了我们最初的问题:每个按钮将使用不同的图像和文本,因此我们需要能够在模板外部指定该内容,而不改变控件的外观。
WPF 允许我们将控件模板内的属性绑定到模板化控件的属性。例如,假设我们使用 Rectangle
为模板化控件提供背景。我们可以将 Rectangle
的 Fill
属性绑定到模板化控件的 Background
属性,如下所示
<Rectangle Fill="{TemplateBinding Background}" />
这在模板化控件具有方便的属性(如 Background
)可供我们连接的情况下效果很好。但对于我们的任务按钮,我们需要属性来提供我们要用作按钮图标的图像文件路径以及按钮的文本。单选按钮既没有 ImagePath
属性也没有 Text
属性。所以,我们只需要创建这些属性。
这就是自定义控件的用武之地。自定义控件将允许我们将控件模板与 C# 类捆绑在一起,该类将提供模板所需的属性。为此,我们将从 RadioButton
类派生我们的自定义控件。
因此,我们知道我们将使用一个派生自 RadioButton
控件的自定义控件来封装一个控件模板,该模板将模仿 Outlook 2010 任务按钮的外观、感觉和行为。让我们从控件模板开始我们的实现。在 第二部分 中,我们将把控件模板封装到一个自定义控件中。
第三步:创建控件模板
您可以在 Expression Blend 或 Visual Studio 中创建控件模板。Blend 使工作变得更加容易;它拥有一个功能齐全的设计环境,几乎消除了手动编码 XAML。它是快速高效地生成用户界面的绝佳方式。请注意,您可以在 Visual Studio 中完成工作,但其 XAML 设计环境相比之下很粗糙,并且大部分 XAML 都需要手动编写。本文不介绍手动编写,但您可以检查完成解决方案中的 XAML 以了解您需要的内容。
我们将使用 Blend 3 来设计我们的控件模板。如果您没有 Blend,可以从 Microsoft 下载 90 天试用版。如果您不熟悉 Blend,您将需要再次绕道学习基础知识。从现在开始,我们将假设您对该程序有基本的了解。
在创建控件模板之前,还有一件事需要您进行一次额外的了解。Outlook 2010 任务按钮是所谓的“玻璃”按钮的一种版本,控件模板将围绕管理玻璃按钮效果构建。因此,您需要了解如何创建玻璃按钮。本文不详细介绍此技术,因为 Martin Grayson 在此主题上有一个很棒的 教程。一旦您阅读完它,您将不仅理解玻璃效果,还将理解创建和使用触发器来控制这些效果的过程,以及创建和修改控件模板的过程。请注意,Grayson 教程涵盖了 Vista 风格的黑色玻璃按钮。但正如您将在下面看到的,Outlook 2010 任务按钮实际上是同一技术的变体。一旦您阅读完教程,您将能够创建几乎任何您需要的玻璃按钮。
创建控件模板
我们将在 Expression Blend 项目中原型化我们的控件模板。我们将在项目的主窗口中完成所有工作。要开始此过程,请在 Blend 中创建一个新项目,并将一个单选按钮添加到主窗口。右键单击左侧“对象和时间线”窗格中的按钮,然后从上下文菜单中选择编辑模板 > 创建空白。这将为按钮创建一个新的、空的控件模板,其中包含一个 Grid 控件作为布局根。将 Grid 命名为 LayoutRoot
。Grid 将只有一行一列。正如您将在下面看到的,我们将在几个地方使用 Grid 来创建我们可以根据需要打开和关闭的图层,就像我们在 Photoshop 或 Expression Design 中所做的那样。
添加内容控件
现在,在 LayoutRoot
Grid 中创建一个 StackPanel。StackPanel 应具有水平方向。向 StackPanel 添加一个 Image 控件和一个 TextBlock。Image 控件应设置为 24x24,左边距为 4 像素,其 Source
属性应设置为项目中的一个示例图像。
<Image Height="24" HorizontalAlignment="Left"
Margin="4,0,0,0" Source="calendar.png"/>
向 StackPanel 添加一个 TextBlock,并设置其属性如下:
<TextBlock Text="Calendar" HorizontalAlignment="Center"
VerticalAlignment="Center" FontFamily="Segoe UI" FontWeight="Bold"
Margin="6,0,0,0" Foreground="#FF1E395B" />
请注意,我们现在从对特定图像和特定文本的硬编码引用开始,以及其他几个硬编码属性。我们这样做是为了方便地调整控件模板的设置。在本文的 第二部分 中,我们将把所有这些属性绑定到自定义控件的属性。
此时,我们已经有了自定义控件的默认行为。它是无边框的,并且融入了宿主窗口的背景。但当然,颜色匹配只是因为我们以编程方式设置了它们。
将视觉效果组织成图层
我们将有几种不同的视觉效果,每种效果都有几个元素。如果我们将它们组织成图层,处理起来会容易得多。如果您曾经使用过 Photoshop 或 Expression Design,您可能熟悉图层,并且知道它们有多么有用。而且,您可能已经注意到 Blend 没有图层功能。但这没问题——我们可以使用 Grid
控件来实现图层功能。
请记住,一个 Grid 可以包含多个控件。我们通常使用 Grid 中的行和列在设计表面上排列控件,但默认情况下,新控件会堆叠在 Row 0, Column 0 中。所以,我们要使用的技巧是:我们控件中的 Grid 将不会被分成行和列。相反,每个 Grid 将有一个单元格。这样,我们添加到 Grid 中的任何内容都会堆叠在我们已添加到其中的任何其他内容之上。
LayoutRoot
Grid 将充当我们图层的主容器。我们将把几个其他的 Grid 堆叠在 LayoutRoot
中;每个 Grid 都将水平和垂直拉伸以填充 LayoutRoot
。这些 Grid 将充当我们应用程序的图层,我们将显示和隐藏它们以显示我们的任务按钮将包含的各种效果。
每个图层 Grid 将包含几个 Rectangle
控件,以创建我们想要显示的效果。我们将把对象堆叠在图层 Grid 中以创建效果。正如您将在按下状态的效果中看到的,我们可以产生一些复杂的结果,例如 WPF 原生不支持的内部阴影。
我的最初想法是为我们要建模的每种状态创建一个图层:默认、鼠标悬停、按下和选中。然而,我很快就发现某些元素,如按钮边框,被多种状态使用。因此,我重构了设计,将按钮分解为其功能元素。
LayoutRoot
:此 Grid 除了作为图层容器外,还为按钮提供默认背景。BorderGrid
:此图层包含按钮边框,由两个Rectangle
对象和一个阴影效果组成。此图层在鼠标悬停和选中状态下使用。IsPressedGrid
:此图层为按下状态提供背景。它包含几个Rectangle
对象,我们将用它们来模拟内部阴影。IsCheckedGlow
:此图层由一个Rectangle
对象组成,该对象为选中状态下的按钮提供光晕。IsMouseOverGlow
:此图层由一个Rectangle
对象组成,该对象为鼠标悬停状态下的按钮提供光晕。Shine
:此图层由一个Rectangle
对象组成,该对象为按钮顶部提供闪耀效果。它在鼠标悬停和按下状态下使用。
您可以在 Expression Blend 的“对象和时间线”面板中看到各种图层是如何堆叠在 LayoutRoot
Grid 中的。
对象的顺序很重要 - 列表中较低的对象会放置在列表中较高的对象之上,并在显示时隐藏这些对象。
创建按钮边框
LayoutRoot
图层提供按钮的默认背景。最终,我们将颜色绑定到宿主窗口的 Background
属性。目前,我只是使用 Blend 的吸管工具复制了 Outlook 2010 窗口的背景。
BorderGrid
图层包含按钮在其鼠标悬停和选中状态下显示的边框效果。边框由外部和内部笔触以及阴影组成。稍后,我们将绑定笔触颜色属性,但现在,我使用吸管工具从 Outlook 2010 中抓取了它们。您可以在控件模板项目中看到各种元素的设置。
请注意,BorderGrid
的不透明度设置为零。这意味着当按钮处于默认状态时,按钮边框是不可见的。稍后,我们将使用触发器在按钮处于鼠标悬停和按下状态时显示该网格。如果您想查看边框的外观,请将不透明度值设置为 100%。完成后请务必将不透明度值重置为 0%,因为该网格在默认状态下应该是不可见的。
创建玻璃效果
Outlook 2010 任务按钮是“经典”Vista 玻璃按钮的演变,但构建方式相同。我的实现保留了按钮顶部的微弱闪耀,并为按钮底部使用了两个发光效果。第一个是鼠标悬停状态下的强烈发光,第二个是选中状态下的较弱发光。这样,当鼠标悬停在选中的按钮上时,发光效果会增强。
与边框一样,闪耀和发光效果的不透明度为 0%,我们稍后将使用触发器来更改这些设置以显示效果。如果您玩不透明度值,请记住在完成后将其重置为 0%。
创建按下按钮效果
按下按钮的背景比其他状态更暗,并且具有内部阴影。WPF 不提供内部阴影,因此我们用三个 Rectangle
对象模拟了该效果。Rectangle
背景和笔触颜色稍后将进行数据绑定;与其它元素一样,我现在使用吸管工具从 Outlook 2010 中抓取了值。
请注意阴影 Rectangle
对象上的边距。对于矩形,顶部和左侧值从 1 增加到 3,但右侧和底部值均为 0。这导致 Rectangle
笔触的右侧和底部部分隐藏在 BorderRect
下方,这就是创建阴影效果的原因。否则,我们将仅仅有一个伪造的方形渐变。与其它非默认状态一样,IsPressedGrid
的 Opacity
设置为零,因此在需要它之前它是不可见的。
实现按钮状态
在控件模板中实现状态有几种方法。WPF 添加了一个非常棒的 Visual State Manager,但我们的按钮状态相当简单,因此我们将使用 WPF 久经考验的 Trigger 机制。
WPF 的触发器链接到定义 WPF 控件状态的各种属性,例如 IsMouseOver
、IsPressed
和 IsChecked
。当这些属性之一发生变化时,WPF 可以调用标记来更改指定控件的设置。在我们的例子中,我们将使用触发器来更改 Grid 的不透明度值以显示各种效果。如果您查看 Grayson 玻璃按钮教程,您将对触发器有一个很好的介绍。因此,从现在开始,我们将假设您大致了解它们是什么,它们如何工作以及如何创建它们。
您可以在触发器面板中的控件模板中检查触发器。
我们正在查看IsMouseOver 触发器。当按钮的 IsPressed
属性变为 true 时,它将被激活,并在属性变为 false 时自动停用。当触发器激活时,WPF 将处理 MouseOverOn 故事板中的标记,并在停用时,WPF 将处理 MouseOverOff 故事板中的标记。
故事板在“对象和时间线”面板中选择。
当您选择一个故事板时,一个时间线会出现在对象列表旁边。您可以使用时间线在故事板执行时更改对象属性。您可以看到在 MouseOverOn 故事板中,我们通过将以下控件的不透明度更改为 100% 来修改它们:
BorderGrid
IsMouseOverGlow
Shine
注意 0:00.300 处的垂直金线。这意味着每个控件将从其默认状态(0% 不透明度)过渡到其触发状态(100% 不透明度),而不是瞬时完成。这就是鼠标悬停产生漂亮的动画效果的原因。
其他故事板的工作方式大致相同。由于我们将效果组织成了功能图层,故事板基本上只是将不透明度值从 0% 更改为 100% 再改回来。您可以查看控件模板的 XAML 来查看每个触发器所做的属性更改。
结论
这基本上就是第一部分的内容。我们创建了一个可用的控件模板。但是,它现在还不是非常有用的,因为它的所有颜色都被硬编码到模板中。在 第二部分 中,我们将通过将其封装到一个自定义控件中并将颜色属性绑定到自定义控件来重构模板。
历史
- 2009-12-31:发布。