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:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user