目录

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 自定义控件开发标准流程

  1. 创建控件类(继承 Control)

    • 添加静态构造函数
    • 注册 DefaultStyleKeyProperty
  2. 声明 TemplatePart(可选)

    • 明确控件模板所需的部件
    • 便于皮肤重写和维护
  3. 声明依赖属性(DependencyProperty)

    • 所有需要绑定或显示的属性必须是 DP
  4. 重写 OnApplyTemplate

    • 获取模板中的 PART 控件
    • 绑定事件(点击、滑动、输入等)
    • 清除旧事件避免内存泄漏
  5. 实现控件功能(业务逻辑)

    • 控制数值变化、图标切换等
    • 不包含 UI 外观
  6. 在 Themes/Generic.xaml 中定义默认控件模板

    • 使用 ControlTemplate
    • 使用 TemplateBinding 绑定控件属性
    • 提供默认界面
  7. 控件直接在 XAML 中使用

    • WPF 会自动加载 Generic.xaml 的样式
  8. 允许用户自定义皮肤(可选)

    • 遵循 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 的情况

  1. 需要访问模板中的部件(PART_XXX)
  2. 需要注册事件(按钮、输入、滑动)
  3. 需要在后台控制模板部件属性
  4. 模板加载后要执行初始化逻辑
  5. 控件功能依赖模板中的具体元素

不需要 OnApplyTemplate 的情况

  1. 控件逻辑全部由依赖属性 + XAML 控制
  2. 没有要访问的 PART_XXX 部件
  3. 没有事件处理或部件操作
  4. 控件只是纯视觉包装或简单属性暴露