1. 为什么用模板
模板是用来定义(或重定义)对象的外观的好东西. WPF已经提供了Style来自定义外观, 那为什么还需要Template呢? 是因为Style只能让你一个一个地设置对象的属性, 但template用来改变对象的组织结构. Style就好象是更改一台电脑的配置, 你可以换个内存, 换个显卡, 但它还是一台电脑, 而Template则是把电脑整个换成一部汽车, 或者是其它一种你想要的东西. Template一般使用于两种情况下:
1) 需要修改一个控件的外观, 这个控件可能是1) WPF内建的控件, 比如你对默认的Button外观不爽了, 想把它换成一些别的什么, 比如一张图片或一个矩形. 2) 用户自建的控件, 它默认是什么都没有,
你得用模板使它可视化.
2) 需要为一种不可见的数据类型提供外观. 比如如果你把一个DateTime的对象作为Button的内容, 因为DateTime并不是一个可视的元素(UIElement), WPF会默认把其渲染成string;
为了提供更好的用户体验, 我们可以给它添加一个模板, 以控制WPF对DateTime对象的渲染
对应于这两种情况, WPF提供了两种模板类型, ControlTemplate和DataTemplate(以下简称CT和DT).
2. 开始使用模板的
例1. 使用CT
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Button>
<Button.Template>
<ControlTemplate>
<Grid>
<Rectangle Fill="LightBlue" Stroke="Black" StrokeThickness="5"
RadiusX="20" RadiusY="20"/>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center">
I'm a CT
</TextBlock>
</Grid>
</ControlTemplate>
</Button.Template>
</Button></Window>
|
例2. 使用DT
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
Title="Window1" Height="300" Width="300">
<Button>
<Button.ContentTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="{Binding Year}"
/>
<Button Content="{Binding Month}"
/>
<Button Content="{Binding Day}"
/>
</StackPanel>
</DataTemplate>
</Button.ContentTemplate>
<sys:DateTime>3/1/2008</sys:DateTime>
</Button>
</Window>
|
有一点需要注意, 例2中的通过ContentTemplate属性设置的DT不会影响外部Button的渲染, 而通过例1中CT则会影响Button的渲染, 如Button的边框, 高亮都不见了——就好像CT会影响外部的Container(在这里是一个Button)的渲染, 而DT则会影响Containner里的Content的渲染; 所以在有些地方, 如ItemsControl中, CT是通过ItemContainerStyle设置,
而DT是通过ItemTemplate设置的.
但是从例1中可以看出, 由于使用了CT, Button的内容被完全自定义了; 那如何在新的CT中使用Button原有的Content呢? 我们当然可以手动在CT中添加一个控件, 并将其Content绑定到TemplatedParent上, 如下:
<ControlTemplate TargetType="Button">
<Grid>
<Rectangle Fill="LightBlue" Stroke="Black" StrokeThickness="5" RadiusX="20" RadiusY="20"/>
<ContentControl Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}"
ContentTemplate="{Binding ContentTemplate, RelativeSource={RelativeSourceTemplatedParent}}"/>
</Grid>
</ControlTemplate>
|
但WPF提供了一种内建的方法——ContentPresenter:
<ControlTemplate TargetType="Button">
<Grid>
<Rectangle Fill="LightBlue" Stroke="Black" StrokeThickness="5" RadiusX="20" RadiusY="20"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Grid>
</ControlTemplate>
|
除了通过ContentTemplate属性来修改CC中内容的模板外, 还可以使用CC.ContentTemplateSelector 来提供DataTemplate, 具体用法可查看MSDN, 在此不做复述.
3. More about CT and DT
WPF中已有CT和DT两种模板, 还有无数 名为***Tempate的属性, 很容易产生混淆, 例如:
类型.属性
|
接受类型
|
Control.Template
|
ControlTemplate
|
ContentControl.ContentTemplate
|
DataTemplate
|
ItemsControl.ItemTemplate
|
DataTemplate
|
GridViewColumn.CellTemplate
|
DataTemplate
|
GridViewColumn.HeaderTemplate
|
DataTemplate
|
Control分两种, ContentControl和ItemsControl (以下简称CC和IC), 其概念上的区别在两者包含的子物件个数不一样 (CC一个, IC多个), 其代码上面的区别在于CC有一个叫Content的属性接受单个对象, IC有一个叫Items的属性接受对象集合. Control.Template和CC.ContentControl的作用无用多说, 可能带来疑问的是IC中的template. IC可以看作是很多个”小”CC的集合.
而这些”小”CC的Template和ContentTemplate属性就是可以通过IC的ItemContainerStyle, ItemTemplate和ItemTemplateSelector属性指定的.
之前的例子演示了CC中的Template, 接下来是关于IC中Template的例子.
4. 在IC中使用模板
1) IC的模板
IC.Template的使用和CC很像; 以 ListView为例, 用以下的XAML生成ListView:
<ListView>
<sys:DateTime>3/1/2008</sys:DateTime>
<sys:DateTime>3/2/2008</sys:DateTime>
<sys:DateTime>3/3/2008</sys:DateTime>
</ListView>
|
更改ListView的Template属性:
<ListView.Template>
<ControlTemplate>
<Border BorderBrush="Blue" BorderThickness="2">
<StackPanel IsItemsHost="True"/>
</Border>
</ControlTemplate>
</ListView.Template>
|
需要特别注意其中的<StackPanel IsItemsHost="True"/>,
这个设置了IsItemsHost属性的StackPanel的作用跟CC中的ContentPresenter的作用是一样, 是用来加载使用该模板的Control原有的内容的. 不同的是CC只有一个子Item, 所以用一个ContentPresenter就可以了, 而IC有多个子Item, 所以用户需要使用一个Panel来放置items. 在这里用户可以通过使用不同的Panel来更改IC对子Item的布局, 比如使用一个UniformGrid:
<ControlTemplate>
<Border BorderBrush="Blue" BorderThickness="2">
<UniformGrid Columns="2" Rows="2" IsItemsHost="True"/>
</Border>
</ControlTemplate>
|
2) IC中Item的模板
在为IC中的子对象添加模板之前, 需要先明确与IC中的子对象有关的两种概念, 一种我们称为Item, 是我们直接在IC.Items中添加的对象; 另一种我们称为Item Container (对象容器), 是一种与IC类型相对应(e.g.与ListView对应的是ListViewItem), 用来存放Item的CC. Item Container的作用在于, 用户可能在IC.Items中添加的是一种数据类型 (无法可视化), 而IC会生成一个Container用来包装这些数据对象.
比如上例中, 我们在ListView的Items中设置了3个DataTime的对象, 则ListView会生成3个的ListViewItem, 并使ListViewItem.Content等于我们添加的DataTime对象.
使用Item Container的好处是显而易见的, 1, IC可以以一种统一的方式来处理它下面的子对象, 2, IC.Items可以接受任何类型的Colleciton, 而不一定非要一个Container的Collection.
所以, 想要控制ListView中子对象的渲染, 我们只需要更改其中Container, 也就是ListViewItem的Template和ContentTemplate就可以了. 对于前者, IC并没有直接提供属性来设置其下Container 的Template, 而是提供了ItemContainerStyle属性, 我们需要在Style中设置其Template属性, 比如:
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Grid>
<Rectangle Fill="LightBlue" Stroke="Black" StrokeThickness="5" RadiusX="20" RadiusY="20"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
|
对于Container的ContentTemplate属性, 有三种方法可以修改, 一是在ItemContainerStyle中设置其ContentTemplate属性, 这里不再复述; 二是通过IC.ItemTemplateSelector指定(跟CC一样); 三是使用IC直接提供的ItemTemplate属性, 如:
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Button Content="{Binding Year}"
/>
<Button Content="{Binding Month}"
/>
<Button Content="{Binding Day}"
/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
|
需要注意的是, 通过IC.ItemTemplate属性设置的模板优先级比在ItemContainerStyle中设置的ContentTemplate高.