Fenstersymbol unter Linux: _NET_WM_ICON selbst setzen
#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:
@@ -36,6 +36,10 @@ namespace PolyTrader.App.Avalonia
|
|||||||
Shell.ThemeManager.Initialize(settings, "server_settings.xml",
|
Shell.ThemeManager.Initialize(settings, "server_settings.xml",
|
||||||
Services.GetRequiredService<PolyTraderSharp.Services.TerminalLogger>());
|
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>();
|
var uiHost = Services.GetRequiredService<AvaloniaUiHost>();
|
||||||
|
|
||||||
CoreViews.Register(uiHost, Services);
|
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 <fenster> _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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user