fix(startup): hide the FMX taskbar proxy the instant it is created

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 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-27 12:28:29 +01:00
parent 283676224f
commit 8fa956f07f
+48 -1
View File
@@ -84,6 +84,11 @@ type
// restore's resize is indistinguishable from a user gesture by placement // restore's resize is indistinguishable from a user gesture by placement
// flags or foreground ownership — both were measured and rejected). // flags or foreground ownership — both were measured and rejected).
FWinEventHook: THandle; 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 // True from "minimise starts" until just after the restore settles, so the
// resizes FMX fires while restoring are not recorded as user intent. // resizes FMX fires while restoring are not recorded as user intent.
FIgnoreResizes: Boolean; FIgnoreResizes: Boolean;
@@ -195,6 +200,11 @@ function MaskAccessToken(const AUrl: string): string; forward;
// ---- WinEvent hook plumbing (see the maximised-state block further down) -- // ---- WinEvent hook plumbing (see the maximised-state block further down) --
const 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_MINIMIZESTART = $0016;
EVENT_SYSTEM_MINIMIZEEND = $0017; EVENT_SYSTEM_MINIMIZEEND = $0017;
WINEVENT_OUTOFCONTEXT = $0000; WINEVENT_OUTOFCONTEXT = $0000;
@@ -207,8 +217,16 @@ function UnhookWinEvent(hWinEventHook: THandle): BOOL;
stdcall; external 'user32.dll' name 'UnhookWinEvent'; stdcall; external 'user32.dll' name 'UnhookWinEvent';
// Defined lower down, next to the window-state logic it serves. // 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; procedure WinStateEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward; 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); procedure TMainForm.FormCreate(Sender: TObject);
begin begin
@@ -223,7 +241,12 @@ begin
begin begin
Visible := False; Visible := False;
OnActivate := TrayStartupHideOnce; 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'); TrayHideNow('hide#1 top-of-FormCreate');
end; end;
@@ -338,6 +361,12 @@ begin
procedure procedure
begin begin
FBridge.MinimizeToTray; FBridge.MinimizeToTray;
GTrayStartupPending := False;
if FShowEventHook <> 0 then
begin
UnhookWinEvent(FShowEventHook);
FShowEventHook := 0;
end;
LogLine('[tray] deferred MinimizeToTray ran at ' + LogLine('[tray] deferred MinimizeToTray ran at ' +
FormatDateTime('hh:nn:ss.zzz', Now)); FormatDateTime('hh:nn:ss.zzz', Now));
end); end);
@@ -351,6 +380,11 @@ begin
UnhookWinEvent(FWinEventHook); UnhookWinEvent(FWinEventHook);
FWinEventHook := 0; FWinEventHook := 0;
end; end;
if FShowEventHook <> 0 then
begin
UnhookWinEvent(FShowEventHook);
FShowEventHook := 0;
end;
if Assigned(FFileSaveChunks) then if Assigned(FFileSaveChunks) then
begin begin
for var LSB in FFileSaveChunks.Values do LSB.Free; for var LSB in FFileSaveChunks.Values do LSB.Free;
@@ -1608,6 +1642,19 @@ begin
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd; else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
end; 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); procedure TMainForm.TrayHideNow(const ATag: string);
var var
LHwnd, LAppHwnd: HWND; LHwnd, LAppHwnd: HWND;