fix(ui): obwódka „systemowa" nie zostaje kobaltowa — dobicie po repaintach WPF-UI (regresja #183)#186
Conversation
…ne dobicie po repaintach WPF-UI Regresja z #183: usunięto handler Activated z odroczonym (ApplicationIdle) ponownym nałożeniem obwódki, zostawiając wyłącznie SYNCHRONICZNY zapis w hooku WM_NCACTIVATE. Problem w tym, że WM_NCACTIVATE leci PRZED przemalowaniem krawędzi na akcent przez WPF-UI (dzieje się po Activated), więc nasz zapis przegrywał „ostatnie słowo" — obwódka zostawała kobaltowa NA STAŁE mimo ustawienia „systemowa"/„brak". Po zmianie ustawień było chwilowo dobrze (ReapplyAll ma własne odroczone dobicie), do pierwszego cyklu (de)aktywacji. Stąd objaw „nie błyska, tylko zostaje". Fix — obie krawędzie cyklu, zamiast wyboru jednej: - zapis synchroniczny w BorderHook zostaje (ogranicza błysk z #183), - BorderHook dodatkowo kolejkuje dobicie na ApplicationIdle (pisze wprost po hwnd — działa też dla deaktywacji i okien w trakcie zamykania), - Keep przywraca sprawdzony sprzed #183 handler Activated (Apply teraz + dobicie na ApplicationIdle) — to on latami gwarantował, że nasza obwódka ląduje PO repaintach WPF-UI. Koszt: kilka zapisów jednego atrybutu DWM na cykl aktywacji — pomijalny. Weryfikacja ręczna (WPF/Win11): ustaw obwódkę „systemowa" → klikaj między oknami/otwórz i zamknij dialog — obwódka pozostaje systemowa (bez trwałego kobaltu); to samo dla „brak" i koloru własnego; zmiana motywu/akcentu nie przywraca kobaltu. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
AI Code Review by LlamaPReview
🎯 TL;DR & Recommendation
Recommendation: Approve with suggestions.
The fix correctly addresses the permanent accent color regression by re-introducing deferred DWM writes after WPF-UI repaints, ensuring the system border remains system.
💡 Suggestions (P2)
- src/RdpManager/WindowBorder.cs: Each call to
Keepattaches a newActivatedhandler without removing previous ones, causing duplicate subscriptions and redundant DWM writes on every activation. This results in unnecessary overhead and could lead to future behavioral confusion. Consider managing subscriptions with aConditionalWeakTableto ensure a single handler per window.
💡 Have feedback? We'd love to hear it in our GitHub Discussions.
✨ This review was generated by LlamaPReview Advanced, which is free for all open-source projects. Learn more.
| window.Activated += (_, __) => | ||
| { | ||
| Apply(window); | ||
| window.Dispatcher.BeginInvoke(new Action(() => Apply(window)), | ||
| System.Windows.Threading.DispatcherPriority.ApplicationIdle); | ||
| }; |
There was a problem hiding this comment.
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
}
Objaw (regresja po #183)
Obwódka okna zostaje kobaltowa na stałe mimo ustawienia „systemowa". Po zmianie ustawień przez chwilę jest poprawnie, potem wraca kobalt — i nie błyska, tylko zostaje.
Root cause
Sekwencja cyklu aktywacji okna:
WM_NCACTIVATE→ nasz synchroniczny zapis obwódki (dodany w fix(ui): obwódka okna nie błyska na kobalt przy starcie i aktywacji #183),Activated→ WPF-UI przemalowuje krawędź na akcent (kobalt) później w tym samym cyklu.#183 usunął jedyny mechanizm, który pisał po repaintach WPF-UI — handler
Activatedz odroczonym (ApplicationIdle) ponownym nałożeniem. Został tylko zapis synchroniczny, który leci przed zapisem WPF-UI → ich kobalt ma zawsze ostatnie słowo → obwódka zostaje kobaltowa na stałe. To dokładnie tłumaczy objawy:ReapplyAllma własne odroczone dobicie; psuje się przy pierwszym kolejnym cyklu (de)aktywacji.Że WPF-UI faktycznie maluje krawędź akcentem, potwierdzają i historia projektu (#49, stare komentarze: „Sam Apply w handlerze aktywacji bywa ZA wcześnie — repaint WPF-UI leci później"), i metadane
Wpf.Ui.dll4.3 (BORDER_COLOR+DwmSetWindowAttribute+OnActivated). W 4.3 nie ma przełącznika, by to wyłączyć u źródła.Fix — obie krawędzie cyklu zamiast wyboru jednej
BorderHookzostaje (to on ograniczał błysk — cel fix(ui): obwódka okna nie błyska na kobalt przy starcie i aktywacji #183),BorderHookdodatkowo kolejkuje dobicie naApplicationIdle(pisze wprost pohwnd— pokrywa też deaktywację i okna w trakcie zamykania; nieaktualny uchwyt = zignorowany błąd DWM),Keepprzywraca sprawdzony sprzed fix(ui): obwódka okna nie błyska na kobalt przy starcie i aktywacji #183 handlerActivated(Apply teraz + dobicie naApplicationIdle) — ta ścieżka tygodniami gwarantowała, że wybrana obwódka ląduje po repaintach WPF-UI.Koszt: kilka zapisów jednego atrybutu DWM (
DwmSetWindowAttribute) na cykl aktywacji — pomijalny. Możliwy powrót krótkiego mgnienia akcentu na krawędzi w rzadkich przypadkach (odstęp między zapisem WPF-UI a naszym dobiciem) — to stan sprzed #183; trwałe zafałszowanie koloru jest naprawione, a zapis synchroniczny osłania wiodącą krawędź cyklu.Zakres
Tylko
WindowBorder.cs(+33/−9). Build 0 błędów, testy 347/347.Weryfikacja ręczna (WPF / Windows 11)
🤖 Generated with Claude Code