Avalonia UI: Enhance Your App with FluentAvalonia Theme Customization

Faruk Akyapak
9 min readMay 26, 2024

--

In my newest Medium article, I’ll be diving into the world of theme customization with Avalonia UI using the FluentAvalonia package. Join me as we explore how you can personalize the look and feel of your application and save these customizations for future use. From choosing the right theme to implementing it seamlessly, we’ll uncover the secrets to creating visually stunning and consistent user experiences.

Let’s Start

Install FluentAvalonia Package

First, open the Avalonia Guide App project we created in my previous article. Right-click on your project, open the NuGet Package Manager, and install the package shown in the image below.

https://github.com/amwx/FluentAvalonia

If you wish, you can review the package’s GitHub repository via the link provided below the image. I will also share the package documentation below for a more detailed examination.

https://amwx.github.io/FluentAvaloniaDocs/

App.axaml Implementation

After downloading the package, we will remove the default Avalonia theme from App.axaml and add FluentAvalonia as the theme.

Note: It is not recommended to use both themes simultaneously. According to the documentation, using both themes together may cause some design issues. Therefore, we will use only one theme to avoid any potential problems.

<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AvaloniaGuideApp.App"
xmlns:local="using:AvaloniaGuideApp"
xmlns:sty="using:FluentAvalonia.Styling">

<!--
RequestedThemeVariant="Light"
"Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options.
-->

<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>

<Application.Styles>
<sty:FluentAvaloniaTheme />
</Application.Styles>
</Application>

Let’s Create a Window for Theme Settings

First, we will create a window named ThemeSettings. Then, we’ll create a ViewModel to manage the states within this window. After that, we’ll design our window.

ViewModel Implementation

Before designing our ThemeSettings window, we need to code our ViewModel class to manage the states and variables used within it. At this stage, we will first modify the ViewModelBase class that our ViewModel inherits. By implementing the INotifyPropertyChanged interface in this class, we will ensure that events occurring during theme changes are recorded in a .json file and that the application opens according to the last saved theme settings on every startup.

using Avalonia.Platform;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System;

namespace AvaloniaGuideApp.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected string GetAssemblyResource(string name)
{
using (var stream = AssetLoader.Open(new Uri(name)))
using (StreamReader reader = new StreamReader(stream))
{
return reader.ReadToEnd();
}
}

protected bool RaiseAndSetIfChanged<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
return false;
}

protected void RaisePropertyChanged(string propName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
}
}

Now, add a JSON resource to your project as shown in the image below.

{
"tsAppTheme": "Dark",
"tsFlowDirection": 0,
"tsUseCustomAccent": true,
"tsCustomAccentColor": {
"r": 180,
"g": 13,
"b": 209,
"a": 255
}
}

Now, we can code the ThemeSettingsViewModel. This class will include the theme that the application will use, the system accent color within the application, and the text flow direction. It will allow modifications to be made on these properties and ensure that these changes are saved.

using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using FluentAvalonia.Styling;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
using Avalonia;
using System.IO;

namespace AvaloniaGuideApp.ViewModels
{
public class ThemeSettingsWindowViewModel : ViewModelBase
{
private bool _useCustomAccentColor;
private Color _customAccentColor;
private string _currentAppTheme;
private FlowDirection _currentFlowDirection;
private Color? _listBoxColor;
private bool _ignoreSetListBoxColor = false;

private const string _fileName = "themesettings.json";
private const string _system = "System";
private const string _dark = "Dark";
private const string _light = "Light";
private readonly FluentAvaloniaTheme _faTheme;

public List<Color> PredefinedColors { get; private set; }
public class TSCustomAccentColorModel
{
public byte A { get; set; }
public byte R { get; set; }
public byte B { get; set; }
public byte G { get; set; }

}

public class ThemeSettings
{
public string TSAppTheme { get; set; }
public FlowDirection TSFlowDirection { get; set; }
public bool TSUseCustomAccent { get; set; }
public TSCustomAccentColorModel TSCustomAccentColor { get; set; }
}
public ThemeSettingsWindowViewModel()
{
GetPredefColors();
_faTheme = Application.Current.Styles[0] as FluentAvaloniaTheme;
LoadSettings();
}

public string[] AppThemes { get; } =
new[] { _system, _light, _dark };

public FlowDirection[] AppFlowDirections { get; } =
new[] { FlowDirection.LeftToRight, FlowDirection.RightToLeft };

private void SetDefaultSettings()
{
CurrentAppTheme = _dark;
CurrentFlowDirection = FlowDirection.LeftToRight;
UseCustomAccent = true;
CustomAccentColor = Color.FromArgb(255, 180, 13, 209);
}
public void LoadSettings()
{
try
{
var options = new JsonSerializerOptions();
string directory = AppDomain.CurrentDomain.BaseDirectory;
string path = Path.Combine(directory, _fileName);

if (!File.Exists(path))
{
SetDefaultSettings();
return;
}

string jsonString = File.ReadAllText(path);
var settings = JsonSerializer.Deserialize<ThemeSettings>(jsonString, options);

if (settings != null)
{
CurrentAppTheme = settings.TSAppTheme;
CurrentFlowDirection = settings.TSFlowDirection;
UseCustomAccent = settings.TSUseCustomAccent;
CustomAccentColor = Color.FromArgb(
settings.TSCustomAccentColor.A,
settings.TSCustomAccentColor.R,
settings.TSCustomAccentColor.G,
settings.TSCustomAccentColor.B);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}

public void SaveSettings()
{
try
{
var settings = new ThemeSettings
{
TSAppTheme = CurrentAppTheme,
TSFlowDirection = CurrentFlowDirection,
TSUseCustomAccent = UseCustomAccent,
TSCustomAccentColor = new TSCustomAccentColorModel
{
A = CustomAccentColor.A,
R = CustomAccentColor.R,
G = CustomAccentColor.G,
B = CustomAccentColor.B
}
};

var options = new JsonSerializerOptions();
string jsonString = JsonSerializer.Serialize(settings, options);
string directory = AppDomain.CurrentDomain.BaseDirectory;
string path = Path.Combine(directory, _fileName);
File.WriteAllText(path, jsonString, Encoding.UTF8);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
}
}
public string CurrentAppTheme
{
get => _currentAppTheme;
set
{
if (RaiseAndSetIfChanged(ref _currentAppTheme, value))
{
var newTheme = GetThemeVariant(value);
if (newTheme != null)
{
Application.Current.RequestedThemeVariant = newTheme;
}
if (value != _system)
{
_faTheme.PreferSystemTheme = false;
}
else
{
_faTheme.PreferSystemTheme = true;
}
}
}
}

private ThemeVariant GetThemeVariant(string value)
{
switch (value)
{
case _light:
return ThemeVariant.Light;
case _dark:
return ThemeVariant.Dark;
case _system:
default:
return null;
}
}

public FlowDirection CurrentFlowDirection
{
get => _currentFlowDirection;
set
{
if (RaiseAndSetIfChanged(ref _currentFlowDirection, value))
{
var lifetime = Application.Current.ApplicationLifetime;
if (lifetime is IClassicDesktopStyleApplicationLifetime cdl)
{
if (cdl.MainWindow.FlowDirection == value)
return;
cdl.MainWindow.FlowDirection = value;
}
else if (lifetime is ISingleViewApplicationLifetime single)
{
var mainWindow = TopLevel.GetTopLevel(single.MainView);
if (mainWindow.FlowDirection == value)
return;
mainWindow.FlowDirection = value;
}
}
}
}

public bool UseCustomAccent
{
get => _useCustomAccentColor;
set
{
if (RaiseAndSetIfChanged(ref _useCustomAccentColor, value))
{
if (value)
{
if (_faTheme.TryGetResource("SystemAccentColor", null, out var curColor))
{
_customAccentColor = (Color)curColor;
_listBoxColor = _customAccentColor;

RaisePropertyChanged(nameof(CustomAccentColor));
RaisePropertyChanged(nameof(ListBoxColor));
}
else
{
throw new Exception("Unable to retreive SystemAccentColor");
}
}
else
{
_customAccentColor = default;
_listBoxColor = default;
RaisePropertyChanged(nameof(CustomAccentColor));
RaisePropertyChanged(nameof(ListBoxColor));
UpdateAppAccentColor(null);
}
}
}
}

public Color? ListBoxColor
{
get => _listBoxColor;
set
{
if (value != null)
{
RaiseAndSetIfChanged(ref _listBoxColor, (Color)value);
_customAccentColor = value.Value;
RaisePropertyChanged(nameof(CustomAccentColor));
UpdateAppAccentColor(value.Value);
}
else
{
_listBoxColor = null;
RaisePropertyChanged(nameof(CustomAccentColor));
UpdateAppAccentColor(null);
}
}
}

public Color CustomAccentColor
{
get => _customAccentColor;
set
{
if (RaiseAndSetIfChanged(ref _customAccentColor, value))
{
_listBoxColor = value;
RaisePropertyChanged(nameof(ListBoxColor));
UpdateAppAccentColor(value);
}
}
}

public string CurrentVersion =>
typeof(Program).Assembly.GetName().Version?.ToString();

public string CurrentAvaloniaVersion =>
typeof(Application).Assembly.GetName().Version?.ToString();

private void GetPredefColors()
{
PredefinedColors = new List<Color>
{
Color.FromRgb(255,185,0),
Color.FromRgb(255,140,0),
Color.FromRgb(247,99,12),
Color.FromRgb(202,80,16),
Color.FromRgb(218,59,1),
Color.FromRgb(239,105,80),
Color.FromRgb(209,52,56),
Color.FromRgb(255,67,67),
Color.FromRgb(231,72,86),
Color.FromRgb(232,17,35),
Color.FromRgb(234,0,94),
Color.FromRgb(195,0,82),
Color.FromRgb(227,0,140),
Color.FromRgb(191,0,119),
Color.FromRgb(194,57,179),
Color.FromRgb(154,0,137),
Color.FromRgb(0,120,212),
Color.FromRgb(0,99,177),
Color.FromRgb(142,140,216),
Color.FromRgb(107,105,214),
Color.FromRgb(135,100,184),
Color.FromRgb(116,77,169),
Color.FromRgb(177,70,194),
Color.FromRgb(136,23,152),
Color.FromRgb(0,153,188),
Color.FromRgb(45,125,154),
Color.FromRgb(0,183,195),
Color.FromRgb(3,131,135),
Color.FromRgb(0,178,148),
Color.FromRgb(1,133,116),
Color.FromRgb(0,204,106),
Color.FromRgb(16,137,62),
Color.FromRgb(122,117,116),
Color.FromRgb(93,90,88),
Color.FromRgb(104,118,138),
Color.FromRgb(81,92,107),
Color.FromRgb(86,124,115),
Color.FromRgb(72,104,96),
Color.FromRgb(73,130,5),
Color.FromRgb(16,124,16),
Color.FromRgb(118,118,118),
Color.FromRgb(76,74,72),
Color.FromRgb(105,121,126),
Color.FromRgb(74,84,89),
Color.FromRgb(100,124,100),
Color.FromRgb(82,94,84),
Color.FromRgb(132,117,69),
Color.FromRgb(126,115,95)
};
}

private void UpdateAppAccentColor(Color? color)
{
_faTheme.CustomAccentColor = color;
}
}
}

Design Theme Settings Window

Let’s design our ThemeSettingsWindow by binding the public variables in our ThemeSettingsWindowViewModel.

<Window xmlns="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"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:vm="using:AvaloniaGuideApp.ViewModels"
mc:Ignorable="d" d:DesignWidth="780" d:DesignHeight="580"
Width="780" Height="580"
x:Class="AvaloniaGuideApp.ThemeSettingsWindow"
x:DataType="vm:ThemeSettingsWindowViewModel"
x:CompileBindings="True"
Title="Theme Settings"
Icon="/Assets/avalonia-logo.ico"
WindowStartupLocation="CenterOwner"
Closing="Window_Closing">

<Design.DataContext>
<vm:ThemeSettingsWindowViewModel/>
</Design.DataContext>

<ScrollViewer Padding="{StaticResource SampleAppPageMargin}">

<StackPanel Spacing="8" Margin="10">
<Grid Margin="0 0 0 10">

<DockPanel HorizontalAlignment="Left">
<Image Source="/Assets/avalonia-logo.ico"
DockPanel.Dock="Left"
Height="80"
RenderOptions.BitmapInterpolationMode="HighQuality"/>

<StackPanel Spacing="0" Margin="15 0">
<TextBlock Text="Avalonia Guide Application Theme"
Theme="{StaticResource TitleTextBlockStyle}" />

<TextBlock Text="{Binding CurrentVersion}"
Theme="{StaticResource BodyTextBlockStyle}" />

<TextBlock Text="MIT License"
Theme="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
</DockPanel>

</Grid>

<ui:SettingsExpander Header="App Theme"
IconSource="DarkTheme"
Description="Change the current app theme">

<ui:SettingsExpander.Footer>
<ComboBox SelectedItem="{Binding CurrentAppTheme}"
ItemsSource="{Binding AppThemes}"
MinWidth="150" />
</ui:SettingsExpander.Footer>

</ui:SettingsExpander>

<ui:SettingsExpander Header="Flow Direction"
IconSource="AlignRight"
Description="Change the current app flow direction">

<ui:SettingsExpander.Footer>
<ComboBox SelectedItem="{Binding CurrentFlowDirection}"
ItemsSource="{Binding AppFlowDirections}"
MinWidth="150" />
</ui:SettingsExpander.Footer>

</ui:SettingsExpander>

<ui:SettingsExpander Header="App Accent Color"
IconSource="ColorLine"
Description="Set a custom accent color for the App"
IsExpanded="True">

<ui:SettingsExpanderItem Content="Preview">
<ui:SettingsExpanderItem.Footer>
<Grid RowDefinitions="*,*,*,*"
ColumnDefinitions="*,*"
HorizontalAlignment="Right"
Grid.Column="1">
<Border Background="{DynamicResource SystemAccentColor}"
Height="40" Grid.ColumnSpan="2">
<TextBlock Text="SystemAccentColor"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

<Border Background="{DynamicResource SystemAccentColorLight1}"
Height="40" Width="90" Grid.Column="0" Grid.Row="1">
<TextBlock Text="Light1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border Background="{DynamicResource SystemAccentColorLight2}"
Height="40" Width="90" Grid.Column="0" Grid.Row="2">
<TextBlock Text="Light2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border Background="{DynamicResource SystemAccentColorLight3}"
Height="40" Width="90" Grid.Column="0" Grid.Row="3">
<TextBlock Text="Light3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>

<Border Background="{DynamicResource SystemAccentColorDark1}"
Height="40" Width="90" Grid.Column="1" Grid.Row="1">
<TextBlock Text="Dark1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border Background="{DynamicResource SystemAccentColorDark2}"
Height="40" Width="90" Grid.Column="1" Grid.Row="2">
<TextBlock Text="Dark2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<Border Background="{DynamicResource SystemAccentColorDark3}"
Height="40" Width="90" Grid.Column="1" Grid.Row="3">
<TextBlock Text="Dark3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>

<ui:SettingsExpanderItem>
<CheckBox Content="Use Custom Accent Color?"
IsChecked="{Binding UseCustomAccent}"
HorizontalAlignment="Right" />
<ui:SettingsExpanderItem.Footer>
<StackPanel>
<TextBlock Text="Pre-set Colors"
Margin="24 24 0 0"
IsVisible="{Binding UseCustomAccent}" />

<ListBox ItemsSource="{Binding PredefinedColors}"
SelectedItem="{Binding ListBoxColor}"
MaxWidth="441"
AutoScrollToSelectedItem="False"
Margin="24 0 24 12"
HorizontalAlignment="Left"
IsVisible="{Binding UseCustomAccent}" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>

<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Margin" Value="1 1 0 0" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border CornerRadius="{StaticResource ControlCornerRadius}"
BorderThickness="2"
Name="Root">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>

<Border Name="Check"
Background="{DynamicResource FocusStrokeColorOuter}"
Width="20" Height="20"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0 2 2 0">
<ui:SymbolIcon Symbol="Checkmark"
Foreground="{DynamicResource SystemAccentColor}"
FontSize="18"/>
</Border>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ListBoxItem /template/ Border#Check">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>

<Style Selector="ListBoxItem:selected /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#Check">
<Setter Property="IsVisible" Value="True" />
</Style>
</ListBox.Styles>

</ListBox>

<Rectangle Fill="{DynamicResource ApplicationPageBackgroundThemeBrush}"
Height="1"
IsVisible="{Binding UseCustomAccent}" />

<DockPanel LastChildFill="False" Margin="24 6 0 0"
IsVisible="{Binding UseCustomAccent}" >
<TextBlock Text="Custom Color"
VerticalAlignment="Center"
DockPanel.Dock="Left" />

<ui:ColorPickerButton Color="{Binding CustomAccentColor}"
IsMoreButtonVisible="True"
UseSpectrum="True"
UseColorWheel="False"
UseColorTriangle="False"
UseColorPalette="False"
IsCompact="True" ShowAcceptDismissButtons="True"
DockPanel.Dock="Right"/>
</DockPanel>
</StackPanel>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>

</ui:SettingsExpander>

</StackPanel>
</ScrollViewer>

</Window>

After coding the window as shown above, you will achieve a view like this:

After completing the design, let’s implement the LoadSettings() method in our app.axaml.cs class to save the design and ensure it opens in that layout every time.

using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using AvaloniaGuideApp.ViewModels;
using AvaloniaGuideApp.Views;

namespace AvaloniaGuideApp
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}

public override async void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var theme = new ThemeSettingsWindowViewModel();
theme.LoadSettings();

var splashScreen = new SplashScreenPage();

desktop.MainWindow = splashScreen;
splashScreen.Show();

await splashScreen.InitApp();

var main = new MainWindow
{
DataContext = new MainWindowViewModel(),
};

desktop.MainWindow = main;
main.Show();
splashScreen.Close();
}

base.OnFrameworkInitializationCompleted();
}
}
}

Add a Button to MainWindow for Theme Settings Test

Lastly, let’s add a button to the main window to open our page, and then run the application to test it.

<Button Name="btnThemeSettings"
Content="Show Theme Settings Screen"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Click="btnThemeSettings_Click"/>
private void btnThemeSettings_Click(object? sender, RoutedEventArgs e)
{
var themeSettings = new ThemeSettingsWindow();
themeSettings.Show();
}

Finally, Let’s Show Theme Settings

As a result of the developments we’ve made, you will be able to set the theme of your application from the opened theme settings page, as shown above. You can also save the theme settings you have determined. From the moment you close the screen, the application will save the changes and restart according to the last change made in the .json file.

Now our application is fully ready. You can run the application and test it along with me. Below, I’ll share an example video showing the test. You can compare it by watching.

Avalonia Guide App Repo: https://github.com/OmerFarukAkyapak/avalonia

Stay Connected: Follow Me on GitHub and LinkedIn!

We have now completed our application. I hope it was useful for you. Feel free to follow me on GitHub and LinkedIn, where you can support me and stay updated on my latest projects.

GitHub: https://github.com/OmerFarukAkyapak

LinkedIn: https://www.linkedin.com/in/farukakyapak/

Thank you!

--

--