Windows Presentation Foundation(WPF)样式和模板化是指一套功能,使开发人员和设计人员能够为其产品创建具有视觉吸引力的效果和一致的外观。 自定义应用的外观时,需要一个强大的样式和模板化模型,用于在应用内和应用之间维护和共享外观。 WPF 提供该模型。
WPF 样式模型的另一个功能是表示和逻辑分离。 设计器只需使用 XAML 即可处理应用的外观,同时开发人员使用 C# 或 Visual Basic 处理编程逻辑。
本概述重点介绍应用的样式设置和模板化方面,不讨论任何数据绑定概念。 有关数据绑定的信息,请参阅 数据绑定概述。
了解资源非常重要,这些资源允许重复使用样式和模板。 有关资源的详细信息,请参阅 XAML 资源的概述。
示例
如下图所示,本概述中提供的示例代码基于 简单的照片浏览应用程序。
这个简单的照片示例使用样式和模板化来创建具有视觉吸引力的用户体验。 该示例具有两个 TextBlock 元素和一个绑定到图像列表的 ListBox 控件。
有关完整示例,请参阅样式设置和模板化示例简介。
风格
可以将 Style 视为将一组属性值应用于多个元素的便捷方法。 可以在派生自 FrameworkElement 或 FrameworkContentElement(如 Window 或 Button)的任何元素上使用样式。
在 XAML 文件的 Resources
部分,声明样式的最常见方法是将其作为资源。 由于样式是资源,因此它们遵循适用于所有资源的相同范围规则。 简单来说,您声明样式的位置会影响该样式可以应用的位置。 例如,如果在应用定义 XAML 文件的根元素中声明样式,则可以在应用中的任意位置使用该样式。
例如,以下 XAML 代码为 TextBlock
声明两种样式,一个样式自动应用于所有 TextBlock
元素,另一个必须显式引用。
<Window.Resources>
<!-- .... other resources .... -->
<!--A Style that affects all TextBlocks-->
<Style TargetType="TextBlock">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="FontFamily" Value="Comic Sans MS"/>
<Setter Property="FontSize" Value="14"/>
</Style>
<!--A Style that extends the previous TextBlock Style with an x:Key of TitleText-->
<Style BasedOn="{StaticResource {x:Type TextBlock}}"
TargetType="TextBlock"
x:Key="TitleText">
<Setter Property="FontSize" Value="26"/>
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0.0" Color="#90DDDD" />
<GradientStop Offset="1.0" Color="#5BFFFF" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
下面是上面声明样式的一个使用示例。
<StackPanel>
<TextBlock Style="{StaticResource TitleText}" Name="textblock1">My Pictures</TextBlock>
<TextBlock>Check out my new pictures!</TextBlock>
</StackPanel>
有关详细信息,请参阅 为控件创建样式。
ControlTemplate
在 WPF 中,控件 ControlTemplate 定义控件的外观。 可以通过定义新的 ControlTemplate 并将其分配给控件来更改控件的结构和外观。 在许多情况下,模板提供了足够的灵活性,因此无需编写自己的自定义控件。
每个控件都有一个分配给 Control.Template 属性的默认模板。 该模板将控件的视觉呈现与控件的功能连接起来。 由于在 XAML 中定义模板,因此无需编写任何代码即可更改控件的外观。 每个模板专为特定控件设计,例如 Button。
通常,在 XAML 文件的 Resources
节上将模板声明为资源。 与所有资源一样,范围规则适用。
控件模板比样式更复杂得多。 这是因为控件模板重写了整个控件的视觉外观,而样式只是将属性更改应用于现有控件。 但是,由于通过设置 control.Template 属性来应用控件的模板,因此可以使用样式来定义或设置模板。
设计器通常允许创建现有模板的副本并对其进行修改。 例如,在 Visual Studio WPF 设计器中,选择 CheckBox
控件,然后右键单击并选择 编辑模板>创建副本。 此命令生成一个 样式,该样式定义了模板。
<Style x:Key="CheckBoxStyle1" TargetType="{x:Type CheckBox}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual1}"/>
<Setter Property="Background" Value="{StaticResource OptionMark.Static.Background1}"/>
<Setter Property="BorderBrush" Value="{StaticResource OptionMark.Static.Border1}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid x:Name="templateRoot" Background="Transparent" SnapsToDevicePixels="True">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border x:Name="checkBoxBorder" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="1" VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid x:Name="markGrid">
<Path x:Name="optionMark" Data="F1 M 9.97498,1.22334L 4.6983,9.09834L 4.52164,9.09834L 0,5.19331L 1.27664,3.52165L 4.255,6.08833L 8.33331,1.52588e-005L 9.97498,1.22334 Z " Fill="{StaticResource OptionMark.Static.Glyph1}" Margin="1" Opacity="0" Stretch="None"/>
<Rectangle x:Name="indeterminateMark" Fill="{StaticResource OptionMark.Static.Glyph1}" Margin="2" Opacity="0"/>
</Grid>
</Border>
<ContentPresenter x:Name="contentPresenter" Grid.Column="1" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="HasContent" Value="true">
<Setter Property="FocusVisualStyle" Value="{StaticResource OptionMarkFocusVisual1}"/>
<Setter Property="Padding" Value="4,-1,0,0"/>
... content removed to save space ...
编辑模板副本是了解模板工作原理的好方法。 与其创建新的空白模板,不如编辑一个副本并更改视觉呈现的几个方面。
有关示例,请参阅 为控件创建模板。
模板绑定
你可能已经注意到,上一部分中定义的模板资源使用了 TemplateBinding 标记扩展。 对于模板方案来说,TemplateBinding
是绑定的优化形式,类似于使用 {Binding RelativeSource={RelativeSource TemplatedParent}}
构造的绑定。
TemplateBinding
可用于将模板的某些部分绑定到控件的属性。 例如,每个控件都有一个 BorderThickness 属性。 使用 TemplateBinding
来管理模板中的哪个元素受此控件设置的影响。
ContentControl 和 ItemsControl
如果在 ContentPresenter的 ControlTemplate 中声明了 ContentControl,ContentPresenter 将自动绑定到 ContentTemplate 和 Content 属性。 同样,ItemsPresenter 的 ControlTemplate 中的 ItemsControl 将自动绑定到 ItemTemplate 和 Items 属性。
数据模板
在此示例应用中,有一个绑定到照片列表的 ListBox 控件。
<ListBox ItemsSource="{Binding Source={StaticResource MyPhotos}}"
Background="Silver" Width="600" Margin="10" SelectedIndex="0"/>
此 ListBox 当前如下所示。
大多数控件都有某种类型的内容,并且该内容通常来自要绑定到的数据。 在此示例中,数据是照片列表。 在 WPF 中,使用 DataTemplate 定义数据的可视表示形式。 基本上,放入 DataTemplate 的内容决定了数据在呈现的应用中的外观。
在我们的示例应用中,每个自定义 Photo
对象都有一个字符串类型的 Source
属性,该属性指定图像的文件路径。 目前,照片对象显示为文件路径。
public class Photo
{
public Photo(string path)
{
Source = path;
}
public string Source { get; }
public override string ToString() => Source;
}
Public Class Photo
Sub New(ByVal path As String)
Source = path
End Sub
Public ReadOnly Property Source As String
Public Overrides Function ToString() As String
Return Source
End Function
End Class
要使照片显示为图像,请创建 DataTemplate 作为资源。
<Window.Resources>
<!-- .... other resources .... -->
<!--DataTemplate to display Photos as images
instead of text strings of Paths-->
<DataTemplate DataType="{x:Type local:Photo}">
<Border Margin="3">
<Image Source="{Binding Source}"/>
</Border>
</DataTemplate>
</Window.Resources>
请注意,DataType 属性类似于 TargetType的 Style 属性。 如果 DataTemplate 位于资源部分,当您将 DataType 属性指定为一种类型并省略 x:Key
时,每当该类型出现时, 将应用 DataTemplate。 始终可以选择为 DataTemplate 分配 x:Key
,然后将其设置为采用 StaticResource
类型的属性(例如 DataTemplate 属性或 ItemTemplate 属性)的 ContentTemplate。
实质上,上述示例中的 DataTemplate 定义每当存在 Photo
对象时,它都应显示为 Image 内的 Border。 有了这个 DataTemplate,我们的应用现在如下所示。
数据模板化模型提供其他功能。 例如,如果使用 HeaderedItemsControl 类型(如 Menu 或 TreeView)显示包含其他集合的集合数据,则存在 HierarchicalDataTemplate。 另一个数据模板化功能是 DataTemplateSelector,该功能允许基于自定义逻辑选择要使用的 DataTemplate。 有关详细信息,请参阅 数据模板化概述,其中更深入地讨论了不同的数据模板化功能。
触发器
触发器设置属性或启动操作(如动画),当属性值发生更改或引发事件时。
Style、ControlTemplate和 DataTemplate 都具有一个可以包含一组触发器的 Triggers
属性。 有多种类型的触发器。
PropertyTrigger
设置属性值或基于属性值启动操作的 Trigger 称为属性触发器。
若要演示如何使用属性触发器,可以使每个 ListBoxItem 在未选中时部分透明。 以下样式将 Opacity 的 ListBoxItem 值设置为 0.5
。 当 IsSelected 属性为 true
时,Opacity 则设置为 1.0
。
<Window.Resources>
<!-- .... other resources .... -->
<Style TargetType="ListBoxItem">
<Setter Property="Opacity" Value="0.5" />
<Setter Property="MaxHeight" Value="75" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.Setters>
<Setter Property="Opacity" Value="1.0" />
</Trigger.Setters>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
此示例使用 Trigger 设置属性值,但请注意,Trigger 类还具有允许触发器执行操作的 EnterActions 和 ExitActions 属性。
请注意,MaxHeight 的 ListBoxItem 属性设置为 75
。 在下图中,第三项是被选中的项。
EventTrigger 和情节提要
另一种类型的触发器是 EventTrigger,它根据事件的出现情况启动一组操作。 例如,以下 EventTrigger 对象指定当鼠标指针进入 ListBoxItem 时,MaxHeight 属性在 90
秒的时间内动画化为值 0.2
。 当鼠标移开该项时,该属性将在 1
秒内恢复到原始值。 请注意,没有必要为 To 动画指定 MouseLeave 值。 这是因为动画能够跟踪原始值。
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.Setters>
<Setter Property="Opacity" Value="1.0" />
</Trigger.Setters>
</Trigger>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:0.2"
Storyboard.TargetProperty="MaxHeight"
To="90" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:1"
Storyboard.TargetProperty="MaxHeight" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Style.Triggers>
有关详细信息,请参阅 故事板概述。
在下图中,鼠标指向第三项。
MultiTrigger、DataTrigger 和 MultiDataTrigger
除了 Trigger 和 EventTrigger,还有其他类型的触发器。 MultiTrigger 允许你根据多个条件设置属性值。 当条件的属性绑定数据时,可以使用 DataTrigger 和 MultiDataTrigger。
视觉状态
控件始终处于特定的 状态。 例如,当鼠标在控件表面上移动时,该控件被视为处于 MouseOver
的常见状态。 没有特定状态的控件被视为处于常见 Normal
状态。 州分为组,前面提到的州是州组 CommonStates
的一部分。 大多数控件都有两个状态组:CommonStates
和 FocusStates
。 应用于控件的每个状态组中,控件始终处于每个组的一个状态,例如 CommonStates.MouseOver
和 FocusStates.Unfocused
。 但是,控件不能在同一组中处于两个不同的状态,例如 CommonStates.Normal
和 CommonStates.Disabled
。 下面是大多数控件识别和使用的状态表。
VisualState 名称 | VisualStateGroup 名称 | DESCRIPTION |
---|---|---|
Normal |
CommonStates |
默认状态。 |
MouseOver |
CommonStates |
鼠标指针悬停在控件上。 |
Pressed |
CommonStates |
已按下控件。 |
Disabled |
CommonStates |
控件已禁用。 |
Focused |
FocusStates |
控件有焦点。 |
Unfocused |
FocusStates |
控件没有焦点。 |
通过在控件模板的根元素上定义 System.Windows.VisualStateManager,可以在控件进入特定状态时触发动画。
VisualStateManager
声明要监视的 VisualStateGroup 和 VisualState 的组合。 当控件进入监视状态时,将启动由 VisualStateManager
定义的动画。
例如,以下 XAML 代码监视 CommonStates.MouseOver
状态,以对名为 backgroundElement
的元素的填充颜色进行动画处理。 当控件返回到 CommonStates.Normal
状态时,将还原名为 backgroundElement
的元素的填充颜色。
<ControlTemplate x:Key="roundbutton" TargetType="Button">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<ColorAnimation Storyboard.TargetName="backgroundElement"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="{TemplateBinding Background}"
Duration="0:0:0.3"/>
</VisualState>
<VisualState Name="MouseOver">
<ColorAnimation Storyboard.TargetName="backgroundElement"
Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
To="Yellow"
Duration="0:0:0.3"/>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
...
有关分镜头脚本的详细信息,请参阅 分镜头脚本概述。
共享资源和主题
典型的 WPF 应用可能有多个在整个应用中应用的 UI 资源。 统一而言,可以将这组资源视为应用的主题。 WPF 支持将 UI 资源打包为主题,这是通过使用封装在 ResourceDictionary 类中的资源字典实现的。
WPF 主题通过使用 WPF 公开的样式设置和模板化机制来定义,以便自定义任何元素的视觉对象。
WPF 主题资源存储在嵌入的资源字典中。 这些资源字典必须嵌入到已签名程序集中,并且可以嵌入到代码本身所在的程序集或并行程序集中。 对于包含 WPF 控件的程序集 PresentationFramework.dll,主题资源位于一系列并行程序集内。
在搜索元素样式时,主题将成为查找的最后一个位置。 通常,搜索首先通过浏览元素树来搜索适当的资源,然后查找应用资源集合,最后查询系统。 这样,应用开发人员就有机会在达到主题之前重新定义树级或应用级别的任何对象的样式。
可以将资源字典定义为单个文件,以便跨多个应用重复使用主题。 还可以通过定义多个资源字典来创建可交换的主题,这些字典提供相同类型的资源,但具有不同的值。 建议在应用级别重新定义这些样式或其他资源,以便对应用进行外观处理。
若要跨应用共享一组资源(包括样式和模板),可以创建 XAML 文件并定义包含对 ResourceDictionary 文件的引用的 shared.xaml
。
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Shared.xaml" />
</ResourceDictionary.MergedDictionaries>
它是 shared.xaml
的共享,用于定义包含一组样式和画笔资源的 ResourceDictionary,从而使应用中的控件具有一致的外观。
有关详细信息,请参阅 合并资源字典。
如果要为自定义控件创建主题,请参阅控件创作概述的“在主题级别定义资源”部分。