diff --git a/delphi-backend/PMServer.dpr b/delphi-backend/PMServer.dpr index b1b5730..d664fd3 100644 --- a/delphi-backend/PMServer.dpr +++ b/delphi-backend/PMServer.dpr @@ -1,6 +1,7 @@ program PMServer; uses + System.SysUtils, // FindCmdLineSwitch System.StartUpCopy, FMX.Forms, PM.SingleInstance in 'Source\PM.SingleInstance.pas', @@ -43,6 +44,13 @@ begin Exit; Application.Initialize; + // --tray ("Start with Windows"): FMX puts its taskbar proxy on screen before + // FormCreate is ever reached — measured — so the watcher has to start here, + // before the form is built. It only touches raw Win32 (FindWindowEx + + // ShowWindow), never an FMX object: an earlier attempt that poked + // MainForm.Visible/Handle at this point died with an access violation. + if FindCmdLineSwitch('tray', True) then + StartTrayProxyHider; Application.CreateForm(TMainForm, MainForm); Application.Run; end. diff --git a/delphi-backend/UMainForm.fmx b/delphi-backend/UMainForm.fmx index e92c557..80800bc 100644 --- a/delphi-backend/UMainForm.fmx +++ b/delphi-backend/UMainForm.fmx @@ -127,4 +127,10 @@ object MainForm: TMainForm Left = 424 Top = 240 end + object Timer1: TTimer + Interval = 1 + OnTimer = Timer1Timer + Left = 568 + Top = 184 + end end diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 95df071..072db7e 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -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