WPF-自定义控件(Custom Control)开发通用步骤
通用、可复用的 WPF 自定义控件开发步骤
Step 1:创建控件类(继承 Control)
自定义控件必须继承 Control 或其子类。
public class NumberBox : Control
{
static NumberBox()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(NumberBox),
new FrameworkPropertyMetadata(typeof(NumberBox))
);
}
}必须写静态构造函数(WPF 默认样式会从 Generic.xaml 查找)
Step 2:声明 TemplatePart(可选但推荐)
告诉使用模板的人,它必须提供模板部件:
[TemplatePart(Name = "PART_IncreaseButton", Type = typeof(Button))]用于标记控件模板中的强制性部件
有助于皮肤设计者避免遗漏控件结构
Step 3:声明依赖属性(DependencyProperty)
所有需要被绑定的属性都必须是依赖属性,例如:
public int Value
{
get { return (int)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value",
typeof(int),
typeof(NumberBox),
new PropertyMetadata(0)
);可绑定
可触发样式、动画
可被控件模板访问
Step 4:重写 OnApplyTemplate(获取模板中的元素)(关于是否重写 OnApplyTemplate 在文末叙述)
从 ControlTemplate 中抓控件:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var btn = GetTemplateChild("PART_IncreaseButton") as Button;
if (btn != null)
{
btn.Click -= BtnIncrease_Click;
btn.Click += BtnIncrease_Click;
}
}此处可以注册事件
必须调用 base.OnApplyTemplate()
Step 5:实现业务逻辑
例如按钮点击、数值运算:
private void BtnIncrease_Click(object sender, RoutedEventArgs e)
{
Value++;
}控件类负责功能
样式模板只负责外观
⚠ 非常重要:控件外观不能写在控件类中
所有 UI 都必须由 XAML 的 ControlTemplate 提供。
Step 6:在 Generic.xaml 中定义默认样式(Template)
路径必须为:(非必须,这只是一种约定,具体可以看 “Template 命名约定” 相关内容)
Themes/Generic.xaml示例:
<Style TargetType="{x:Type local:NumberBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NumberBox}">
<Grid>
<!-- 模板内容 -->
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>所有控制模板必须放在 Generic.xaml
WPF 会自动加载此样式作为控件默认皮肤
Step 7:在项目中使用控件
正常使用即可:
<local:NumberBox Value="10" Width="150"/>不需要手动引用 Generic.xaml
WPF自动查找默认样式
Step8 (可选):支持自定义皮肤 / 主题
用户可以自己写一个皮肤:
<Style TargetType="{x:Type local:NumberBox}" BasedOn="{StaticResource {x:Type local:NumberBox}}">
<Setter Property="Background" Value="Black"/>
</Style>遵守 TemplatePart 规则
不破坏功能
总结
WPF 自定义控件开发标准流程
-
创建控件类(继承 Control)
- 添加静态构造函数
- 注册 DefaultStyleKeyProperty
-
声明 TemplatePart(可选)
- 明确控件模板所需的部件
- 便于皮肤重写和维护
-
声明依赖属性(DependencyProperty)
- 所有需要绑定或显示的属性必须是 DP
-
重写 OnApplyTemplate
- 获取模板中的 PART 控件
- 绑定事件(点击、滑动、输入等)
- 清除旧事件避免内存泄漏
-
实现控件功能(业务逻辑)
- 控制数值变化、图标切换等
- 不包含 UI 外观
-
在 Themes/Generic.xaml 中定义默认控件模板
- 使用 ControlTemplate
- 使用 TemplateBinding 绑定控件属性
- 提供默认界面
-
控件直接在 XAML 中使用
- WPF 会自动加载 Generic.xaml 的样式
-
允许用户自定义皮肤(可选)
- 遵循 TemplatePart
- 样式可重写
附:什么时候需要 override OnApplyTemplate?什么时候不需要?
OnApplyTemplate() 的目的只有 一个:
在控件模板(ControlTemplate)应用后,获得模板中的元素(如 PART_Button),并对它们做事情,例如注册事件或运行逻辑。
因此——
是否需要重写它,完全取决于你 是否需要操作模板中的元素。
【需要】 override OnApplyTemplate 的情况
满足下面任意一条,就应当重写:
1. 需要获取模板中的部件(PART_XXX)
例如:
- 一个按钮 PART_IncreaseButton
- 一个 TextBox PART_Editor
- 一个 Slider PART_Slider
你要用它们 → 就必须在 OnApplyTemplate 里获取。
var btn = GetTemplateChild("PART_Button") as Button;典型控件:
- NumberBox(需要按钮)
- ToggleIconButton(需要按钮)
- 日期控件(需要日历部件)
- RichTextEditor(需要工具栏部件)
2. 需要对模板部件注册事件
如果控件逻辑依赖按钮点击、输入变化等:
btn.Click += Btn_Click;如果不重写 OnApplyTemplate,你根本拿不到按钮。
例如:
- 加号 / 减号按钮
- 清除按钮(SearchBox)
- Toggle 状态按钮
3. 你需要对模板部件设置属性
例如控制动画、显示隐藏:
_progressRing.Visibility = Value > 0 ? Visible : Collapsed;4. 你需要替换或操作某个视觉元素
例如:
- 修改模板中 Ellipse 的动画
- 通过控件属性操作内部图标的显示方式
5. 模板部件是可选的,加载成功后才做某些逻辑
例如:
if (_shadow != null)
_shadow.Opacity = 0.5;【不需要】override OnApplyTemplate 的情况
如果满足以下场景,不需要重写:
1. 控件不需要访问模板里的元素
比如控件没有 PART_xxx,也不需要操作模板元素。
例如:
- ProgressRing(逻辑全部在 XAML 动画中)
- 纯视觉控件(只依赖依赖属性 + XAML template)
- 没有事件、没有按钮、没有部件操作
此类控件不需要在后台操作模板。
2. 控件逻辑完全依赖依赖属性和绑定,不依赖代码
例如:
- Text 内容 → 绑定属性
- 背景颜色 → 绑定属性
- Icon → 绑定属性
- Visibility → 使用 XAML 触发器控制
模板策略完全在 XAML 中解决 → 不需要后台操作。
3. 使用简单模板,不含需要单独操作的部件
例如:
<ControlTemplate TargetType="local:SimpleTag">
<Border Background="{TemplateBinding Background}"/>
</ControlTemplate>只用 TemplateBinding → 不需要 C# 介入。
4. 自定义控件只是把属性暴露给模板
例如:
- DisplayMode(Large / Small)
- CornerRadius(用在 TemplateBinding 中)
- Icon(用 ContentPresenter 展示)
只需要依赖属性,不需要 OnApplyTemplate。
实例对比:需要 vs 不需要
| 控件类型 | 是否需要OnApplyTemplate | 原因 |
|---|---|---|
| NumberBox | 需要 | 需要获取PART_IncreaseButtonv并绑定事件 |
| ToggleIconButton | 需要 | 需要监听按钮点击 |
| LabeledTextBox | 可选 | 如果TextBox的双向绑定在C#中实现,需要获取PART_TextBox绑定Text |
| ProgressRing | 不需要 | 动画全部在XAML中实现,不需要获取模板元素 |
| 仅显示型控件(如Tag、Badge) | 不需要 | 无事件逻辑,模板不需要后台操作 |
| 自定义按钮(仅修改样式) | 不需要 | 只是样式变化,没有额外逻辑 |
常见判断原则
| 判断原则 | 是否需要OnApplyTemplate | 说明 |
|---|---|---|
| 需要访问 TemplatePart | 需要 | 通过 GetTemplateChild(“PART_xxx”)获取控件 |
| 需要绑定模板内部事件 | 需要 | 如Button.Click |
| 需要操作模板内部状态 | 需要 | 如改变VisualState 或控制元素 |
| 仅依赖Binding / VisualStateManager | 不需要 | 逻辑完全在XAML |
| 纯样式控件 | 不需要 | 只修改Style / Template |
最终结论
需要 OnApplyTemplate 的情况
- 需要访问模板中的部件(PART_XXX)
- 需要注册事件(按钮、输入、滑动)
- 需要在后台控制模板部件属性
- 模板加载后要执行初始化逻辑
- 控件功能依赖模板中的具体元素
不需要 OnApplyTemplate 的情况
- 控件逻辑全部由依赖属性 + XAML 控制
- 没有要访问的 PART_XXX 部件
- 没有事件处理或部件操作
- 控件只是纯视觉包装或简单属性暴露