WPF上按钮、标签、文本框等等控件,在窗体中展示出来的默认外观就是运用了对应的模板。
控件模板
一、默认模板分析
在WPF的设计窗口中,右键点击控件->编辑样式->编辑副本,会自动生成默认模板代码:
<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
......
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}"><!--这里的x:Type Button 类似与typeof,因此可以直接写成Button-->
<Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="true">
<!--ContentPresenter相当于一个内容占位符,button的内容会直接封装到这个元素中,最后得以展示-->
<ContentPresenter x:Name="contentPresenter" Focusable="False" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Opacity" TargetName="border" Value="0.56"/>
</Trigger>
......
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
普通控件一般通过Template
属性对象中的ControlTemplate
属性来定义模板。
仔细观察默认的模板代码(这里以Button
控件为例),其中需要关注的是,Content
属性的内容最终是封装到ContentPresenter
中最后得以展示,ContentPresenter
相当于一个内容占位符。(仅仅是Content
属性,如果没有Content
属性的控件使用了有ContentPresenter
元素的模板,会报错)。
此外可以看到,Button
控件的Background
、BorderBrush
、BorderThickness
等属性,在默认模板中都进行了设置。
默认的模板中还进行了一些触发器的定义,有兴趣可以看看。
在对任何控件进行自定义模板无从下手时,建议直接使用默认模板来进行更改。
二、简单使用
1、在控件内部直接定义样式(内联)
<Grid>
<Button Content="Schuyler" Height="100" Width="100" Name="btn">
<Button.Template>
<ControlTemplate>
<Grid>
<TextBlock Text="Button"/>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
仔细观察上述代码的效果,会发现Button
的默认灰色背景没了,按钮内容为Button
,这是因为在给Button
控件指定模板后,其样式方面全部交由模板来管控。
2、指定模板
这里的指定模板是指,直接在<Window.Resources>
元素中,通过ControlTemplate
来定义模板资源,此时必须指定TargetType
和Key
,由需要使用此模板的控件通过控件的template
属性使用模板资源。
<Window.Resources>
<ControlTemplate TargetType="Button" x:Key="buttonTemp">
<TextBox Text="这里是个模板"/>
</ControlTemplate>
</Window.Resources>
<Grid>
<Button Content="Schuyler" Template="{StaticResource buttonTemp}"/>
</Grid>
3、统一模板
模板也可以直接在<Window.Resources>
的Style
属性中定义,当Style
不设置key
时,就可以让作用域内的同类型控件都使用该模板。
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBox Text="这里是个模板"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Content="Schuyler"/>
</Grid>
4、模板触发器
模板中也可以定义触发器,仔细观察默认模板中的触发器会发现与常规的触发器有不同之处,模板中触发器的Setter
属性都会指定TargetName
,这是因为常规的触发器要么放在Style
元素下,Style
指定了TargetType
(样式触发器、事件触发器),要么放在EventTrigger
元素中指定了SourceName
(事件触发器),因此触发器都知道自己要改变样式的目标控件。由于模板中的触发器是针对模板内部的控件对象的,而模板内部的控件对象有多个,因此模板中触发器的Setter
,必须通过设定TargetName
指定模板中要采用该触发器的控件对象,否则是不会有效果的。
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<TextBlock Text="Schuyler" Name="tb"/>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<!--这里的Setter指定了TargetName,对应模板内部的TextBlock-->
<Setter Property="Background" Value="Red" TargetName="tb"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<Button Content="Schuyler" />
</Grid>
5、ContentPresenter的使用
观察默认模板的代码时可以看到,Content
属性的内容最终是封装到ContentPresenter
中最后得以展示,ContentPresenter
相当于一个内容占位符。在使用时候可以将ContentPresenter
元素当作调用此模板的元素中Content
属性的变量(注意控件要有Content
属性才能去使用ContentPresenter
)。
- PS:WPF中看到后缀为Presenter的元素时,都可考虑以下占位符的用法。
<Window.Resources>
<ControlTemplate TargetType="Button" x:Key="buttonTemp">
<Grid>
<TextBlock Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
<!--可以将ContentPresenter属性作为调用此模板的元素的Content属性值的变量-->
<ContentPresenter/>
</TextBlock>
</Grid>
</ControlTemplate>
</Window.Resources>
<Grid>
<Button Content="Schuyler" Template="{StaticResource buttonTemp}"/>
</Grid>
除了ContentPresenter
外,还有如ItemPresenter
和ScrollContentPresenter
等元素也是具有相同的作用。
三、TemplateBinding
思考一下,上文中提及了,Content
属性的值可以通过模板中的ContentPresenter
元素来进行传递,那么其他属性的值呢?
正常情况下一些常用的属性如Text
、Width
、Height
等属性都需要通过在控件上设置然后再传递给模板使用,这个时候就需要在模板中使用TemplateBinding
关键字进行值的绑定传递了。
TemplateBinding
是一个轻量级的控件到模板的数据绑定,专门针对控件模板设计的,其有如下两个特点:
TemplateBinding
的数据绑定是单向的,从数据源到目标(即从应用Template
的控件到Template
)。TemplateBinding
不能对数据对象进行自动转换,数据源和目标的数据类型若不同,需要自己写转换器。Binding
会对部分数据源和目标的数据类型进行自动转换。
<Window.Resources>
<ControlTemplate TargetType="Button" x:Key="buttonTemp">
<Border BorderBrush="Red" BorderThickness="2" Height="{TemplateBinding Height}" Width="{TemplateBinding Width}" CornerRadius="5">
<TextBlock Text="{TemplateBinding Content}"/>
</Border>
</ControlTemplate>
</Window.Resources>
<Grid>
<Button Content="Schuyler" Width="200" Height="100" Template="{StaticResource buttonTemp}"/>
</Grid>
数据模板与容器模板
一、数据模板
继承了ItemConrol
的控件对象(如ListView
、ListBox
、DataGrid
、TabControl
等等),都可以使用数据模板DataTemplate
。
数据模板的作用在于决定每个Item中的数据的展示形式。
普通控件通过Template
属性来定义模板,而子项容器控件则通过ItemTemplate
属性来定义子项模板。
先创建作为数据源对象的类
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public int Gender { get; set; }
public double Left { get; set; }
public double Top { get; set; }
}
在xaml中定义数据集合,然后在子项容器控件中通过ItemTemplate
属性定义子项数据模板。
<Window ......
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFStudy"
......>
<Window.Resources>
<!--数据集合-->
<x:Array Type="local:Person" x:Key="persons">
<local:Person Name="Hello" Age="20" Gender="1" Left="10" Top="50"/>
<local:Person Name="Schuyler" Age="21" Gender="2"/>
<local:Person Name="Cai" Age="22" Gender="1" Left="100" Top="250"/>
</x:Array>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{StaticResource persons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
<TextBlock Text="{Binding Gender}" Grid.Column="2"/>
<TextBlock Text="{Binding Left}" Grid.Column="3"/>
<TextBlock Text="{Binding Top}" Grid.Column="4"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
二、子项容器模板
数据模板决定了数据的展示形式,而子项容器模板则决定了数据模板的展示形式。
子项容器模板可以通过子项容器控件的ItemsPanel
进行设置。
<Window.Resources>
<!--数据集合-->
<x:Array Type="local:Person" x:Key="persons">
<local:Person Name="Hello" Age="20" Gender="1" Left="10" Top="50"/>
<local:Person Name="Schuyler" Age="21" Gender="2"/>
<local:Person Name="Cai" Age="22" Gender="1" Left="100" Top="250"/>
</x:Array>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{StaticResource persons}">
<!--这里是子项容器模板-->
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<!--这里是数据模板-->
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Canvas.Top="{Binding Top}" Canvas.Left="{Binding Left}">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
<TextBlock Text="{Binding Gender}" Grid.Column="2"/>
<TextBlock Text="{Binding Left}" Grid.Column="3"/>
<TextBlock Text="{Binding Top}" Grid.Column="4"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
数据模板决定了每个Item中的数据怎么展示出来,而子项容器模板则决定了每个Item怎么展示出来。
如上述代码所示,会将ItemsControl
的每个Item按序放入到Canvas
中,由于Canvas
的每个子元素都以左上角为原始坐标,所以堆在左上角了。
需要注意的是,在上述代码中数据模板的Grid
设置了Canvas.Top
和Canvas.Left
属性值,但却没有效果,这是因为这两个属性只针对Canvas
的直接子元素起作用。然而ItemControl
的数据模板中的容器对象(这里是Grid
)并不是直接放置到子项容器模板的容器对象(这里是Canvas
)中的,而是先将Grid
放入到ContentPresenter
元素后,再将ContentPresenter
元素放置到Canvas
中。
因此正确的做法应该是在ItemsControl
的ItemContainerStyle
属性中对ContentPresenter
进行样式设置。
<Grid>
<ItemsControl ItemsSource="{StaticResource persons}">
<!--应该在这里编写子项容器样式-->
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
</Style>
</ItemsControl.ItemContainerStyle>
......
</ItemsControl>
</Grid>
完整的渲染过程(ItemsControl中的ItemsPresenter与ContentPresenter)
为了便于理解,这里简单描述一下ItemsControl
的渲染过程,顺便缕一缕ItemsPresenter
与ContentPresenter
在ItemsControl
控件中的作用
ItemsControl
在渲染时,使用一个名为ItemsPresenter
的控件来展示各个子项,主要用来控制如何布局(使用ItemsPanelTemplate
中设定的容器)和显示这些子项,但它不是用于承载单个子项内容的控件。ItemsControl
在渲染时,会使用ContentPresenter
控件来展示单个子项内容,并且ContentPresenter
会放置子项容器中(使用ItemsPanelTemplate
中设定的容器)
控件层次示意图如下(注意这个图只是为了方便理解,不是完全照着视觉树来的):
注意,并不是所有控件都是将子项放入ContentPresenter
元素中的,例如ListView
就是将子项放入ListViewItem
中再放入子项容器模板的容器对象中的,ComboBox
则是放入到ComboBoxItem
中然后再放入到容器对象中的,在使用时可以配合snoop软件进行查看。
三、层级数据模板
继承了ItemsControl
的控件,除了可以使用上面说的DataTemplate
数据模板外,还可以使用继承DataTemplate
的层级数据模板HeaderedItemsControl
,该模板对象一般用于TreeView
和Menu
等具有层级关系的子项容器控件中。TreeView
和Menu
控件都是通过ItemTemplate
属性来定义层级数据模板。
菜单模型
public class Menu
{
public string Header { get; set; }
public int Index { get; set; }
public ObservableCollection<Menu> Children { get; set; }
}
视图模型
public class MainWindowViewModel
{
public ObservableCollection<Menu> Menus { get; set; }
public MainWindowViewModel()
{
Menus = new ObservableCollection<Menu> {
new Menu
{
Header = "系统配置",
Index = 0,
Children = new ObservableCollection<Menu>{
new Menu{Header="用户设置", Index=0},
new Menu{Header="权限设置", Index=1}
}
},
new Menu
{
Header = "样式配置",
Index=1,
Children = new ObservableCollection<Menu>{
new Menu{Header="主题", Index=0},
new Menu{Header="图片", Index=1}
}
}
};
}
}
XAML代码
<Window ......>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<TreeView ItemsSource="{Binding Menus}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Index}"/>
<TextBlock Text="{Binding Header}" Grid.Column="1"/>
</Grid>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
四、资源中的数据模板与容器模板
上面的例子中是将数据模板与子项容器模板直接使用在控件中,通过子项容器控件(继承了ItemControl
的控件)的ItemTemplate
和ItemsPanel
属性来进行设置的(内联的用法),样式则是通过子项容器控件的ItemContainerStyle
属性来设置。而数据模板、容器模板、子项容器样式的内容与控件模板的内容一样,都可以放置在<Window.Resources>
作为资源来使用。
因此,上面的例子可以写成下面的形式:
<Window.Resources>
<!--数据集合-->
<x:Array Type="local:Person" x:Key="persons">
<local:Person Name="Hello" Age="20" Gender="1" Left="10" Top="50"/>
<local:Person Name="Schuyler" Age="21" Gender="2"/>
<local:Person Name="Cai" Age="22" Gender="1" Left="100" Top="250"/>
</x:Array>
<!--数据模板-->
<DataTemplate x:Key="dateTemplate">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
<TextBlock Text="{Binding Gender}" Grid.Column="2"/>
<TextBlock Text="{Binding Left}" Grid.Column="3"/>
<TextBlock Text="{Binding Top}" Grid.Column="4"/>
</Grid>
</DataTemplate>
<!--子项容器模板-->
<ItemsPanelTemplate x:Key="itemsPanelTemplate">
<Canvas/>
</ItemsPanelTemplate>
<!--子项容器样式-->
<Style x:Key="itemContainerStyle" TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Top}"/>
<Setter Property="Canvas.Left" Value="{Binding Left}"/>
</Style>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{StaticResource persons}"
ItemTemplate="{StaticResource dateTemplate}"
ItemContainerStyle="{StaticResource itemContainerStyle}"
ItemsPanel="{StaticResource itemsPanelTemplate}">
</ItemsControl>
</Grid>
模板选择器
考虑这么一个场景,子项容器控件中,子项源为Person的数组,每个Person对象中都有年龄Age属性,现在需要在大于20岁时,使用模板A;小于等于20岁时使用模板B。这样的需求显然只靠xaml代码是难以实现的,此时可以配合C#代码,定义一个模板选择器类型,根据不同的条件返回不同的模板对象。
使用模板选择器具体有如下几个步骤
1、定义模板资源
<Window.Resources>
<!--数据集合-->
<x:Array Type="local:Person" x:Key="persons">
<local:Person Name="Hello" Age="20" Gender="1" Left="10" Top="50"/>
<local:Person Name="Schuyler" Age="21" Gender="2"/>
<local:Person Name="Cai" Age="22" Gender="1" Left="100" Top="250"/>
</x:Array>
<DataTemplate x:Key="dataTemplateA">
<Grid>
<TextBlock Text="{Binding Name}"/>
</Grid>
</DataTemplate>
<DataTemplate x:Key="dataTemplateB">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}" Grid.Column="1"/>
</Grid>
</DataTemplate>
</Window.Resources>
2、创建模板选择器类型
可以通过查看ItemControl
控件的ItemTemplateSelector
属性的定义代码,得知其类型为DataTemplateSelector
,因此可以创建一个MyDataTemplateSelector类型,继承DataTemplateSelector
类,并重写其SelectTemplate
函数。
DataTemplate SelectTemplate(object item, DependencyObject container)
:此函数会接收两个参数,item参数为子项的数据对象,本例中为Person对象;container参数则为子项所在的父类容器对象。
public class MyDataTemplateSelector: DataTemplateSelector
{
//模板A,可以在xaml中赋值
public DataTemplate TemplateA { get; set; }
//模板B,可以在xaml中赋值
public DataTemplate TemplateB { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var person = item as Person;
if (person != null && person.Age > 20)
{
return TemplateA;
}
return TemplateB;
}
}
3、在控件中使用模板选择器
<Window ......>
......
<Grid>
<ItemsControl ItemsSource="{StaticResource persons}">
<ItemsControl.ItemTemplateSelector>
<local:MyDataTemplateSelector
TemplateA="{StaticResource dataTemplateA}"
TemplateB="{StaticResource dataTemplateB}"
/>
</ItemsControl.ItemTemplateSelector>
</ItemsControl>
</Grid>
</Window>
子项容器控件中除了数据模板选择器ItemTemplateSelector
外,还有子项容器模板选择器ItemContainerStyleSelector
。
其他控件也有自己的模板选择器,比如继承了ContenteControl
的控件(例如Button
)都会有ContentTemplateSelector
模板选择器。
模板实例
一、CheckBox模板
<Window.Resources>
<Style TargetType="CheckBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<Border BorderBrush="Gray" BorderThickness="1" Height="30" Width="100" CornerRadius="10">
<Border Background="Orange" Width="49" CornerRadius="10" HorizontalAlignment="Left" Margin="1" Name="border"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="HorizontalAlignment" Value="Right" TargetName="border"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<CheckBox></CheckBox>
</Grid>
二、ListView斑马线
AlternationCount
:设置以多少个子项为一个循环。
ItemsControl.AlternationIndex
:读取当前子项在循环中的下标,从0开始。(只读属性)
<Window.Resources>
<x:Array Type="local:Person" x:Key="persons">
<local:Person Name="schuyler1" Age="22"/>
<local:Person Name="schuyler2" Age="22"/>
<local:Person Name="schuyler3" Age="22"/>
<local:Person Name="schuyler4" Age="22"/>
<local:Person Name="schuyler5" Age="22"/>
<local:Person Name="schuyler6" Age="22"/>
</x:Array>
</Window.Resources>
<Grid>
<ListView ItemsSource="{StaticResource persons}" AlternationCount="2">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="Orange"/>
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
三、TabControl页签样式
<Window.Resources>
<Style TargetType="TabItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TabItem">
<Border Background="LightBlue" Margin="5 0 5 0" CornerRadius="5" Name="tempBorder">
<TextBlock Text="{TemplateBinding Header}" Padding="5"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="Background" Value="LightGreen" TargetName="tempBorder"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid >
<TabControl AlternationCount="2">
<TabItem Header="AAA"/>
<TabItem Header="BBB"/>
<TabItem Header="CCC"/>
<TabItem Header="DDD"/>
</TabControl>
</Grid>