一、前言
控件(Control)是数据内容表现形式和算法内容表现形式的双重载体。控件的数据内容表现形式让用户可以直观的看到数据,算法内容形式可以让用户方便的操作逻辑。作为“表现形式”,每个控件都是为了实现用户某种操作算法和直观展示某种数据而生。即控件由“算法内容”和“数据内容”所决定(内容决定形式):
- 控件的“算法内容”:即控件能够实现的功能,它们是一组算法逻辑。
- 控件的“数据内容”:即控件所展示的具体内容是什么。
以往的 GUI 开发技术(如MFC、 Windows Forms 和 ASP.NET)中,控件内部的逻辑和数据是固定的,控件的外观更改必须通过控件属性去更改,无法改变控件内部结构。如果要扩展一个控件的功能,则必须创建控件的子类或用户控件。造成这个问题的根本原因是算法内容和数据内容耦合的太过紧密。
为了解决以上问题,WPF t推出了模板(Template)。WPF 的 Template 分为两大类:
- ControlTemplate:算法内容的表现形式,控制控件内部结构更符合业务逻辑、更方便用户操作。它决定了控件“长的样子”。
- DataTemplate:内容数据的表现形式,决定一条数据展现成什么样子。
即 Template 就是“外衣”—— ControlTemplate 是控件的“外衣”,“DataTemplate” 是数据的外衣。
二、数据的外衣—— DataTemplate
DataTemplate 常用的地方有三处:
- ContentControl 的 ContentTemplate 属性:相当于给 ContentCtrol 的内容穿外衣。
- ItemsControl 的 ItemsTemplate 属性:相当于给 ItemsControl 的数据条目穿外衣。
- GridViewColumn 的 CellTemplate 属性:相当于给 GridViewColumn 的单元格里的数据穿外衣。
例如,我们实现如下功能:
我们先定义一个数据模型:
public class Unit
{
public string Year { get; set; }
public uint Price { get; set; }
}
}
然后定义 DataTemplate,并将该 DataTemplate 绑定到ListBox上,具体如下:
<Window.Resources>
<x:Array x:Key="ListName" Type="{x:Type local:Unit}">
<local:Unit Year="2001" Price="60"/>
<local:Unit Year="2002" Price="120"/>
<local:Unit Year="2003" Price="100"/>
<local:Unit Year="2004" Price="200"/>
</x:Array>
<DataTemplate x:Key="DataTemplateYear">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Year,StringFormat={}{0}年}"></TextBlock>
<Rectangle Margin="2,0,2,0" Fill="SlateBlue" Width="{Binding Path=Price}"></Rectangle>
<TextBlock Text="{Binding Path=Price}"></TextBlock>
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{StaticResource ListName}" ItemTemplate="{DynamicResource DataTemplateYear}"></ListBox>
</Grid>
三、控件的外衣—— ControlTemplate
ControlTemplate 的作用:
- 通过更换 ControlTemplate 改变控件外观,使之拥有更优的用户体验
- 设计师与程序员可以并行工作,程序员先完成开发工作,等设计师完成 ControlTemplate 后更换即可。
需要注意的是编辑 ControlTemplate ,但实际是把 ControlTemplate 包含在 Style里面。例如,我们设置一个圆角 TextBox 和圆角 Button,如下:
<Style x:Key="RoundCornerTextBoxStyle" BasedOn="{x:Null}" TargetType="{x:Type TextBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBox}">
<Border CornerRadius="5" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="PART_ContentHost"></ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RoundCornerButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="5" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"></ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
注意:Logical Tree 和 Visual Tree 的交点即是 ControlTemplate。
Style 包含 Setter 和 Trigger。Setter 设置控件的静态外观风格,即控件的属性设置器。Trigger 设置控件的行为风格,行为风格是由对外界刺激的响应体现出来的。
Setter,为属性设置器,采用“属性名=属性值”的方式进行属性。
Trigger :基本触发器,类似于 Setter , property 是 Trigger 关注的属性名,Value 是触发条件。
Setter 和 Trigger 使用如下,我们实现圆角按钮并实现当鼠标移动至按钮上方时,按钮变为灰色功能:
<Window.Resources>
<Style x:Key="RoundCornerButtonStyle" BasedOn="{x:Null}" TargetType="{x:Type Button}">
<Setter Property="Background" >
<Setter.Value>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="SeaGreen" Offset="0.3"></GradientStop>
<GradientStop Color="Teal" Offset="0.6"></GradientStop>
<GradientStop Color="Yellow" Offset="1.1"></GradientStop>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="FontSize" Value="24"></Setter>
<Setter Property="Foreground" Value="White"></Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border CornerRadius="5" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"></ContentPresenter>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="SlateGray"></Setter>
<Setter Property="FontSize" Value="32"></Setter>
<Setter Property="Foreground" Value="GreenYellow"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<Button Margin="10" Height="100" Style="{DynamicResource RoundCornerButtonStyle}" Content="Update"></Button>
</StackPanel>
</Window>
MultiTrigger:必须多个条件同时成立时才会被触发。具体实例如下,当 CheckBox 选中,且内容为“Test”时,才触发字体显示灰色且放大的功能:
<Style TargetType="{x:Type CheckBox}">
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="true"></Condition>
<Condition Property="Content" Value="Test"></Condition>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Foreground" Value="DarkGray"></Setter>
<Setter Property="FontSize" Value="18"></Setter>
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
</Style>
DataTrigger:基于数据执行某些判断情况。具体实例如下,当输入 TextBox 文本内容的长度小于3时,TextBox 的边框会是红色:
public class LengthToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (int.TryParse(value.ToString(),out var length) && length>3)
{
return true;
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
<local:LengthToBooleanConverter x:Key="LTBC"></local:LengthToBooleanConverter>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding ="{Binding RelativeSource={RelativeSource Self},Path=Text.Length,Converter={StaticResource LTBC}}" Value="false">
<Setter Property="BorderBrush" Value="Red"></Setter>
<Setter Property="BorderThickness" Value="1"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
MultiDataTrigger:要求多个数据条件同时满足才能触发。具体实例如下,使用 ListBox 显示一列 Student 数据,当 Student 的 Name=“dwayne” 且 Age=10 时,该列高亮:
<x:Array x:Key="ListStudent" Type="{x:Type local:Student}">
<local:Student Name="dwayne" Age="10"></local:Student>
<local:Student Name="Tom" Age="10"></local:Student>
<local:Student Name="Jho" Age="22"></local:Student>
</x:Array>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Width="100" Text="{Binding Path=Name}"></TextBlock>
<TextBlock Width="100" Text="{Binding Path=Age}"></TextBlock>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=Name}" Value="dwayne"></Condition>
<Condition Binding ="{Binding Path=Age}" Value="10"></Condition>
</MultiDataTrigger.Conditions>
<MultiDataTrigger.Setters>
<Setter Property="Background" Value="Orange"></Setter>
</MultiDataTrigger.Setters>
</MultiDataTrigger>
</Style.Triggers>
</Style>
EventTrigger:为触发器中最特殊的一个,首先它由事件触发,其次触发的是一组动画,而非 Setter。具体实例如下,鼠标进入 Button 界面后,按钮放大字体放大,离开后恢复:
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="FontSize" Value="24"></Setter>
</Trigger>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation To="150"></DoubleAnimation>
<DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Height"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
<EventTrigger RoutedEvent="MouseLeave">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Width"></DoubleAnimation>
<DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
四、DataTemplate 与 ControlTemplate 的关系与应用
凡是 Template ,都是最终作用到控件上,这个控件就是 Template 的目标控件,也叫模板化控件(Template Control)。
DataTemplate 的目标是数据,但展示数据需要载体,这个载体一般是 ContentPresenter 对象上,而 ContentPresenter 对象只有ContentTemplate 而没有 Template 属性,这就说明 ContentPresener 是一组专门用于承载 DataTemplate 的控件。具体关系如下所示:
即由 ControlTemplate 生成的控制树,其树根是 ControlTemplate 的目标控件,目标控件的 Template 属性值就是 ControlTemplate 的实例。而 DataTmeplate 生成的控制树,其树根是 ContentPresenter 控件,ControlPresenter 控件的 ContentTemplate 属性值就是 DataTemplate 的实例。