从 WPF 到 Avalonia 的迁移系列实战篇4:控件模板与 TemplatedControl

发布于:2025-09-01 ⋅ 阅读:(22) ⋅ 点赞:(0)

从 WPF 到 Avalonia 的迁移系列实战篇4:控件模板与 TemplatedControl

我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目

在前几篇中,我们聊过了依赖属性、路由事件、资源等迁移相关的基础,这一篇我们进入 自定义控件开发
如果你在 WPF 中做过复杂控件,就一定绕不开 Control + ControlTemplate 的组合。

那么问题来了:

Avalonia 中,我们该如何实现类似的机制?

答案就是 —— TemplatedControl

为了直观演示,我写了一个小 Demo:自定义一个 三角形控件(TriangleControl),让它自动闪烁。我们分别用 WPFAvalonia 来实现,最后对比一下两者的异同。


一、为什么要用 TemplatedControl?

在 UI 框架里,我们有两种常见的自定义控件方式:

  1. UserControl

    • 逻辑 + UI 写在一起
    • 简单场景够用,但换皮肤、换模板比较困难
  2. TemplatedControl(WPF 中就是 Control)

    • 逻辑(C#)和外观(XAML 模板)解耦
    • 控件类只负责属性和逻辑,不关心 UI 长什么样
    • 外观完全交给 ControlTemplate 控制

例如 WPF 的 Button,内部逻辑并不知道它有边框还是圆角,这些都由 ControlTemplate 决定。
Avalonia 完全继承了这一思想,只不过对应的基类换成了 TemplatedControl


二、WPF 版 TriangleControl

在 WPF 中,我们继承 Control,通过 OnApplyTemplate() 获取模板里的元素,然后对它做动画。

控件类

public class TriangleControl : Control
{
    private Path? _path;

    static TriangleControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(TriangleControl),
            new FrameworkPropertyMetadata(typeof(TriangleControl)));
    }

    public static readonly DependencyProperty FillProperty =
        DependencyProperty.Register(nameof(Fill), typeof(Brush),
            typeof(TriangleControl), new PropertyMetadata(Brushes.Black));

    public Brush Fill
    {
        get => (Brush)GetValue(FillProperty);
        set => SetValue(FillProperty, value);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (_path != null)
        {
            // 避免重复应用模板导致多次动画
            _path.ClearValue(UIElement.OpacityProperty);
        }

        _path = GetTemplateChild("PART_Path") as Path;

        if (_path != null)
        {
            var blinkAnimation = new DoubleAnimation
            {
                From = 1.0,
                To = 0.2,
                Duration = TimeSpan.FromSeconds(0.5),
                AutoReverse = true,
                RepeatBehavior = RepeatBehavior.Forever
            };

            _path.BeginAnimation(UIElement.OpacityProperty, blinkAnimation);
        }
    }
}

样式模板

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:controls="clr-namespace:WpfDemo.controls"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Style TargetType="{x:Type controls:TriangleControl}">
        <Setter Property="Width" Value="50" />
        <Setter Property="Height" Value="50" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:TriangleControl}">
                    <Viewbox Stretch="Fill">
                        <Path
                            Data="M 0,1 L 0.5,0 L 1,1 Z"
                            Fill="{TemplateBinding Fill}"
                            Stretch="Uniform"
                            x:Name="PART_Path" />
                    </Viewbox>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

效果:一个三角形控件,透明度循环闪烁。


三、Avalonia 版 TriangleControl

在 Avalonia 中,流程几乎一模一样:

  • 继承 TemplatedControl
  • 属性用 StyledProperty<T>
  • 模板元素通过 e.NameScope.Find<T>() 获取
  • 动画系统换成 Animation + KeyFrame

控件类

public class TriangleControl : TemplatedControl
{
    private Path? _path;

    public static readonly StyledProperty<IBrush> FillProperty =
        AvaloniaProperty.Register<TriangleControl, IBrush>(
            nameof(Fill), Brushes.Black);

    public IBrush Fill
    {
        get => GetValue(FillProperty);
        set => SetValue(FillProperty, value);
    }

    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
    {
        base.OnApplyTemplate(e);

        _path = e.NameScope.Find<Path>("PART_Path");

        if (_path != null)
        {
            var animation = new Animation
            {
                Duration = TimeSpan.FromSeconds(1),
                IterationCount = IterationCount.Infinite,
                Easing = new SineEaseInOut(),
                Children =
                {
                    new KeyFrame
                    {
                        Cue = new Cue(0),
                        Setters = { new Setter(Visual.OpacityProperty, 1.0) }
                    },
                    new KeyFrame
                    {
                        Cue = new Cue(0.5),
                        Setters = { new Setter(Visual.OpacityProperty, 0.2) }
                    },
                    new KeyFrame
                    {
                        Cue = new Cue(1),
                        Setters = { new Setter(Visual.OpacityProperty, 1.0) }
                    }
                }
            };

            animation.RunAsync(_path, CancellationToken.None);
        }
    }
}

样式模板

<Styles
    xmlns="https://github.com/avaloniaui"
    xmlns:controls="clr-namespace:AvaloniaDemo.Controls"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Design.PreviewWith>
        <controls:TriangleControl />
    </Design.PreviewWith>

    <Style Selector="controls|TriangleControl">
        <Setter Property="Width" Value="50" />
        <Setter Property="Height" Value="50" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:TriangleControl">
                    <Viewbox Stretch="Uniform">
                        <Path
                            Data="M 0,1 L 0.5,0 L 1,1 Z"
                            Fill="{TemplateBinding Fill}"
                            Stretch="Uniform"
                            x:Name="PART_Path" />
                    </Viewbox>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</Styles>

效果:与 WPF 一致,三角形控件透明度闪烁。


四、WPF vs Avalonia 对比分析

下面是一个详细对比表格:

功能点 WPF Avalonia
基类 Control TemplatedControl
依赖属性 DependencyProperty StyledProperty<T>
应用模板方法 OnApplyTemplate() OnApplyTemplate(TemplateAppliedEventArgs)
查找模板元素 GetTemplateChild("PART_X") e.NameScope.Find<T>("PART_X")
模板绑定 {TemplateBinding ...} {TemplateBinding ...}(完全一致)
动画系统 Storyboard / DoubleAnimation Animation / KeyFrame

可以看到:

  • 整体思想几乎完全一致,因此从 WPF 迁移过来没有学习门槛
  • Avalonia 在语法上更简洁,比如属性直接用 StyledProperty,不需要 DependencyProperty.Register 那么啰嗦
  • 动画系统的 API 不同,但用法也很直观

五、心得体会

通过这个 Demo,我们发现:

  • WPF 和 Avalonia 在自定义控件的核心思路上保持了高度一致性

  • 只要你熟悉 WPF 的 ControlTemplate 模型,迁移到 Avalonia 基本无缝

  • 不同点主要体现在:

    1. API 命名(DependencyPropertyStyledProperty
    2. 模板元素查找方式
    3. 动画系统

因此,掌握 TemplatedControl = 掌握 Avalonia 自定义控件的核心

✍️ 结语
如果你正打算从 WPF 迁移到 Avalonia,不妨从这种简单的自定义控件开始练手。理解了 TemplatedControl,你就掌握了 Avalonia 的控件扩展基础。

我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目


网站公告

今日签到

点亮在社区的每一天
去签到