When I started to think about features for my project it came to my mind, that when the user minimalizes the application it should be hidden from the taskbar, and when I was thinking deeply then I figured out that only hiding will not be enough. The user needs to be able somehow to return from the hidden application. And the solution is to have a Tray Icon, on Windows, it is always on the taskbar on the right side, where you can find some tray icons like Network or Volume. I was curious if it will work on Linux too. Because I am using Endeavour with Plasma flavour, I knew there it should be possible somehow, but I was not sure if the Avalonia framework supports it. I googled this problem and found out there are some git projects solving this problem too. So I decided to open IDE and write some code. Firstly I started with a minimalizing problem because I thought it will be easier. When I started coding this, I easily figured out that it will not be so easy. So I googled a little bit for the first problem in official Avalonia Sources. I found out that I can do it in a way that I can register to PropertyChanged event on MainWindow, filter my specific property and then hide MainWindow. So I tried it like bellow
private void MyMainWindow_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is MainWindow && e.NewValue is WindowState windowState && windowState == WindowState.Minimized)
{
myMainWindow?.Hide();
}
}
I felt there should be an easier solution like this. So I googled a little bit more. I ended on google around page 10, where I find an Avalonion Gitter post on how I can try it. The post was telling about overriding the HandleWindowStateChanged method. So I tried it, and it worked properly. So then I tried it on Endeavour Linux and easily figured it out, that on Linux Task Bar Icon is not hiding after minimalize. I also saw some glitches on Windows and Linux, when you returned back to MainWindow it was not shown properly. The same post suggested calling some methods. I tried it and it worked on both platforms. And final code for Minimalize, Hide app and Hide Icon Bar is written below. Maybe it will help someone someday.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
protected override void HandleWindowStateChanged(WindowState state)
{
if (state == WindowState.Minimized)
{
ShowInTaskbar = false;
Hide();
}
if(state == WindowState.Normal)
{
ShowInTaskbar = true;
this.BringIntoView();
Activate();
Focus();
base.HandleWindowStateChanged(state);
}
}
}
Right now I do not know how to write tests for this behaviour, but at least it can be the next Touch article for it.
At this moment I still had not implemented a solution for Tray Icon. On the official Avalonia webpage, I found that Avalonia supports Tray Icon already, so I do not need to use some solution from git. But unlucky there was no documentation or Wiki for this how to use it. So I experimented.
At the end I had a code similar to this.
public partial class App : Application
{
private MainWindow? myMainWindow;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
myMainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
desktop.MainWindow = myMainWindow;
RegisterTrayIcon();
}
base.OnFrameworkInitializationCompleted();
}
private void RegisterTrayIcon()
{
var trayIcon = new TrayIcon
{
IsVisible = true,
ToolTipText = "TestToolTipText",
Command = ReactiveCommand.Create(ShowApplication),
Icon = new WindowIcon("/Assets/avalonia-logo.ico")
};
}
private void ShowApplication()
{
if(myMainWindow != null)
{
myMainWindow.WindowState = WindowState.Normal;
myMainWindow.Show();
}
}
}
And It was not working. I used also the ReactiveUI library for easy Command creation as shown below.
Command = ReactiveCommand.Create(ShowApplication),
Where I also implemented behaviour what will happen when the user clicks on Tray Icon, for us it will show the application back.
private void ShowApplication()
{
if(myMainWindow != null)
{
myMainWindow.WindowState = WindowState.Normal;
myMainWindow.Show();
}
}
It failed during runtime when it was loading Icon. I figured it out that for this I cannot use .ico, but some transparent png image. So I needed to load it as Bitmap and then create WindowIcon. So I replaced this line with the code below.
Icon = new WindowIcon(new Bitmap("C:/Icons/test.png"))
After this change, the code worked correctly. The app was hiding, when I clicked on Tray Icon it was shown back. but when I closed an application it failed during closing. Luckily exception thrown at this moment helped me to solve this problem. It was saying that I did not set Tray Icon value as Attached property. So I implemented it and yes, it works how it is expected on both platforms.
So the final code is shown below. Feel free to use and comment below the article. What do you think about this?
public partial class App : Application
{
private MainWindow? myMainWindow;
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
myMainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
desktop.MainWindow = myMainWindow;
RegisterTrayIcon();
}
base.OnFrameworkInitializationCompleted();
}
private void RegisterTrayIcon()
{
var trayIcon = new TrayIcon
{
IsVisible = true,
ToolTipText = "TestToolTipText",
Command = ReactiveCommand.Create(ShowApplication),
Icon = new WindowIcon(new Bitmap("C:/Icons/test.png"))
};
var trayIcons = new TrayIcons
{
trayIcon
};
SetValue(TrayIcon.IconsProperty, trayIcons);
}
private void ShowApplication()
{
if(myMainWindow != null)
{
myMainWindow.WindowState = WindowState.Normal;
myMainWindow.Show();
}
}
}
Top comments (4)
Thank you very much, I doubt I could have gotten this to work if I hadn't found this post. As you mentioned the documentation for this is basically non-existent.
There is however a little mistake in the code in the end (I think?) and a little bit of room for improvement. The little mistake is the window never gets subscribed to the MyMainWindow_PropertyChanged method, so the window never gets hidden from the taskbar.
The little improvement I made is making the icon path non-static, I'm quite new to all of this so there might be a better way to go about loading the icon still but this should works as long as you are working within a Namespace and your project has a /Assets/icon.png in place.
I figured out how to do this the MVVM (sort of) way as well - mainly thanks to looking at the code of WalletWasabi (github).
My App.axaml:
And my App.axaml.cs
Disclaimer: I'm posting this rather late at night right as I got it working, don't know if further propagating the MainWindow.PropertyChanged event is the way to go. It does however work.
Nice work, I was also wondering how to do it in View,(but as I wrote above, currently I do not have time to dive deeply in this topic to understand it complety,) because TrayIcon is UI component :)
That is nice that my article helped someone :) I am working on different thing so my coding hobby went a little bit aside. Yes Avalonia offers multiple ways of loading images, it depends which suits you :)
The problem you mentioned I havent noticed when I played with it :O but indeed it needs to be fixed :)