Графические приложения на C# с использованием Avalonia UI

Введение

В мире кроссплатформенной разработки под .NET долгое время доминировали Windows-ориентированные технологии WPF и WinForms, которые плохо подходили для создания приложений под Linux и macOS. С появлением Avalonia UI ситуация кардинально изменилась. Avalonia — это кроссплатформенный XAML-фреймворк для создания нативных графических интерфейсов, который работает на Windows, Linux, macOS, iOS, Android и даже в браузерах через WebAssembly. Построенный на принципах, схожих с WPF, но с современной архитектурой, Avalonia позволяет разработчикам C# создавать красивые, производительные приложения с единой кодовой базой для всех платформ, что делает его идеальным выбором для современных desktop- и mobile-приложений.


Основы Avalonia UI: архитектура и первое приложение

1.1. Установка и настройка окружения

1.1.1. Создание первого проекта

// Установка шаблонов Avalonia
// dotnet new install Avalonia.Templates
// Создание нового проекта
// dotnet new avalonia.app -n MyAvaloniaApp -o ./MyAvaloniaApp

// Program.cs - точка входа
public class Program
{
    // Инициализация Avalonia
    [STAThread]
    public static void Main(string[] args) => BuildAvaloniaApp()
        .StartWithClassicDesktopLifetime(args);
    
    // Конфигурация Avalonia приложения
    public static AppBuilder BuildAvaloniaApp()
        => AppBuilder.Configure<App>()
            .UsePlatformDetect() // Автоматическое определение платформы
            .LogToTrace() // Логирование в Trace
            .WithInterFont(); // Использование шрифта Inter
}

// App.axaml.cs - основное приложение
public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this); // Загрузка XAML
    }
    
    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            // Создание главного окна для desktop
            desktop.MainWindow = new MainWindow();
        }
        else if (ApplicationLifetime is ISingleViewApplicationLifetime singleView)
        {
            // Для mobile/браузера
            singleView.MainView = new MainView();
        }
        
        base.OnFrameworkInitializationCompleted();
    }
}

1.1.2. Основное окно и XAML разметка

<!-- MainWindow.axaml -->
<Windowxmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        x:Class="MyAvaloniaApp.MainWindow"
        Title="My Avalonia App"
        Width="800" Height="450"
        WindowStartupLocation="CenterScreen">
    
    <!-- Стили и ресурсы -->
    <Window.Styles>
        <StyleIncludeSource="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
        <StyleIncludeSource="avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"/>
    </Window.Styles>
    
    <DockPanelBackground="#1E1E1E">
        
        <!-- Верхняя панель инструментов -->
        <BorderDockPanel.Dock="Top"
                Height="40"
                Background="#252526"
                BorderBrush="#3E3E40"
                BorderThickness="0 0 0 1">
            
            <StackPanelOrientation="Horizontal"
                        HorizontalAlignment="Left"
                        Margin="10 0">
                
                <ButtonContent="Файл"
                        Background="Transparent"
                        BorderBrush="Transparent"
                        Foreground="White"
                        Margin="0 0 10 0">
                    <Button.ContextMenu>
                        <ContextMenu>
                            <MenuItemHeader="Новый проект"/>
                            <MenuItemHeader="Открыть"/>
                            <Separator/>
                            <MenuItemHeader="Выход"/>
                        </ContextMenu>
                    </Button.ContextMenu>
                </Button>
                
                <ButtonContent="Правка"
                        Background="Transparent"
                        BorderBrush="Transparent"
                        Foreground="White"/>
                
                <ButtonContent="Вид"
                        Background="Transparent"
                        BorderBrush="Transparent"
                        Foreground="White"/>
                
            </StackPanel>
            
        </Border>
        
        <!-- Основной контент -->
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinitionWidth="200"/>
                <ColumnDefinitionWidth="*"/>
                <ColumnDefinitionWidth="300"/>
            </Grid.ColumnDefinitions>
            
            <!-- Левая панель навигации -->
            <BorderGrid.Column="0"
                    Background="#252526"
                    BorderBrush="#3E3E40"
                    BorderThickness="0 0 1 0">
                
                <StackPanelMargin="10">
                    <TextBlockText="Навигация"
                               Foreground="#CCCCCC"
                               FontWeight="Bold"
                               Margin="0 0 0 10"/>
                    
                    <ListBoxx:Name="NavigationList"
                             Background="Transparent"
                             BorderThickness="0"
                             ItemsSource="{Binding NavigationItems}"
                             SelectedItem="{Binding SelectedNavigationItem}">
                        
                        <ListBox.ItemTemplate>
                            <DataTemplate>
                                <StackPanelOrientation="Horizontal"
                                            Margin="5 2">
                                    <PathIconData="{Binding IconPath}"
                                              Width="16"
                                              Height="16"
                                              Margin="0 0 10 0"
                                              Foreground="#569CD6"/>
                                    
                                    <TextBlockText="{Binding Title}"
                                               Foreground="White"
                                               VerticalAlignment="Center"/>
                                </StackPanel>
                            </DataTemplate>
                        </ListBox.ItemTemplate>
                        
                    </ListBox>
                    
                </StackPanel>
                
            </Border>
            
            <!-- Центральная область -->
            <BorderGrid.Column="1"
                    Background="#1E1E1E"
                    Padding="20">
                
                <ContentControlContent="{Binding CurrentView}"
                                ContentTemplate="{Binding ViewTemplateSelector}"/>
                
            </Border>
            
            <!-- Правая панель свойств -->
            <BorderGrid.Column="2"
                    Background="#252526"
                    BorderBrush="#3E3E40"
                    BorderThickness="1 0 0 0">
                
                <ScrollViewer>
                    <StackPanelMargin="15"
                                Spacing="10">
                        
                        <TextBlockText="Свойства"
                                   Foreground="#CCCCCC"
                                   FontWeight="Bold"/>
                        
                        <SeparatorBackground="#3E3E40"/>
                        
                        <StackPanelSpacing="5">
                            <TextBlockText="Имя:"
                                       Foreground="#9CDCFE"/>
                            <TextBoxText="{Binding SelectedItem.Name}"
                                     Watermark="Введите имя"
                                     Background="#3C3C3C"
                                     Foreground="White"
                                     BorderBrush="#3E3E40"/>
                        </StackPanel>
                        
                        <StackPanelSpacing="5">
                            <TextBlockText="Описание:"
                                       Foreground="#9CDCFE"/>
                            <TextBoxText="{Binding SelectedItem.Description}"
                                     AcceptsReturn="True"
                                     TextWrapping="Wrap"
                                     Height="60"
                                     Background="#3C3C3C"
                                     Foreground="White"
                                     BorderBrush="#3E3E40"/>
                        </StackPanel>
                        
                        <StackPanelSpacing="5">
                            <TextBlockText="Тип:"
                                       Foreground="#9CDCFE"/>
                            <ComboBoxItemsSource="{Binding ItemTypes}"
                                      SelectedItem="{Binding SelectedItem.Type}"
                                      Background="#3C3C3C"
                                      Foreground="White"
                                      BorderBrush="#3E3E40"/>
                        </StackPanel>
                        
                        <ButtonContent="Сохранить"
                                HorizontalAlignment="Stretch"
                                Background="#0E639C"
                                Foreground="White"
                                Margin="0 20 0 0"
                                Command="{Binding SaveCommand}"/>
                        
                    </StackPanel>
                </ScrollViewer>
                
            </Border>
            
        </Grid>
        
        <!-- Статус бар -->
        <BorderDockPanel.Dock="Bottom"
                Height="25"
                Background="#007ACC"
                BorderBrush="#3E3E40"
                BorderThickness="0 1 0 0">
            
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinitionWidth="*"/>
                    <ColumnDefinitionWidth="Auto"/>
                </Grid.ColumnDefinitions>
                
                <TextBlockText="{Binding StatusMessage}"
                           Foreground="White"
                           VerticalAlignment="Center"
                           Margin="10 0"/>
                
                <StackPanelGrid.Column="1"
                            Orientation="Horizontal"
                            Margin="0 0 10 0">
                    
                    <TextBlockText="Готово"
                               Foreground="White"
                               VerticalAlignment="Center"
                               Margin="0 0 10 0"/>
                    
                    <ProgressBarWidth="100"
                                 Height="10"
                                 Value="{Binding Progress}"
                                 Maximum="100"
                                 IsIndeterminate="{Binding IsProcessing}"
                                 VerticalAlignment="Center"/>
                    
                </StackPanel>
                
            </Grid>
            
        </Border>
        
    </DockPanel>
    
</Window>

1.2. ViewModel и привязка данных

1.2.1. Реализация ViewModel с ReactiveUI

// Установка: dotnet add package ReactiveUI
//          dotnet add package ReactiveUI.Avalonia

using ReactiveUI;
using System.Reactive.Linq;
using System.Collections.ObjectModel;

public class MainViewModel : ReactiveObject
{
    private string _statusMessage = "Готово";
    private double _progress;
    private bool _isProcessing;
    private NavigationItem _selectedNavigationItem;
    private object _currentView;
    private ProjectItem _selectedItem;
    
    public MainViewModel()
    {
        // Инициализация коллекций
        NavigationItems = new ObservableCollection<NavigationItem>
        {
            new NavigationItem 
            { 
                Title = "Обзор", 
                IconPath = "M12 2L1 21h22L12 2zm0 4l7.53 13H4.47L12 6z", 
                ViewType = typeof(DashboardView) 
            },
            new NavigationItem 
            { 
                Title = "Проекты", 
                IconPath = "M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z", 
                ViewType = typeof(ProjectsView) 
            },
            new NavigationItem 
            { 
                Title = "Настройки", 
                IconPath = "M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z",
                ViewType = typeof(SettingsView) 
            }
        };
        
        ItemTypes = new ObservableCollection<string>
        {
            "Проект", "Документ", "Изображение", "Видео", "Аудио"
        };
        
        // Команды
        SaveCommand = ReactiveCommand.CreateFromTask(SaveItemAsync);
        LoadDataCommand = ReactiveCommand.CreateFromTask(LoadDataAsync);
        
        // Реактивные свойства
        this.WhenAnyValue(x => x.SelectedNavigationItem)
            .Where(x => x != null)
            .Subscribe(item =>
            {
                CurrentView = Activator.CreateInstance(item.ViewType);
                StatusMessage = $"Выбрано: {item.Title}";
            });
        
        this.WhenAnyValue(x => x.SelectedItem)
            .Subscribe(item =>
            {
                if (item != null)
                {
                    StatusMessage = $"Выбран элемент: {item.Name}";
                }
            });
        
        // Загрузка данных при старте
        LoadDataCommand.Execute().Subscribe();
    }
    
    // Свойства
    public ObservableCollection<NavigationItem> NavigationItems { get; }
    public ObservableCollection<string> ItemTypes { get; }
    public ObservableCollection<ProjectItem> ProjectItems { get; } = new();
    
    public string StatusMessage
    {
        get => _statusMessage;
        set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
    }
    
    public double Progress
    {
        get => _progress;
        set => this.RaiseAndSetIfChanged(ref _progress, value);
    }
    
    public bool IsProcessing
    {
        get => _isProcessing;
        set => this.RaiseAndSetIfChanged(ref _isProcessing, value);
    }
    
    public NavigationItem SelectedNavigationItem
    {
        get => _selectedNavigationItem;
        set => this.RaiseAndSetIfChanged(ref _selectedNavigationItem, value);
    }
    
    public object CurrentView
    {
        get => _currentView;
        set => this.RaiseAndSetIfChanged(ref _currentView, value);
    }
    
    public ProjectItem SelectedItem
    {
        get => _selectedItem;
        set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
    }
    
    // Команды
    public ReactiveCommand<Unit, Unit> SaveCommand { get; }
    public ReactiveCommand<Unit, Unit> LoadDataCommand { get; }
    
    // Методы
    private async Task SaveItemAsync()
    {
        IsProcessing = true;
        
        try
        {
            await Task.Delay(1000); // Имитация сохранения
            
            Progress = 100;
            StatusMessage = "Элемент сохранен успешно!";
            
            await Task.Delay(1000);
            Progress = 0;
        }
        finally
        {
            IsProcessing = false;
        }
    }
    
    private async Task LoadDataAsync()
    {
        IsProcessing = true;
        
        try
        {
            // Имитация загрузки данных
            for (int i = 0; i <= 100; i += 10)
            {
                Progress = i;
                await Task.Delay(100);
            }
            
            // Загрузка тестовых данных
            ProjectItems.Clear();
            
            for (int i = 1; i <= 10; i++)
            {
                ProjectItems.Add(new ProjectItem
                {
                    Id = i,
                    Name = $"Проект {i}",
                    Description = $"Описание проекта {i}",
                    Type = "Проект",
                    CreatedDate = DateTime.Now.AddDays(-i),
                    Size = i * 1024 * 1024
                });
            }
            
            StatusMessage = "Данные загружены успешно";
        }
        finally
        {
            IsProcessing = false;
        }
    }
}

// Модели данных
public class NavigationItem
{
    public string Title { get; set; }
    public string IconPath { get; set; }
    public Type ViewType { get; set; }
}

public class ProjectItem : ReactiveObject
{
    private int _id;
    private string _name;
    private string _description;
    private string _type;
    private DateTime _createdDate;
    private long _size;
    
    public int Id
    {
        get => _id;
        set => this.RaiseAndSetIfChanged(ref _id, value);
    }
    
    public string Name
    {
        get => _name;
        set => this.RaiseAndSetIfChanged(ref _name, value);
    }
    
    public string Description
    {
        get => _description;
        set => this.RaiseAndSetIfChanged(ref _description, value);
    }
    
    public string Type
    {
        get => _type;
        set => this.RaiseAndSetIfChanged(ref _type, value);
    }
    
    public DateTime CreatedDate
    {
        get => _createdDate;
        set => this.RaiseAndSetIfChanged(ref _createdDate, value);
    }
    
    public long Size
    {
        get => _size;
        set => this.RaiseAndSetIfChanged(ref _size, value);
    }
    
    [IgnoreDataMember]
    public string FormattedSize => FormatSize(Size);
    
    [IgnoreDataMember]
    public string FormattedDate => CreatedDate.ToString("dd.MM.yyyy HH:mm");
    
    private static string FormatSize(long bytes)
    {
        string[] suffixes = { "B", "KB", "MB", "GB", "TB" };
        int counter = 0;
        decimal number = bytes;
        
        while (Math.Round(number / 1024) >= 1)
        {
            number /= 1024;
            counter++;
        }
        
        return $"{number:n1} {suffixes[counter]}";
    }
}

Сложные UI компоненты и кастомные контролы

2.1. Создание кастомных контролов

2.1.1. Кастомный графический редактор

// CustomCanvas.axaml.cs
public partial class CustomCanvas : UserControl
{
    private Point? _startPoint;
    private List<Shape> _shapes = new();
    private Shape? _currentShape;
    private Pen _pen;
    
    public static readonly StyledProperty<Color> StrokeColorProperty =
        AvaloniaProperty.Register<CustomCanvas, Color>(nameof(StrokeColor), Colors.Black);
    
    public static readonly StyledProperty<double> StrokeThicknessProperty =
        AvaloniaProperty.Register<CustomCanvas, double>(nameof(StrokeThickness), 2);
    
    public static readonly StyledProperty<Color> FillColorProperty =
        AvaloniaProperty.Register<CustomCanvas, Color>(nameof(FillColor), Colors.Transparent);
    
    public static readonly StyledProperty<DrawingMode> ModeProperty =
        AvaloniaProperty.Register<CustomCanvas, DrawingMode>(nameof(Mode), DrawingMode.Select);
    
    public CustomCanvas()
    {
        InitializeComponent();
        
        _pen = new Pen(Brushes.Black, 2);
        
        PointerPressed += OnPointerPressed;
        PointerMoved += OnPointerMoved;
        PointerReleased += OnPointerReleased;
    }
    
    public Color StrokeColor
    {
        get => GetValue(StrokeColorProperty);
        set => SetValue(StrokeColorProperty, value);
    }
    
    public double StrokeThickness
    {
        get => GetValue(StrokeThicknessProperty);
        set => SetValue(StrokeThicknessProperty, value);
    }
    
    public Color FillColor
    {
        get => GetValue(FillColorProperty);
        set => SetValue(FillColorProperty, value);
    }
    
    public DrawingMode Mode
    {
        get => GetValue(ModeProperty);
        set => SetValue(ModeProperty, value);
    }
    
    public IReadOnlyList<Shape> Shapes => _shapes.AsReadOnly();
    
    private void OnPointerPressed(object sender, PointerPressedEventArgs e)
    {
        var point = e.GetPosition(this);
        
        switch (Mode)
        {
            case DrawingMode.Rectangle:
                _startPoint = point;
                _currentShape = new RectangleShape
                {
                    Stroke = new SolidColorBrush(StrokeColor),
                    StrokeThickness = StrokeThickness,
                    Fill = new SolidColorBrush(FillColor),
                    StartPoint = point,
                    EndPoint = point
                };
                break;
                
            case DrawingMode.Ellipse:
                _startPoint = point;
                _currentShape = new EllipseShape
                {
                    Stroke = new SolidColorBrush(StrokeColor),
                    StrokeThickness = StrokeThickness,
                    Fill = new SolidColorBrush(FillColor),
                    StartPoint = point,
                    EndPoint = point
                };
                break;
                
            case DrawingMode.Line:
                _startPoint = point;
                _currentShape = new LineShape
                {
                    Stroke = new SolidColorBrush(StrokeColor),
                    StrokeThickness = StrokeThickness,
                    StartPoint = point,
                    EndPoint = point
                };
                break;
                
            case DrawingMode.Freehand:
                _startPoint = point;
                _currentShape = new PolylineShape
                {
                    Stroke = new SolidColorBrush(StrokeColor),
                    StrokeThickness = StrokeThickness,
                    Points = new List<Point> { point }
                };
                break;
        }
        
        if (_currentShape != null)
        {
            _shapes.Add(_currentShape);
            InvalidateVisual();
        }
    }
    
    private void OnPointerMoved(object sender, PointerEventArgs e)
    {
        if (_startPoint == null || _currentShape == null) return;
        
        var point = e.GetPosition(this);
        
        switch (Mode)
        {
            case DrawingMode.Rectangle:
            case DrawingMode.Ellipse:
            case DrawingMode.Line:
                if (_currentShape is IResizableShape resizable)
                {
                    resizable.EndPoint = point;
                }
                break;
                
            case DrawingMode.Freehand:
                if (_currentShape is PolylineShape polyline)
                {
                    polyline.Points.Add(point);
                }
                break;
        }
        
        InvalidateVisual();
    }
    
    private void OnPointerReleased(object sender, PointerReleasedEventArgs e)
    {
        _startPoint = null;
        _currentShape = null;
    }
    
    public override void Render(DrawingContext context)
    {
        base.Render(context);
        
        // Отрисовка сетки
        DrawGrid(context);
        
        // Отрисовка всех фигур
        foreach (var shape in _shapes)
        {
            shape.Draw(context);
        }
        
        // Отрисовка временной фигуры
        _currentShape?.Draw(context);
    }
    
    private void DrawGrid(DrawingContext context)
    {
        var gridSize = 20;
        var bounds = Bounds;
        
        var pen = new Pen(Brushes.Gray, 0.5);
        
        // Вертикальные линии
        for (double x = 0; x < bounds.Width; x += gridSize)
        {
            context.DrawLine(pen, new Point(x, 0), new Point(x, bounds.Height));
        }
        
        // Горизонтальные линии
        for (double y = 0; y < bounds.Height; y += gridSize)
        {
            context.DrawLine(pen, new Point(0, y), new Point(bounds.Width, y));
        }
    }
    
    public void Clear()
    {
        _shapes.Clear();
        InvalidateVisual();
    }
    
    public void Undo()
    {
        if (_shapes.Count > 0)
        {
            _shapes.RemoveAt(_shapes.Count - 1);
            InvalidateVisual();
        }
    }
    
    public void SaveToFile(string filePath)
    {
        var target = new RenderTargetBitmap(new PixelSize((int)Bounds.Width, (int)Bounds.Height));
        
        using (var context = target.CreateDrawingContext())
        {
            Render(context);
        }
        
        target.Save(filePath);
    }
}

// Базовый класс фигуры
public abstract class Shape
{
    public IBrush? Stroke { get; set; }
    public double StrokeThickness { get; set; }
    public IBrush? Fill { get; set; }
    
    public abstract void Draw(DrawingContext context);
}

// Интерфейс для изменяемых фигур
public interface IResizableShape
{
    Point StartPoint { get; set; }
    Point EndPoint { get; set; }
}

// Конкретные реализации фигур
public class RectangleShape : Shape, IResizableShape
{
    public Point StartPoint { get; set; }
    public Point EndPoint { get; set; }
    
    public override void Draw(DrawingContext context)
    {
        var rect = new Rect(StartPoint, EndPoint);
        
        if (Fill != null)
        {
            context.FillRectangle(Fill, rect);
        }
        
        if (Stroke != null)
        {
            context.DrawRectangle(Stroke, new Pen(Stroke, StrokeThickness), rect);
        }
    }
}

public class EllipseShape : Shape, IResizableShape
{
    public Point StartPoint { get; set; }
    public Point EndPoint { get; set; }
    
    public override void Draw(DrawingContext context)
    {
        var rect = new Rect(StartPoint, EndPoint);
        var center = rect.Center;
        var radiusX = rect.Width / 2;
        var radiusY = rect.Height / 2;
        
        var geometry = new EllipseGeometry(center, radiusX, radiusY);
        
        if (Fill != null)
        {
            context.DrawGeometry(Fill, null, geometry);
        }
        
        if (Stroke != null)
        {
            context.DrawGeometry(null, new Pen(Stroke, StrokeThickness), geometry);
        }
    }
}

public class LineShape : Shape, IResizableShape
{
    public Point StartPoint { get; set; }
    public Point EndPoint { get; set; }
    
    public override void Draw(DrawingContext context)
    {
        if (Stroke != null)
        {
            context.DrawLine(new Pen(Stroke, StrokeThickness), StartPoint, EndPoint);
        }
    }
}

public class PolylineShape : Shape
{
    public List<Point> Points { get; set; } = new();
    
    public override void Draw(DrawingContext context)
    {
        if (Points.Count < 2 || Stroke == null) return;
        
        var pen = new Pen(Stroke, StrokeThickness);
        
        for (int i = 0; i < Points.Count - 1; i++)
        {
            context.DrawLine(pen, Points[i], Points[i + 1]);
        }
    }
}

public enum DrawingMode
{
    Select,
    Rectangle,
    Ellipse,
    Line,
    Freehand
}

2.2. Сложные DataTemplate и стили

2.2.1. DataTemplateSelector для динамического отображения

// DataTemplateSelector для разных типов данных
public class ProjectItemTemplateSelector : IDataTemplate
{
    public IDataTemplate? ProjectTemplate { get; set; }
    public IDataTemplate? DocumentTemplate { get; set; }
    public IDataTemplate? ImageTemplate { get; set; }
    public IDataTemplate? DefaultTemplate { get; set; }
    
    public Control Build(object? param)
    {
        if (param is ProjectItem item)
        {
            return item.Type switch
            {
                "Проект" => ProjectTemplate?.Build(param) ?? DefaultTemplate?.Build(param)!,
                "Документ" => DocumentTemplate?.Build(param) ?? DefaultTemplate?.Build(param)!,
                "Изображение" => ImageTemplate?.Build(param) ?? DefaultTemplate?.Build(param)!,
                _ => DefaultTemplate?.Build(param)!
            };
        }
        
        return new TextBlock { Text = "Неизвестный тип" };
    }
    
    public bool Match(object? data)
    {
        return data is ProjectItem;
    }
}

// Использование в XAML
<DataTemplate x:Key="ProjectTemplate">
    <Border Background="#2D2D30"
            BorderBrush="#3E3E40"
            BorderThickness="1"
            CornerRadius="4"
            Margin="0 0 0 5">
        
        <StackPanel Orientation="Horizontal"
                    Margin="10">
            
            <PathIcon Data="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"
                      Width="24"
                      Height="24"
                      Foreground="#569CD6"
                      Margin="0 0 10 0"/>
            
            <StackPanel>
                <TextBlock Text="{Binding Name}"
                           FontWeight="Bold"
                           Foreground="White"/>
                
                <TextBlock Text="{Binding Description}"
                           Foreground="#CCCCCC"
                           TextWrapping="Wrap"/>
                
                <StackPanel Orientation="Horizontal"
                            Spacing="10"
                            Margin="0 5 0 0">
                    
                    <TextBlock Text="{Binding FormattedDate}"
                               Foreground="#808080"
                               FontSize="11"/>
                    
                    <TextBlock Text="{Binding FormattedSize}"
                               Foreground="#808080"
                               FontSize="11"/>
                    
                </StackPanel>
                
            </StackPanel>
            
        </StackPanel>
        
    </Border>
</DataTemplate>

<DataTemplate x:Key="DocumentTemplate">
    <Border Background="#2D2D30"
            BorderBrush="#3E3E40"
            BorderThickness="1"
            CornerRadius="4"
            Margin="0 0 0 5">
        
        <StackPanel Orientation="Horizontal"
                    Margin="10">
            
            <PathIcon Data="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"
                      Width="24"
                      Height="24"
                      Foreground="#CE9178"
                      Margin="0 0 10 0"/>
            
            <!-- Остальной контент аналогично -->
            
        </StackPanel>
        
    </Border>
</DataTemplate>

Анимации и эффекты

3.1. Использование анимаций в Avalonia

3.1.1. Анимация элементов интерфейса

// Анимация через XAML
<UserControl.Resources>
    <Style Selector="Button.animated">
        <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
        <Setter Property="RenderTransform">
            <TransformGroup>
                <ScaleTransform x:Name="ScaleTransform" ScaleX="1" ScaleY="1"/>
            </TransformGroup>
        </Setter>
        <Style.Animations>
            <Animation Duration="0:0:0.2">
                <KeyFrame Cue="0%">
                    <Setter Property="Opacity" Value="1"/>
                </KeyFrame>
                <KeyFrame Cue="100%">
                    <Setter Property="Opacity" Value="0.7"/>
                </KeyFrame>
            </Animation>
        </Style.Animations>
    </Style>
    
    <Style Selector="Button.animated:pointerover /template/ ContentPresenter">
        <Style.Animations>
            <Animation Duration="0:0:0.1">
                <KeyFrame Cue="0%">
                    <Setter Property="ScaleTransform.ScaleX" Value="1"/>
                    <Setter Property="ScaleTransform.ScaleY" Value="1"/>
                </KeyFrame>
                <KeyFrame Cue="100%">
                    <Setter Property="ScaleTransform.ScaleX" Value="1.05"/>
                    <Setter Property="ScaleTransform.ScaleY" Value="1.05"/>
                </KeyFrame>
            </Animation>
        </Style.Animations>
    </Style>
    
    <Style Selector="Border.glow">
        <Style.Animations>
            <Animation Duration="0:0:1" IterationCount="Infinite">
                <KeyFrame Cue="0%">
                    <Setter Property="BoxShadow" 
                            Value="0 0 5px 2px #569CD6, 0 0 10px 5px rgba(86, 156, 214, 0.5)"/>
                </KeyFrame>
                <KeyFrame Cue="50%">
                    <Setter Property="BoxShadow" 
                            Value="0 0 10px 5px #569CD6, 0 0 20px 10px rgba(86, 156, 214, 0.7)"/>
                </KeyFrame>
                <KeyFrame Cue="100%">
                    <Setter Property="BoxShadow" 
                            Value="0 0 5px 2px #569CD6, 0 0 10px 5px rgba(86, 156, 214, 0.5)"/>
                </KeyFrame>
            </Animation>
        </Style.Animations>
    </Style>
</UserControl.Resources>

// Анимация через код
public class AnimatedPanel : Panel
{
    private readonly Dictionary<Control, AnimationState> _animations = new();
    
    protected override void ChildrenChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        base.ChildrenChanged(sender, e);
        
        if (e.NewItems != null)
        {
            foreach (Control child in e.NewItems)
            {
                AnimateAdd(child);
            }
        }
        
        if (e.OldItems != null)
        {
            foreach (Control child in e.OldItems)
            {
                AnimateRemove(child);
            }
        }
    }
    
    private async void AnimateAdd(Control control)
    {
        control.Opacity = 0;
        control.RenderTransform = new ScaleTransform(0.5, 0.5);
        control.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative);
        
        var animation = new Animation
        {
            Duration = TimeSpan.FromSeconds(0.3),
            Easing = new CubicEaseInOut(),
            Children =
            {
                new KeyFrame
                {
                    Cue = new Cue(0),
                    Setters =
                    {
                        new Setter(OpacityProperty, 0),
                        new Setter(ScaleTransform.ScaleXProperty, 0.5),
                        new Setter(ScaleTransform.ScaleYProperty, 0.5)
                    }
                },
                new KeyFrame
                {
                    Cue = new Cue(1),
                    Setters =
                    {
                        new Setter(OpacityProperty, 1),
                        new Setter(ScaleTransform.ScaleXProperty, 1),
                        new Setter(ScaleTransform.ScaleYProperty, 1)
                    }
                }
            }
        };
        
        await animation.RunAsync(control);
    }
    
    private async void AnimateRemove(Control control)
    {
        var animation = new Animation
        {
            Duration = TimeSpan.FromSeconds(0.2),
            Easing = new CubicEaseIn(),
            Children =
            {
                new KeyFrame
                {
                    Cue = new Cue(0),
                    Setters =
                    {
                        new Setter(OpacityProperty, 1),
                        new Setter(ScaleTransform.ScaleXProperty, 1),
                        new Setter(ScaleTransform.ScaleYProperty, 1)
                    }
                },
                new KeyFrame
                {
                    Cue = new Cue(1),
                    Setters =
                    {
                        new Setter(OpacityProperty, 0),
                        new Setter(ScaleTransform.ScaleXProperty, 0.5),
                        new Setter(ScaleTransform.ScaleYProperty, 0.5)
                    }
                }
            }
        };
        
        await animation.RunAsync(control);
        Children.Remove(control);
    }
}

// Параллакс эффект для прокрутки
public class ParallaxScrollViewer : ScrollViewer
{
    private readonly List<ParallaxElement> _parallaxElements = new();
    
    protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
    {
        base.OnPointerWheelChanged(e);
        
        var delta = e.Delta.Y * 20; // Скорость параллакса
        ApplyParallax(delta);
    }
    
    public void RegisterParallaxElement(Control element, double factor)
    {
        _parallaxElements.Add(new ParallaxElement(element, factor));
    }
    
    private void ApplyParallax(double delta)
    {
        foreach (var element in _parallaxElements)
        {
            var transform = element.Control.RenderTransform as TranslateTransform;
            
            if (transform == null)
            {
                transform = new TranslateTransform();
                element.Control.RenderTransform = transform;
            }
            
            transform.Y += delta * element.Factor;
        }
    }
    
    private class ParallaxElement
    {
        public Control Control { get; }
        public double Factor { get; }
        
        public ParallaxElement(Control control, double factor)
        {
            Control = control;
            Factor = factor;
        }
    }
}

Кроссплатформенные возможности и развертывание

4.1. Работа с файловой системой на разных платформах

4.1.1. Абстракция файловой системы

public interface IFileSystemService
{
    Task<string> PickFolderAsync();
    Task<string[]> PickFilesAsync(string filter = "All files (*.*)|*.*");
    Task<string> SaveFileAsync(string defaultFileName);
    Task<string> ReadTextAsync(string path);
    Task WriteTextAsync(string path, string content);
    Task<Stream> OpenReadAsync(string path);
    Task<Stream> OpenWriteAsync(string path);
    bool FileExists(string path);
    bool DirectoryExists(string path);
    void CreateDirectory(string path);
    IEnumerable<string> GetFiles(string path, string searchPattern = "*.*");
}

public class FileSystemService : IFileSystemService
{
    private readonly Window _parentWindow;
    
    public FileSystemService(Window parentWindow)
    {
        _parentWindow = parentWindow;
    }
    
    public async Task<string> PickFolderAsync()
    {
        var dialog = new OpenFolderDialog
        {
            Title = "Выберите папку",
            Directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
        };
        
        var result = await dialog.ShowAsync(_parentWindow);
        return result;
    }
    
    public async Task<string[]> PickFilesAsync(string filter = "All files (*.*)|*.*")
    {
        var dialog = new OpenFileDialog
        {
            Title = "Выберите файлы",
            AllowMultiple = true,
            Filters = ParseFilter(filter),
            Directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
        };
        
        var result = await dialog.ShowAsync(_parentWindow);
        return result ?? Array.Empty<string>();
    }
    
    public async Task<string> SaveFileAsync(string defaultFileName)
    {
        var dialog = new SaveFileDialog
        {
            Title = "Сохранить файл",
            InitialFileName = defaultFileName,
            DefaultExtension = Path.GetExtension(defaultFileName),
            Directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
        };
        
        var result = await dialog.ShowAsync(_parentWindow);
        return result;
    }
    
    private List<FileDialogFilter> ParseFilter(string filter)
    {
        var filters = new List<FileDialogFilter>();
        var parts = filter.Split('|');
        
        for (int i = 0; i < parts.Length; i += 2)
        {
            if (i + 1 < parts.Length)
            {
                filters.Add(new FileDialogFilter
                {
                    Name = parts[i],
                    Extensions = parts[i + 1]
                        .Split(';')
                        .Select(ext => ext.Trim('*', '.'))
                        .ToList()
                });
            }
        }
        
        return filters;
    }
    
    public async Task<string> ReadTextAsync(string path)
    {
        using var reader = new StreamReader(path);
        return await reader.ReadToEndAsync();
    }
    
    public async Task WriteTextAsync(string path, string content)
    {
        using var writer = new StreamWriter(path);
        await writer.WriteAsync(content);
    }
    
    public async Task<Stream> OpenReadAsync(string path)
    {
        return File.OpenRead(path);
    }
    
    public async Task<Stream> OpenWriteAsync(string path)
    {
        return File.OpenWrite(path);
    }
    
    public bool FileExists(string path) => File.Exists(path);
    public bool DirectoryExists(string path) => Directory.Exists(path);
    
    public void CreateDirectory(string path)
    {
        Directory.CreateDirectory(path);
    }
    
    public IEnumerable<string> GetFiles(string path, string searchPattern = "*.*")
    {
        return Directory.GetFiles(path, searchPattern);
    }
}

4.2. Публикация для разных платформ

4.2.1. Конфигурация публикации

<!-- MyAvaloniaApp.csproj -->
<ProjectSdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
  </PropertyGroup>

  <ItemGroup>
    <AvaloniaResourceInclude="Assets\**"/>
  </ItemGroup>

  <ItemGroup>
    <!-- Основные пакеты Avalonia -->
    <PackageReferenceInclude="Avalonia" Version="11.0.6" />
    <PackageReferenceInclude="Avalonia.Desktop" Version="11.0.6" />
    <PackageReferenceInclude="Avalonia.Themes.Fluent" Version="11.0.6" />
    <PackageReferenceInclude="Avalonia.Fonts.Inter" Version="11.0.6" />
    
    <!-- Пакеты для конкретных платформ -->
    <PackageReferenceInclude="Avalonia.Native" Version="11.0.6" Condition="'$(RuntimeIdentifier)' == 'osx-x64'" />
    <PackageReferenceInclude="Avalonia.LinuxFramebuffer" Version="11.0.6" Condition="'$(RuntimeIdentifier)' == 'linux-x64'" />
    
    <!-- Дополнительные пакеты -->
    <PackageReferenceInclude="Avalonia.ReactiveUI" Version="11.0.6" />
    <PackageReferenceInclude="CommunityToolkit.Mvvm" Version="8.2.1" />
    <PackageReferenceInclude="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
    <PackageReferenceInclude="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageReferenceInclude="Serilog.Extensions.Logging.File" Version="3.0.0" />
  </ItemGroup>
</Project>

4.2.2. Скрипты сборки для разных платформ

#!/bin/bash
# build-all-platforms.sh

# Windows
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/win-x64

# Linux
dotnet publish -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/linux-x64

# macOS
dotnet publish -c Release -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o publish/osx-x64

# Создание установщиков
# Для Windows (используя WiX или Inno Setup)
# Для Linux (deb/rpm пакеты)
# Для macOS (dmg пакет)

echo "Build completed for all platforms!"

4.3. Нативные интеграции

4.3.1. Работа с системными уведомлениями

public interface INotificationService
{
    void ShowNotification(string title, string message, NotificationType type = NotificationType.Info);
    Task<bool> ShowDialogAsync(string title, string message, DialogButtons buttons);
}

public class NotificationService : INotificationService
{
    private readonly Window _mainWindow;
    private readonly ILogger<NotificationService> _logger;
    
    public NotificationService(Window mainWindow, ILogger<NotificationService> logger)
    {
        _mainWindow = mainWindow;
        _logger = logger;
    }
    
    public void ShowNotification(string title, string message, NotificationType type = NotificationType.Info)
    {
        // Использование системных уведомлений
        if (OperatingSystem.IsWindows())
        {
            ShowWindowsNotification(title, message, type);
        }
        else if (OperatingSystem.IsLinux())
        {
            ShowLinuxNotification(title, message, type);
        }
        else if (OperatingSystem.IsMacOS())
        {
            ShowMacOSNotification(title, message, type);
        }
        else
        {
            // Fallback на кастомное уведомление
            ShowCustomNotification(title, message, type);
        }
    }
    
    private void ShowWindowsNotification(string title, string message, NotificationType type)
    {
        try
        {
            // Использование Windows API для уведомлений
            var icon = type switch
            {
                NotificationType.Success => System.Drawing.SystemIcons.Information,
                NotificationType.Warning => System.Drawing.SystemIcons.Warning,
                NotificationType.Error => System.Drawing.SystemIcons.Error,
                _ => System.Drawing.SystemIcons.Information
            };
            
            // Создание всплывающего уведомления
            var notification = new System.Windows.Forms.NotifyIcon
            {
                Icon = icon,
                Visible = true,
                BalloonTipTitle = title,
                BalloonTipText = message,
                BalloonTipIcon = ConvertToWinFormsIcon(type)
            };
            
            notification.ShowBalloonTip(3000);
            
            // Автоматическое скрытие
            Task.Delay(4000).ContinueWith(_ =>
            {
                notification.Dispose();
            });
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to show Windows notification");
            ShowCustomNotification(title, message, type);
        }
    }
    
    private void ShowCustomNotification(string title, string message, NotificationType type)
    {
        // Кастомное уведомление на Avalonia
        var notificationWindow = new NotificationWindow
        {
            Title = title,
            Message = message,
            Type = type
        };
        
        notificationWindow.Show();
        
        // Автоматическое закрытие через 3 секунды
        Task.Delay(3000).ContinueWith(_ =>
        {
            _mainWindow.Dispatcher.InvokeAsync(() =>
            {
                notificationWindow.Close();
            });
        });
    }
    
    public async Task<bool> ShowDialogAsync(string title, string message, DialogButtons buttons)
    {
        var dialog = new MessageBoxWindow
        {
            Title = title,
            Message = message,
            Buttons = buttons
        };
        
        var result = await dialog.ShowDialog<bool>(_mainWindow);
        return result;
    }
}

// Окно для кастомных уведомлений
public class NotificationWindow : Window
{
    public static readonly StyledProperty<string> MessageProperty =
        AvaloniaProperty.Register<NotificationWindow, string>(nameof(Message));
    
    public static readonly StyledProperty<NotificationType> TypeProperty =
        AvaloniaProperty.Register<NotificationWindow, NotificationType>(nameof(Type));
    
    public NotificationWindow()
    {
        InitializeComponent();
        
        // Позиционирование в правом нижнем углу
        PositionWindow();
        
        // Анимация появления
        Opacity = 0;
        this.Animate(OpacityProperty, 0, 1, TimeSpan.FromMilliseconds(300));
    }
    
    private void PositionWindow()
    {
        var screen = Screens.Primary;
        if (screen != null)
        {
            var workingArea = screen.WorkingArea;
            Position = new PixelPoint(
                (int)(workingArea.Right - Width - 20),
                (int)(workingArea.Bottom - Height - 20)
            );
        }
    }
    
    public string Message
    {
        get => GetValue(MessageProperty);
        set => SetValue(MessageProperty, value);
    }
    
    public NotificationType Type
    {
        get => GetValue(TypeProperty);
        set => SetValue(TypeProperty, value);
    }
}

public enum NotificationType
{
    Info,
    Success,
    Warning,
    Error
}

public enum DialogButtons
{
    Ok,
    OkCancel,
    YesNo,
    YesNoCancel
}

Производительность и оптимизация

5.1. Виртуализация списков

5.1.1. Использование ItemsRepeater для больших списков

public class VirtualizedListControl : UserControl
{
    private readonly ItemsRepeater _itemsRepeater;
    private readonly ObservableCollection<DataItem> _items;
    private readonly Func<DataItem, Control> _itemTemplate;
    
    public VirtualizedListControl(IEnumerable<DataItem> items, Func<DataItem, Control> itemTemplate)
    {
        _items = new ObservableCollection<DataItem>(items);
        _itemTemplate = itemTemplate;
        
        InitializeComponent();
    }
    
    private void InitializeComponent()
    {
        _itemsRepeater = new ItemsRepeater
        {
            ItemsSource = _items,
            ItemTemplate = new FuncTemplate<DataItem, Control>(item =>
            {
                var control = _itemTemplate(item);
                
                // Ленивая загрузка изображений
                if (control is IAsyncImageLoader imageLoader)
                {
                    imageLoader.LoadImageAsync(item.ImageUrl);
                }
                
                return control;
            }),
            Layout = new UniformGridLayout
            {
                Orientation = Orientation.Vertical,
                MinRowSpacing = 10,
                MinColumnSpacing = 10,
                ItemsStretch = UniformGridLayoutItemsStretch.Fill
            },
            VirtualizationInfo = new ItemVirtualizationInfo
            {
                VirtualizationMode = ItemVirtualizationMode.Recycling
            }
        };
        
        var scrollViewer = new ScrollViewer
        {
            Content = _itemsRepeater,
            VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
            HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
        };
        
        Content = scrollViewer;
        
        // Оптимизация: загрузка по мере прокрутки
        scrollViewer.ScrollChanged += OnScrollChanged;
    }
    
    private async void OnScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        var offset = e.Offset.Y;
        var extent = e.Extent.Height;
        var viewport = e.Viewport.Height;
        
        // Загрузка дополнительных данных при достижении конца
        if (offset + viewport >= extent - 100) // 100px до конца
        {
            await LoadMoreItemsAsync();
        }
    }
    
    private async Task LoadMoreItemsAsync()
    {
        // Симуляция загрузки данных
        await Task.Delay(500);
        
        var newItems = await DataService.LoadMoreItemsAsync(_items.Count, 50);
        
        foreach (var item in newItems)
        {
            _items.Add(item);
        }
    }
    
    public void FilterItems(Func<DataItem, bool> predicate)
    {
        // Использование CollectionView для фильтрации
        var view = new DataCollectionView(_items);
        view.Filter = item => predicate((DataItem)item);
        
        _itemsRepeater.ItemsSource = view;
    }
    
    public void SortItems<TKey>(Func<DataItem, TKey> keySelector, bool descending = false)
    {
        var sorted = descending 
            ? _items.OrderByDescending(keySelector)
            : _items.OrderBy(keySelector);
        
        _items.Clear();
        
        foreach (var item in sorted)
        {
            _items.Add(item);
        }
    }
}

// Оптимизация отрисовки
public class OptimizedPanel : Panel
{
    private readonly HashSet<Control> _visibleChildren = new();
    
    protected override Size MeasureOverride(Size availableSize)
    {
        var visibleRect = new Rect(0, 0, availableSize.Width, availableSize.Height);
        
        foreach (var child in Children)
        {
            var childRect = new Rect(child.Bounds.Position, child.DesiredSize);
            
            if (visibleRect.Intersects(childRect))
            {
                child.Measure(availableSize);
                _visibleChildren.Add(child);
            }
            else
            {
                child.IsVisible = false;
                _visibleChildren.Remove(child);
            }
        }
        
        return availableSize;
    }
    
    protected override Size ArrangeOverride(Size finalSize)
    {
        foreach (var child in _visibleChildren)
        {
            child.Arrange(child.Bounds);
            child.IsVisible = true;
        }
        
        return finalSize;
    }
}

Заключение

Avalonia UI представляет собой мощный, современный фреймворк для создания кроссплатформенных графических приложений на C#, который успешно сочетает в себе лучшие черты WPF и современные подходы к разработке UI.