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;
|
program PMServer;
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
System.SysUtils, // FindCmdLineSwitch
|
||||||
System.StartUpCopy,
|
System.StartUpCopy,
|
||||||
FMX.Forms,
|
FMX.Forms,
|
||||||
PM.SingleInstance in 'Source\PM.SingleInstance.pas',
|
PM.SingleInstance in 'Source\PM.SingleInstance.pas',
|
||||||
@@ -43,6 +44,13 @@ begin
|
|||||||
Exit;
|
Exit;
|
||||||
|
|
||||||
Application.Initialize;
|
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.CreateForm(TMainForm, MainForm);
|
||||||
Application.Run;
|
Application.Run;
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -127,4 +127,10 @@ object MainForm: TMainForm
|
|||||||
Left = 424
|
Left = 424
|
||||||
Top = 240
|
Top = 240
|
||||||
end
|
end
|
||||||
|
object Timer1: TTimer
|
||||||
|
Interval = 1
|
||||||
|
OnTimer = Timer1Timer
|
||||||
|
Left = 568
|
||||||
|
Top = 184
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ type
|
|||||||
Memo: TMemo;
|
Memo: TMemo;
|
||||||
Splitter: TSplitter;
|
Splitter: TSplitter;
|
||||||
PopupMenu1: TPopupMenu;
|
PopupMenu1: TPopupMenu;
|
||||||
|
Timer1: TTimer;
|
||||||
procedure FormCreate(Sender: TObject);
|
procedure FormCreate(Sender: TObject);
|
||||||
procedure FormDestroy(Sender: TObject);
|
procedure FormDestroy(Sender: TObject);
|
||||||
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
|
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
|
||||||
@@ -71,6 +72,7 @@ type
|
|||||||
procedure btnStopClick(Sender: TObject);
|
procedure btnStopClick(Sender: TObject);
|
||||||
procedure btnToggleLogClick(Sender: TObject);
|
procedure btnToggleLogClick(Sender: TObject);
|
||||||
procedure btnReloadClick(Sender: TObject);
|
procedure btnReloadClick(Sender: TObject);
|
||||||
|
procedure Timer1Timer(Sender: TObject);
|
||||||
private
|
private
|
||||||
// Windows minimising the window from the outside (Win+D / Show desktop /
|
// Windows minimising the window from the outside (Win+D / Show desktop /
|
||||||
// taskbar) makes FMX forget it was maximised: the window comes back
|
// taskbar) makes FMX forget it was maximised: the window comes back
|
||||||
@@ -190,6 +192,14 @@ type
|
|||||||
var
|
var
|
||||||
MainForm: TMainForm;
|
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
|
implementation
|
||||||
|
|
||||||
{$R *.fmx}
|
{$R *.fmx}
|
||||||
@@ -229,6 +239,29 @@ procedure TrayShowEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
|||||||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
||||||
function FindOurFMXAppWindow: HWND; 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);
|
procedure TMainForm.FormCreate(Sender: TObject);
|
||||||
begin
|
begin
|
||||||
// --tray ("Start with Windows"): get the window off screen as early as
|
// --tray ("Start with Windows"): get the window off screen as early as
|
||||||
@@ -248,29 +281,9 @@ begin
|
|||||||
@TrayShowEventProc, GetCurrentProcessId, 0, WINEVENT_INCONTEXT);
|
@TrayShowEventProc, GetCurrentProcessId, 0, WINEVENT_INCONTEXT);
|
||||||
LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now) +
|
LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now) +
|
||||||
' show-hook=' + BoolToStr(FShowEventHook <> 0, True));
|
' show-hook=' + BoolToStr(FShowEventHook <> 0, True));
|
||||||
// The decisive piece. Everything else runs on the main thread, which is
|
// The proxy watcher is NOT started here: FMX shows the taskbar proxy
|
||||||
// BUSY inside FormCreate and pumps no message — so no hook, timer or
|
// before FormCreate is even reached, so it is launched from the .dpr
|
||||||
// queued call can fire while the FMX taskbar proxy is created and shown.
|
// (StartTrayProxyHider) right after Application.Initialize.
|
||||||
// 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;
|
|
||||||
TrayHideNow('hide#1 top-of-FormCreate');
|
TrayHideNow('hide#1 top-of-FormCreate');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1709,7 +1722,12 @@ begin
|
|||||||
ShowWindow(AWnd, SW_HIDE);
|
ShowWindow(AWnd, SW_HIDE);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TMainForm.TrayHideNow(const ATag: string);
|
pprocedure TMainForm.Timer1Timer(Sender: TObject);
|
||||||
|
begin
|
||||||
|
|
||||||
|
end;
|
||||||
|
|
||||||
|
rocedure TMainForm.TrayHideNow(const ATag: string);
|
||||||
var
|
var
|
||||||
LHwnd, LAppHwnd: HWND;
|
LHwnd, LAppHwnd: HWND;
|
||||||
LWasVisible: Boolean;
|
LWasVisible: Boolean;
|
||||||
@@ -1730,6 +1748,11 @@ begin
|
|||||||
OnActivate := nil; // one shot — never fight the user afterwards
|
OnActivate := nil; // one shot — never fight the user afterwards
|
||||||
Visible := False;
|
Visible := False;
|
||||||
TrayHideNow('hide#4 on-activate');
|
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;
|
end;
|
||||||
|
|
||||||
// Single entry point for every tray restore. ExecuteAutofill really minimises
|
// Single entry point for every tray restore. ExecuteAutofill really minimises
|
||||||
|
|||||||
Reference in New Issue
Block a user