Fenstersymbol unter Linux: _NET_WM_ICON selbst setzen
CI / Build & Tests (Linux) (push) Canceled after 0s
CI / Plattformneutralität & Hygiene (push) Canceled after 0s

#410: Avalonia 11.3.19 laedt Icon="..." korrekt, schreibt _NET_WM_ICON aber nie auf
das X11-Fenster - Fensterliste und Alt-Tab zeigten kein Symbol. Richards
Entscheidung vom 15.09.2026: nicht auf Avalonia 12 warten.

LinuxWindowIcon haengt sich klassenweit an Window.WindowOpenedEvent und setzt die
Eigenschaft ueber eine eigene libX11-Verbindung (64 und 32 px aus icon.png). Nur
Linux, nur X11; scheitert es, einmal eine Warnung ins Log, nie ein Abbruch. Zu
entfernen, sobald Avalonia es selbst tut.

Auf der VM geprueft: Hauptfenster und Beenden-Dialog tragen _NET_WM_ICON (64 x 64),
keine Warnung im Log.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-09-15 11:38:33 +02:00
co-authored by Claude Opus 5
parent cec8b91ebd
commit c343dd57f8
2 changed files with 125 additions and 0 deletions
+4
View File
@@ -36,6 +36,10 @@ namespace PolyTrader.App.Avalonia
Shell.ThemeManager.Initialize(settings, "server_settings.xml",
Services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>());
// #410: Unter Linux setzt Avalonia 11 kein Fenstersymbol - Umweg, bevor das erste Fenster erscheint.
var terminal = Services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>();
LinuxWindowIcon.Register(m => terminal.Warning(m));
var uiHost = Services.GetRequiredService<AvaloniaUiHost>();
CoreViews.Register(uiHost, Services);
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Platform;
using SkiaSharp;
namespace PolyTrader.App.Avalonia.Shell
{
/// <summary>
/// Setzt unter Linux das Fenstersymbol (_NET_WM_ICON) selbst - Umweg für #410.
///
/// <para>Avalonia 11.3.19 lädt das Symbol aus <c>Icon="..."</c> zwar korrekt, schreibt die Eigenschaft
/// aber nie auf das X11-Fenster (am 01.09.2026 mit einem Minimalprogramm nachgewiesen). Fensterliste und
/// Alt-Tab zeigen dann kein Symbol. Richards Entscheidung vom 15.09.2026: nicht auf Avalonia 12 warten.</para>
///
/// <para>Nach dem Öffnen jedes Fensters wird die Eigenschaft über eine eigene Verbindung zum X-Server gesetzt;
/// sie gehört dem Fenster und bleibt nach dem Schließen der Verbindung bestehen. Unter Windows trägt
/// ApplicationIcon, dort passiert hier nichts.</para>
///
/// <para><b>Entfernen</b>, sobald Avalonia die Eigenschaft selbst setzt (nachprüfen mit
/// <c>xprop -id &lt;fenster&gt; _NET_WM_ICON</c> nach dem Wechsel auf eine neuere Avalonia-Fassung).</para>
/// </summary>
internal static class LinuxWindowIcon
{
private const string IconUri = "avares://PolyTrader.App.Avalonia/Assets/icon.png";
private static readonly int[] Sizes = { 64, 32 };
private static IntPtr[]? _data;
private static bool _failed;
private static Action<string>? _warn;
/// <summary>Einmal beim Start, bevor das erste Fenster erscheint.</summary>
public static void Register(Action<string> warn)
{
if (!OperatingSystem.IsLinux()) return;
_warn = warn;
Window.WindowOpenedEvent.AddClassHandler<Window>((window, _) => Apply(window));
}
private static void Apply(Window window)
{
if (_failed) return;
try
{
var handle = window.TryGetPlatformHandle();
if (handle == null || handle.HandleDescriptor != "XID") return; // z.B. kein X11
var data = _data ??= BuildData();
IntPtr display = XOpenDisplay(IntPtr.Zero);
if (display == IntPtr.Zero) return;
try
{
IntPtr atom = XInternAtom(display, "_NET_WM_ICON", false);
XChangeProperty(display, handle.Handle, atom, XaCardinal, 32, PropModeReplace, data, data.Length);
XFlush(display);
}
finally
{
XCloseDisplay(display);
}
}
catch (Exception ex)
{
// Das Symbol ist Kosmetik und darf nie den Start oder ein Fenster stören. Einmal melden, dann Ruhe.
_failed = true;
_warn?.Invoke($"Fenstersymbol unter Linux nicht gesetzt ({ex.GetType().Name}: {ex.Message}).");
}
}
/// <summary>
/// _NET_WM_ICON-Format: je Größe Breite, Höhe, dann Breite×Höhe Bildpunkte als ARGB (nicht vormultipliziert).
/// Bei Format 32 erwartet Xlib je Wert ein <c>long</c> - auf 64-Bit also 8 Byte, daher IntPtr.
/// </summary>
private static IntPtr[] BuildData()
{
using var stream = AssetLoader.Open(new Uri(IconUri));
using var source = SKBitmap.Decode(stream)
?? throw new InvalidOperationException("icon.png nicht lesbar");
var values = new List<IntPtr>();
foreach (int size in Sizes)
{
using var scaled = source.Resize(new SKImageInfo(size, size, SKColorType.Bgra8888, SKAlphaType.Unpremul),
SKFilterQuality.High)
?? throw new InvalidOperationException($"icon.png nicht auf {size} px skalierbar");
values.Add((IntPtr)size);
values.Add((IntPtr)size);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
SKColor c = scaled.GetPixel(x, y);
long argb = ((long)c.Alpha << 24) | ((long)c.Red << 16) | ((long)c.Green << 8) | c.Blue;
values.Add((IntPtr)argb);
}
}
return values.ToArray();
}
private static readonly IntPtr XaCardinal = (IntPtr)6;
private const int PropModeReplace = 0;
[DllImport("libX11.so.6")]
private static extern IntPtr XOpenDisplay(IntPtr displayName);
[DllImport("libX11.so.6")]
private static extern IntPtr XInternAtom(IntPtr display, string atomName, bool onlyIfExists);
[DllImport("libX11.so.6")]
private static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type,
int format, int mode, IntPtr[] data, int nelements);
[DllImport("libX11.so.6")]
private static extern int XFlush(IntPtr display);
[DllImport("libX11.so.6")]
private static extern int XCloseDisplay(IntPtr display);
}
}