每个控件都有自己的视觉外观,比如我们一眼就能分清楚 Button 和 CheckBox 两个按钮。为什么?因为这两者呈现出来的外观完全不同。WPF 为每一种控件都提供了一个默认的视觉外观,同时支持开发者去重写这个视觉外观,只需要将重写的视觉外观赋值到 Template 属性即可——这就是 Template(模板) 的由来。模板定义了控件的视觉外观。
WPF 的控件大致可以分为好几种,比如以 Panel 为基类的布局控件,以 ContentControl 为基类的内容控件,以 ItemsControl 为基类的集合控件。这些不同种类的控件都有各自的视觉外观,也意味着它们都有不同的模板。
以面向对象(OOP)的思想,这些不同的模板肯定会继承同一个基类。WPF 的模板基类叫 FrameworkTemplate ,它是一个抽象类,有三个核心子类:
ControlTemplate(控件模板) :用于定义控件的外观(Control 基类的 Template 属性)。
DataTemplate(数据模板) :数据的“外衣”,用于从一个对象中提取数据并在内容控件或列表控件中显示数据(如 ContentTemplate 或 ItemTemplate)。
ItemsPanelTemplate(元素面板模板) :用于 ItemsControl 及其子类控件,定义集合中各个元素之间的布局方式(ItemsPanel 属性)。
一、 FrameworkTemplate 基类 我们先来看一下 FrameworkTemplate 的源码定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public abstract class FrameworkTemplate : DispatcherObject , INameScope , ISealable , IHaveResources , IQueryAmbient { protected FrameworkTemplate () ; public bool IsSealed { get ; } public FrameworkElementFactory VisualTree { get ; set ; } public TemplateContent Template { get ; set ; } public ResourceDictionary Resources { get ; set ; } public bool HasContent { get ; } public object FindName (string name, FrameworkElement templatedParent ) ; public DependencyObject LoadContent () ; public void RegisterName (string name, object scopedElement ) ; public void Seal () ; public bool ShouldSerializeResources (XamlDesignerSerializationManager manager ) ; public bool ShouldSerializeVisualTree () ; public void UnregisterName (string name ) ; protected virtual void ValidateTemplatedParent (FrameworkElement templatedParent ) ; }
在 FrameworkTemplate 基类中有一个 VisualTree 属性,这是我们首次看到“视觉树”这个关键词。实际上,WPF 拥有两棵树:逻辑树(Logical Tree) 和 视觉树(Visual Tree) ,并提供了两个帮助类:LogicalTreeHelper 和 VisualTreeHelper。
LogicalTreeHelper :提供用于查询逻辑树中的对象的静态帮助器方法。
VisualTreeHelper :提供用于执行涉及可视化树节点的常规任务的实用工具方法。
二、 LogicalTree 逻辑树 WPF 使用了若干树结构形式来定义程序元素之间的关系。要了解逻辑树与视觉树,需要先了解两个关键基类:FrameworkElement 和 Visual。
FrameworkElement :主要实现控件的布局、逻辑树、支持数据绑定和动态资源引用、控件样式定义和动画。它与 WPF 控件“更靠近一些”。
Visual :更关注控件的命中测试、坐标转换和边界框计算,提供更基础的视觉呈现支持。
由控件组成的 XAML 代码本质上就是一棵逻辑树 。逻辑树的主要用途之一是在后端代码中查找前端 XAML 的某个控件,以便进行操作。
遍历逻辑树示例 1. 前端 XAML 1 2 3 4 5 6 7 8 9 10 11 12 <Grid > <Grid.ColumnDefinitions > <ColumnDefinition Width ="auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions > <Border Background ="LightCyan" Grid.Column ="0" Width ="188" x:Name ="_LeftBorder" > <Button Click ="Button_Click" Content ="当前逻辑树" HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Border > <Border Grid.Column ="1" x:Name ="_RightBorder" > <TreeView Margin ="5" x:Name ="_TreeView" /> </Border > </Grid >
2. 后端 C# 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private void Button_Click (object sender, RoutedEventArgs e ){ TreeViewItem item = new TreeViewItem() { Header = "逻辑树根" }; LogicalTree(item, this ); _TreeView.Items.Add(item); } private void LogicalTree (TreeViewItem item, object element ){ if (!(element is DependencyObject)) return ; TreeViewItem treeViewItem = new TreeViewItem { Header = element.GetType().Name }; item.Items.Add(treeViewItem); var elements = LogicalTreeHelper.GetChildren(element as DependencyObject); foreach (object child in elements) { LogicalTree(treeViewItem, child); } }
运行后,TreeView 中展开的节点结构与 XAML 代码完全一致。但逻辑树并没有展示控件内部的细节结构,若要看到控件内部是如何构成的,就需要用到视觉树(VisualTree)。
三、 VisualTree 可视化树 可视化树描述由 Visual 基类表示的可视化对象的完整结构。为控件编写模板时,实际上就是在定义或重新定义适用于该控件的可视化树。
在 WPF 编程中,可视化树的一个重要应用是:路由事件的事件路由大多遍历可视化树而非逻辑树 。
遍历可视化树示例 使用与上文相同的 XAML 结构,后端替换为 VisualTreeHelper 进行遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private void Button_Click (object sender, RoutedEventArgs e ){ TreeViewItem item = new TreeViewItem() { Header = "可视化树根" }; VisualTree(item, this ); _TreeView.Items.Add(item); } private void VisualTree (TreeViewItem item, object element ){ if (!(element is DependencyObject)) return ; TreeViewItem treeViewItem = new TreeViewItem { Header = element.GetType().Name }; item.Items.Add(treeViewItem); for (int i = 0 ; i < VisualTreeHelper.GetChildrenCount(element as DependencyObject); i++) { VisualTree(treeViewItem, VisualTreeHelper.GetChild(element as DependencyObject, i)); } }
运行结果会比逻辑树丰富很多。除了 XAML 显式声明的控件外,树中还会包含 Window 的外壳模板(如 AdornerDecorator、ContentPresenter)、Button 内部的 ButtonChrome / Border 等细粒度组件。
核心结论 :可视化树粒度更细,包含控件内部模板结构(可视化树范围 > 逻辑树范围)。
四、 ControlTemplate 控件模板 4.1 控件模板概述 界面由多个控件构成逻辑树,而每个控件内部内部由控件模板构成可视化树。
模板与样式的区别
Style(样式) :相当于人的肤色、身高、字号大小、内外边距。改变的是控件已有的属性。
ControlTemplate(模板) :相当于人的骨骼结构。如果你想把一个矩形的 Button 变成圆形按钮、或者带图标的圆角按钮,就需要替换 ControlTemplate。
FrameworkElement 的 Template 属性允许重新定义控件的外观。需要注意:不能仅替换可视化树的一部分;若要更改控件的结构,必须将 Template 设置为新的完整 ControlTemplate 。
4.2 查看控件的默认模板 在 Visual Studio 或 Blend 的设计视图中,右键控件 -> 编辑模板 -> 编辑副本 ,系统会在资源字典(如 Window.Resources)中生成该控件的默认 Style 与 ControlTemplate。
以标准的 Button 模板简化版为例:
1 2 3 4 5 <ControlTemplate TargetType ="{x:Type Button}" > <Border Background ="{TemplateBinding Background}" BorderBrush ="{TemplateBinding BorderBrush}" BorderThickness ="{TemplateBinding BorderThickness}" SnapsToDevicePixels ="true" x:Name ="border" > <ContentPresenter Focusable ="False" HorizontalAlignment ="{TemplateBinding HorizontalContentAlignment}" Margin ="{TemplateBinding Padding}" RecognizesAccessKey ="True" SnapsToDevicePixels ="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment ="{TemplateBinding VerticalContentAlignment}" x:Name ="contentPresenter" /> </Border > </ControlTemplate >
4.3 什么是 ContentPresenter 对象? ContentPresenter 继承于 FrameworkElement,常用于 ContentControl 内容控件的模板中。它是一个“占位控件”,专门负责承载和呈现控件的 Content 内容。在 ControlTemplate 中放置 ContentPresenter,就相当于指定了用户写入 Content="..." 里的内容到底渲染在模板的什么位置。
4.4 控件模板的 4 种设置方式 方式一:直接定义在控件内部(内联定义) 1 2 3 4 5 6 7 8 9 <Button Content ="内联模板" Foreground ="#747787" Height ="40" Width ="280" > <Button.Template > <ControlTemplate TargetType ="Button" > <Border Background ="Transparent" BorderBrush ="#C9CCD5" BorderThickness ="1" CornerRadius ="5" > <ContentPresenter HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Border > </ControlTemplate > </Button.Template > </Button >
方式二:定义在 Window/Page 资源字典中 1 2 3 4 5 6 7 8 9 <Window.Resources > <ControlTemplate TargetType ="Button" x:Key ="ButtonTemplate" > <Border Background ="#C6D2FC" BorderBrush ="#545BAD" BorderThickness ="1" CornerRadius ="5" > <ContentPresenter HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Border > </ControlTemplate > </Window.Resources > <Button Content ="资源模板" Height ="40" Template ="{StaticResource ButtonTemplate}" Width ="280" />
方式三:结合 Style 定义在样式中(推荐) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <Button Content ="Style组合模板" Foreground ="White" Height ="40" Width ="280" > <Button.Style > <Style TargetType ="Button" > <Setter Property ="Template" > <Setter.Value > <ControlTemplate TargetType ="Button" > <Border Background ="#7AAB7D" CornerRadius ="5" > <Grid > <Grid.ColumnDefinitions > <ColumnDefinition Width ="auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions > <TextBlock FontSize ="18" Grid.Column ="0" Margin ="3" Text ="☻" VerticalAlignment ="Center" /> <ContentPresenter Grid.Column ="1" HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Grid > </Border > </ControlTemplate > </Setter.Value > </Setter > </Style > </Button.Style > </Button >
方式四:定义在独立资源字典文件中(模块化/全局复用) 新建 ButtonTemplates.xaml 资源字典:
1 2 3 4 5 6 7 <ResourceDictionary xmlns ="[http://schemas.microsoft.com/winfx/2006/xaml/presentation](http://schemas.microsoft.com/winfx/2006/xaml/presentation)" xmlns:x ="[http://schemas.microsoft.com/winfx/2006/xaml](http://schemas.microsoft.com/winfx/2006/xaml)" > <ControlTemplate TargetType ="Button" x:Key ="RoundButtonTemplate" > <Border Background ="#FF6B6B" BorderBrush ="#C92A2A" BorderThickness ="1" CornerRadius ="20" > <ContentPresenter HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Border > </ControlTemplate > </ResourceDictionary >
在 App.xaml 中合并:
1 2 3 4 5 6 7 <Application.Resources > <ResourceDictionary > <ResourceDictionary.MergedDictionaries > <ResourceDictionary Source ="ButtonTemplates.xaml" /> </ResourceDictionary.MergedDictionaries > </ResourceDictionary > </Application.Resources >
方式对比总结
方式
作用域
复用性
适用场景
1. 定义在控件中
单个控件
不可复用
临时测试、极特殊定制
2. 定义在资源中
当前窗口/容器
窗口内复用
单窗口内多个相同控件
3. 定义在 Style 中
当前窗口/全局
高(样式+模板联动)
规范化 UI 库开发
4. 定义在独立资源字典
全局
最高(跨项目)
大型项目、模块化皮肤系统
五、 ControlTemplate 的触发器 ControlTemplate 内部拥有 Triggers 集合,允许我们在模板内部捕捉交互事件(如鼠标移入、按下)并动态修改可视化树中元素的属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Button Height ="40" Width ="280" > <Button.Template > <ControlTemplate TargetType ="Button" > <Border Background ="#E0E0E0" CornerRadius ="5" x:Name ="border" > <ContentPresenter HorizontalAlignment ="Center" VerticalAlignment ="Center" x:Name ="contentPresenter" /> </Border > <ControlTemplate.Triggers > <Trigger Property ="IsMouseOver" Value ="True" > <Setter Property ="Background" TargetName ="border" Value ="#4A90E2" /> <Setter Property ="TextBlock.Foreground" TargetName ="contentPresenter" Value ="White" /> </Trigger > </ControlTemplate.Triggers > </ControlTemplate > </Button.Template > </Button >
六、 TemplateBinding 模板绑定 TemplateBinding 是 ControlTemplate 内部专用的轻量化绑定方式。它将模板内部元素的属性与宿主控件(TemplatedParent)的依赖属性进行单向关联。
1 2 3 4 5 6 public class TemplateBindingExtension : MarkupExtension { public TemplateBindingExtension (DependencyProperty property ) ; public DependencyProperty Property { get ; set ; } }
示例:绘制一个属性可调的圆形按钮 1 2 3 4 5 6 7 8 9 10 11 12 <Window.Resources > <ControlTemplate TargetType ="Button" x:Key ="CircleButtonTemplate" > <Grid > <Ellipse Fill ="{TemplateBinding Background}" Stroke ="{TemplateBinding BorderBrush}" StrokeThickness ="2" /> <ContentPresenter HorizontalAlignment ="Center" VerticalAlignment ="Center" /> </Grid > </ControlTemplate > </Window.Resources > <Button Background ="LightCoral" BorderBrush ="Red" Content ="圆按钮" Height ="80" Template ="{StaticResource CircleButtonTemplate}" Width ="80" />
七、 DataTemplate 数据模板(数据的外衣) 如果说 ControlTemplate 解决的是“控件长什么样” ,那么 DataTemplate 解决的就是 “数据对象怎么呈现”。
DataTemplate 适用于 ContentControl.ContentTemplate 或 ItemsControl.ItemTemplate。
7.1 DataTemplate 示例 假设有一个 C# 数据模型:
1 2 3 4 5 public class UserInfo { public string Name { get ; set ; } public string Role { get ; set ; } }
在 XAML 中定义 DataTemplate 来决定 UserInfo 对象在 UI 上的长相:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Window.Resources > <DataTemplate x:Key ="UserInfoDataTemplate" > <Border Background ="White" BorderBrush ="#DEE2E6" BorderThickness ="1" CornerRadius ="6" Margin ="4" Padding ="8" > <StackPanel Orientation ="Horizontal" > <Ellipse Fill ="#4ECDC4" Height ="30" Margin ="0,0,10,0" Width ="30" /> <StackPanel VerticalAlignment ="Center" > <TextBlock FontWeight ="Bold" Text ="{Binding Name}" /> <TextBlock FontSize ="11" Foreground ="Gray" Text ="{Binding Role}" /> </StackPanel > </StackPanel > </Border > </DataTemplate > </Window.Resources > <ListBox ItemTemplate ="{StaticResource UserInfoDataTemplate}" ItemsSource ="{Binding UserList}" />
7.2 DataType 隐式数据模板 当不指定 x:Key,而是指定 DataType 时,WPF 会自动在作用域内寻找对应数据类型的渲染模板:
1 2 3 4 5 6 7 8 9 <Window.Resources > <DataTemplate DataType ="{x:Type local:UserInfo}" > <TextBlock Foreground ="DarkBlue" Text ="{Binding Name, StringFormat='用户:{0}'}" /> </DataTemplate > </Window.Resources > <ListBox ItemsSource ="{Binding UserList}" />
八、 ItemsPanelTemplate 元素面板模板(集合的容器) ItemsControl(如 ListBox、ListView)默认以垂直 StackPanel 布局。如果需要改变集合元素的排布规则(如横向排列、网格布局),就需要使用 ItemsPanelTemplate。
1 2 3 4 5 6 7 8 9 <ListBox ItemsSource ="{Binding UserList}" > <ListBox.ItemsPanel > <ItemsPanelTemplate > <WrapPanel IsItemsHost ="True" Orientation ="Horizontal" /> </ItemsPanelTemplate > </ListBox.ItemsPanel > </ListBox >
注意 :根面板中的 IsItemsHost="True" 告诉 WPF:“把集合子项放入该容器中排布”。在 ItemsPanelTemplate 内部此属性默认为 true。
九、 终极融合:ControlTemplate + DataTemplate + ItemsPanelTemplate 在企业级项目中,这三种模板通常结合使用。以下构建一个高度解耦的卡片列表控件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <Window.Resources > <DataTemplate x:Key ="CardItemDataTemplate" > <Border Background ="#F8F9FA" BorderBrush ="#DEE2E6" BorderThickness ="1" CornerRadius ="8" Margin ="5" Padding ="12" > <StackPanel HorizontalAlignment ="Center" > <TextBlock FontSize ="16" FontWeight ="SemiBold" Text ="{Binding Name}" /> <TextBlock FontSize ="12" Foreground ="#6C757D" Margin ="0,4,0,0" Text ="{Binding Role}" /> </StackPanel > </Border > </DataTemplate > <ItemsPanelTemplate x:Key ="GridItemsPanelTemplate" > <UniformGrid Columns ="3" /> </ItemsPanelTemplate > <Style TargetType ="ItemsControl" x:Key ="CustomCardListStyle" > <Setter Property ="ItemsPanel" Value ="{StaticResource GridItemsPanelTemplate}" /> <Setter Property ="ItemTemplate" Value ="{StaticResource CardItemDataTemplate}" /> <Setter Property ="Template" > <Setter.Value > <ControlTemplate TargetType ="ItemsControl" > <Border Background ="White" BorderBrush ="#CED4DA" BorderThickness ="1" CornerRadius ="12" Padding ="16" > <ScrollViewer VerticalScrollBarVisibility ="Auto" > <ItemsPresenter /> </ScrollViewer > </Border > </ControlTemplate > </Setter.Value > </Setter > </Style > </Window.Resources > <Grid Padding ="20" > <ItemsControl ItemsSource ="{Binding UserList}" Style ="{StaticResource CustomCardListStyle}" /> </Grid >
十、 三大模板职责对比图
模板类型
继承基类
核心作用
常见挂载属性
核心占位元素/技术
ControlTemplate
FrameworkTemplate
定义控件的外观与视觉结构 (如按钮变圆、边框交互)
Control.Template
ContentPresenter / ItemsPresenter
DataTemplate
FrameworkTemplate
定义数据对象的渲染方式 (把 C# Class 转为 UI)
ContentTemplate / ItemTemplate
属性数据绑定{Binding}
ItemsPanelTemplate
FrameworkTemplate
定义集合控件中子项的布局容器 (如纵向变网格/横排)
ItemsControl.ItemsPanel
面板属性IsItemsHost="True"