Day 3 - ControlTemplate
建议用时:210-240 分钟
你将学会什么
- 知道
ControlTemplate是控件内部结构模板。 - 分清
Style和ControlTemplate的边界。 - 会用
TemplateBinding把按钮属性传进模板。 - 会用
ContentPresenter显示按钮内容。 - 会写一个可复用的圆角按钮模板。
能用 Style 解决的问题,不要先写 ControlTemplate。只有需要改变控件内部结构时,才使用 ControlTemplate。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“DataTemplate”,今天继续学习“ControlTemplate”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“ControlTemplate”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。ControlTemplate 的重点不是“改颜色”,而是“改控件内部由哪些元素组成”。
1. Style 和 ControlTemplate 的区别
| 需求 | 用什么 |
|---|---|
| 改按钮背景色 | Style |
| 改按钮文字颜色 | Style |
| 改按钮内边距 | Style |
| 改按钮内部结构 | ControlTemplate |
| 让按钮内部包一层自定义 Border | ControlTemplate |
如果只是改背景、字体、边距,先用 Style。不要为了改一个颜色就重写模板。
2. ControlTemplate 是什么
ControlTemplate 是控件的内部结构。
<ControlTemplate>
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>这表示:这个控件内部由一个 Border 和一个 ContentPresenter 组成。
3. TemplateBinding 是什么
模板内部要使用控件自身的属性,比如 Background、Padding、Content。
Background="{TemplateBinding Background}"意思是:模板里的 Border.Background 使用外层按钮的 Background。
如果按钮样式里写:
<Setter Property="Background" Value="#2563EB" />模板里的 Border 就能拿到这个背景色。
4. ContentPresenter 是什么
按钮的文字来自:
<Button Content="保存" />模板内部如果没有 ContentPresenter,按钮文字可能不显示。
<ContentPresenter Content="{TemplateBinding Content}" />这句话的意思是:把按钮的 Content 显示到模板内部。
5. 为什么模板里不直接写“保存”
错误思路:
<TextBlock Text="保存" />这样模板只能显示固定文字。任何按钮都变成 保存。
正确思路:
<ContentPresenter Content="{TemplateBinding Content}" />这样不同按钮可以显示不同内容。
6. Classes 和模板的关系
本页使用:
<Style Selector="Button.rounded">这表示:只有带 rounded 类的按钮会使用这个模板。
<Button Classes="rounded" Content="保存" />普通按钮不受影响:
<Button Content="默认按钮" />7. ControlTemplate 的风险
重写模板会覆盖控件原来的内部结构。风险是:
- 忘记显示内容。
- 忘记禁用状态。
- 忘记鼠标悬停反馈。
- 模板太复杂后不好维护。
所以一开始先做最小模板,确认能显示,再加状态细节。
ControlTemplate 常用概念速查
ControlTemplate 是改控件内部结构,不是普通布局练习。大多数业务页面先不需要它。
| 需求 | 优先选择 |
|---|---|
| 只是摆放控件 | Grid、StackPanel |
| 只是复用一块界面 | UserControl |
| 只是重复外观 | Style |
| 要改变控件内部结构 | ControlTemplate |
常见写法:
<Style Selector="Button.rounded">
<Setter Property="Template">
<ControlTemplate>
<Border>
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>
</Setter>
</Style>先能读懂即可,不建议一开始大量自定义控件模板。
第二部分:把知识组合成完整例子
今天要做一个自定义按钮模板。按钮仍然是 Button,但内部显示结构由我们重新定义。
Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="300"
Title="ControlTemplate">
<Window.Styles>
<Style Selector="Button.rounded">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="8">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Window.Styles>
<StackPanel Margin="20"
Spacing="10">
<Button Classes="rounded"
Content="保存" />
<Button Content="默认按钮" />
</StackPanel>
</Window>先用一句话理解
Style 选中带 rounded 类的按钮,Template 重写按钮内部结构,ContentPresenter 负责把 Button.Content 显示出来。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以完整替换 Views/MainWindow.axaml。
例子 1:只用 Style 改外观
先确认:如果只是改背景和文字颜色,不需要模板。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="480"
Height="260"
Title="只用 Style">
<Window.Styles>
<Style Selector="Button.primary">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
</Style>
</Window.Styles>
<StackPanel Margin="20">
<Button Classes="primary"
Content="保存" />
</StackPanel>
</Window>例子 2:加入 ControlTemplate
现在改按钮内部结构。
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="300"
Title="ControlTemplate">
<Window.Styles>
<Style Selector="Button.rounded">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="8">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Window.Styles>
<StackPanel Margin="20"
Spacing="10">
<Button Classes="rounded"
Content="保存" />
<Button Classes="rounded"
Content="提交" />
</StackPanel>
</Window>两个按钮使用同一个模板,但显示不同内容,因为内容来自 ContentPresenter。
例子 3:普通按钮和模板按钮对比
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="320"
Title="模板对比">
<Window.Styles>
<Style Selector="Button.rounded">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="8">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Window.Styles>
<StackPanel Margin="20"
Spacing="10">
<Button Classes="rounded"
Content="模板按钮" />
<Button Content="默认按钮" />
</StackPanel>
</Window>这个例子说明:模板只影响带 rounded 类的按钮。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 为了小调整就改 ControlTemplate | 成本高,容易破坏控件行为 | 普通外观先用 Style |
| 模板丢掉必要内容 | 控件无法显示内容或状态 | 保留内容展示和状态相关部分 |
| 控件状态没处理 | 鼠标悬停、禁用看不出来 | 模板里考虑常见状态 |
| 样式和模板职责混乱 | 难维护 | Style 改属性,Template 改结构 |
| 没有最小例子验证 | 一改就不知道哪里错 | 先做单个 Button 模板再扩展 |
小白重复敲写训练
ControlTemplate 先改变按钮外观,内容仍使用 ContentPresenter。
训练 1:最小按钮模板
<ControlTemplate x:Key="SimpleButtonTemplate">
<Border Background="#2563EB" Padding="14,8" CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate><Button Content="保存" Template="{StaticResource SimpleButtonTemplate}" />训练 2:让内容颜色可见
<Button Content="保存"
Foreground="White"
Template="{StaticResource SimpleButtonTemplate}" />改动任务:换成“删除”并使用红色背景。
训练 3:模板与数据模板对比
分别手写一句:
DataTemplate:决定一条数据怎么显示
ControlTemplate:决定一个控件本身怎么组成第三遍各写一个最小 XAML 例子,不看答案。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:ControlTemplate。今天至少要掌握的 3 个点是:
1. 知道 `ControlTemplate` 是控件内部结构模板。
2. 分清 `Style` 和 `ControlTemplate` 的边界。
3. 会用 `TemplateBinding` 把按钮属性传进模板。答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:知道 ControlTemplate 是控件内部结构模板。。
上位机专项练习
ControlTemplate 改变控件结构,适合统一状态灯或特殊操作按钮;普通外观优先用 Style。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:状态灯模板
<ControlTemplate x:Key="StatusLampTemplate">
<Border Width="18" Height="18" CornerRadius="9"
Background="{TemplateBinding Background}" />
</ControlTemplate>
<Control Template="{StaticResource StatusLampTemplate}" Background="Green" />运行结果或界面效果:
显示圆形绿色状态灯改动任务: 再放一个灰色离线灯。
专项例子 2:带图标的操作按钮
<Button Content="连接">
<Button.Template>
<ControlTemplate>
<Border Background="#1565C0" Padding="14,8" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>运行结果或界面效果:
按钮拥有统一圆角结构改动任务: 增加 pointerover 样式。
专项例子 3:模板绑定内容
<ControlTemplate x:Key="ActionButtonTemplate">
<Border Background="{TemplateBinding Background}" Padding="12">
<ContentPresenter Content="{TemplateBinding Content}" />
</Border>
</ControlTemplate>运行结果或界面效果:
模板仍能接收按钮 Content 和 Background改动任务: 创建“停止采集”红色按钮复用模板。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先照第三部分敲一遍,再用这里检查结果。
作业要求
做一个圆角按钮模板:
- 样式选择器是
Button.rounded。 - 样式设置
Background、Foreground、Padding。 - 模板内部使用
Border。 Border.Background使用{TemplateBinding Background}。Border.Padding使用{TemplateBinding Padding}。- 模板内部必须有
ContentPresenter。 - 页面上至少放一个模板按钮和一个默认按钮做对比。
Views/MainWindow.axaml 完整答案
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ProductApp.Views.MainWindow"
Width="520"
Height="320"
Title="ControlTemplate">
<Window.Styles>
<Style Selector="Button.rounded">
<Setter Property="Background" Value="#2563EB" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="8">
<ContentPresenter Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Window.Styles>
<StackPanel Margin="20"
Spacing="10">
<Button Classes="rounded"
Content="保存" />
<Button Classes="rounded"
Content="提交" />
<Button Content="默认按钮" />
</StackPanel>
</Window>必须验证的结果
| 控件 | 应该看到 |
|---|---|
| 保存 | 使用圆角模板 |
| 提交 | 使用同一个圆角模板,但文字不同 |
| 默认按钮 | 不使用圆角模板 |
如果模板不生效,按这个顺序检查
- 检查样式选择器是不是
Button.rounded。 - 检查按钮是否写了
Classes="rounded"。 - 检查模板是否写在
Setter Property="Template"里面。 - 检查
ContentPresenter是否存在。 - 检查
ContentPresenter是否绑定了{TemplateBinding Content}。
今天真正要掌握的闭环
Style 选中 Button.rounded
-> Setter 设置 Template
-> ControlTemplate 定义按钮内部结构
-> TemplateBinding 读取按钮自身属性
-> ContentPresenter 显示 Button.ContentControlTemplate 是更强的工具。先会用 Style,再谨慎使用 ControlTemplate。