Avalonia自定义控件开发:从入门到精通
引言
还在为找不到合适的UI控件而烦恼?Avalonia作为.NET平台的跨平台UI框架,提供了强大的自定义控件开发能力。本文将带你从零开始,深入掌握Avalonia自定义控件的开发技巧,让你能够创建出功能强大、外观精美的自定义控件。
读完本文,你将掌握:
- Avalonia控件体系的核心概念
- 三种自定义控件的开发方式
- 依赖属性(Dependency Property)的完整用法
- 模板化控件的设计与实现
- 控件样式和伪类的应用
- 性能优化和最佳实践
一、Avalonia控件体系概述
1.1 控件继承体系
Avalonia的控件体系采用了层次化的设计,理解这个体系是开发自定义控件的基础:
1.2 控件开发的三条路径
在Avalonia中,开发自定义控件主要有三种方式:
| 开发方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| UserControl | 组合现有控件 | 开发简单,可视化设计 | 性能较低,复用性差 |
| 继承Control | 完全自定义绘制 | 性能最优,完全控制 | 开发复杂,需要手动绘制 |
| 继承TemplatedControl | 可换肤的控件 | 样式分离,高度可定制 | 学习曲线较陡 |
二、基础自定义控件开发
2.1 最简单的自定义控件
让我们从一个简单的自定义控件开始,创建一个显示当前时间的时钟控件:
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using System;
namespace CustomControls
{
public class SimpleClockControl : Control
{
static SimpleClockControl()
{
// 注册影响渲染的属性
AffectsRender<SimpleClockControl>(TimeFormatProperty);
}
public SimpleClockControl()
{
// 创建定时器更新显示
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += (s, e) => InvalidateVisual();
timer.Start();
}
// 定义时间格式属性
public static readonly StyledProperty<string> TimeFormatProperty =
AvaloniaProperty.Register<SimpleClockControl, string>(
nameof(TimeFormat), "HH:mm:ss");
public string TimeFormat
{
get => GetValue(TimeFormatProperty);
set => SetValue(TimeFormatProperty, value);
}
public override void Render(DrawingContext context)
{
base.Render(context);
// 绘制背景
var backgroundBrush = new SolidColorBrush(Colors.LightGray);
var borderBrush = new SolidColorBrush(Colors.DarkGray);
var textBrush = new SolidColorBrush(Colors.Black);
var bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.DrawRectangle(backgroundBrush, new Pen(borderBrush, 2), bounds);
// 绘制时间文本
var currentTime = DateTime.Now.ToString(TimeFormat);
var formattedText = new FormattedText(
currentTime,
System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight,
new Typeface("Arial"),
24,
textBrush);
var textPosition = new Point(
(Bounds.Width - formattedText.Width) / 2,
(Bounds.Height - formattedText.Height) / 2);
context.DrawText(formattedText, textPosition);
}
}
}
2.2 使用XAML定义控件模板
对于更复杂的控件,我们可以使用TemplatedControl并定义XAML模板:
<!-- Styles/ClockControl.xaml -->
<Style xmlns="https://github.com/avaloniaui"
xmlns:local="clr-namespace:CustomControls">
<Style.Resources>
<SolidColorBrush x:Key="ClockBackground">#FF2D2D30</SolidColorBrush>
<SolidColorBrush x:Key="ClockForeground">#FFFFFFFF</SolidColorBrush>
</Style.Resources>
<Style Selector="local|ClockControl">
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="8">
<TextBlock x:Name="PART_TimeText"
Foreground="{TemplateBinding Foreground}"
FontSize="24"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>
对应的C#代码:
[TemplatePart(Name = "PART_TimeText", Type = typeof(TextBlock))]
public class ClockControl : TemplatedControl
{
private TextBlock? _timeText;
private DispatcherTimer? _timer;
static ClockControl()
{
BackgroundProperty.OverrideDefaultValue<ClockControl>(
Brushes.Transparent);
ForegroundProperty.OverrideDefaultValue<ClockControl>(
Brushes.Black);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_timeText = e.NameScope.Find<TextBlock>("PART_TimeText");
// 启动定时器
_timer?.Stop();
_timer = new DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += OnTimerTick;
_timer.Start();
UpdateTime();
}
private void OnTimerTick(object? sender, EventArgs e)
{
UpdateTime();
}
private void UpdateTime()
{
if (_timeText != null)
{
_timeText.Text = DateTime.Now.ToString("HH:mm:ss");
}
}
}
三、高级属性系统
3.1 依赖属性详解
Avalonia的属性系统是其核心特性之一,支持数据绑定、样式设置和动画:
public class AdvancedControl : Control
{
// 1. 简单的样式属性
public static readonly StyledProperty<double> RotationAngleProperty =
AvaloniaProperty.Register<AdvancedControl, double>(
nameof(RotationAngle),
defaultValue: 0.0,
coerce: CoerceRotationAngle);
// 2. 支持数据验证的属性
public static readonly StyledProperty<string> TextContentProperty =
AvaloniaProperty.Register<AdvancedControl, string>(
nameof(TextContent),
defaultValue: string.Empty,
enableDataValidation: true);
// 3. 影响多个方面的属性
public static readonly StyledProperty<IBrush> CustomBrushProperty =
AvaloniaProperty.Register<AdvancedControl, IBrush>(
nameof(CustomBrush),
defaultValue: Brushes.Blue);
static AdvancedControl()
{
// 属性变更时影响渲染
AffectsRender<AdvancedControl>(
RotationAngleProperty,
CustomBrushProperty);
// 属性变更时影响测量
AffectsMeasure<AdvancedControl>(TextContentProperty);
}
public double RotationAngle
{
get => GetValue(RotationAngleProperty);
set => SetValue(RotationAngleProperty, value);
}
public string TextContent
{
get => GetValue(TextContentProperty);
set => SetValue(TextContentProperty, value);
}
public IBrush CustomBrush
{
get => GetValue(CustomBrushProperty);
set => SetValue(CustomBrushProperty, value);
}
private static double CoerceRotationAngle(AvaloniaObject sender, double value)
{
// 确保旋转角度在0-360度之间
value %= 360;
if (value < 0) value += 360;
return value;
}
}
3.2 附加属性(Attached Property)
附加属性允许在一个控件上设置另一个控件定义的属性:
public class LayoutProperties
{
public static readonly AttachedProperty<bool> IsFloatingProperty =
AvaloniaProperty.RegisterAttached<LayoutProperties, Control, bool>(
"IsFloating", defaultValue: false);
public static bool GetIsFloating(Control element)
{
return element.GetValue(IsFloatingProperty);
}
public static void SetIsFloating(Control element, bool value)
{
element.SetValue(IsFloatingProperty, value);
}
static LayoutProperties()
{
IsFloatingProperty.Changed.AddClassHandler<Control>(
(control, args) => OnIsFloatingChanged(control, args));
}
private static void OnIsFloatingChanged(Control control, AvaloniaPropertyChangedEventArgs args)
{
var isFloating = (bool)args.NewValue!;
// 根据属性值更新控件的布局行为
}
}
四、模板化控件开发
4.1 完整的模板化控件示例
下面是一个完整的可定制进度条控件:
[TemplatePart(Name = "PART_Track", Type = typeof(Border))]
[TemplatePart(Name = "PART_Indicator", Type = typeof(Border))]
public class CustomProgressBar : RangeBase
{
private Border? _track;
private Border? _indicator;
static CustomProgressBar()
{
MinimumProperty.OverrideDefaultValue<CustomProgressBar>(0);
MaximumProperty.OverrideDefaultValue<CustomProgressBar>(100);
ValueProperty.OverrideDefaultValue<CustomProgressBar>(0);
}
// 自定义属性:进度条方向
public static readonly StyledProperty<Orientation> OrientationProperty =
AvaloniaProperty.Register<CustomProgressBar, Orientation>(
nameof(Orientation), Orientation.Horizontal);
public Orientation Orientation
{
get => GetValue(OrientationProperty);
set => SetValue(OrientationProperty, value);
}
// 自定义属性:指示器圆角
public static readonly StyledProperty<CornerRadius> IndicatorCornerRadiusProperty =
AvaloniaProperty.Register<CustomProgressBar, CornerRadius>(
nameof(IndicatorCornerRadius));
public CornerRadius IndicatorCornerRadius
{
get => GetValue(IndicatorCornerRadiusProperty);
set => SetValue(IndicatorCornerRadiusProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_track = e.NameScope.Find<Border>("PART_Track");
_indicator = e.NameScope.Find<Border>("PART_Indicator");
UpdateIndicator();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ValueProperty ||
change.Property == MinimumProperty ||
change.Property == MaximumProperty ||
change.Property == OrientationProperty)
{
UpdateIndicator();
}
}
private void UpdateIndicator()
{
if (_indicator == null || _track == null) return;
var min = Minimum;
var max = Maximum;
var val = Value;
// 计算进度百分比
var percentage = max > min ? (val - min) / (max - min) : 0;
percentage = Math.Max(0, Math.Min(1, percentage));
if (Orientation == Orientation.Horizontal)
{
_indicator.Width = _track.Bounds.Width * percentage;
_indicator.Height = double.NaN; // 自动高度
}
else
{
_indicator.Height = _track.Bounds.Height * percentage;
_indicator.Width = double.NaN; // 自动宽度
}
}
}
对应的XAML模板:
<Style xmlns="https://github.com/avaloniaui"
xmlns:local="clr-namespace:CustomControls">
<Style Selector="local|CustomProgressBar">
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="PART_Track"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Border x:Name="PART_Indicator"
Background="{DynamicResource ThemeAccentBrush}"
CornerRadius="{TemplateBinding IndicatorCornerRadius}"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"/>
</Border>
</ControlTemplate>
</Setter>
<Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderLowBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="3"/>
<Setter Property="IndicatorCornerRadius" Value="2"/>
</Style>
<!-- 垂直方向的样式 -->
<Style Selector="local|CustomProgressBar:vertical">
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="PART_Track"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Border x:Name="PART_Indicator"
Background="{DynamicResource ThemeAccentBrush}"
CornerRadius="{TemplateBinding IndicatorCornerRadius}"
HorizontalAlignment="Stretch"
VerticalAlignment="Bottom"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>
五、控件样式和伪类
5.1 使用伪类实现状态切换
伪类是Avalonia中实现控件状态切换的强大工具:
[PseudoClasses(":pressed", ":hover", ":disabled", ":checked")]
public class ToggleButton : Button
{
public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<ToggleButton, bool>(nameof(IsChecked));
public bool IsChecked
{
get => GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
static ToggleButton()
{
IsCheckedProperty.Changed.AddClassHandler<ToggleButton>(
(sender, args) => sender.UpdatePseudoClasses());
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsPressedProperty ||
change.Property == IsPointerOverProperty ||
change.Property == IsEffectivelyEnabledProperty ||
change.Property == IsCheckedProperty)
{
UpdatePseudoClasses();
}
}
private void UpdatePseudoClasses()
{
PseudoClasses.Set(":pressed", IsPressed);
PseudoClasses.Set(":hover", IsPointerOver);
PseudoClasses.Set(":disabled", !IsEffectivelyEnabled);
PseudoClasses.Set(":checked", IsChecked);
}
protected override void OnClick()
{
base.OnClick();
IsChecked = !IsChecked;
}
}
5.2 完整的样式定义
<Style xmlns="https://github.com/avaloniaui"
xmlns:local="clr-namespace:CustomControls">
<Style Selector="local|ToggleButton">
<Setter Property="Background" Value="{DynamicResource ThemeControlBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,4"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
<!-- 状态样式 -->
<Style Selector="local|ToggleButton:pressed">
<Setter Property="Background" Value="{DynamicResource ThemeControlHighBrush}"/>
</Style>
<Style Selector="local|ToggleButton:hover">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
</Style>
<Style Selector="local|ToggleButton:disabled">
<Setter Property="Opacity" Value="0.6"/>
</Style>
<Style Selector="local|ToggleButton:checked">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeAccentForegroundBrush}"/>
</Style>
<Style Selector="local|ToggleButton:checked:pressed">
<Setter Property="Background" Value="{DynamicResource ThemeAccentHighBrush}"/>
</Style>
</Style>
六、性能优化和最佳实践
6.1 性能优化技巧
- 合理使用AffectsXXX方法
static MyControl()
{
// 只有影响渲染的属性
AffectsRender<MyControl>(ColorProperty, SizeProperty);
// 影响布局测量的属性
AffectsMeasure<MyControl>(WidthProperty, HeightProperty);
// 影响排列的属性
AffectsArrange<MyControl>(MarginProperty, PaddingProperty);
}
- 避免频繁的属性更新
// 不好的做法:频繁更新属性
timer.Tick += (s, e) =>
{
RotationAngle += 1; // 每帧都会触发属性变更
};
// 好的做法:批量更新或直接渲染
timer.Tick += (s, e) =>
{
_rotationAngle += 1;
InvalidateVisual(); // 只触发重绘
};
- 使用DirectProperty提升性能
public static readonly DirectProperty<MyControl, int> CounterProperty =
AvaloniaProperty.RegisterDirect<MyControl, int>(
nameof(Counter),
o => o.Counter,
(o, v) => o.Counter = v);
private int _counter;
public int Counter
{
get => _counter;
set => SetAndRaise(CounterProperty, ref _counter, value);
}
6.2 最佳实践总结
| 实践领域 | 推荐做法 | 避免做法 |
|---|---|---|
| 属性设计 | 使用StyledProperty,合理设置默认值 | 使用普通CLR属性 |
| 模板部件 | 使用TemplatePartAttribute声明 | 硬编码查找逻辑 |
| 状态管理 | 使用伪类和样式 | 手动修改视觉属性 |
| 性能优化 | 使用AffectsXXX方法 | 不必要的重绘和布局 |
| 代码组织 | 分离样式和逻辑 | 样式和代码混合 |
七、实战:创建一个完整的自定义控件
让我们创建一个完整的图表控件,展示Avalonia自定义控件的综合应用:
[TemplatePart(Name = "PART_Canvas", Type = typeof(Canvas))]
public class ChartControl : Control
{
private Canvas? _canvas;
private List<DataPoint> _dataPoints = new();
public static readonly StyledProperty<ICollection<DataPoint>> DataPointsProperty =
AvaloniaProperty.Register<ChartControl, ICollection<DataPoint>>(
nameof(DataPoints),
defaultValue: new List<DataPoint>());
public static readonly StyledProperty<IBrush> LineBrushProperty =
AvaloniaProperty.Register<ChartControl, IBrush>(
nameof(LineBrush), Brushes.Blue);
public static readonly StyledProperty<double> LineThicknessProperty =
AvaloniaProperty.Register<ChartControl, double>(
nameof(LineThickness), 2.0);
static ChartControl()
{
AffectsRender<ChartControl>(
DataPointsProperty,
LineBrushProperty,
LineThicknessProperty);
}
public ICollection<DataPoint> DataPoints
{
get => GetValue(DataPointsProperty);
set => SetValue(DataPointsProperty, value);
}
public IBrush LineBrush
{
get => GetValue(LineBrushProperty);
set => SetValue(LineBrushProperty, value);
}
public double LineThickness
{
get => GetValue(LineThicknessProperty);
set => SetValue(LineThicknessProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_canvas = e.NameScope.Find<Canvas>("PART_Canvas");
RedrawChart();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == DataPointsProperty ||
change.Property == LineBrushProperty ||
change.Property == LineThicknessProperty ||
change.Property == BoundsProperty)
{
RedrawChart();
}
}
private void RedrawChart()
{
if (_canvas == null || DataPoints == null || DataPoints.Count < 2)
return;
_canvas.Children.Clear();
var points = NormalizePoints(DataPoints);
var polyline = new Polyline
{
Points = points,
Stroke = LineBrush,
StrokeThickness = LineThickness,
StrokeLineJoin = PenLineJoin.Round
};
_canvas.Children.Add(polyline);
}
private Points NormalizePoints(ICollection<DataPoint> dataPoints)
{
var points = new Points();
var maxY = dataPoints.Max(p => p.Value);
var minY = dataPoints.Min(p => p.Value);
var width = Bounds.Width;
var height = Bounds.Height;
var xStep = width / (dataPoints.Count - 1);
for (int i = 0; i < dataPoints.Count; i++)
{
var point = dataPoints.ElementAt(i);
var normalizedY = height - ((point.Value - minY) / (maxY - minY)) * height;
points.Add(new Point(i * xStep, normalizedY));
}
return points;
}
}
public class DataPoint
{
public DateTime Timestamp { get; set; }
public double Value { get; set; }
}
结语
通过本文的学习,你已经掌握了Avalonia自定义控件开发的核心技术和最佳实践。从简单的控件继承到复杂的模板化控件,从基础属性系统到高级性能优化,这些知识将帮助你在实际项目中创建出高质量的自定义控件。
记住,优秀的自定义控件应该具备以下特点:
- 良好的封装性:隐藏内部实现细节
- 灵活的扩展性:支持样式定制和模板重写
- 优异的性能:合理使用属性系统和渲染优化
- 完整的文档:提供清晰的API说明和使用示例
现在,拿起你的代码编辑器,开始创建属于你自己的Avalonia自定义控件吧!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



