WPF 之 ControlTemplate 和 DataTemplate(十)

一、前言

​ 控件(Control)是数据内容表现形式和算法内容表现形式的双重载体。控件的数据内容表现形式让用户可以直观的看到数据,算法内容形式可以让用户方便的操作逻辑。作为“表现形式”,每个控件都是为了实现用户某种操作算法和直观展示某种数据而生。即控件由“算法内容”和“数据内容”所决定(内容决定形式):

  • 控件的“算法内容”:即控件能够实现的功能,它们是一组算法逻辑。
  • 控件的“数据内容”:即控件所展示的具体内容是什么。

​ 以往的 GUI 开发技术(如MFC、 Windows Forms 和 ASP.NET)中,控件内部的逻辑和数据是固定的,控件的外观更改必须通过控件属性去更改,无法改变控件内部结构。如果要扩展一个控件的功能,则必须创建控件的子类或用户控件。造成这个问题的根本原因是算法内容和数据内容耦合的太过紧密。

​ 为了解决以上问题,WPF t推出了模板(Template)。WPF 的 Template 分为两大类:

  • ControlTemplate:算法内容的表现形式,控制控件内部结构更符合业务逻辑、更方便用户操作。它决定了控件“长的样子”。
  • DataTemplate:内容数据的表现形式,决定一条数据展现成什么样子。

​ 即 Template 就是“外衣”—— ControlTemplate 是控件的“外衣”,“DataTemplate” 是数据的外衣

二、数据的外衣—— DataTemplate

DataTemplate 常用的地方有三处:

  1. ContentControl 的 ContentTemplate 属性:相当于给 ContentCtrol 的内容穿外衣。
  2. ItemsControl 的 ItemsTemplate 属性:相当于给 ItemsControl 的数据条目穿外衣。
  3. GridViewColumn 的 CellTemplate 属性:相当于给 GridViewColumn 的单元格里的数据穿外衣。

例如,我们实现如下功能:

image-20210221154921483

我们先定义一个数据模型:

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 的控件。具体关系如下所示:

image-20210222102631231

即由 ControlTemplate 生成的控制树,其树根是 ControlTemplate 的目标控件,目标控件的 Template 属性值就是 ControlTemplate 的实例。而 DataTmeplate 生成的控制树,其树根是 ContentPresenter 控件,ControlPresenter 控件的 ContentTemplate 属性值就是 DataTemplate 的实例。

原文地址:https://www.cnblogs.com/dongweian/p/14428889.html