Avalonia FluentTheme zur Laufzeit wechseln

Viele moderne Applikationen bieten neben einer ansprechenden UI auch den Wechsel zwischen verschiedenen Themes an. Für gewöhnlich wird zumindest zwischen Hell und Dunkel unterschieden. Gleiches gilt für das Betriebssystem selbst – Windows und macOS bieten dem User jeweils die Wahl zwischen Hell und Dunkel. Auch Avalonia bietet mit dem FluentTheme seit Version 0.10 einen sehr einfachen Weg an, zwischen hellen und dunklen Modus zu unterscheiden. Typischerweise wird das FluentTheme in der App.xaml angegeben und bekommt als Mode entweder „Light“ oder „Dark“. In diesem Artikel wollen wir uns damit beschäftigen, wie diese Einstellung zur Laufzeit der Applikation verändert werden kann. Weiterhin schauen wir uns an, wie man unter Windows den aktuell konfigurierten Theme herausbekommt und sogar auf Änderung des aktuellen Theme reagieren kann.

FluentTheme per App.xaml setzen

Typischerweise sieht eine per Template erzeugte App.xaml in Avalonia wie im nachfolgenden Codeausschnitt aus. Unter Application.Styles liegt das FluentTheme mit dem Modus „Light“ (oder „Dark“). Diese Einstellung ist die Basis für das Theme der eigenen Applikation.

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="HappyCoding.GRpcCommunication.ServerApp.App">
    <Application.Styles>
        <FluentTheme Mode="Light"/>
    </Application.Styles>
</Application>

Auf diese Weise wird der FluentTheme allerdings fix auf einen Modus gesetzt und wird später nicht mehr verändert. Unter dem Link [1] sehen wir aber, dass der aktuelle Theme eben dadurch geändert wird, dass man die Eigenschaft Mode im FluentTheme zu einem späteren Zeitpunkt ändert. Um das zu ermöglichen, bauen wir den Code etwas um. Im ersten Schritt löschen wir das FluentTheme wieder aus der App.xaml raus. Anschließend erhalten wir folgenden Code.

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="HappyCoding.GRpcCommunication.ServerApp.App">
    <Application.Styles>
        <FluentTheme Mode="Light"/>
    </Application.Styles>
</Application>

Anschließend gehen wir in die App.xaml.cs und fügen dort das FluentTheme wieder hinzu. Diesmal allerdings so, dass wir das FluentTheme auch als statischen Member haben. Dieser ermöglicht uns, dass wir zu einem späteren Zeitpunkt leicht darauf zugreifen und die Eigenschaft Mode ändern können. Im nachfolgenden Codeausschnitt sehen wir ab Zeile 16 eine Methode, um den Modus des FluentTheme umzustellen. Somit kann das Theme jederzeit von anderer Stelle der Applikation geändert werden – sofern der Zugriff auf die Klasse App möglich ist.

using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Themes.Fluent;

namespace HappyCoding.AvaloniaFluentThemeSwitch;

public partial class App : Application
{
    private static readonly FluentTheme s_fluentTheme = new(new Uri("avares://ControlCatalog/Styles"));

    /// <summary>
    /// Manually sets current theme.
    /// </summary>
    public static void SetFluentThemeMode(FluentThemeMode mode)
    {
        s_fluentTheme.Mode = mode;
    }

    public override void Initialize()
    {
        this.Styles.Insert(0, s_fluentTheme);

        AvaloniaXamlLoader.Load(this);
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (this.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            desktop.MainWindow = new MainWindow();
        }

        base.OnFrameworkInitializationCompleted();
    }
}

FluentTheme anhand dem aktuellen Windows Theme setzen

Mit obiger Methode lässt sich mühelos ein Button oder Ähnliches einbauen, über den der Benutzer das Theme händisch zwischen Hell und Dunkel hin und her schalten kann. Besser wäre es natürlich, wenn sich das Theme stattdessen direkt nach dem Theme des Betriebssystems richtet. Unter Windows ist das auch relativ einfach zu erreichen. Denn ob in Windows das helle oder das dunkle Theme aktiviert ist, lässt sich aus der Registry auslesen. Unter [2] ist beschrieben, wie das für WPF Applikationen funktioniert. Nachfolgender Codeausschnitt zeigt eine darauf aufbauende Implementierung und stellt zwei statische Methoden bereit:

  • ListenForThemeChangeEvent: Ruft die übergebene Action auf, nachdem der Benutzer das aktuelle Theme des Betriebssystems verändert hat
  • GetFluentThemeByCurrentWindowsTheme: Gibt den Modus für das FluentTheme zurück, welches zum aktuellen Theme des Betriebssystems passt

Im verwiesenen Codebeispiel ist zu sehen, wie man diese Klasse in die Avalonia Applikation einbinden kann. Das Codebeispiel liegt auf GitHub unter [3].

using System;
using System.Globalization;
using System.Management;
using System.Runtime.Versioning;
using System.Security.Principal;
using Avalonia.Logging;
using Avalonia.Themes.Fluent;
using Microsoft.Win32;

namespace HappyCoding.AvaloniaFluentThemeSwitch.WindowsThemeDetection;

/// <summary>
/// Checks for currently configured theme in windows.
/// See: https://engy.us/blog/2018/10/20/dark-theme-in-wpf/
/// </summary>
internal static class WindowsThemeDetector
{
    private const string REGISTRY_KEY_PATH = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
    private const string REGISTRY_VALUE_NAME = "AppsUseLightTheme";

    [SupportedOSPlatform("windows")]
    public static void ListenForThemeChangeEvent(Action<FluentThemeMode> setWindowsThemeAction)
    {
        var currentUser = WindowsIdentity.GetCurrent();
        if (currentUser.User == null) { return; }

        string query = string.Format(
            CultureInfo.InvariantCulture,
            @"SELECT * FROM RegistryValueChangeEvent WHERE Hive = 'HKEY_USERS' AND KeyPath = '{0}\\{1}' AND ValueName = '{2}'",
            currentUser.User.Value,
            REGISTRY_KEY_PATH.Replace(@"\", @"\\"),
            REGISTRY_VALUE_NAME);
        try
        {
            var watcher = new ManagementEventWatcher(query);
            watcher.EventArrived += (_, _) => setWindowsThemeAction(GetFluentThemeByCurrentWindowsTheme());

            // Start listening for events
            watcher.Start();
        }
        catch (Exception)
        {
            // This can fail on Windows 7
        }
    }

    [SupportedOSPlatform("windows")]
    public static FluentThemeMode GetFluentThemeByCurrentWindowsTheme(FluentThemeMode defaultTheme = FluentThemeMode.Light)
    {
        var subKey = Registry.CurrentUser.OpenSubKey(REGISTRY_KEY_PATH);
        if (subKey == null)
        {
            Logger.Sink?.Log(
                LogEventLevel.Error,
                nameof(WindowsThemeDetector),
                null,
                "Unable to get registry key {RegistryKey}. Returning default theme {Theme}",
                REGISTRY_KEY_PATH, defaultTheme);
            return defaultTheme;
        }
        try
        {
            var registryValueObject = subKey.GetValue(REGISTRY_VALUE_NAME);
            if (registryValueObject == null)
            {
                Logger.Sink?.Log(
                    LogEventLevel.Error,
                    nameof(WindowsThemeDetector),
                    null,
                    "Unable to get registry key value (key: {RegistryKey}, value: {RegistryValueName}). Returning default theme {Theme}",
                    REGISTRY_KEY_PATH, REGISTRY_VALUE_NAME, defaultTheme);
                return defaultTheme;
            }

            var registryValue = (int)registryValueObject;
            var windowsTheme = registryValue > 0 ? WindowsTheme.Light : WindowsTheme.Dark;
            return GetFluentThemeByWindowsTheme(windowsTheme);
        }
        finally
        {
            subKey.Dispose();
        }
    }

    private static FluentThemeMode GetFluentThemeByWindowsTheme(WindowsTheme windowsTheme)
    {
        switch (windowsTheme)
        {
            case WindowsTheme.Light:
                return FluentThemeMode.Light;

            case WindowsTheme.Dark:
            default:
                return FluentThemeMode.Dark;
        }
    }
}

internal enum WindowsTheme
{
    Light,
    Dark
}

Fazit

Avalonia bringt den FluentTheme von Haus aus mit einem Hell- und einem Dunkel-Modus mit. Das Umschalten zwischen beiden Modi erfolgt entsprechend einfach und kann jederzeit zur Laufzeit gemacht werden. Für Windows lässt sich das FluentTheme sogar mit wenig Aufwand an das Betriebssystem anpassen. Für macOS kenne ich bis dato aber leider noch keine einfache Methode – Kommentare sind willkommen.

Downloads

  1. Quellcode der Beispiele aus diesem Artikel
    http://www.rolandk.de/files/2022/HappyCoding.AvaloniaFluentThemeSwitch.zip

Verweise

  1. PR zu „Fast Fluent theme switching“ auf GitHub
    https://github.com/AvaloniaUI/Avalonia/pull/7256
  2. Dark Theme in WPF
    https://engy.us/blog/2018/10/20/dark-theme-in-wpf/
  3. Beispiel-Quellcode für diesen Artikel auf GitHub
    https://github.com/RolandKoenig/HappyCoding/tree/main/2022/HappyCoding.AvaloniaFluentThemeSwitch

Ebenfalls interessant

  1. Cross-Plattform GUI mit C# und Avalonia
    https://www.rolandk.de/wp-posts/2020/07/cross-platform-gui-mit-c-und-avalonia/
  2. Custom Window Chrome mit Avalonia
    https://www.rolandk.de/wp-posts/2021/05/custom-window-chrome-mit-avalonia/
  3. Das DataGrid von Avalonia
    https://www.rolandk.de/wp-posts/2022/10/das-datagrid-von-avalonia/
  4. Avalonia Applikationen übersetzen
    https://www.rolandk.de/wp-posts/2022/12/avalonia-applikationen-uebersetzen/

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.