1 模板的概念
在WPF中,控件只是一个数据和算法行为的载体,是个抽象的概念。至于控件本身的外观和行为、控件数据的呈现方式都是靠 Template 来具体定义的,通过引入模板(Template)微软将数据和算法的“内容”与“形式”解耦了,我们可以轻松的创造、修改、调整控件长什么样、有什么行为、数据如何展示等。这就是为什么默认情况下 Button 或 TextBox 形状是矩形的,因为它是在其默认模板中定义(正是模板决定了TextBox是方方正正的输入框,可以输入数据)。深入到控件内部,每个控件本身是一棵 UI元素树,其内部都是由很多子元素节点挂载组成。WPF的模板主要包括 ControlTemplate(控件模板)和DataTemplate(数据模板)两种类型。
- ControlTemplate:控件模板由控件的Template属性定义,用于决定控件的整体外观(控件UI元素树整体结构)与功能(触发器与事件)。由ControlTemplate生成的控件树其树根就是ControlTemplate的目标控件,此模板化控件的Template属性值就是这个ControlTemplate实例。
- DataTemplate:数据模板由控件的xxxTemplate属性定义(比如ContentControl的ContentTemplate、ItemsControl的ItemTemplate等),用于决定控件数据的呈现方式。DataTemplate 可以包含 UI 元素和数据,也构成了一颗UI元素树。由 DataTemplate 生成的控件树其树根是一个 ContentPresenter 控件,ContentPresenter 控件只有ContentTemplate属性没有Template属性,用于通过Content来承载DataTemplate,在ControlTemplate中挂载。ContentPresenter 控件是 ControlTemplate 控件树上的一个结点,所以DataTemplate 控件树是 ControlTemplate 控件树的一棵子树。
2 模板的定义
2.1 数据模板 DataTemplate
DataTemplate常用的位置主要包括三处,分别如下。最重要的一点是为DataTemplate中的每个控件设置Binding,告诉DataTemplate中各个元素应该关注数据的哪个属性。
- ContentControl的ContentTemplate属性:设置ContentControl控件内容Content数据的外观展示方式,使用较少。比如Button内的文字、UserControl的Content等
- ItemsControl的ItemTemplate属性:获取或设置用来显示每个列表[数据项条目]的 DataTemplate,该属性可以视为与内部 xxxItem 的数据模板绑定(主要)
- GridViewColumn的CellTemplate属性:定制GridViewColumn每个单元格数据的外观
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--定制“数据详情”的数据展示外观-->
<DataTemplate x:Key="detailViewTemplate">
<Border BorderBrush="Black" CornerRadius="6">
<StackPanel Margin="5">
<Border Width="380" Height="200" Background="{Binding Code}"/>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Color Name:" FontSize="20" FontWeight="Bold"/>
<TextBlock Text="{Binding Name}" FontSize="20" Margin="5,0"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="5,0">
<TextBlock Text="Color Code:" FontWeight="Bold"/>
<TextBlock Text="{Binding Code}" Margin="5,0"/>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
<!--定制“数据列表条目”的数据展示外观-->
<DataTemplate x:Key="ListItemViewTemplate">
<StackPanel Orientation="Horizontal" Margin="5,0">
<Border Width="20" Height="20" Background="{Binding Code}"/>
<StackPanel Margin="5,10">
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold"/>
<TextBlock Text="{Binding Code}" FontSize="14"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ResourceDictionary>
<Window x:Class="WPF_Demo.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:WPF_Demo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="600">
<StackPanel Orientation="Horizontal" x:Name="stackPanel" Margin="5">
<UserControl ContentTemplate="{StaticResource detailViewTemplate}" Content="{Binding SelectedItem,ElementName=listBoxColors}"/>
<StackPanel Width="180">
<ListBox x:Name="listBoxColors" Height="280" Margin="5,0" ItemTemplate="{StaticResource ListItemViewTemplate}"/>
<Button Content="Add" Margin="5" x:Name="bntAdd" Click="bntAdd_Click"/>
</StackPanel>
</StackPanel>
</Window>
- 对ContentControl来说(这里的UserControl):使用ContentTemplate相当于给控件的Content内容的展示提供了模板。而ContentPresenter又是DataTemplate在ControlTemplate中挂载展示的载体,是DataTemplate的根元素,设置Content内容绑定数据之后,模板会自动将控件的 Content 属性和ContentTemplate属性绑定到其
ContentPresenter
上(上下文),所以在ContentControl中我们直接使用Binding就可以获取上下文数据。- 对ItemsControl来说(这里的ListBox):使用ItemTemplate相当于给控件的每个数据条目内容数据的展示提供了模板。当ListBox的ItemSource被赋值时,会自动为每一个数据元素创建等量的条目容器 xxxItem(继承自ContentControl ),并自动使用Binding在每个数据条目容器与数据元素之间建立关联和绑定(将每个列表集合数据作为每条Item的DataContext)。
- 以ListBoxItem为例,ListBoxItem继承自ContentControl,其Content属性承载了列表的每条数据条目,其ContentTemplate数据模板来自于ItemsControl的ItemTemplate,作为展示数据的模板。并在创建时自动绑定了ItemsSource的每条数据作为Item的上下文DataContext。
- 同样,ItemsPresenter也是列表控件的整个DataTemplate在ControlTemplate中挂载展示的载体,是DataTemplate的根元素(一系列定义了DataTemplate 的 Item挂载到ItemsPresenter,然后ItemsPresenter在列表控件的ControlTemplate UI元素树中挂载展示)。
namespace WPF_Demo
{
public partial class MainWindow : Window
{
ObservableCollection<ColorItem> colorList;
public MainWindow()
{
InitializeComponent();
//列表数据源
colorList = new ObservableCollection<ColorItem>()
{
new ColorItem(){Name="浅绿色",Code="#33FF00"},
new ColorItem(){Name="深绿色",Code="#006600"},
new ColorItem(){Name="红色",Code="#FF0033"},
new ColorItem(){Name="紫色",Code="#9900FF"}
};
//设置上下文
this.listBoxColors.ItemsSource = colorList;
}
private void bntAdd_Click(object sender, RoutedEventArgs e)
{
//动态添加数据
colorList.Add(new ColorItem() { Name = "未知", Code = "#000000" });
}
}
public class ColorItem
{
public string Name { get; set; }
public string Code { get; set; }
}
}
2.2 控件模板 ControlTemplate
ControlTemplate在DataTemplate之上,用于组织整个控件UI元素树的结构和行为。其内部通过Presenter来绑定挂载DataTemplate的数据内容Content。ControlTemplate(控件模板)不仅是用于来定义控件的外观、样式, 还可通过控件模板的触发器(ControlTemplate.Triggers)修改控件的行为、响应动画等。
(1) Button的ControlTemplate源码
- TargetType : 获取或设置此 ControlTemplate 所针对的类型,默认为null。注意如果模板定义中包含 TargetType,则 ContentPresenter 属性不能为
null
。- ContentPresenter:挂载DataTemplate的控件,实现了数据内容的显示。等价于
- TemplateBinding:ControlTemplate最终被应用到一个控件上,这个控件被称为模板目标控件,ControlTemplate 里的控件元素可以使用TemplateBinding将自己的属性单向关联/绑定到目标控件的属性值上。
TemplateBinding
是绑定的优化形式,类似于使用{Binding RelativeSource={RelativeSource TemplatedParent}}
构造的绑定。TemplateBinding
可用于将模板的各个部分绑定到控件的各个属性。
(2) ListBox的ControlTemplate源码
定义 ItemsControl 主要分两个步骤:
1.设置ItemsPanel容器, 用于容纳列表布局的最外层容器 2.定义子项的DataTemplate
ItemsPanel :ItemsPanelTemplate类型,获取或设置模板,该模板定义对项的布局进行控制的面板。是容纳列表布局的外层容器。只能使用Panel族元素。 ItemsControl 的默认值是一个 StackPanel
3 模板的使用
我们这里以 ItemsControl 的自定义模板为例,用两个实际例子介绍模板的实际应用。
- IsItemsHost 属性:在此示例中,一个必需的重要属性是 IsItemsHost 属性。IsItemsHost 属性用于指示在 ItemsControl(如处理项列表的 ListBox 控件)的模板中,生成的元素应放在什么位置。如果将 StackPanel 的这一属性设置为 true,则添加到 ListBox 的所有项都将进入 StackPanel。请注意,此属性只对 Panel 类型有效。请注意,如果以这种方式在 ControlTemplate 中指定一个面板并将其标记为 IsItemsHost,控件的用户不使用 ControlTemplate 就无法替换 ItemsPanel。因此,除非您确信必须使用模板才能替换面板,否则不要采用这种方式。
- ItemsPresenter 和 ContentPresenter:此外,您也可以使用 ItemsPresenter 元素来标记项的位置,然后通过设置 ItemsPanel 属性来指定 ItemsPanelTemplate。如果要创建 ContentControl(如 Button)的模板,则对应元素为 ContentPresenter。同样,将此元素放置到 ContentControl 类型的 ControlTemplate 中,可以指示内容应在什么位置显示。
(1)实例1
<!-- 1.声明自定义ItemsControl控件 -->
<ItemsControl ItemsSource="{Binding Scans}">
<!-- 2.定义ItemsControl的整体样式模板ControlTemplate -->
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<ScrollViewer HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Hidden">
<!--IsItemsHost 属性用于指示在 ItemsControl(如处理项列表的 ListBox 控件)的模板中,生成的元素应放在什么位置。用于代替ItemsPresenter -->
<StackPanel IsItemsHost="True" Orientation="Vertical" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<!-- 3.定义ItemsControl每个数据项的展示模板DataTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl x:Name="CardPresenter">
<RadioButton
Height="35"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Content="{Binding ScanName}"
FontSize="20"
Foreground="White"
GroupName="scanbtn">
<RadioButton.Style>
<Style BasedOn="{StaticResource ScanButtonStyle}" TargetType="RadioButton">
<Setter Property="Background" Value="{Binding Status, Converter={StaticResource bgCV}}" />
</Style>
</RadioButton.Style>
<!-- 声明RadioButton的交互触发器触发行为 -->
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<i:InvokeCommandAction Command="{Binding ScanClickCommand, Source={x:Static vm:EDESPageStatus.Instance}}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</RadioButton>
</ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
(2)实例2
<!-- 1.声明自定义ItemsControl控件 -->
<ItemsControl
x:Name="itemsControl"
Padding="0,5,10,5"
DataContext="{x:Static vm:ExplorePageStatus.Instance}"
ItemsSource="{Binding Cards}">
<!-- 2.定义ItemsControl的整体样式模板ControlTemplate -->
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
SnapsToDevicePixels="True">
<ScrollViewer VerticalScrollBarVisibility="Hidden">
<!-- 使用 ItemsPresenter 元素来标记数据项列表在整体样式中的的位置(挂载) -->
<ItemsPresenter HorizontalAlignment="Center" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
</Border>
</ControlTemplate>
</ItemsControl.Template>
<!-- 3.定义对数据项的布局进行控制的面板Panel(控制数据项的排布方式,作用在ItemsPresenter内数据项列表外) -->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal">
<WrapPanel.Style>
<Style TargetType="WrapPanel">
<Setter Property="ItemWidth" Value="420" />
<Setter Property="ItemHeight" Value="326.67" />
</Style>
</WrapPanel.Style>
</WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!-- 4.定义ItemsControl每个数据项的展示模板DataTemplate -->
<ItemsControl.ItemTemplate>
<DataTemplate>
<!--内部放置UI元素以及Binding数据(默认来自ItemsSource的每一项)-->
<Grid x:Name="OuterGrid">
<Grid x:Name="InnerGrid" DataContext="{x:Static vm:ExplorePageStatus.Instance}">
<Border
x:Name="ButtonBorder"
Margin="8,4.8"
Background="Transparent"
BorderBrush="{c:Binding 'IsChecked ? \'Yellow\' : \'Transparent\'',
Mode=OneWay}"
BorderThickness="3"
DataContext="{Binding DataContext, ElementName=OuterGrid}">
<Grid DataContext="{Binding DataContext, ElementName=OuterGrid}">
<Canvas Panel.ZIndex="1">
<Label
Canvas.Left="10"
Canvas.Top="12"
Width="40"
Height="19.2"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="{c:Binding '(Phase == e:PhaseEnum.ED ? \'#FF6B6B\' : \'#44D7B6\')',
Mode=OneWay}"
BorderThickness="0"
Content="{c:Binding '(Phase == e:PhaseEnum.ED ? \'ED\' : \'ES\')',
Mode=OneWay}"
FontFamily="PingFangSC-Semibold"
FontSize="19px"
FontWeight="SemiBold"
Foreground="White" />
<Label
Canvas.Left="5"
Canvas.Bottom="10"
Width="30"
Height="20"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="{c:Binding '(LVChecked ? \'#9ACD32\' : \'#C0C0C0\')',
Mode=OneWay}"
BorderThickness="0"
Content="LV"
FontFamily="PingFangSC-Semibold"
FontSize="16px"
FontWeight="SemiBold"
Foreground="White" />
<Label
Canvas.Left="40"
Canvas.Bottom="10"
Width="30"
Height="20"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="{c:Binding '(LAChecked ? \'#9ACD32\' : \'#C0C0C0\')',
Mode=OneWay}"
BorderThickness="0"
Content="LA"
FontFamily="PingFangSC-Semibold"
FontSize="16px"
FontWeight="SemiBold"
Foreground="White" />
<Label
Canvas.Left="75"
Canvas.Bottom="10"
Width="30"
Height="20"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="{c:Binding '(RVChecked ? \'#9ACD32\' : \'#C0C0C0\')',
Mode=OneWay}"
BorderThickness="0"
Content="RV"
FontFamily="PingFangSC-Semibold"
FontSize="16px"
FontWeight="SemiBold"
Foreground="White" />
<Label
Canvas.Left="110"
Canvas.Bottom="10"
Width="30"
Height="20"
Padding="0"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Background="{c:Binding '(RAChecked ? \'#9ACD32\' : \'#C0C0C0\')',
Mode=OneWay}"
BorderThickness="0"
Content="RA"
FontFamily="PingFangSC-Semibold"
FontSize="16px"
FontWeight="SemiBold"
Foreground="White" />
</Canvas>
<Image
Panel.ZIndex="0"
Source="{c:Binding '(Phase == e:PhaseEnum.ED ? EDImage : ESImage)',
Mode=OneWay}"
Stretch="Uniform" />
</Grid>
</Border>
</Grid>
<!--每个数据项的设备输入行为-->
<Grid.InputBindings>
<MouseBinding
Command="{Binding Source={x:Static vm:ExplorePageStatus.Instance}, Path=KeyFrameDetailsCommand}"
CommandParameter="{Binding ElementName=OuterGrid, Path=DataContext}"
MouseAction="LeftClick" />
</Grid.InputBindings>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
4.模板机制的内部原理
(1)原理: ItemsControl的默认Template里的ItemsPresenter只起一个占位符(placeholder)的作用,它的主要角色是接收ItemsControl的ItemsPanelTemplate模板,并在ItemsControl应用模板时应用这个模板。这时一个ItemsControl的Template模板里的ItemsPresenter在应用这个ItemsControl的ItemsPanel模板时,会将模板里面的Panel类控件的TemplateParent设定为这个ItemsControl,同时将其IsItemsHost属性标识为true。ItemsControl还有一种用法是忽略ItemsPanel,直接在其Template内指定一个"ItemsPanel",即上述指定IsItemsHost的方式。这时ItemsPanel模板的设置将被直接忽略。不过,这时一定要将这个Panel的IsItemsHost设定为True,否则ItemsControl将找不到一个合适的ItemsPanel来显示列表项。
(2)总结: 我们再按照从上至下的顺序从整体上梳理一下ItemsControl的模板应用机制即一个ItemsControl在应用模板时,首先会应用Template模板(ControlTemplate类型)生成自身的visual tree(Control类的模板机制),然后Template模板中的ItemsPresenter应用其TemplateParent(即这个ItemsControl)的ItemsPanel模板(ItemsPanelTemplate类型)生成一个visual tree,并把这个visual tree放置在这个ItemsPresenter的位置(ItemsPresenter这时起到占位符的作用)。在ItemsPanel模板被应用时,这个面板的TemplateParent会被指向这个ItemsControl,同时其IsItemsHost属性被标识为true。ItemsControl的ItemContainerGeneror在遍历自己的ItemsInternal列表并为每个列表项(item)生成一个container(ItemContainer),并将ItemsControl的ItemTemplate模板“转交”(forward)给这个container,这样这个container就可以应用模板(ItemTemplate),为与自己对应的数据项(item)生成一个由这个ItemTemplate定义的visual tree。