fix(startup): start the proxy watcher from the .dpr, before the form exists

Decisive observation: the taskbar button is already up before FormCreate is
entered. Everything I had tried lived inside the form, so all of it ran after
the button was drawn — the watcher included, which is why moving it around
never helped.

StartTrayProxyHider is now exported from UMainForm and called from the .dpr
right after Application.Initialize, before CreateForm. It polls every 5 ms and
hides our TFMAppClass proxy unconditionally (the button can be up before
WS_VISIBLE reads as set, which the previous IsWindowVisible check would have
skipped), stopping as soon as the tray startup completes so a user restoring
from the tray is never fought.

Raw Win32 only — FindWindowEx + ShowWindow, no FMX object touched at that
point, which is what made the earlier .dpr attempt crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-27 13:15:32 +01:00
parent 4f2aff9de4
commit ab8186ef75
3 changed files with 61 additions and 24 deletions
+47 -24
View File
@@ -64,6 +64,7 @@ type
Memo: TMemo;
Splitter: TSplitter;
PopupMenu1: TPopupMenu;
Timer1: TTimer;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
@@ -71,6 +72,7 @@ type
procedure btnStopClick(Sender: TObject);
procedure btnToggleLogClick(Sender: TObject);
procedure btnReloadClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
// Windows minimising the window from the outside (Win+D / Show desktop /
// taskbar) makes FMX forget it was maximised: the window comes back
@@ -190,6 +192,14 @@ type
var
MainForm: TMainForm;
// Call from the .dpr right after Application.Initialize, BEFORE CreateForm,
// for a --tray launch. FMX shows its taskbar proxy before FormCreate ever
// runs — measured — so anything installed from inside the form is already too
// late. Starts a worker that hides the proxy the instant it exists and keeps
// watching until the tray startup completes; the main thread is blocked
// loading the .fmx and starting the server, so only another thread can react.
procedure StartTrayProxyHider;
implementation
{$R *.fmx}
@@ -229,6 +239,29 @@ procedure TrayShowEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
function FindOurFMXAppWindow: HWND; forward;
procedure StartTrayProxyHider;
begin
GTrayStartupPending := True;
TThread.CreateAnonymousThread(
procedure
var
LWnd: HWND;
LTicks: Integer;
begin
LTicks := 0;
// 5 ms cadence, 20 s ceiling. Stops as soon as the tray startup marks
// itself done, so a user restoring from the tray is never fought.
while GTrayStartupPending and (LTicks < 4000) do
begin
LWnd := FindOurFMXAppWindow;
if LWnd <> 0 then
ShowWindow(LWnd, SW_HIDE); // unconditional: the button can be up
Sleep(5); // before WS_VISIBLE reads as set
Inc(LTicks);
end;
end).Start;
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
// --tray ("Start with Windows"): get the window off screen as early as
@@ -248,29 +281,9 @@ begin
@TrayShowEventProc, GetCurrentProcessId, 0, WINEVENT_INCONTEXT);
LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now) +
' show-hook=' + BoolToStr(FShowEventHook <> 0, True));
// The decisive piece. Everything else runs on the main thread, which is
// BUSY inside FormCreate and pumps no message — so no hook, timer or
// queued call can fire while the FMX taskbar proxy is created and shown.
// A worker thread is the only thing still running then. It polls for the
// proxy and hides it the moment it appears; ShowWindow is safe to call
// cross-thread. Stops on GTrayStartupPending or after 10 s, whichever
// comes first, so it can never linger.
TThread.CreateAnonymousThread(
procedure
var
LWnd: HWND;
LTicks: Integer;
begin
LTicks := 0;
while GTrayStartupPending and (LTicks < 1000) do
begin
LWnd := FindOurFMXAppWindow;
if (LWnd <> 0) and IsWindowVisible(LWnd) then
ShowWindow(LWnd, SW_HIDE);
Sleep(10);
Inc(LTicks);
end;
end).Start;
// The proxy watcher is NOT started here: FMX shows the taskbar proxy
// before FormCreate is even reached, so it is launched from the .dpr
// (StartTrayProxyHider) right after Application.Initialize.
TrayHideNow('hide#1 top-of-FormCreate');
end;
@@ -1709,7 +1722,12 @@ begin
ShowWindow(AWnd, SW_HIDE);
end;
procedure TMainForm.TrayHideNow(const ATag: string);
pprocedure TMainForm.Timer1Timer(Sender: TObject);
begin
end;
rocedure TMainForm.TrayHideNow(const ATag: string);
var
LHwnd, LAppHwnd: HWND;
LWasVisible: Boolean;
@@ -1730,6 +1748,11 @@ begin
OnActivate := nil; // one shot — never fight the user afterwards
Visible := False;
TrayHideNow('hide#4 on-activate');
var AppHandle := ApplicationHWND;
ShowWindow(AppHandle, SW_HIDE);
SetWindowLong(AppHandle, GWL_EXSTYLE,
GetWindowLong(AppHandle, GWL_EXSTYLE) and (not WS_EX_APPWINDOW) or WS_EX_TOOLWINDOW);
end;
// Single entry point for every tray restore. ExecuteAutofill really minimises