WPF学习(10)模板
在前面一篇我们粗略说了Style和Behaviors,如果要自定义一个个性十足的控件,仅仅用Style和Behaviors是不行的,Style和Behaviors只能通过控件的既有属性来简单改变外观,还需要有ControlTemplate来彻底定制,这是改变Control的呈现,也可以通过DataTemplate来改变Data的呈现,对于ItemsControl,还可以通过ItemsPanelTemplate来改变Items容器的呈现。
1.模板
WPF模板有三种:ControlTemplate、DataTemplate和ItemsPanelTemplate,它们都继承自FrameworkTemplate抽象类。在这个抽象类中有一个FrameworkElementFactory类型的VisualTree变量,通过该变量可以设置或者获取模板的根节点,包含了你想要的外观元素树。
先来看下ControlTemplate的例子:
<Window x:Class="TemplateDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<ControlTemplate x:Key="buttonTemplate" TargetType="{x:Type Button}">
<Grid>
<Ellipse Width="100" Height="100">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Cyan" />
<GradientStop Offset="1" Color="LightCyan" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Ellipse Width="80" Height="80">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Offset="0" Color="Yellow" />
<GradientStop Offset="1" Color="Transparent" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</ControlTemplate>
</Window.Resources>
<Grid>
<StackPanel>
<Button Content="Hi,WPF" Template="{StaticResource buttonTemplate}" Click="Button_Click"/>
</StackPanel>
</Grid>
</Window>
这里是将ControlTemplate作为资源的方式共享的,当然也可以通过Style的Setter来设置Button的Template属性来做。
效果如下:

在该ControlTemplate的VisualTree中,Button是被作为TemplatedParent的,这个属性定义在FrameworkElement和FrameworkContentElement中。关于TemplatedParent的介绍,这里可以看Mgen这篇文章。
这里完成了一个自定义风格的Button,然后在很多时候,我们只是想稍微修改下Button的外观,仍然像保留其阴影特性等功能,这时候我们就要"解剖"Button来了解其内部结构,VS2012自带的Expression Blend 5就具有这样的解剖功能。

生成了这样的代码:
<Style x:Key="FocusVisual">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle Margin="2" SnapsToDevicePixels="true" Stroke="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" StrokeThickness="1" StrokeDashArray="1 2"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="Button.Static.Background" Color="#FFDDDDDD"/>
<SolidColorBrush x:Key="Button.Static.Border" Color="#FF707070"/>
<SolidColorBrush x:Key="Button.MouseOver.Background" Color="#FFBEE6FD"/>
<SolidColorBrush x:Key="Button.MouseOver.Border" Color="#FF3C7FB1"/>
<SolidColorBrush x:Key="Button.Pressed.Background" Color="#FFC4E5F6"/>
<SolidColorBrush x:Key="Button.Pressed.Border" Color="#FF2C628B"/>
<SolidColorBrush x:Key="Button.Disabled.Background" Color="#FFF4F4F4"/>
<SolidColorBrush x:Key="Button.Disabled.Border" Color="#FFADB2B5"/>
<SolidColorBrush x:Key="Button.Disabled.Foreground" Color="#FF838383"/>
<Style x:Key="ButtonStyle1" TargetType="{x:Type Button}">
<Setter Property="FocusVisualStyle" Value="{StaticResource FocusVisual}"/>
<Setter Property="Background" Value="{StaticResource Button.Static.Background}"/>
<Setter Property="BorderBrush" Value="{StaticResource Button.Static.Border}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="1"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="border" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
<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="IsDefaulted" Value="true">
<Setter Property="BorderBrush" TargetName="border" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.MouseOver.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.MouseOver.Border}"/>
</Trigger>
<Trigger Property="IsPressed" Value="true">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.Pressed.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Pressed.Border}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Background" TargetName="border" Value="{StaticResource Button.Disabled.Background}"/>
<Setter Property="BorderBrush" TargetName="border" Value="{StaticResource Button.Disabled.Border}"/>
<Setter Property="TextElement.Foreground" TargetName="contentPresenter" Value="{StaticResource Button.Disabled.Foreground}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
看的出来,是由一个Border里面放了一个ContentPresenter构成的,然后是触发器定义的默认行为,相对比较简单,像ScrollBar等控件内部是很复杂的。关于ContentPresent,我们将在第二小节详细描述。
接下来,我们以Selector中的ListBox为例,来说明DataTemplate和ItemsPanelTemplate。
<!--ItemsPanelTemplate-->
<ItemsPanelTemplate x:Key="itemspanel">
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
<!--DataTemplate-->
<DataTemplate x:Key="datatemplate">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ID}" Width="30"/>
<TextBlock Text="{Binding Name}" Width="60"/>
<Image Source="{Binding imgPath}" Width="30"/>
</StackPanel>
</DataTemplate>
cs代码:
List<Student> studentList = new List<Student>()
{
new Student(){ID=,Name="Rethinker",imgPath="/TemplateDemo;component/Images/1.png"},
new Student(){ID=,Name="Jello",imgPath="/TemplateDemo;component/Images/2.png"},
new Student(){ID=,Name="Taffy",imgPath="/TemplateDemo;component/Images/3.png"}
};
this.lbStudentList.ItemsSource = studentList;
注意:这里Image的Source采用的是Pack Uri,详细内容请查看WPF中的Pack Uri
效果如下:

2.ContentPresenter
在第一节,我们发现在解剖的Button内部有个叫ContentPresenter的东东,根据名字也许你已经猜到它是干嘛的了,它就是呈现ContentControl的内容的。这里,当我们将ContentPresenter换成TextBlock好像效果也没变化。
<TextBlock Text="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
我们知道TextBlock的Text属性是String类型,这就限制了其显示的丰富性。有人会说,既然这样那换成ContentControl算了,它可以显示更丰富的东西,类似这样:
<ContentControl Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
看起来也没什么问题,我们知道Button本身就是一个ContentControl,它是一个很重量级的Control,它的Content其实也是通过ContentPresenter来表现的,看起来ContentPresenter是一个更轻量级的Control。另外,ContentPresenter还有一个比较特别的地方,当你未指定它的Content时,它会默认去取该模板的使用者的Content。
在继承自ItemsControl的控件内部也有个类似的ItemsPresenter,它是来负责Item的展示。

在ItemsPresenter内部会以ItemsPanelTemplate中的容器作为自己的容器,会以ItemTemplate中的布局作为ListBoxItem的布局。当然,在具体显示内容的地方,还是要用到ContentTemplate的。归根结底,我们可以将Presenter看作是一个占位符,设置了Button的Content,它就获取,否则默认。
3.TemplatePart机制
TemplatePart机制是某些WPF控件,如ProgressBar等,通过内建的逻辑来控制控件的可是行为的方式。我们先来解剖下ProgressBar一探究竟。

我们发现里面有几个很特别的东西,一个名为PART_Track的Rectangle,一个名为PART_Indicator的Grid。这并不是偶然,实际上在ComboBox和TextBox中也有这样类似PART_×××这样命名的元素。在这些类的定义中,我们也会发现一些端倪,例如ProgressBar类的定义:
[TemplatePart(Name = "PART_GlowRect", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "PART_Indicator", Type = typeof(FrameworkElement))]
[TemplatePart(Name = "PART_Track", Type = typeof(FrameworkElement))]
public class ProgressBar : RangeBase
{ }
ProgressBar用了TemplatePart这个Attribute,那它到底如何有何作用呢?实际上,如果在ControlTemplate中找到了这样的元素,就会应用一些附加的行为。
例如,在ComboBox的空间模板中有个名为PART_Popup的Popup,当它关闭时,ComboBox的DropDownClosed事件会自动触发,如果ComboBox控件模板中有名为PART_EditableTextBox的TextBox,它就会将用户的选项作为它的显示项。在后面的自定义控件这一篇中,我们也将使用它。
4.如何找Template中的控件
在Template的基类FrameworkTemplate中有FindName方法,通过它我们可以找到模板中的控件。这个方法对于ControlTemplate和ItemsPanelTemplate很直接有效,但是,在ItemsControl的DataTemplate中,因为展示的数据是集合,所以相对复杂些。在前面我们已经剖析了ListBox内部,这对于找控件是最本质的。我们将前面的DataTemplate稍微修改下,如下:
<!--DataTemplate-->
<DataTemplate x:Key="datatemplate">
<StackPanel x:Name="sp" Orientation="Horizontal">
<TextBlock x:Name="tbID" Text="{Binding ID}" Width="30"/>
<TextBlock x:Name="tbName" Text="{Binding Name}" Width="60"/>
<Image x:Name="tbImgPath" Source="{Binding imgPath}" Width="30"/>
<TextBlock x:Name="tbNameLen" Text="{Binding Path=Name.Length}" />
</StackPanel>
</DataTemplate>
要查找由某个ListBoxItem的DataTemplate生成的TextBlock元素,需要获得ListBoxItem,在该ListBoxItem内查找ContentPresenter,然后对在该 ContentPresenter 上设置的 DataTemplate 调用 FindName,在由ListBoxItem查找ContentPresenter时,需要遍历VisualTree,这里给出遍历方法:
class CommonHelper
{
public static T ChildOfType<T>(DependencyObject Parent) where T : DependencyObject
{
for (int i = ; i < VisualTreeHelper.GetChildrenCount(Parent); i++)
{
DependencyObject obj = VisualTreeHelper.GetChild(Parent, i);
if (obj != null && obj is T)
{
return (T)obj;
}
else
{
T child = ChildOfType<T>(obj);
if (child != null)
return child;
}
}
return default(T);
}
}
在这里通过监听ListBox的SelectionChanged事件来展示效果,cs代码:
private void lbStudentList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//第一步:找到ListBoxItem
ListBoxItem item = this.lbStudentList.ItemContainerGenerator.ContainerFromIndex(this.lbStudentList.SelectedIndex) as ListBoxItem;
if (item == null) return;
//第二步:遍历找到ContentPresenter,这里需要写个辅助方法
ContentPresenter cp = CommonHelper.ChildOfType<ContentPresenter>(item);
if (cp == null) return;
//第三步:找到DataTemplate
DataTemplate dt = cp.ContentTemplate;
//第四步:通过DataTemplate的FindName方法
TextBlock tb = dt.FindName("tbName", cp) as TextBlock;
if (tb != null)
MessageBox.Show(tb.Text);
}
效果如下:

最后,推荐几篇比较好的文章:
1)Creating WPF Data Templates in Code: The Right Way
2)Customizing WPF Expander with ControlTemplate
WPF学习(10)模板的更多相关文章
- WPF学习10:基于MVVM Light 制作图形编辑工具(1)
图形编辑器的功能如下图所示: 除了MVVM Light 框架是一个新东西之外,本文所涉及内容之前的WPF学习0-9基本都有相关介绍. 本节中,将搭建编辑器的界面,搭建MVVM Light 框架的使用环 ...
- nodeJs学习-10 模板引擎 ejs语法案例
ejs语法案例 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <t ...
- WPF学习11:基于MVVM Light 制作图形编辑工具(2)
本文是WPF学习10:基于MVVM Light 制作图形编辑工具(1)的后续 这一次的目标是完成 两个任务. 画布 效果: 画布上,选择的方案是:直接以Image作为画布,使用RenderTarget ...
- WPF学习之深入浅出话模板
图形用户界面应用程序较之控制台界面应用程序最大的好处就是界面友好.数据显示直观.CUI程序中数据只能以文本的形式线性显示,GUI程序则允许数据以文本.列表.图形等多种形式立体显示. 用户体验在GUI程 ...
- WPF学习12:基于MVVM Light 制作图形编辑工具(3)
本文是WPF学习11:基于MVVM Light 制作图形编辑工具(2)的后续 这一次的目标是完成 两个任务. 本节完成后的效果: 本文分为三个部分: 1.对之前代码不合理的地方重新设计. 2.图形可选 ...
- 【WPF学习】第五十章 故事板
正如上一章介绍,WPF动画通过一组动画类(Animation类)表示.使用少数几个熟悉设置相关信息,如开始值.结束值以及持续时间.这显然使得它们非常适合于XAML.不是很清晰的时:如何为特定的事件和属 ...
- WPF学习概述
引言 在桌面开发领域,虽然在某些领域,基于electron的跨平台方案能够为我们带来某些便利,但是由于WPF技术能够更好的运用Direct3D带来的性能提升.以及海量Windows操作系统和硬件资源的 ...
- WPF学习开发客户端软件-任务助手(下 2015年2月4日代码更新)
时光如梭,距离第一次写的 WPF学习开发客户端软件-任务助手(已上传源码) 已有三个多月,期间我断断续续地对该项目做了优化.完善等等工作,现在重新向大家介绍一下,希望各位可以使用,本软件以实用性为主 ...
- WPF学习之路初识
WPF学习之路初识 WPF 介绍 .NET Framework 4 .NET Framework 3.5 .NET Framework 3.0 Windows Presentation Found ...
- ThinkPhp学习10
原文:ThinkPhp学习10 查询操作 Action模块 User下的search public function search(){ //判断username是否已经传入,且不为空 if(isse ...
随机推荐
- C#区域截图——调用API截图
原文:C#区域截图——调用API截图 前言:截图对于一个C++开发者来说无非是小菜一碟,也有朋友使用C#的 Graphics.CopyFromScreen 方法屏幕操作,作为一名整天想着用 C++ 开 ...
- C#之再议数组和集合
1.数组 1.1简单数组 1.2 多维数组 1.3锯齿数组 1.4Array数组 1.5作为参数的数组 1.6枚举 1.7结构 以上部分可参考 http://www.cnblogs.com/ztb12 ...
- Android学习之 AChartEngine 图表绘制
Android 开源图表绘制工具AChartEngine地址:http://code.google.com/p/achartengine/ AChartEngine Android实现图表绘制和展示( ...
- [HDU 1427]速度计算24点(DFS暴力搜索)
主题连接: pid=1427">http://acm.hdu.edu.cn/showproblem.php?pid=1427 思路:简单的DFS.dfs(sum,next,p)表 ...
- Spring系列
Spring系列之访问数据库 阅读目录 一.概述 二.JDBC API的最佳实践 三.Spring对ORM的集成 回到顶部 一.概述 Spring的数据访问层是以统一的数据访问异常层体系为核心,结 ...
- PKI系统深入的介绍
公钥基础设施(Public Key Infrastructure,缩写PKI)的基础与核心.是电子商务安全实施的基本保障.因此.对PKI技术的研究和开发成为眼下信息安全领域的热点. 本文对PKI技术进 ...
- 简单的反射 把datatable 转换成list对象
/// <summary> /// 把datatable 转换成list对象 /// </summary> /// <typeparam name="T&quo ...
- Android 设计模式模式适配器
自定义适配器模式:一类的接口,转换成客户的期望,也是一个接口.适配器使原本接口不是与类兼容可以无缝.下面两个图看起来更加清晰 watermark/2/text/aHR0cDovL2Jsb2cuY3Nk ...
- Zookeeper实践方案:(4)命名服务
1.基本介绍 命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息.利用Zookeeper非常easy创建一个全局的路径,而这个路径就能够作为一个名字.它能够指向集群中的集群.提供的服务的 ...
- 经纬Zhang英拉垫背的企业家VC没有到这种地步这么卑鄙
经纬的合伙人张颖前不久发了一篇名为<泡沫就在那里------致经纬系CEO的一封信>,这篇文章所提到的泡沫论在业界引发了强烈的反响,随后有评论觉得,这明明是VC(风险投资)们自己的危机,为 ...