Avalonia自定义控件开发:从入门到精通

Avalonia自定义控件开发:从入门到精通

【免费下载链接】Avalonia AvaloniaUI/Avalonia: 是一个用于 .NET 平台的跨平台 UI 框架,支持 Windows、macOS 和 Linux。适合对 .NET 开发、跨平台开发以及想要使用现代的 UI 框架的开发者。 【免费下载链接】Avalonia 项目地址: https://gitcode.com/GitHub_Trending/ava/Avalonia

引言

还在为找不到合适的UI控件而烦恼?Avalonia作为.NET平台的跨平台UI框架,提供了强大的自定义控件开发能力。本文将带你从零开始,深入掌握Avalonia自定义控件的开发技巧,让你能够创建出功能强大、外观精美的自定义控件。

读完本文,你将掌握:

  • Avalonia控件体系的核心概念
  • 三种自定义控件的开发方式
  • 依赖属性(Dependency Property)的完整用法
  • 模板化控件的设计与实现
  • 控件样式和伪类的应用
  • 性能优化和最佳实践

一、Avalonia控件体系概述

1.1 控件继承体系

Avalonia的控件体系采用了层次化的设计,理解这个体系是开发自定义控件的基础:

mermaid

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 性能优化技巧

  1. 合理使用AffectsXXX方法
static MyControl()
{
    // 只有影响渲染的属性
    AffectsRender<MyControl>(ColorProperty, SizeProperty);
    
    // 影响布局测量的属性
    AffectsMeasure<MyControl>(WidthProperty, HeightProperty);
    
    // 影响排列的属性
    AffectsArrange<MyControl>(MarginProperty, PaddingProperty);
}
  1. 避免频繁的属性更新
// 不好的做法:频繁更新属性
timer.Tick += (s, e) => 
{
    RotationAngle += 1; // 每帧都会触发属性变更
};

// 好的做法:批量更新或直接渲染
timer.Tick += (s, e) => 
{
    _rotationAngle += 1;
    InvalidateVisual(); // 只触发重绘
};
  1. 使用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自定义控件吧!

【免费下载链接】Avalonia AvaloniaUI/Avalonia: 是一个用于 .NET 平台的跨平台 UI 框架,支持 Windows、macOS 和 Linux。适合对 .NET 开发、跨平台开发以及想要使用现代的 UI 框架的开发者。 【免费下载链接】Avalonia 项目地址: https://gitcode.com/GitHub_Trending/ava/Avalonia

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值