'How to load an application to systemtray using Avalonia

How can I load an Avalonia application to the systemtray and set the menu items?



Solution 1:[1]

Avalonia seems to be a UI/WPF library/resource, so I don't think that would affect how your application runs. This would be related to WPF application development.

A bit of reading around, and it appears you may need to use the System.Windows.Forms.NotifyIcon.

You would want to instantiate the icon in the context of your main application.

I created a sample WPF application using .NET Framework (so that I was able to reference System.Windows.Forms) and was able to have a system tray icon appear for my application.

Here is some example code:

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : System.Windows.Application
    {
        NotifyIcon TrayIcon;
        public App()
        {
            // we initialize the tray icon in the application constructor
            // and have that reference for the lifetime of the application
            TrayIcon = new NotifyIcon()
            {
                Icon = SystemIcons.Information,
                ContextMenu = new ContextMenu(new MenuItem[] { new MenuItem("Show/Hide MyApp", ShowHide), new MenuItem("Exit", OnExit) }),
                Visible = true
            };
        }

        private void OnExit(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }

        private void ShowHide(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }
    }

It appears Avalonia does offer their own version of a TrayIcon. Here is the class I was able to find in their Source Code:

TrayIcon.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Input;
using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Utilities;

namespace Avalonia.Controls
{
    public sealed class TrayIcons : AvaloniaList<TrayIcon>
    {
    }

    public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable
    {
        private readonly ITrayIconImpl? _impl;
        private ICommand? _command;

        private TrayIcon(ITrayIconImpl? impl)
        {
            if (impl != null)
            {
                _impl = impl;

                _impl.SetIsVisible(IsVisible);

                _impl.OnClicked = () =>
                {
                    Clicked?.Invoke(this, EventArgs.Empty);

                    if (Command?.CanExecute(CommandParameter) == true)
                    {
                        Command.Execute(CommandParameter);
                    }
                };
            }
        }

        public TrayIcon() : this(PlatformManager.CreateTrayIcon())
        {
        }

        static TrayIcon()
        {
            IconsProperty.Changed.Subscribe(args =>
            {
                if (args.Sender is Application)
                {
                    if (args.OldValue.Value != null)
                    {
                        RemoveIcons(args.OldValue.Value);
                    }

                    if (args.NewValue.Value != null)
                    {
                        args.NewValue.Value.CollectionChanged += Icons_CollectionChanged;
                    }
                }
            });

            var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");

            if (app.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
            {
                lifetime.Exit += Lifetime_Exit;
            }
        }

        /// <summary>
        /// Raised when the TrayIcon is clicked.
        /// Note, this is only supported on Win32 and some Linux DEs,
        /// on OS X this event is not raised.
        /// </summary>
        public event EventHandler? Clicked;

        /// <summary>
        /// Defines the <see cref="Command"/> property.
        /// </summary>
        public static readonly DirectProperty<TrayIcon, ICommand?> CommandProperty =
            Button.CommandProperty.AddOwner<TrayIcon>(
                trayIcon => trayIcon.Command,
                (trayIcon, command) => trayIcon.Command = command,
                enableDataValidation: true);

        /// <summary>
        /// Defines the <see cref="CommandParameter"/> property.
        /// </summary>
        public static readonly StyledProperty<object?> CommandParameterProperty =
            Button.CommandParameterProperty.AddOwner<MenuItem>();

        /// <summary>
        /// Defines the <see cref="TrayIcons"/> attached property.
        /// </summary>
        public static readonly AttachedProperty<TrayIcons> IconsProperty
            = AvaloniaProperty.RegisterAttached<TrayIcon, Application, TrayIcons>("Icons");

        /// <summary>
        /// Defines the <see cref="Menu"/> property.
        /// </summary>
        public static readonly StyledProperty<NativeMenu?> MenuProperty
            = AvaloniaProperty.Register<TrayIcon, NativeMenu?>(nameof(Menu));

        /// <summary>
        /// Defines the <see cref="Icon"/> property.
        /// </summary>
        public static readonly StyledProperty<WindowIcon?> IconProperty =
            Window.IconProperty.AddOwner<TrayIcon>();

        /// <summary>
        /// Defines the <see cref="ToolTipText"/> property.
        /// </summary>
        public static readonly StyledProperty<string?> ToolTipTextProperty =
            AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText));

        /// <summary>
        /// Defines the <see cref="IsVisible"/> property.
        /// </summary>
        public static readonly StyledProperty<bool> IsVisibleProperty =
            Visual.IsVisibleProperty.AddOwner<TrayIcon>();

        public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);

        public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);

        /// <summary>
        /// Gets or sets the <see cref="Command"/> property of a TrayIcon.
        /// </summary>
        public ICommand? Command
        {
            get => _command;
            set => SetAndRaise(CommandProperty, ref _command, value);
        }

        /// <summary>
        /// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
        /// <see cref="TrayIcon"/>.
        /// </summary>
        public object? CommandParameter
        {
            get { return GetValue(CommandParameterProperty); }
            set { SetValue(CommandParameterProperty, value); }
        }

        /// <summary>
        /// Gets or sets the Menu of the TrayIcon.
        /// </summary>
        public NativeMenu? Menu
        {
            get => GetValue(MenuProperty);
            set => SetValue(MenuProperty, value);
        }

        /// <summary>
        /// Gets or sets the icon of the TrayIcon.
        /// </summary>
        public WindowIcon? Icon
        {
            get => GetValue(IconProperty);
            set => SetValue(IconProperty, value);
        }

        /// <summary>
        /// Gets or sets the tooltip text of the TrayIcon.
        /// </summary>
        public string? ToolTipText
        {
            get => GetValue(ToolTipTextProperty);
            set => SetValue(ToolTipTextProperty, value);
        }

        /// <summary>
        /// Gets or sets the visibility of the TrayIcon.
        /// </summary>
        public bool IsVisible
        {
            get => GetValue(IsVisibleProperty);
            set => SetValue(IsVisibleProperty, value);
        }

        public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter;

        private static void Lifetime_Exit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
        {
            var app = Application.Current ?? throw new InvalidOperationException("Application not yet initialized.");
            var trayIcons = GetIcons(app);

            RemoveIcons(trayIcons);
        }

        private static void Icons_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (e.OldItems is not null)
                RemoveIcons(e.OldItems.Cast<TrayIcon>());
        }

        private static void RemoveIcons(IEnumerable<TrayIcon> icons)
        {
            foreach (var icon in icons)
            {
                icon.Dispose();
            }
        }

        protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
        {
            base.OnPropertyChanged(change);

            if (change.Property == IconProperty)
            {
                _impl?.SetIcon(Icon?.PlatformImpl);
            }
            else if (change.Property == IsVisibleProperty)
            {
                _impl?.SetIsVisible(change.GetNewValue<bool>());
            }
            else if (change.Property == ToolTipTextProperty)
            {
                _impl?.SetToolTipText(change.GetNewValue<string?>());
            }
            else if (change.Property == MenuProperty)
            {
                _impl?.MenuExporter?.SetNativeMenu(change.GetNewValue<NativeMenu?>());
            }
        }

        /// <summary>
        /// Disposes the tray icon (removing it from the tray area).
        /// </summary>
        public void Dispose() => _impl?.Dispose();
    }
}

And an application example from them as well which shows an example implementation:

App.xaml

<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="using:ControlCatalog.ViewModels"
             x:DataType="vm:ApplicationViewModel"
             x:CompileBindings="True"
             Name="Avalonia ControlCatalog"
             x:Class="ControlCatalog.App">
  <Application.Styles>
    <Style Selector="TextBlock.h1, TextBlock.h2, TextBlock.h3">
      <Setter Property="TextWrapping" Value="Wrap" />
    </Style>
    <Style Selector="TextBlock.h1">
      <Setter Property="FontSize" Value="16" />
      <Setter Property="FontWeight" Value="Medium" />
    </Style>
    <Style Selector="TextBlock.h2">
      <Setter Property="FontSize" Value="14" />
    </Style>
    <Style Selector="TextBlock.h3">
      <Setter Property="FontSize" Value="12" />
    </Style>
    <Style Selector="Label.h1">
      <Setter Property="FontSize" Value="16" />
      <Setter Property="FontWeight" Value="Medium" />
    </Style>
    <Style Selector="Label.h2">
      <Setter Property="FontSize" Value="14" />
    </Style>
    <Style Selector="Label.h3">
      <Setter Property="FontSize" Value="12" />
    </Style>
    <StyleInclude Source="avares://ControlSamples/HamburgerMenu/HamburgerMenu.xaml" />
  </Application.Styles>
  <TrayIcon.Icons>
    <TrayIcons>
      <TrayIcon Icon="/Assets/test_icon.ico" ToolTipText="Avalonia Tray Icon ToolTip">
        <TrayIcon.Menu>
          <NativeMenu>
            <NativeMenuItem Header="Settings">
              <NativeMenu>
                <NativeMenuItem Header="Option 1" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
                <NativeMenuItem Header="Option 2" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
                <NativeMenuItemSeparator />
                <NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
                <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
              </NativeMenu>
            </NativeMenuItem>
            <NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
          </NativeMenu>
        </TrayIcon.Menu>
      </TrayIcon>
    </TrayIcons>
  </TrayIcon.Icons>
</Application>

They do have some documentation here.

Unfortunately their documentation website is hard to navigate, and doesn't seem to mention anything regarding "TrayIcon" when using their search feature.

Fortunately, it seems they also have a Customer Support team, and you could always post questions to their support team directly on their GitHub place:

https://github.com/AvaloniaUI/Avalonia/discussions

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Peter Mortensen