简介
这是一篇记录笔者阅读学习刘铁猛老师的《深入浅出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的条目显示汽车的厂商标志和简要参数,单击某个条目后在窗口的详细内容区域显示汽车的照片和详细参数。
- 添加资源文件夹引入对应汽车图标
- 创建详细内容窗口的DataTemplate
- 创建ListBox的DataTemplate
- 使用对应模板
- 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应用到程序中就可以了。
示例:
- 文档大纲-》选中需要设计的控件-》右键编辑模板-编辑副本-》设置名称和位置
- 修改设计需要的模板ControTemplate,TemplateBinding将控件模板中的属性值关联到目标控件上,产生的效果就是你为目标控件设置的值以后,控件模板的值也会随之改变。
- 目标控件使用对应的模板,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设置其应用目标有两种方法:
- 逐个设置控件的Template、ContentTemplate、ItemsTemplate、CellTemplate等属性,不想应用Template的控件不设置。
- 把Template应用在某个类型的控件或数据上。
使用方法
-
把ControlTemplate应用在所有目标上需要借助Style来实现,但Style不能标记x:Key。Style没有x:Key标记,默认为应用到所有由x:Type指定的控件上,如果不想应用则需把控件的Style标记为{x:Null}.
-
把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>