Avalonia

WPFスタイルのクロスプラットフォーム.NETデスクトップフレームワーク。XAML、データバインディング、MVVMパターンをサポートし、Windows、macOS、Linuxで動作。WPF開発者の学習コストが低く、現代的なUI開発が可能。

デスクトップアプリC#XAMLクロスプラットフォーム.NETオープンソース

GitHub概要

AvaloniaUI/Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology

スター28,163
ウォッチ470
フォーク2,434
作成日:2013年12月5日
言語:C#
ライセンス:MIT License

トピックス

androidapp-frameworkavaloniaavaloniauibrowserc-sharpcross-platformcsharpdesktopdotnetguiioslinuxmacosmobilemvvmwasmwindowsxamarinxaml

スター履歴

AvaloniaUI/Avalonia Star History
データ取得日時: 2025/7/15 23:10

デスクトップフレームワーク

Avalonia

概要

Avaloniaは、C#とXAMLを使用してデスクトップ、組み込み、モバイル、WebAssemblyアプリケーションを開発できるクロスプラットフォームUI フレームワークです。WPFライクなXAML構文を使用しながら、Windows、macOS、Linux、iOS、Android、WebAssemblyで動作するアプリケーションを単一のコードベースで開発できます。.NET最も人気のあるUIクライアント技術として位置づけられています。

詳細

Avaloniaは、WPFの設計思想を受け継いだ現代的なUI フレームワークです。XAMLによる宣言的UI定義、MVVMパターンのサポート、データバインディング、コマンドパターンなど、WPF開発者には馴染みのある機能を提供します。

主な特徴として、真のクロスプラットフォーム対応(Windows、macOS、Linux、モバイル、Web)、WPF互換のXAML、強力なデータバインディング、カスタムコントロール作成、豊富なレイアウトシステム、テーマとスタイリング、Hot Reload対応などがあります。

Telegram Desktop、Wasabi Wallet、GameBuilderなど、多くの商用・オープンソースプロジェクトで採用されています。また、ReactiveUIとの統合により、リアクティブプログラミングパターンも活用できます。

メリット・デメリット

メリット

  • 真のクロスプラットフォーム: 一つのコードで全プラットフォーム対応
  • WPF類似のAPI: WPF開発者の学習コストが低い
  • 高いパフォーマンス: ネイティブレンダリングによる高速描画
  • 豊富なコントロール: 標準コントロールとカスタマイズ性
  • オープンソース: MIT ライセンスで自由に使用可能
  • 活発なコミュニティ: 頻繁なアップデートとサポート

デメリット

  • WPF完全互換ではない: 一部APIや機能に差異がある
  • エコシステム: サードパーティライブラリがWPFより少ない
  • パッケージサイズ: 自己完結型アプリケーションは大きくなりがち
  • 学習リソース: WPFと比較してドキュメントや教材が少ない
  • プラットフォーム固有機能: 各プラットフォーム特有の機能利用に制限
  • 成熟度: WPFと比較すると実績がまだ少ない

参考ページ

書き方の例

Hello World

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="HelloAvalonia.MainWindow"
        Title="Hello Avalonia" Width="400" Height="300">
    <StackPanel Margin="20">
        <TextBlock Text="Hello, Avalonia World!" 
                   FontSize="24" 
                   FontWeight="Bold"
                   HorizontalAlignment="Center"
                   Margin="0,20"/>
        <Button Content="クリックしてください"
                Name="HelloButton"
                HorizontalAlignment="Center"
                Padding="20,10"
                Click="HelloButton_Click"/>
    </StackPanel>
</Window>
using Avalonia.Controls;
using Avalonia.Interactivity;

namespace HelloAvalonia
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void HelloButton_Click(object? sender, RoutedEventArgs e)
        {
            var button = sender as Button;
            if (button != null)
            {
                button.Content = "ありがとうございます!";
            }
        }
    }
}

データバインディングとMVVM

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="AvaloniaApp.MainWindow"
        Title="データバインディング例" Width="450" Height="350">
    <Grid Margin="20">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <TextBox Grid.Row="0" Text="{Binding UserName, Mode=TwoWay}"
                 Watermark="名前を入力してください..." Margin="0,10"/>
        
        <TextBlock Grid.Row="1" Text="{Binding Greeting}" 
                   FontSize="18" FontWeight="Bold" Margin="0,10"/>
        
        <Button Grid.Row="2" Content="挨拶を更新" 
                Command="{Binding UpdateGreetingCommand}"
                HorizontalAlignment="Center" Margin="0,10"/>
        
        <ListBox Grid.Row="3" ItemsSource="{Binding Messages}" Margin="0,10">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding}" Margin="5"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
using Avalonia.Controls;

namespace AvaloniaApp
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private string _userName = "";
        private string _greeting = "こんにちは!";

        public string UserName
        {
            get => _userName;
            set
            {
                _userName = value;
                OnPropertyChanged();
                UpdateGreeting();
            }
        }

        public string Greeting
        {
            get => _greeting;
            set
            {
                _greeting = value;
                OnPropertyChanged();
            }
        }

        public ObservableCollection<string> Messages { get; } = new();

        public ICommand UpdateGreetingCommand => new RelayCommand(UpdateGreeting);

        private void UpdateGreeting()
        {
            var message = string.IsNullOrEmpty(UserName) 
                ? "こんにちは!" 
                : $"こんにちは、{UserName}さん!";
            
            Greeting = message;
            Messages.Add($"{DateTime.Now:HH:mm:ss} - {message}");
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool>? _canExecute;

        public RelayCommand(Action execute, Func<bool>? canExecute = null)
        {
            _execute = execute;
            _canExecute = canExecute;
        }

        public bool CanExecute(object? parameter) => _canExecute?.Invoke() ?? true;

        public void Execute(object? parameter) => _execute();

        public event EventHandler? CanExecuteChanged;
    }
}

レイアウトとスタイリング

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="AvaloniaApp.LayoutWindow"
        Title="レイアウト例" Width="600" Height="400">
    
    <Window.Styles>
        <Style Selector="Button.primary">
            <Setter Property="Background" Value="#007ACC"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="Padding" Value="12,6"/>
            <Setter Property="Margin" Value="5"/>
            <Setter Property="BorderRadius" Value="4"/>
        </Style>
        
        <Style Selector="Button.primary:pointerover">
            <Setter Property="Background" Value="#005A9E"/>
        </Style>

        <Style Selector="TextBlock.header">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontWeight" Value="Bold"/>
            <Setter Property="Margin" Value="0,0,0,10"/>
        </Style>
    </Window.Styles>

    <DockPanel Margin="20">
        <!-- ヘッダー -->
        <TextBlock DockPanel.Dock="Top" Classes="header"
                   Text="Avaloniaレイアウト例" HorizontalAlignment="Center"/>
        
        <!-- サイドバー -->
        <Border DockPanel.Dock="Left" Width="200" Background="#F5F5F5" 
                Padding="10" Margin="0,0,10,0">
            <StackPanel>
                <TextBlock Text="サイドメニュー" FontWeight="Bold" Margin="0,0,0,10"/>
                <Button Classes="primary" Content="ホーム" HorizontalAlignment="Stretch"/>
                <Button Classes="primary" Content="設定" HorizontalAlignment="Stretch"/>
                <Button Classes="primary" Content="ヘルプ" HorizontalAlignment="Stretch"/>
            </StackPanel>
        </Border>
        
        <!-- ステータスバー -->
        <Border DockPanel.Dock="Bottom" Height="30" Background="#E0E0E0" Padding="10,5">
            <TextBlock Text="準備完了" VerticalAlignment="Center"/>
        </Border>
        
        <!-- メインコンテンツ -->
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            
            <TabControl Grid.Row="0">
                <TabItem Header="フォーム">
                    <ScrollViewer Padding="20">
                        <StackPanel Spacing="15">
                            <TextBox Watermark="名前" Width="300" HorizontalAlignment="Left"/>
                            <TextBox Watermark="メールアドレス" Width="300" HorizontalAlignment="Left"/>
                            <ComboBox Width="300" HorizontalAlignment="Left">
                                <ComboBoxItem Content="オプション1"/>
                                <ComboBoxItem Content="オプション2"/>
                                <ComboBoxItem Content="オプション3"/>
                            </ComboBox>
                            <CheckBox Content="ニュースレターを購読する"/>
                            <StackPanel Orientation="Horizontal" Spacing="10">
                                <Button Classes="primary" Content="送信"/>
                                <Button Content="リセット"/>
                            </StackPanel>
                        </StackPanel>
                    </ScrollViewer>
                </TabItem>
                
                <TabItem Header="データ">
                    <DataGrid ItemsSource="{Binding SampleData}" 
                              AutoGenerateColumns="True"
                              GridLinesVisibility="All"
                              IsReadOnly="True"/>
                </TabItem>
            </TabControl>
            
            <GridSplitter Grid.Row="0" Height="5" Background="#CCC" 
                         HorizontalAlignment="Stretch" VerticalAlignment="Bottom"/>
            
            <TextBox Grid.Row="1" Height="100" 
                     Watermark="ログ出力エリア..." 
                     TextWrapping="Wrap" AcceptsReturn="True"/>
        </Grid>
    </DockPanel>
</Window>

カスタムコントロール

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Media;

namespace AvaloniaApp.Controls
{
    public class CustomButton : Button
    {
        public static readonly StyledProperty<IBrush> HoverBackgroundProperty =
            AvaloniaProperty.Register<CustomButton, IBrush>(nameof(HoverBackground));

        public static readonly StyledProperty<double> CornerRadiusProperty =
            AvaloniaProperty.Register<CustomButton, double>(nameof(CornerRadius));

        static CustomButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(CustomButton), 
                new FuncStyleKey<CustomButton>(x => typeof(CustomButton)));
        }

        public IBrush HoverBackground
        {
            get => GetValue(HoverBackgroundProperty);
            set => SetValue(HoverBackgroundProperty, value);
        }

        public double CornerRadius
        {
            get => GetValue(CornerRadiusProperty);
            set => SetValue(CornerRadiusProperty, value);
        }
    }
}
<!-- Styles.axaml -->
<Styles xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="using:AvaloniaApp.Controls">
    
    <Style Selector="controls|CustomButton">
        <Setter Property="Template">
            <ControlTemplate>
                <Border Name="PART_Border"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="{TemplateBinding CornerRadius}"
                        Padding="{TemplateBinding Padding}">
                    <ContentPresenter Content="{TemplateBinding Content}"
                                      HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                      VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
                </Border>
            </ControlTemplate>
        </Setter>
    </Style>
    
    <Style Selector="controls|CustomButton:pointerover /template/ Border#PART_Border">
        <Setter Property="Background" Value="{Binding $parent[controls:CustomButton].HoverBackground}"/>
    </Style>
    
    <Style Selector="controls|CustomButton:pressed /template/ Border#PART_Border">
        <Setter Property="RenderTransform" Value="scale(0.98)"/>
    </Style>
</Styles>

ファイル操作とダイアログ

using Avalonia.Controls;
using Avalonia.Platform.Storage;
using System.IO;

namespace AvaloniaApp
{
    public partial class FileOperationWindow : Window
    {
        public FileOperationWindow()
        {
            InitializeComponent();
        }

        private async void OpenFileButton_Click(object? sender, RoutedEventArgs e)
        {
            var topLevel = GetTopLevel(this);
            if (topLevel == null) return;

            var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
            {
                Title = "ファイルを開く",
                AllowMultiple = false,
                FileTypeFilter = new[]
                {
                    new FilePickerFileType("テキストファイル")
                    {
                        Patterns = new[] { "*.txt", "*.md" }
                    },
                    new FilePickerFileType("すべてのファイル")
                    {
                        Patterns = new[] { "*" }
                    }
                }
            });

            if (files.Count > 0)
            {
                var file = files[0];
                using var stream = await file.OpenReadAsync();
                using var reader = new StreamReader(stream);
                var content = await reader.ReadToEndAsync();
                
                // TextBlockに内容を表示
                if (this.FindControl<TextBlock>("ContentTextBlock") is TextBlock textBlock)
                {
                    textBlock.Text = content;
                }
            }
        }

        private async void SaveFileButton_Click(object? sender, RoutedEventArgs e)
        {
            var topLevel = GetTopLevel(this);
            if (topLevel == null) return;

            var file = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
            {
                Title = "ファイルを保存",
                FileTypeChoices = new[]
                {
                    new FilePickerFileType("テキストファイル")
                    {
                        Patterns = new[] { "*.txt" }
                    }
                }
            });

            if (file != null)
            {
                var content = "Avaloniaから保存されたファイル\n" + 
                             $"保存日時: {DateTime.Now:yyyy/MM/dd HH:mm:ss}";
                
                using var stream = await file.OpenWriteAsync();
                using var writer = new StreamWriter(stream);
                await writer.WriteAsync(content);
            }
        }

        private async void ShowMessageButton_Click(object? sender, RoutedEventArgs e)
        {
            var result = await ShowDialog("確認", "操作を実行しますか?", "はい", "いいえ");
            
            if (result)
            {
                await ShowDialog("結果", "操作が実行されました。", "OK");
            }
        }

        private async Task<bool> ShowDialog(string title, string message, string primaryButton, string? secondaryButton = null)
        {
            var dialog = new Window
            {
                Title = title,
                Width = 400,
                Height = 200,
                WindowStartupLocation = WindowStartupLocation.CenterOwner,
                CanResize = false
            };

            var result = false;
            var panel = new StackPanel { Margin = new Thickness(20), Spacing = 20 };
            
            panel.Children.Add(new TextBlock 
            { 
                Text = message, 
                TextWrapping = TextWrapping.Wrap,
                HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center
            });

            var buttonPanel = new StackPanel 
            { 
                Orientation = Avalonia.Layout.Orientation.Horizontal,
                HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
                Spacing = 10
            };

            var primaryBtn = new Button { Content = primaryButton, Padding = new Thickness(20, 5) };
            primaryBtn.Click += (s, e) => { result = true; dialog.Close(); };
            buttonPanel.Children.Add(primaryBtn);

            if (secondaryButton != null)
            {
                var secondaryBtn = new Button { Content = secondaryButton, Padding = new Thickness(20, 5) };
                secondaryBtn.Click += (s, e) => { result = false; dialog.Close(); };
                buttonPanel.Children.Add(secondaryBtn);
            }

            panel.Children.Add(buttonPanel);
            dialog.Content = panel;

            await dialog.ShowDialog(this);
            return result;
        }
    }
}

アニメーションとトランジション

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        x:Class="AvaloniaApp.AnimationWindow"
        Title="アニメーション例" Width="500" Height="400">
    
    <Window.Styles>
        <Style Selector="Button.animated">
            <Setter Property="Background" Value="#4CAF50"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="Padding" Value="20,10"/>
            <Setter Property="Margin" Value="10"/>
            <Setter Property="Transitions">
                <Transitions>
                    <BrushTransition Property="Background" Duration="0:0:0.3"/>
                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.2"/>
                </Transitions>
            </Setter>
        </Style>
        
        <Style Selector="Button.animated:pointerover">
            <Setter Property="Background" Value="#45a049"/>
            <Setter Property="RenderTransform" Value="scale(1.05)"/>
        </Style>
        
        <Style Selector="Button.animated:pressed">
            <Setter Property="RenderTransform" Value="scale(0.95)"/>
        </Style>
        
        <Style Selector="Border.slide-in">
            <Setter Property="Opacity" Value="0"/>
            <Setter Property="RenderTransform" Value="translateX(-50)"/>
        </Style>
        
        <Style Selector="Border.slide-in.visible">
            <Setter Property="Opacity" Value="1"/>
            <Setter Property="RenderTransform" Value="translateX(0)"/>
            <Setter Property="Transitions">
                <Transitions>
                    <DoubleTransition Property="Opacity" Duration="0:0:0.5"/>
                    <TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.5"/>
                </Transitions>
            </Setter>
        </Style>
    </Window.Styles>

    <StackPanel Margin="20" Spacing="20">
        <TextBlock Text="Avaloniaアニメーション例" 
                   FontSize="24" FontWeight="Bold" 
                   HorizontalAlignment="Center"/>
        
        <Button Classes="animated" Content="ホバーアニメーション" 
                HorizontalAlignment="Center"/>
        
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="10">
            <Button Content="スライドイン表示" Click="ShowSlideIn_Click"/>
            <Button Content="フェードアウト" Click="FadeOut_Click"/>
            <Button Content="回転アニメーション" Click="Rotate_Click"/>
        </StackPanel>
        
        <Border Name="AnimatedBorder" Classes="slide-in"
                Width="300" Height="100" 
                Background="#2196F3" CornerRadius="10"
                HorizontalAlignment="Center">
            <TextBlock Text="アニメーション対象" 
                       Foreground="White" FontSize="16"
                       HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Border>
        
        <Rectangle Name="RotatingRectangle" 
                   Width="50" Height="50" 
                   Fill="#FF5722" 
                   HorizontalAlignment="Center"
                   RenderTransformOrigin="0.5,0.5"/>
    </StackPanel>
</Window>
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Styling;

namespace AvaloniaApp
{
    public partial class AnimationWindow : Window
    {
        public AnimationWindow()
        {
            InitializeComponent();
        }

        private void ShowSlideIn_Click(object? sender, RoutedEventArgs e)
        {
            var border = this.FindControl<Border>("AnimatedBorder");
            if (border != null)
            {
                if (border.Classes.Contains("visible"))
                {
                    border.Classes.Remove("visible");
                }
                else
                {
                    border.Classes.Add("visible");
                }
            }
        }

        private async void FadeOut_Click(object? sender, RoutedEventArgs e)
        {
            var border = this.FindControl<Border>("AnimatedBorder");
            if (border != null)
            {
                var animation = new Animation
                {
                    Duration = TimeSpan.FromSeconds(1),
                    Children =
                    {
                        new KeyFrame
                        {
                            Cue = new Cue(0d),
                            Setters = { new Setter(OpacityProperty, 1d) }
                        },
                        new KeyFrame
                        {
                            Cue = new Cue(1d),
                            Setters = { new Setter(OpacityProperty, 0d) }
                        }
                    }
                };
                
                await animation.RunAsync(border);
                
                // フェードインで戻す
                var fadeInAnimation = new Animation
                {
                    Duration = TimeSpan.FromSeconds(0.5),
                    Children =
                    {
                        new KeyFrame
                        {
                            Cue = new Cue(0d),
                            Setters = { new Setter(OpacityProperty, 0d) }
                        },
                        new KeyFrame
                        {
                            Cue = new Cue(1d),
                            Setters = { new Setter(OpacityProperty, 1d) }
                        }
                    }
                };
                
                await fadeInAnimation.RunAsync(border);
            }
        }

        private async void Rotate_Click(object? sender, RoutedEventArgs e)
        {
            var rectangle = this.FindControl<Rectangle>("RotatingRectangle");
            if (rectangle != null)
            {
                var animation = new Animation
                {
                    Duration = TimeSpan.FromSeconds(2),
                    IterationCount = IterationCount.Infinite,
                    Children =
                    {
                        new KeyFrame
                        {
                            Cue = new Cue(0d),
                            Setters = 
                            { 
                                new Setter(RenderTransformProperty, new RotateTransform(0)) 
                            }
                        },
                        new KeyFrame
                        {
                            Cue = new Cue(1d),
                            Setters = 
                            { 
                                new Setter(RenderTransformProperty, new RotateTransform(360)) 
                            }
                        }
                    }
                };
                
                await animation.RunAsync(rectangle);
            }
        }
    }
}