Avalonia
WPFスタイルのクロスプラットフォーム.NETデスクトップフレームワーク。XAML、データバインディング、MVVMパターンをサポートし、Windows、macOS、Linuxで動作。WPF開発者の学習コストが低く、現代的なUI開発が可能。
GitHub概要
AvaloniaUI/Avalonia
Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
トピックス
スター履歴
デスクトップフレームワーク
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);
}
}
}
}