• 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 的实例。

  • 相关阅读:
    HTML: 表单标签、CSS语法、CSS选择器、CSS属性
    HTML:快速入门、表格标签
    JDBC连接池&JDBCTemplate
    JDBC
    MYSQL多表查询&事务
    使用CompletionService批处理任务(线程池阻塞线程)
    java运行字符串代码
    Linux常用命令大全(非常全!!!)
    SpringBoot防止重复请求,重复表单提交超级简单的注解实现
    在Spring-boot中,为@Value注解添加从数据库读取properties支持
  • 原文地址:https://www.cnblogs.com/dongweian/p/14428889.html
Copyright © 2020-2023  润新知