Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions src/RdpManager/WindowBorder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,36 +56,60 @@ public static void Apply(Window window)
catch { /* starszy DWM bez tego atrybutu — bez znaczenia */ }
}

/// <summary>Nakłada TERAZ i utrzymuje: przy każdej aktywacji okna oraz raz po pełnym wyrenderowaniu
/// (ApplicationIdle — już po tym, jak WPF-UI skończy malować krawędź).</summary>
/// <summary>Nakłada TERAZ i utrzymuje: synchronicznie z hooka WM_NCACTIVATE (ogranicza błysk),
/// z handlera Activated + odroczonych dobić (ostatnie słowo po repaintach WPF-UI) oraz raz po
/// pełnym wyrenderowaniu.</summary>
public static void Keep(Window window)
{
if (window == null) return;
Apply(window);
// WM_NCACTIVATE: Windows/WPF-UI przemalowują krawędź (non-client) przy (de)aktywacji — m.in. gdy
// zamknie się okno potomne (np. „O aplikacji") i główne wraca na wierzch. Hookujemy tę wiadomość i
// przywracamy wybraną obwódkę SYNCHRONICZNIE, w tym samym cyklu repaint (bez odroczenia = bez błysku
// kobaltu). Hook jest statyczny (bez domknięcia per-okno) i zwalnia się z HwndSource okna — brak wycieku.
// zamknie się okno potomne (np. „O aplikacji") i główne wraca na wierzch. Zapis synchroniczny w tym
// hooku ogranicza błysk, a odroczone dobicie w BorderHook domyka cykl. Hook jest statyczny (bez
// domknięcia per-okno) i zwalnia się z HwndSource okna — brak wycieku.
HwndSource.FromHwnd(new WindowInteropHelper(window).Handle)?.AddHook(BorderHook);
// Handler Activated — sprawdzona ścieżka sprzed #183: WPF-UI maluje akcent PO Activated, więc
// dobicie odroczone stąd ląduje jako ostatnie. #183 usunął to w całości (zostawiając tylko zapis
// synchroniczny z hooka, który leci PRZED repaintem WPF-UI) — obwódka zostawała kobaltowa NA STAŁE.
window.Activated += (_, __) =>
{
Apply(window);
window.Dispatcher.BeginInvoke(new Action(() => Apply(window)),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
};
Comment on lines +74 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 | Confidence: High

Each call to Keep attaches a new Activated event handler without removing any previously attached one. If Keep is invoked multiple times for the same window (e.g., when border settings are reapplied via ReapplyAll), duplicate handlers accumulate. This results in redundant synchronous and deferred Apply calls on every activation, and a growing delegate invocation list. While the immediate effect is benign (idempotent DWM writes), it violates best practices for event subscription lifecycle, adds unnecessary overhead, and could lead to confusion or subtle behavioral changes if any side‑effect is added to the handler in the future. The current fix focuses on correctness of the border color; tackling this subscription pattern would improve long‑term maintainability.

Code Suggestion:

// In the class, store a WeakReference-based map to ensure single subscription per window.
private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<Window, EventHandler> _activatedHandlers = new();

public static void Keep(Window window)
{
    if (window == null) return;
    Apply(window);

    // Remove any previously attached handler (safe if none).
    if (_activatedHandlers.TryGetValue(window, out var oldHandler))
        window.Activated -= oldHandler;

    EventHandler handler = (_, __) =>
    {
        Apply(window);
        window.Dispatcher.BeginInvoke(new Action(() => Apply(window)),
            System.Windows.Threading.DispatcherPriority.ApplicationIdle);
    };
    _activatedHandlers.AddOrUpdate(window, handler);
    window.Activated += handler;

    // ... rest of Keep unchanged
}

// Dobij raz po pełnym wyrenderowaniu (WPF-UI kończy malować krawędź po Loaded) — asekuracja pierwszej klatki.
window.Dispatcher.BeginInvoke(new Action(() => Apply(window)),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
}

private const int WM_NCACTIVATE = 0x0086;

// Przy każdym WM_NCACTIVATE ponownie nakłada bieżącą specyfikację obwódki — zanim klatka trafi na ekran.
// Przy każdym WM_NCACTIVATE ponownie nakłada bieżącą specyfikację obwódki: raz SYNCHRONICZNIE
// (ogranicza błysk — zanim klatka trafi na ekran) i raz ODROCZONO (ApplicationIdle).
// Odroczone dobicie jest KONIECZNE: WPF-UI przemalowuje krawędź na akcent PÓŹNIEJ w tym samym
// cyklu aktywacji (po zdarzeniu Activated), więc sam synchroniczny zapis przegrywa „ostatnie
// słowo" i obwódka ZOSTAWAŁA kobaltowa na stałe (regresja z #183, która to dobicie usunęła).
private static IntPtr BorderHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == WM_NCACTIVATE && hwnd != IntPtr.Zero)
{
uint val = SpecToColorRef(_spec);
try { DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, ref val, sizeof(uint)); }
catch { /* starszy DWM bez atrybutu — bez znaczenia */ }
WriteBorder(hwnd);
var h = hwnd; // kopia do domknięcia (hwnd to parametr ref-świata WndProc)
Application.Current?.Dispatcher.BeginInvoke(new Action(() => WriteBorder(h)),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
}
return IntPtr.Zero;
}

// Zapis bieżącej specyfikacji wprost na hwnd (okno mogło już nie mieć obiektu Window — np. w trakcie
// zamykania; nieaktualny uchwyt jest nieszkodliwy, DWM zwróci błąd, który ignorujemy).
private static void WriteBorder(IntPtr hwnd)
{
uint val = SpecToColorRef(_spec);
try { DwmSetWindowAttribute(hwnd, DWMWA_BORDER_COLOR, ref val, sizeof(uint)); }
catch { /* starszy DWM bez atrybutu — bez znaczenia */ }
}

// "" → brak; "System"/"default" → systemowy akcent; "#RRGGBB" → COLORREF (0x00BBGGRR); błędny → brak.
private static uint SpecToColorRef(string spec)
{
Expand Down
Loading