随着 Windows Vista™ 和 Microsoft® .NET Framework 3.0 的发布,出现了许多可供开发人员学习、讨论和使用的新技术。 新的工具、库和范例将改变构建托管应用程序的方法,带来了巨大的可能性。 我们推出的这一新的每月专栏将介绍用于开发应用程序的基本技术。您所熟知的业内专家将轮番与您探讨 Windows® Presentation Foundation、Windows Communication Foundation 和 Windows Workflow Foundation。我们开始吧。
在 Windows 中自定义现有控件通常需要四个步骤。首先需要有灵感。然后需要进行研究和探索。这一过程难免会有困难。而最终发现需要完全重写。由于很难访问到将控件的可 视部分与其功能相关联的代码,因而通常无法自定义控件。此代码对于控件至关重要,因此必须完全接受它,或者完全跳过并替换它。
Windows Presentation Foundation(作为 .NET Framework 3.0 的一部分提供)的开发人员已经不可避免地感受到了自定义控件的艰辛。他们提出了一个令人耳目一新的强大解决方案,我们称之为“模板”。
Windows Presentation Foundation 模板不仅简单,而且功能强大,让我能迅速理解其概念。我很快就理解了 Windows Presentation Foundation 样式(通常容易与模板混淆),但是模板需要我们花费更多的时间去了解。
Windows Presentation Foundation 中的每个具有可视外观的预定义控件也都具有一个完全定义了其外观的模板。此模板是类型 ControlTemplate(设置为由 Control 类定义的 Template 属性)的一个对象。
在 应用程序中使用 Windows Presentation Foundation 控件时,您可以用自己设计的模板替换该默认模板。您可以保留控件的基本功能(包括所有键盘和鼠标操作的处理),但您可以为其设置完全不同的外观。这就是在 提到 Windows Presentation Foundation 控件时所说的“变脸”(不太文雅)的含义。控件都有默认外观,但是此外观并不是固定地对应控件的内部功能。
用代码编写模板不太合适。而使用可扩展应用程序标记语言 (XAML) 进行编写会更容易, 因为模板完全可以用 XAML 来表示,从而能够借助于可视化设计工具进行设计。 如果您要编写像现有 Windows Presentation Foundation 控件一样工作但外观不同的自定义控件,请立即停止!很可能仅使用一个模板即可得到这样的控件。
可 下载的源代码包含七个独立的 XAML 文件,我将在整个专栏中进行讨论。在制作此专栏的过程中,未编译任何 C# 代码!如果您已安装 .NET Framework 3.0 SDK,则可以使用我的《Applications = Code + Markup: A Guide to the Microsoft Windows Presentation Foundation》一书中的类似程序 XAMLPad 或 XAML Cruncher 编辑这些文件。
Windows Presentation Foundation 支持用于显示控件内容的其他类型的模板,但本专栏将仅讨论类型 ControlTemplate 的对象。
转储默认值
Windows Presentation Foundation 中的每个具有可视外观的预定义控件都有一个默认模板。如果您对编写自定义模板感兴趣,请研究这些默认值。
我的书中第 25 章的 DumpControlTemplate 显示了控件的简单 XAML 格式的默认 Template 属性。(您可以下载源代码。)如果希望自己编写模板转储程序,以下是执行关键步骤的代码:
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = new string(' ', 4);
settings.NewLineOnAttributes = true;
StringBuilder strbuild = new StringBuilder();
XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings);
XamlWriter.Save(ctrl.Template, xmlwrite);
此 代码假设 ctrl 是 Control 派生类的实例,并且已在屏幕上呈现 ctrl。(这是我的个人经验,否则 Template 属性将为 null。)如果 Template 属性为 null,XamlWriter.Save 调用将引发异常,因为该值将用于某些无可视外观的控件。在此代码的末尾,对 strbuild 调用 ToString 可以提供包含模板的完整 XAML 文档。
此默认模板中的 XAML 可能比您编写的 XAML 冗长一些。例如,您可能会编写:
<Trigger Property="IsEnabled" Value="False">
但是在由 XamlWriter.Save 生成的 XAML 文件中,您将看到如下标记:
<Trigger Property="UIElement.IsEnabled">
<Trigger.Value>
<s:Boolean>False</s:Boolean>
</Trigger.Value>
s 前缀是用 .NET 系统命名空间的 xmlns 命名空间声明定义的。
可视树
让 我们首先来看一下一个非常简单但是很有用的控件模板:CheckBox 模板。假设您希望得到外观比标准 CheckBox 更生动一些的控件。您可能希望用户所选择的选项显示为大的绿色复选标记或大的红色 X,并显示在 CheckBox 的全部内容上。文件 BigCheckCheckBox.xaml 显示了一个这样的 CheckBox 模板。
在 XAML 文件中,模板是类型 ControlTemplate 的元素。在小型的 Windows Presentation Foundation 程序中,通常在 XAML 文件根元素的 Resources 部分定义 ControlTemplate 元素。对于大型应用程序或定义多个应用程序共用的模板时,ControlTemplate 元素位于自己的具有 ResourceDictionary 根元素的 XAML 文件中。
无 论是哪种情况,ControlTemplate 元素通常均包含三个部分。首先是 Resources 部分(可选),定义了模板所使用的样式或画笔。(BigCheckCheckBox.xaml 的模板没有 Resources 部分。)然后,模板定义了模板的可视树。此树以元素或其他控件的布局的形式描述所需要的控件外观。模板包含一个 Triggers 部分,用于指明在响应控件属性更改时,可视树元素应如何变化。图 1 和图 2 显示了自定义 CheckBox 的大部分 ControlTemplate 元素。图 1 中的代码显示了可视树,图 2 接着显示了 ControlTemplate 元素的 Triggers 部分。
Figure 2 Triggers 部分
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="path"
Property="Data"
Value="M 0 5 L 3 10 10 0" />
<Setter TargetName="path"
Property="Stroke"
Value="Green" />
</Trigger>
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="path"
Property="Data"
Value="M 0 2.5 A 5 2.5 0 1 1 5 5
L 5 8 M 5 10 L 5 10" />
<Setter TargetName="path"
Property="Stroke"
Value="Blue" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground"
Value="{DynamicResource
{x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
...
</ControlTemplate.Triggers>
Figure 1 描述 CheckBox 的可视树
<ControlTemplate x:Key="templateBigCheck"
TargetType="{x:Type CheckBox}">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}">
<Grid>
<!-- ContentPresenter displays content of CheckBox -->
<ContentPresenter
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding
HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding
VerticalContentAlignment}" />
<!-- This Border displays a semi-transparent red X -->
<Border>
<Border.Background>
<VisualBrush Opacity="0.5">
<VisualBrush.Visual>
<Path Name="path"
Data="M 0 0 L 10 10 M 10 0 L 0 10"
Stroke="Red"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
StrokeLineJoin="Round" />
</VisualBrush.Visual>
</VisualBrush>
</Border.Background>
</Border>
</Grid>
</Border>
图 1 显示了 BigCheckBox.xaml 的摘要,说明了 ControlTemplate 开始标记和可视树的使用。由于此模板是资源,因此它必须包含一个带有其资源名称的 x:Key 属性。您会注意到其中使用了 TargetType 属性,尽管不是必需使用,但这么做便无需在每个属性前面加上类名,从而简化了模版的其余部分。
此可视树的顶级元素为一个 Border(边框),后者可以作为单个子元素的父元素。在本示例中,此 Border 的三个属性均使用 TemplateBinding 标记扩展进行指定。TemplateBinding 用于将可视树中的元素属性绑定到控件属性。
ContentPresenter 元素中的 TemplateBinding 表达式更有趣,它可以将按钮和 ContentControl 派生而来的其他控件的内容格式化。正是 ContentPresenter 允许在 Button 或 CheckBox 内显示几乎任何内容。请注意,TemplateBinding 用于将 ContentPresenter 的 Margin 属性设置为正在模板化的 CheckBox 控件的 Padding 属性。Margin(边缘)是元素外部的额外空间;Padding(留白)是控件中未被其内容占据的空间,据此您可以看出此绑定所隐含的原理。
ContentPresenter 与另一个边框一起显示在单个单元格的网格面板内。网格单元格通常用于容纳必须分层显示的多个子项。因此,第二个边框显示在 ContentPresenter 顶部。为此边框指定包含 50% 不透明画笔(其中包含红色 X 标记)的背景。请注意,应为呈现红色 X 标记的 Path 元素指定一个路径名。模板的 Triggers 部分会引用此名称。
模板触发器
ControlTemplate 的最后部分通常专用于 Trigger 元素。对可视树的组成元素的这些属性更改是基于控件的属性更改进行的。图 2 显示了自定义 CheckBox 模板的 Triggers 部分的很大部分。
默 认情况下,CheckBox 的 IsChecked 属性为 false 并且 CheckBox 显示红色 X 标记。第一个 Trigger 元素指示在 IsChecked 变为 true 时,名为 path 的元素的 Data 属性和 Stroke 属性应设置为显示绿色复选标记。下一个 Trigger 元素通过显示一个蓝色问号来反映 IsChecked 为 null 值时的情况(在 CheckBox 设置为三态操作时会发生)。当 IsEnabled 属性为 false 时,最后一个 Trigger 元素将控件的 Foreground 属性更改为灰色画笔。(BigCheckCheckBox.xaml 文件具有一个额外的 Trigger 元素,此元素在控件获得输入焦点时将在控件内容周围显示虚线。)
模板的 Triggers 部分可以非常宽泛。通常还会在其中包含 IsMouseOver 属性的 Trigger 元素,因此当鼠标从控件上面经过时,控件会做出响应。
然后您可以在创建 CheckBox 的元素中引用模板,例如:
<CheckBox Template="{StaticResource templateBigCheck}" ...
图 3 显示了由 BigCheckCheckBox.xaml 文件显示的三个 CheckBox 控件。两个包含文本内容,第三个(其 IsThreeState 属性设置为 true,IsChecked 属性设置为 null)包含位图。
图 3 使用自定义模板的 CheckBox 控件 (单击该图像获得较大视图)
改进
您 可能会怀疑模板概念应用于较复杂控件的能力。毕竟,某些控件具有可动部分并更多地涉及与用户的交互。为了消除您的此类疑虑,我们将在本专栏的其余部分中介 绍一下从 RangeBase 派生而来的三个控件的模板。它们是 ProgressBar(进度条)、ScrollBar(滚动条)和 Slider(滑块)。
要 正确运行,更复杂控件的模板需要具有特定名称的某些类型的元素。例如,ProgressBar 可视树必须具有类型 FrameworkElement(或由 FrameworkElement 派生)的两个元素,名为 PART_Track 和 PART_Indicator。这两个元素被称为模板的“已命名部件”。这些名称可以在 ProgressBar 类的 SDK 文档中找到,它们是 TemplatePart 属性。如果您的模板不包括具有这些名称的元素,控件将无法正确运行。
如 果 ProgressBar 以其默认的水平方向显示,则控件的内部逻辑将 PART_Indicator 元素的 Width 属性设置为 PART_Track 元素的 ActualWidth 属性的一个分数。该分数取决于 ProgressBar 的 Minimum、Maximum 和 Value 属性。如果 ProgressBar 为竖直方向,则使用这两个元素的 Height 和 ActualHeight 属性。
实 际上,两种不同方向的 ProgressBar 控件对应两个默认模板。(对于 ScrollBar 和 Slider,也是如此。)如果希望您的新 ProgressBar 对两个方向均支持,则应单独编写两个模板,并在同样为 ProgressBar 定义的 Style 元素的 Triggers 部分中选择这两个模板。
图 4 是 BareBonesProgressBar.xaml 文件的摘要,该文件显示了一个完整的 ProgressBar 元素,其中 ControlTemplate 对象作为该元素的属性。该文件还包含一个 ScrollBar,用于通过绑定来测试 ProgressBar。在操作 ScrollBar 时,蓝色矩形(PART_Indicator 元素)的宽度可以变化,范围为零到红色矩形(PART_Track 元素)的宽度。请注意,尽管 BareBonesProgressBar.xaml 为 PART_Track 矩形指定了明确的宽度,但实际上您应在任何可能的情况下避免使用明确的尺寸。
Figure 4 ProgressBar 模板骨架
<ProgressBar Margin="50" HorizontalAlignment="Center"
Value="{Binding ElementName=scroll, Path=Value}">
<ProgressBar.Template>
<ControlTemplate>
<StackPanel>
<Rectangle Name="PART_Track"
Height="20" Width="300" Fill="Red" />
<Rectangle Name="PART_Indicator"
Height="20" Fill="Blue" />
</StackPanel>
</ControlTemplate>
</ProgressBar.Template>
</ProgressBar>
指 示元素通常在跟踪元素之内,而且您可能会考虑将 Border 元素用于跟踪。但是请注意:如果为该 Border 指定的 BorderThickness 不为零,则其粗细度将属于 Border 元素总宽度的一部分,边框内的宽度将会略微小一些。如果出于显示目的而采用了非零边框厚度的 Border,则应为跟踪元素指定位于第一个边框内的粗细度为零的 Border,并将其他内容放入第二个 Border 内以用作指示器。(如果使用 Border 元素而不使用其边框或背景属性,请考虑使用 Decorator,Decorator 是 Border 的无边框、无背景的祖先类。)
图 5 显示了 ThermometerProgressBar.xaml 文件中的一个更广泛的 ControlTemplate 对象。此模板具有 Resources 部分但没有 Triggers 部分。尽管可视树将一些明确坐标用于边框和角,但未定义 ProgressBar 的总尺寸。这一职责落到了任何使用如下模板定义 ProgressBar 的标记的身上,例如:
<ProgressBar
Template="{StaticResource
templateThermometer}"
Orientation="Vertical" Minimum="0"
Maximum="100"
Width="50" Height="350" ...
Figure 5 温度计进度条的 ControlTemplate
<ControlTemplate x:Key="templateThermometer"
TargetType="{x:Type ProgressBar}">
<!-- Define two brushes for the thermometer liquid -->
<ControlTemplate.Resources>
<LinearGradientBrush x:Key="brushStem"
StartPoint="0 0" EndPoint="1 0">
<GradientStop Offset="0" Color="Red" />
<GradientStop Offset="0.3" Color="Pink" />
<GradientStop Offset="1" Color="Red" />
</LinearGradientBrush>
<RadialGradientBrush x:Key="brushBowl"
GradientOrigin="0.3 0.3">
<GradientStop Offset="0" Color="Pink" />
<GradientStop Offset="1" Color="Red" />
</RadialGradientBrush>
</ControlTemplate.Resources>
<!-- Two-row Grid divides thermometer into stem and bowl -->
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Second grid divides stem area in three columns -->
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="25*" />
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="25*" />
</Grid.ColumnDefinitions>
<!-- This border displays the stem -->
<Border Grid.Column="1" BorderBrush="SteelBlue"
BorderThickness="3 3 3 0"
CornerRadius="6 6 0 0" >
<!-- Track and Indicator elements -->
<Decorator Name="PART_Track">
<Border Name="PART_Indicator"
CornerRadius="6 6 0 0"
VerticalAlignment="Bottom"
Background="{StaticResource brushStem}" />
</Decorator>
</Border>
</Grid>
<!-- The bowl outline goes in the main Grid second row -->
<Ellipse Grid.Row="1"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Width}"
Stroke="SteelBlue" StrokeThickness="3" />
<!-- Another grid goes in the same cell -->
<Grid Grid.Row="1" >
<Grid.RowDefinitions>
<RowDefinition Height="50*" />
<RowDefinition Height="50*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="25*" />
<ColumnDefinition Width="50*" />
<ColumnDefinition Width="25*" />
</Grid.ColumnDefinitions>
<!-- This is to close up the gap between bowl and stem -->
<Border Grid.Row="0" Grid.Column="1"
BorderBrush="SteelBlue"
BorderThickness="3 0 3 0"
Background="{StaticResource brushStem}" />
</Grid>
<!-- Another ellipse to fill up the bowl -->
<Ellipse Grid.Row="1"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Width}"
Stroke="Transparent" StrokeThickness="6"
Fill="{StaticResource brushBowl}" />
</Grid>
</ControlTemplate>
得到的 ProgressBar 如图 6 所示。请注意,此 ProgressBar 的 Orientation(方向)属性必须设置为 Vertical(竖直),否则将无法正常运行。您可以记住在使用此模板时再设置 Orientation 属性,也可以为 ProgressBar 定义一个既能设置 Orientation 又能引用模版的样式。(稍后即向您介绍此方法的示例。)
图 6
本专栏的可下载代码中还包括 SpeedometerProgressBar.xaml,用于产生更多如图 7 所示的基本 ProgressBar。
图 7 速度计进度条
这需要一点技巧。此模板包括两个不可见矩形,它们没有高度、填充颜色或笔画颜色,如下所示:
<Rectangle Name="PART_Track" Width="180" />
<Rectangle Name="PART_Indicator" />
PART_Track 元素的宽度设置为半圆的度数。红色指针的 Polygon 元素取决于 RotateTransform,RotateTransform 的 Angle 属性与 PART_Indicator 的 ActualWidth 绑定在一起,例如:
<RotateTransform
Angle="{Binding ElementName=PART_Indicator,
Path=ActualWidth}" />
剖析 ScrollBar
ProgressBar 和 ScrollBar 均派生自抽象的 RangeBase 类。RangeBase 的全部内容包括:Value 属性、ValueChanged 事件以及 Minimum、Maximum、SmallChange 和 LargeChange 的定义。ProgressBar 只采用了 Minimum、Maximum 和 Value 属性。
ScrollBar 比 ProgressBar 更复杂,它以多种方式响应用户输入。此行为分布在标准 ScrollBar 的五个子控件中。可移动的缩略图是名为 Thumb 的控件。直接位于缩略图两侧的是两个通常用于执行 Page Up 和 Page Down 命令的 RepeatButton 控件。(RepeatButton 类似于常规的 Button,不同的是,它以多次 Click 事件响应持续的鼠标按压。)在 ScrollBar 的 Value 属性更改时,这两个 RepeatButton 控件的大小也会发生更改,有时,其中的一个会缩小为零。在 ScrollBar 的两个端点还有两个标有箭头、大小固定的 RepeatButton 控件,通常用于执行 Line Up 和 Line Down 命令。
Thumb 和中间的 RepeatButton 控件是 Track 元素的子项,Track 元素负责它们的交互、移动和大小更改。ScrollBar 的大部分核心功能和复杂的逻辑都由 Track 处理。
在为 ScrollBar 定义 ControlTemplate 时,可视树需要包含一个名为 PART_Track 的 Track 元素。这是 ScrollBar 模板唯一需要命名的部分。
Track 不是控件,而是从 FrameworkElement 派生而来,并且由于 Template 属性是由 Control 定义的,因此 Track 没有 Template 属性。尽管不能为 Track 提供模板,但可以为组成 Track 元素的三个控件提供模板。
Track 定义了与它的三个子项相对应的三个属性:Thumb(类型为 Thumb)、DecreaseRepeatButton(类型为 RepeatButton)和 IncreaseRepeatButton(类型也为 RepeatButton)。默认情况下,这三个属性为 null,这意味着模板的可视树应包含这三个控件的明确定义。如果您希望在 ScrollBar 的两端提供两个按钮,可视树应包括用于这两个按钮的元素。您可以为这五个子控件指定它们自己的模板,或者使用现有模板而仅分配某些属性。
按钮命令
标 准 ScrollBar 模板中的四个 RepeatButton 控件必须能够与 ScrollBar 的内部逻辑进行通信。通过使用被 ScrollBar 类定义为静态只读字段的预定义 RoutedCommand 对象可实现通信。ScrollBar 定义了至少 17 个这样的静态只读字段,这些字段对 ScrollBar 可以执行的各种操作做出响应。其中的某些命令与 ScrollBar 上下文菜单一起使用;某些命令用于被用作 ScrollViewer 控件的一部分的 ScrollBar。还有一些命令,特别是以单词 Line 和 Page 开头的命令,用于模板。
图 8 显示了 NoFrillsScrollBar.xaml 文件中的 ScrollBar 模板。 Track 元素和位于端点处的两个 RepeatButton 控件可在 Grid 面板中进行组织。Track 元素的三个属性设置为两个其他 RepeatButton 控件和一个 Thumb。
Figure 8 一个简单的 ScrollBar 模板
<ControlTemplate x:Key="templateNoFrillsScroll"
TargetType="{x:Type ScrollBar}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<RepeatButton Grid.Column="0"
Command="ScrollBar.LineLeftCommand"
FontFamily="Wingdings" Content="E" />
<Track Grid.Column="1" Name="PART_Track">
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand" />
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand" />
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb />
</Track.Thumb>
</Track>
<RepeatButton Grid.Column="2"
Command="ScrollBar.LineRightCommand"
FontFamily="Wingdings" Content="F" />
</Grid>
</ControlTemplate>
这是全功能的 ScrollBar,但是正如您在图 9 所见的那样,这四个 RepeatButton 控件看起来像常规按钮。两端的两个按钮原来很小,所以我增大了它们的尺寸,方法是将它们的内容设置为 Wingdings 字体中的指示方向的手形字符。此外,五个控件的所有属性中,除了 Command 属性和 Grid.Column 附加属性之外,其他属性均未设置。
图 9 基于图 8 的 ScrollBar (单击该图像获得较大视图)
图 8 中的模板用于水平 ScrollBar。竖直方向将需要网格中的三个行而不是三个列,并将引用包括字 Up 和 Down 而非 Left 和 Right 的 ScrollBar 类中的 RoutedCommand 字段。
默 认的 ScrollBar 模板使用 Microsoft.Windows.Themes 命名空间的 ScrollChrome 类来显示它的某些控件。您还会注意到,模板看起来好像比需要的长度要长出许多,这是因为可视树中控件的很多属性是通过样式而非特性定义的。例如,在可以使 用如下代码的地方:
<RepeatButton IsFocusable="False" ...
您会看到以下代码:
<RepeatButton ... >
<RepeatButton.Style>
<Style TargetType="RepeatButton">
...
<Setter Property="UIElement.IsFocusable">
<Setter.Value>
<s:Boolean>False</s:Boolean>
</Setter.Value>
</Setter>
...
我想默认的模板是包含此类标记的,因为这些属性最初是在模板的 Resources 部分的 Style 元素中定义的。但是,在编写自己的模板时,您可以直接在元素中设置属性,知道这一点很重要。
默 认模板将每个 RepeatButton 的 IsFocusable 和 IsTabStop 属性设置为 false,在尝试使用 NoFrillsScrollBar.xaml 时,您将明白为什么。单击最左侧的按钮。现在按几次 Tab 键。您将发现输入焦点从 RepeatButton 转移到了 RepeatButton,这当然是不应该发生的。
由于模板中有四个 RepeatButton 控件,最简单的方法可能是使用一个 Style 元素为它们设置统一属性。您可以将以下标记添加到 NoFrillsScrollBar.xaml 中紧跟 ControlTemplate 开始标记的位置,以解决焦点问题:
<ControlTemplate.Resources>
<Style TargetType="{x:Type RepeatButton}">
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
</Style>
</ControlTemplate.Resources>
您还可以使用 ControlTemplate 的 Resources 部分,来为组成 ScrollBar 可视树的子控件定义属性。我在 SpringLoadedScrollBar.xaml 文件中进行了此操作。图 10 显示了该模板的可视树部分。
Figure 10 对其他模板进行引用的可视树
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<!-- Line-left button on left side -->
<RepeatButton Grid.Column="0"
Command="ScrollBar.LineLeftCommand"
Foreground="{TemplateBinding Foreground}"
Template="{StaticResource templateArrow}" />
<!-- Named track occupies most of the ScrollBar -->
<Track Grid.Column="1" Name="PART_Track" >
<Track.DecreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageLeftCommand"
Foreground="{TemplateBinding Foreground}"
Template="{StaticResource templateSpring}" />
</Track.DecreaseRepeatButton>
<Track.IncreaseRepeatButton>
<RepeatButton Command="ScrollBar.PageRightCommand"
Foreground="{TemplateBinding Foreground}"
Template="{StaticResource templateSpring}" />
</Track.IncreaseRepeatButton>
<Track.Thumb>
<Thumb Background="{TemplateBinding Foreground}" />
</Track.Thumb>
</Track>
<!-- Line-right button on right side -->
<RepeatButton Grid.Column="2"
Command="ScrollBar.LineRightCommand"
Foreground="{TemplateBinding Foreground}"
Template="{StaticResource templateArrow}"
LayoutTransform="-1 0 0 1 0 0" />
</Grid>
请注意,模板中的四个 RepeatButton 元素将 Foreground 属性设置为 ScrollBar 本身的 Foreground 属性,并设置之前在 Resources 部分定义的 Template。其他模板(图 10 中未显示)使用 RepeatButton 的 Foreground 属性为其可视化元素着色。图 11 显示了两个使用此模板但具有不同 Foreground 设置的 ScrollBar 控件。
图 11 使用不同的 Foreground 设置 (单击该图像获得较大视图)
我将 DropShadowBitmapEffect 元素添加到了这四个按钮。实际上,我非常喜欢这个效果,因此决定添加一个 Triggers 部分,以便在鼠标经过按钮时出现一个额外的阴影。
假设这个像是安装了弹簧的 ScrollBar 的模板为水平方向,当高度为与设备无关的 50 个单位时,效果最佳。这些设置并不是模板自身的一部分,模板中实在没有它们的位置。您可能会考虑将它们放在 Style 定义中,我稍后会对这种做法进行说明。
ScrollBar 中 Thumb 的大小取决于 ScrollBar ViewportSize 属性,通常用于显示当前查看的文档的比例。我没有找到不通过设置 ViewportSize 而更改 Thumb 大小的方法。如果需要较大的 Thumb,您应为 Slider 编写模板,实际上您可以编写模板,使 Slider 看起来更像 ScrollBar。
创建三维滑块
与 ScrollBar 不同,默认的 Slider 控件的两端并没有两个小的更改按钮。但是您可以将此类控件添加至 Slider 模板。Slider 将六个 RoutedCommand 对象定义为只能获取的静态属性,包括 DecreaseSmall、IncreaseSmall、DecreaseLarge 和 IncreaseLarge。Slider 也可以在一侧或两侧显示刻度线(可选)。
查看默认 Slider 模板时首先需会注意到的可能是,模板长度超过一千行,这是默认 ScrollBar 模板长度的三倍还多。部分原因是由于 Slider 模板并不依赖于 Microsoft.Windows.Themes 中的类。而是在模板内部生成所有对象。
此 模板定义了不同的缩略图形状,以用于刻度线只显示在 Slider 的一侧时的情况,此模板包含很多用于为此缩略图着色的渐变画笔。如果要将可选的 TickBar 元素包括在 Slider 模板中,应将其 Visibility 属性设置为 Visibility.Collapsed。ControlTemplate 的 Triggers 部分应包含根据 Slider 的 TickPlacement 属性将 Visibility 属性设置为 Visibility.Visible 的 Trigger 元素。因此,有必要为 TickBar 元素指定名称,但不必是特殊名称。
对 于我自定义的竖直 Slider,我希望 Thumb 像混音板上的滑杆。目标是使滑杆像一个具有三维外观的突出的塑料块。要模拟三维透视,则需要在上下移动时改变形状。推至 Slider 顶部时,您应可以看到操纵杆的底部;而在位于 Slider 底部时,应可以看到其顶部。
Windows Presentation Foundation 具有某些三维图形功能,是处理此类任务的理想选择。Slider3D.xaml 文件包含该模板。图 12 显示了其中五个设置在不同位置的三维 Slider 控件。请注意,滑杆的形状变化取决于其位置。
图 12 三维 Slider 控件 (单击该图像获得较大视图)
竖 直 Slider 开始处通常是一个三列的 Grid 面板;外侧的两个列用于两个 TickBar 元素。在默认的竖直 Slider 模板中,中间的 Grid 包含一个提供凹陷图像的 Border 元素以及一个包含 Thumb 和两个 RepeatButton 控件的 Track 元素。我在自己的模板中使用同样的方法,并将两个 RepeatButton 控件定义为具有 Transparent(透明)背景的 Border 元素。它们不会使凹陷变暗,但仍会响应鼠标。
我 确定 Thumb 应为 50 个像素高(由于 Grid 中的 ColumnDefinition,其宽度已为 50 个像素)。可视树包含另一个具有透明背景的 Border。此时,尽管您无法看到缩略图,但 Slider 运行完全正常。此透明 Border 的子项是一个 Viewport3D 元素,这正是其有趣之处。
我认为,三维图形编程基本上包含两个部分:单调枯燥和轻松自在。单调枯燥的工作主要涉及定义 MeshGeometry3D 元素,这些元素是在三维坐标空间中完整定义为一组互相连接的三角形的可视对象。对于 Thumb,我定义了一个对象,是切掉顶部的矩形棱锥:
<MeshGeometry3D
Positions="-2 -1 0, -1 -0.25 4, -2 1 0, -1 0.25 4,
2 -1 0, 1 -0.25 4, 2 1 0, 1 0.25 4"
TriangleIndices="0 1 2, 1 3 2, 0 2 4, 2 6 4,
0 4 1, 1 4 5, 1 5 7, 1 7 3,
4 6 5, 7 5 6, 2 3 6, 3 7 6"
TextureCoordinates="0 1, 0.2 0.6, 0 0, 0.2 0.4,
1 1, 0.8 0.6, 1 0, 0.8 0.4" />
八 个顶点中每个顶点的 Positions 属性都由各自的三维坐标组成。图形底部(Z 等于零)的跨度为从 X 值 -2 到 +2 和从 Y 值 -1 到 +1。顶部矩形(Z 等于 4)的跨度为从 X 值 -1 到 +1 和从 Y 值 -0.25 到 +0.25。
TriangleIndices 属性中的每个三元组确定了图的一个面,并对应储存在 Positions 数组中的编号。例如,第一个 TriangleIndices 的三元组为 0、1 和 2,是指 Positions 集合中的第一、第二和第三个顶点。TextureCoordinates 集合包含图中每个三维顶点的一个二维点。这些点对应于基于 GeometryDrawing(用于包括三维对象外侧)的画笔中的点:
<GeometryDrawing Brush="LightGray"
Geometry="F 1 M 0 0 L 1 0 L 1 1 L 0 1 Z
M 0.2 0.4
L 0.8 0.4 0.8 0.6 0.2 0.6 Z
M 0 0 L 0.2 0.4
M 1 0 L 0.8 0.4
M 1 1 L 0.8 0.6
M 0 1 L 0.2 0.6">
<GeometryDrawing.Pen>
<Pen Brush="DarkGray"
Thickness=".05" />
</GeometryDrawing.Pen>
</GeometryDrawing>
此画笔只是用淡灰色覆盖整个对象,并以深灰色使对象的边缘颜色加重。
我 所说的三维图形编程中轻松自在的部分是指使用照明和照相机。通常,使用照明都希望获得 DirectionalLight 和 AmbientLight 结合的效果。前者本身太生硬;而后者会冲淡所有对象。模板中的照相机最初设置是在滑杆从 Z 轴向下看,但也可以进行旋转变换从上面和下面查看滑杆。我无法通过对 Slider 的 Value 属性执行 TemplateBinding 而达到上述目的,因此改用常规绑定:
<AxisAngleRotation3D Axis="1 0 0"
Angle="{Binding RelativeSource={RelativeSource
AncestorType={x:Type Slider}}, Path=Value}" />
由 于 Value 属性决定了照相机的旋转角度,我需要分别硬性地将 Minimum 和 Maximum 属性设置为 -25 和 25。这使得照相机的最大摆动角度为 50 度,如果更大,滑杆将卡住。小于 50 度也可以,但是三维效果会变弱。Slider3D.xaml 文件使用 Style 设置这两个属性以及 Orientation 属性和 Template 属性,如下所示:
<Style x:Key="styleSlider3D"
TargetType="Slider">
<Setter Property="Orientation" Value="Vertical" />
<Setter Property="Minimum" Value="-25" />
<Setter Property="Maximum" Value="25" />
<Setter Property="Template"
Value="{StaticResource templateSlider3D}" />
</Style>
使用此模板的 Slider 实际上引用的是此样式而非此模板。
记住要适度
正如您所见,只需要对 XAML 进行少许编码就可以极大地改变标准控件的外观。如果您像我认识的多数编程人员一样,您可能会琢磨着编写打破所有逻辑和风格的模板。
在您对模板着迷之前,请记住 Windows 的重要价值之一就是用户界面的一致性。如果界面看起来相似,则用户便能猜测到下一步的操作。因此,要完全控制好您的创新欲望并适度使用模板。
代码下载(158 KB)