从 WPF 到 Avalonia 的迁移系列实战篇4:控件模板与 TemplatedControl
我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目
在前几篇中,我们聊过了依赖属性、路由事件、资源等迁移相关的基础,这一篇我们进入 自定义控件开发。
如果你在 WPF 中做过复杂控件,就一定绕不开 Control
+ ControlTemplate
的组合。
那么问题来了:
在 Avalonia 中,我们该如何实现类似的机制?
答案就是 —— TemplatedControl
。
为了直观演示,我写了一个小 Demo:自定义一个 三角形控件(TriangleControl),让它自动闪烁。我们分别用 WPF 和 Avalonia 来实现,最后对比一下两者的异同。
一、为什么要用 TemplatedControl?
在 UI 框架里,我们有两种常见的自定义控件方式:
UserControl
- 逻辑 + UI 写在一起
- 简单场景够用,但换皮肤、换模板比较困难
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 基本无缝不同点主要体现在:
- API 命名(
DependencyProperty
→StyledProperty
) - 模板元素查找方式
- 动画系统
- API 命名(
因此,掌握 TemplatedControl = 掌握 Avalonia 自定义控件的核心。
✍️ 结语
如果你正打算从 WPF 迁移到 Avalonia,不妨从这种简单的自定义控件开始练手。理解了 TemplatedControl
,你就掌握了 Avalonia 的控件扩展基础。
我的GitHub仓库Avalonia学习项目包含完整的Avalonia实践案例与代码对比。
我的gitcode仓库是Avalonia学习项目