From 8fa956f07fc94105976a3bbf577dfae41c236f8a Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:28:29 +0100 Subject: [PATCH] fix(startup): hide the FMX taskbar proxy the instant it is created MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The instrumented run pinned it down: the FORM is never visible (form_visible_was=False at all four hide points). What shows is the FMX taskbar proxy — app=0 at 20.145, app=854682 at 20.974 — so it is created partway through FormCreate and stayed on screen for the best part of a second until the first hide that could see it. A WinEvent hook on EVENT_OBJECT_SHOW, active only for the --tray startup, hides any top-level window of ours the moment Windows shows it, and is torn down once MinimizeToTray has run. Two details that would each have broken it: - WINEVENT_INCONTEXT, not OUTOFCONTEXT: the latter queues the callback until the thread pumps messages, which does not happen during FormCreate — exactly when the proxy appears. - The pending flag is a unit-level var, not a form field: CreateForm assigns the MainForm global only after the constructor returns, so a callback firing during FormCreate could not have reached the form. Co-Authored-By: Claude Opus 4.8 --- delphi-backend/UMainForm.pas | 49 +++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index d6ce83a..cabb1f5 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -84,6 +84,11 @@ type // restore's resize is indistinguishable from a user gesture by placement // flags or foreground ownership — both were measured and rejected). FWinEventHook: THandle; + // --tray only: catches EVENT_OBJECT_SHOW so any top-level window of ours + // is hidden the instant Windows shows it. The FMX taskbar proxy is created + // partway through FormCreate — it does not exist at the first hide points + // and was therefore visible for the best part of a second. + FShowEventHook: THandle; // True from "minimise starts" until just after the restore settles, so the // resizes FMX fires while restoring are not recorded as user intent. FIgnoreResizes: Boolean; @@ -195,6 +200,11 @@ function MaskAccessToken(const AUrl: string): string; forward; // ---- WinEvent hook plumbing (see the maximised-state block further down) -- const + EVENT_OBJECT_SHOW = $8002; + // Synchronous delivery, in our own process. OUTOFCONTEXT would queue the + // callback until the thread pumps messages — which does not happen during + // FormCreate, exactly when we need to react. + WINEVENT_INCONTEXT = $0004; EVENT_SYSTEM_MINIMIZESTART = $0016; EVENT_SYSTEM_MINIMIZEEND = $0017; WINEVENT_OUTOFCONTEXT = $0000; @@ -207,8 +217,16 @@ function UnhookWinEvent(hWinEventHook: THandle): BOOL; stdcall; external 'user32.dll' name 'UnhookWinEvent'; // Defined lower down, next to the window-state logic it serves. +// Unit-level, NOT a form field: Application.CreateForm only assigns MainForm +// after the constructor returns, so during FormCreate the global is still nil +// and the callback could not reach the form at all. +var + GTrayStartupPending: Boolean = False; + procedure WinStateEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND; idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward; +procedure TrayShowEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND; + idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward; procedure TMainForm.FormCreate(Sender: TObject); begin @@ -223,7 +241,12 @@ begin begin Visible := False; OnActivate := TrayStartupHideOnce; - LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now)); + GTrayStartupPending := True; + FShowEventHook := SetWinEventHook( + EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, 0, + @TrayShowEventProc, GetCurrentProcessId, 0, WINEVENT_INCONTEXT); + LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now) + + ' show-hook=' + BoolToStr(FShowEventHook <> 0, True)); TrayHideNow('hide#1 top-of-FormCreate'); end; @@ -338,6 +361,12 @@ begin procedure begin FBridge.MinimizeToTray; + GTrayStartupPending := False; + if FShowEventHook <> 0 then + begin + UnhookWinEvent(FShowEventHook); + FShowEventHook := 0; + end; LogLine('[tray] deferred MinimizeToTray ran at ' + FormatDateTime('hh:nn:ss.zzz', Now)); end); @@ -351,6 +380,11 @@ begin UnhookWinEvent(FWinEventHook); FWinEventHook := 0; end; + if FShowEventHook <> 0 then + begin + UnhookWinEvent(FShowEventHook); + FShowEventHook := 0; + end; if Assigned(FFileSaveChunks) then begin for var LSB in FFileSaveChunks.Values do LSB.Free; @@ -1608,6 +1642,19 @@ begin else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd; end; +// While a --tray launch is still starting up, every top-level window our +// process shows gets hidden again immediately. Scoped to our process by the +// hook itself; WebView2 runs in separate processes so its windows never reach +// here. Torn down as soon as startup finishes. +procedure TrayShowEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND; + idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; +begin + if not GTrayStartupPending then Exit; + if (idObject <> OBJID_WINDOW) or (AWnd = 0) then Exit; + if GetParent(AWnd) <> 0 then Exit; // top-level windows only + ShowWindow(AWnd, SW_HIDE); +end; + procedure TMainForm.TrayHideNow(const ATag: string); var LHwnd, LAppHwnd: HWND;