Template和Style

简介

这是一篇记录笔者阅读学习刘铁猛老师的《深入浅出WPF》的读书笔记,如果文中内容阅读不畅,推荐购买正版书籍详细阅读。

Template 模板的内涵

WPF系统不但支持传统的Windows Forms编程的用户界面和用户体验设计,更支持使用专门设计工具Microsoft Expreession Blend进行专业设计,更推出了以模板为核心的新一代设计理念

程序的本质是算法和数据结构,WPF中作为一种“形式”,它要表现的“内容”就是算法和数据结构,Binding传递的是数据,事件参数携带的也是数据;方法和委托调用的是算法,事件传递消息也是算法······,作为“表现形式”,每个控件都是为了实现某种用户操作算法和直观显示某种数据而生,一个控件看上去是什么样子由它的“算法内容”和“数据内容”决定,这就是内容决定形式

  • 控件的“算法内容”:指控件能展示哪些数据、具有哪些方法、能响应那些操作、能激发什么事件、简而言之就是控件的功能,它们是一组相关的算法逻辑。
  • 控件的“数据内容”:控件所展示的具体数据是什么。

以往的GUI开发技术(Windows Forms)耦合度过高,控件内部的逻辑和数据是固定的,程序员无法改变,外观可以操作的空间也较少,造成这个局面的根本原因就是数据和算法的”形式“和”内容“耦合度太紧了。

在WPF中,通过引入Template(模板)将数据和算法的”内容“与“形式”解耦了。WPF中的Template分为两大类:

  • ControlTemplate 是算法内容的表现形式,一个控件怎样组织其内部结构才能让它更符合业务逻辑,让用户操作起来更舒服就是由它来控制的。它决定了控件”长什么样子“,并让程序员有机会在控件原有的内部逻辑基础上扩展自己的逻辑。
  • DataTemplate 是数据内容的表现形式,一条数据显示成什么样子,是简单的文本还是直观的图形动画就是由它来决定。

一言蔽之,Template就是”外衣“——ControlTemplate是控件的外衣,DataTemplate是数据的外衣。

DataTemplate 数据外衣-数据内容的表现形式

DataTemplate常用的地方有3处,分别是:

  • ContentControl 的ContentTemplate 属性,相当于给ContentControl的内容穿衣服。
  • ItemsControl 的ItemTemplate属性,相当于给ItemsControl 的数据条目传衣服。
  • GridViewColumn的CellTemplate属性,相当于给GridViewColumn单元格里的数据穿衣服。

示例:

需求:有一列汽车数据,这里数据显示在一个ListBox里,要求ListBox的条目显示汽车的厂商标志和简要参数,单击某个条目后在窗口的详细内容区域显示汽车的照片和详细参数。

  1. 添加资源文件夹引入对应汽车图标
  2. 创建详细内容窗口的DataTemplate
  3. 创建ListBox的DataTemplate
  4. 使用对应模板
  5. Binding对应数据
<Window x:Class="Template.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Template"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <!--Converters-->
        <!--创建详细视图的数据模板-->
        <DataTemplate x:Key="carDetailViewTemplate">
            <Border BorderBrush="Black" BorderThickness="1" CornerRadius="6">
                <StackPanel Margin="5">
                    <Image Width="400" Height="250"
                           Source="G:VsProjectWPF练习TemplateResourcesAodi.jpg"/>
                    <StackPanel Orientation="Horizontal" Margin="5">
                        <TextBlock Text="Name:" FontWeight="Bold" FontSize="20"/>
                        <TextBlock Text="{Binding Name}" FontSize="20" Margin="5,0"/>
                    </StackPanel>
                    <StackPanel Orientation="Horizontal" Margin="5,0">
                        <TextBlock Text="Automaker:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Automaker}" Margin="5,0"/>
                        <TextBlock Text="Year:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Year}" Margin="5,0"/>
                        <TextBlock Text="Top Speed:" FontWeight="Bold"/>
                        <TextBlock Text="{Binding TopSpeed}" Margin="5,0"/>
                    </StackPanel>
                </StackPanel>
            </Border>
        </DataTemplate>
        <!--创建ListBox条目的数据模板-->
        <DataTemplate x:Key="carListItemViewTemplate">
            <Grid Margin="2">
                <StackPanel Orientation="Horizontal">
                    <Image Grid.RowSpan="3" Width="64" Height="64"
                           Source="G:VsProjectWPF练习TemplateResourcesAodi.png"/>
                    <StackPanel Margin="5,0">
                        <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Year}" FontSize="14"/>
                    </StackPanel>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </Window.Resources>
    <!--窗体内容 使用对应模板-->
    <Grid>
        <UserControl ContentTemplate="{StaticResource carDetailViewTemplate}"
                     Content="{Binding SelectedItem,ElementName=listBoxCars}"/>
        <ListBox x:Name="listBoxCars" Width="180" Margin="607,0,13,0"
                 ItemTemplate="{StaticResource carListItemViewTemplate}"/>
    </Grid>
</Window>

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        InitialCarList();
    }

    private void InitialCarList()
    {
        List<Car> carList = new List<Car>()
        {
            new Car(){Automaker = "Lamborghini",Name = "Diablo", Year = "1990",TopSpeed = "340"},
            new Car(){Automaker = "Lamborghini",Name = "Murcielago",Year = "2001",TopSpeed="353"},
            new Car(){Automaker = "Lamborghini",Name="Callardo",Year="2003",TopSpeed="325"},
            new Car(){Automaker="Lamborghini",Name="Reventon",Year="2008",TopSpeed="356"},
        };
        this.listBoxCars.ItemsSource = carList;
    }
}

ControlTemplate 控件的外衣-算法内容的表现形式

ControlTemplate的两个作用:

  • 通过更换ControlTemplate改变控件外观,使之具有更优的用户使用体验及外观。
  • 借助ControlTemplate,程序员与设计师可以并行工作,程序员可以先用WPF标准控件进行编程,等设计师的工作完成后,只需把新的ControlTemplate应用到程序中就可以了。

示例:

  1. 文档大纲-》选中需要设计的控件-》右键编辑模板-编辑副本-》设置名称和位置
  2. 修改设计需要的模板ControTemplate,TemplateBinding将控件模板中的属性值关联到目标控件上,产生的效果就是你为目标控件设置的值以后,控件模板的值也会随之改变。
  3. 目标控件使用对应的模板,Style="{DynamicResource RoundCornerTexBoxStyle}

TemplateBinding是为了某个特定场景优化出来的数据绑定版本--需要把ControlTemplate里面的某个Property绑定到应用该ControlTemplate的控件的对应Property上。
中文表达比较拗口,MSDN的原文“Links the value of a property in a control template to be the value of a property on the templated control.”

翻译:将控件模板中属性的值链接为模板化控件上的属性的值

ItemsControl的PanelTemplate

ItemsControl具有一个名为ItemsPanel的属性,它的数据类型为ItemsPanelTemplate,也是一种控件Template,可以控制ItemsControl的条目容器。

示例:制作一个横向排列的ListBox

<ListBox>
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <TextBlock Text="菜单"/>
    <TextBlock Text="帮助"/>
    <TextBlock Text="请求"/>
    <TextBlock Text="张三"/>
</ListBox>

DataTemplate与ControlTemplate的关系与应用

DataTemplate与ControlTemplate的关系

控件只是数据和行为的载体、是个抽象的概念,至于它本身长什么样子(控件的内部结构)、它的数据会长成什么样子(数据显示结构)都是靠Template生成的。

  • ControlTemplate决定控件的外观,生成的控件树的树根是ControlTemplate的目标控件,此模块化控件的Template属性值就是这个ControlTemplate实例。
  • DataTemplate决定数据外观,生成的控件树的树根是一个ContentPresenter控件,此模块化控件的ContentTemplate属性值就是这个DataTemplate示例。

因为ContentPresenter控件是ControlTemplate控件树上的一个节点,所以DataTemplate控件树是ControlTemplate控件树的一棵子树。

DataTemplate与ControlTemplate的应用

为Template设置其应用目标有两种方法:

  1. 逐个设置控件的Template、ContentTemplate、ItemsTemplate、CellTemplate等属性,不想应用Template的控件不设置。
  2. 把Template应用在某个类型的控件或数据上。

使用方法

  1. 把ControlTemplate应用在所有目标上需要借助Style来实现,但Style不能标记x:Key。Style没有x:Key标记,默认为应用到所有由x:Type指定的控件上,如果不想应用则需把控件的Style标记为{x:Null}.

  2. 把DataTemplate应用在某个数据类型上的方法是设置DataTemplate的DataType属性,并且DataTemplate作为资源时也不能带有x:Key标记。DataTemplate具有直接把XML数据节点当作目标对象的功能——XML数据中的元素名(标签名)可以作为DataType,元素的子节点可以使用XPath来访问

    • HierarchicalDataTemplate层级数据模板能够帮助层级控件显示数据,例如:TreeView,MenuItem控件。

DataTemplate示例:

<Window.Resources>
        <DataTemplate DataType="{x:Type local:Unit}">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <Grid>
                        <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding Price}"/>
                        <TextBlock Text="{Binding Year}"/>
                    </Grid>
                    <TextBlock Text="{Binding Price}" Margin="5"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
        <!--数据源-->
        <c:ArrayList x:Key="ds">
            <local:Unit Year="2001年" Price="100"/>
            <local:Unit Year="2002年" Price="120"/>
            <local:Unit Year="2001年" Price="100"/>
            <local:Unit Year="2002年" Price="120"/>
            <local:Unit Year="2001年" Price="100"/>
        </c:ArrayList>
    </Window.Resources>
<Grid>
        <StackPanel>
            <ListBox ItemsSource="{StaticResource ds}"/>
            <ComboBox ItemsSource="{StaticResource ds}"/>
        </StackPanel>
    </Grid>
//C#代码 
 public class Unit
 {
     public int Price { get; set; }
     public string Year { get; set; }
 }
<!--使用XPath访问元素的子节点-->
<Window.Resources>
        <DataTemplate DataType="Unit">
            <Grid>
                <StackPanel Orientation="Horizontal">
                    <Grid>
                        <Rectangle Stroke="Yellow" Fill="Orange" Width="{Binding XPath=@Price}"/>
                        <TextBlock Text="{Binding XPath=@Year}"/>
                    </Grid>
                    <TextBlock Text="{Binding XPath=@Price}" Margin="5"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
        <!--数据源-->
        <XmlDataProvider x:Key="ds" XPath="Units/Unit">
            <x:XData>
                <Units xmlns="">
                    <Unit Year="2001年" Price="100"/>
                    <Unit Year="2002年" Price="120"/>
                    <Unit Year="2001年" Price="100"/>
                    <Unit Year="2002年" Price="120"/>
                    <Unit Year="2001年" Price="100"/>
                </Units>
            </x:XData>
        </XmlDataProvider>
</Window.Resources>
<Grid>
        <StackPanel>
            <ListBox ItemsSource="{Binding Source={StaticResource ds}}"/>
            <ComboBox ItemsSource="{Binding Source={StaticResource ds}}"/>
        </StackPanel>
</Grid>

HierarchicalDataTemplate示例:

<!--TreeView示例-->
<Window.Resources>
        <!--数据源-->
        <XmlDataProvider x:Key="ds" Source="G:VsProjectWPF练习TreeViewData.xml" XPath="Data/Grade"/>
        <!--年级模板-->
        <HierarchicalDataTemplate DataType="Grade" ItemsSource="{Binding XPath=Class}">
            <TextBlock Text="{Binding XPath=@Name}"/>
        </HierarchicalDataTemplate>
        <!--班级模板-->
        <HierarchicalDataTemplate DataType="Class" ItemsSource="{Binding XPath=Group}">
            <RadioButton Content="{Binding XPath=@Name}" GroupName="gn"/>
        </HierarchicalDataTemplate>
        <!--小组模板-->
        <HierarchicalDataTemplate DataType="Group" ItemsSource="{Binding XPath=Student}">
            <CheckBox Content="{Binding XPath=@Name}"/>
        </HierarchicalDataTemplate>
</Window.Resources>
<Grid>
        <TreeView Margin="5" ItemsSource="{Binding Source={StaticResource ds}}"/>
</Grid>
<!--TreeView示例-->
<Window.Resources>
        <!--数据源-->
        <XmlDataProvider x:Key="ds" Source="G:VsProjectWPF练习MenuData.xml" XPath="Data/Operation"/>
        <!--Operation模板-->
        <HierarchicalDataTemplate DataType="Operation"
                                  ItemsSource="{Binding XPath=Operation}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding XPath=@Name}" Margin="10,0"/>
                <TextBlock Text="{Binding XPath=@Gesture}"/>
            </StackPanel>
        </HierarchicalDataTemplate>
</Window.Resources>
<Grid>
        <StackPanel>
            <Menu ItemsSource="{Binding Source={StaticResource ds}}"/>
        </StackPanel>
</Grid>

Style样式

Style简单来说,就是一种对属性值的批处理,类似于Html的CSS,可以快速的设置一系列属性值到UI元素。

Style最重要的两个元素是Setter和Trigger,Setter类设置控件的静态外观风格,Trigger类设置控件的行为风格。

Style和Template就如同化妆和整容,Style可以为某类控件设置统一的样式,如果不想使用该样式使用{x:Null}就可清空Style.

Setter设置器

Setter设置器的两个重要元素是Property和Value,Property属性用来指明你想为那个目标的那个属性赋值;Value属性则是你提供的属性值。

<Window.Resources>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="24"/>
            <Setter Property="TextDecorations" Value="Underline"/>
            <Setter Property="FontStyle" Value="Italic"/>
        </Style>
</Window.Resources>
<StackPanel Margin="5">
        <TextBlock Text="你好"/>
        <TextBlock Text="这是设置好的样式"/>
        <TextBlock Text="没有风格" Style="{x:Null}"/>
</StackPanel>

Trigger触发器

Trigger,触发器,即当某些条件满足时会触发一个行为(比如某些值的变化或动画的发生等)。触发器比较像事件。事件一般是由用户操作触发的,而触发器除了由事件触发的EventTrigger外还有数据变化触发型的Trigger、DataTrigger及多条件触发型的MultiTrigger、MultiDataTrigger等。

基本Trigger

Trigger类是最基本的触发器,类似于Setter,Trigger也有Property和Value这两个属性,Property是Trigger关注的属性名称,Value是触发条件。Trigger还有一个Setters属性,此属性值是一组Setter,一旦触发条件被满足,这组Seteer的“属性-值”就会被应用,触发条件不再满足后,各属性值会被还原。

示例:

CheckBox的Style,当IsChecked属性为true时,前景色和字体变化。

<Window.Resources>
        <Style TargetType="CheckBox">
            <Style.Triggers>
                <Trigger Property="IsChecked" Value="True">
                    <Trigger.Setters>
                        <Setter Property="FontSize" Value="20"/>
                        <Setter Property="Foreground" Value="Orange"/>
                    </Trigger.Setters>
                </Trigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<Grid>
        <StackPanel>
            <CheckBox Content="悄悄的我走了" Margin="5"/>
            <CheckBox Content="悄悄的我走了" Margin="5"/>
            <CheckBox Content="悄悄的我走了" Margin="5"/>
            <CheckBox Content="悄悄的我走了" Margin="5"/>
            <CheckBox Content="悄悄的我走了" Margin="5"/>
        </StackPanel>
</Grid>

MultiTrigger

MultiTrigger必须多个条件同时成立才会被触发,MultiTrigger比Trigger多了一个Conditions属性,需要同时成立的条件就存储在这个集合中。

示例:同时满足CheckBox被选中且选中为“吃饭”时才会被触发。

<Window.Resources>
        <Style TargetType="CheckBox">
            <Style.Triggers>
                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsChecked" Value="true" />
                        <Condition Property="Content" Value="吃饭"/>
                    </MultiTrigger.Conditions>
                    <MultiTrigger.Setters>
                        <Setter Property="FontSize" Value="20"/>
                        <Setter Property="Foreground" Value="Orange"/>
                    </MultiTrigger.Setters>
                </MultiTrigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<Grid>
        <StackPanel>
            <CheckBox Content="吃饭"/>
            <CheckBox Content="睡觉"/>
            <CheckBox Content="打豆豆"/>
            <CheckBox Content="用四川话说"/>
        </StackPanel>
</Grid>

由数据触发DataTrigger

DataTrigger,基于数据执行某些判断,DataTrigger对象的Binding属性会源源不断送过来,一旦送过来的值与Value属性一致,DataTrigger即被触发。

示例:当TextBox的Text长度小于7个字符时其Border会保持红色

<Window.Resources>
        <local:L2BConverter x:Key="cvtr"/>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <DataTrigger Value="false"
                    Binding="{Binding RelativeSource={x:Static RelativeSource.Self},Path=Text.Length,Converter={StaticResource cvtr}}">
                    <Setter Property="BorderBrush" Value="Red"/>
                    <Setter Property="BorderThickness" Value="1"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<StackPanel>
        <TextBox Margin="5"/>
        <TextBox Margin="5,0"/>
        <TextBox Margin="5"/>
</StackPanel>

这个例子中唯一需要解释的就是DataTrigger的Binding,为了将自己作为数据源,使用了RelativeSource,初学者经常认为“不明确指出Source的值Binding就会将控件自己作为数据的来源”,这是错误的,因为不明确指出Source时Binding会把控件的DataContext属性当作数据源而非把控件自身当作数据源。Binding的Path被设置为Text.Lenght,即我们关注的是字符串的长度,长度是一个具体的数字,如何基于这个长度值做判断呢?这就用到了Converter。我们创建如下的Converter:

class L2BConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            int textLenght = (int)value;
            return textLenght > 6 ? true : false;
        }
        public object ConvertBack(object value,Type targetType,object parmeter,CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

多条数据条件触发的MultiDataTrigger

示例:用户界面上使用ListBox显示了一列Student数据,当Student对象同时满足ID为2、Name为张三的时候,条目就会高亮显示。

<Window.Resources>
        <Style TargetType="ListBoxItem">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal">
                            <TextBlock Text="{Binding ID}" Width="60"/>
                            <TextBlock Text="{Binding Name}" Width="120"/>
                            <TextBlock Text="{Binding Age}" Width="60"/>
                        </StackPanel>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <MultiDataTrigger>
                    <MultiDataTrigger.Conditions>
                        <Condition Binding="{Binding Path=ID}" Value="2"/>
                        <Condition Binding="{Binding Path=Name}" Value="张三"/>
                    </MultiDataTrigger.Conditions>
                    <MultiDataTrigger.Setters>
                        <Setter Property="Background" Value="Orange"/>
                    </MultiDataTrigger.Setters>
                </MultiDataTrigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<StackPanel>
        <ListBox x:Name="listBoxStudent" Margin="5"/>
</StackPanel>
public class Student
{
    public int ID { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Student> Students { get; set; }
}

public MainWindow()
{
    InitializeComponent();
    Student st = new Student();
    List<Student> students = new List<Student>();
    students.Add(new Student() { ID = 1, Name = "张三", Age = 20 });
    students.Add(new Student() { ID = 2, Name = "张三", Age = 20 });
    students.Add(new Student() { ID = 3, Name = "张三", Age = 20 });
    listBoxStudent.Items.Clear();
    listBoxStudent.ItemsSource = students;
}

由事件触发的EventTrigger

EventTrigger是触发器中最特殊的一个。首先,它不是由属性值或数据的变化来触发而是由事件来触发;其次被触发后它并非应用一组Setter,而是执行一段动画。因此UI层的动画效果往往与EventTrigger相关联。

示例:创建一个针对Button的Style,这个Style包含两个EventTrigger,一个由MouseEnter事件触发,另外一个由MouseLeave事件触发。

<Window.Resources>
        <Style TargetType="Button">
            <Style.Triggers>
                <EventTrigger RoutedEvent="MouseEnter">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Width"/>
                            <DoubleAnimation To="150" Duration="0:0:0.2" Storyboard.TargetProperty="Height"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
                <EventTrigger RoutedEvent="MouseLeave">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Width"/>
                            <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="Height"/>
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </Style.Triggers>
        </Style>
</Window.Resources>
<Canvas>
        <Button Width="40" Height="40" Content="OK"/>
</Canvas>
登峰造极的成就源于自律
原文地址:https://www.cnblogs.com/fishpond816/p/13590591.html