fix(startup): revert the .dpr hide (AV) and hide on first activate instead

The .dpr version crashed with an EAccessViolation writing to 000000CE —
touching MainForm.Visible/Handle between CreateForm and Run is evidently too
early for FMX. Reverted; the app builds and runs as before.

Replaced with a one-shot OnActivate handler installed in FormCreate only for
--tray: it fires the instant FMX shows the form, hides it (FMX + Win32) and
unhooks itself, so it can never fight the user later. That runs far earlier
than the deferred MinimizeToTray, which waits for the message loop and hence
for the DB open, server start and WebView2 init — the second the window used
to spend on screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-27 05:42:04 +01:00
parent 169cdddf9d
commit eec7bc4f80
2 changed files with 25 additions and 24 deletions
-16
View File
@@ -1,11 +1,8 @@
program PMServer; program PMServer;
uses uses
Winapi.Windows,
System.SysUtils,
System.StartUpCopy, System.StartUpCopy,
FMX.Forms, FMX.Forms,
FMX.Platform.Win,
PM.SingleInstance in 'Source\PM.SingleInstance.pas', PM.SingleInstance in 'Source\PM.SingleInstance.pas',
UMainForm in 'UMainForm.pas' {MainForm}, UMainForm in 'UMainForm.pas' {MainForm},
PM.JSON in 'Source\PM.JSON.pas', PM.JSON in 'Source\PM.JSON.pas',
@@ -47,18 +44,5 @@ begin
Application.Initialize; Application.Initialize;
Application.CreateForm(TMainForm, MainForm); Application.CreateForm(TMainForm, MainForm);
// --tray ("Start with Windows"): keep the window off screen entirely.
// FormCreate already sets Visible := False, but FMX re-shows the main form
// after the constructor returns, which is why the window still flashed for
// ~1 s during the Windows logon. Doing it HERE — after CreateForm, before
// Run pumps any message — is the point where it sticks. The handle still
// exists, so WebView2 initialises normally and the vault is ready the
// moment the user clicks the tray icon.
if FindCmdLineSwitch('tray', True) then
begin
MainForm.Visible := False;
if MainForm.Handle <> nil then
ShowWindow(WindowHandleToPlatform(MainForm.Handle).Wnd, SW_HIDE);
end;
Application.Run; Application.Run;
end. end.
+24 -7
View File
@@ -171,6 +171,9 @@ type
// our own message queue, so these already run on the main thread. // our own message queue, so these already run on the main thread.
// Every tray restore goes through here so the minimise guard cannot get // Every tray restore goes through here so the minimise guard cannot get
// stuck raised (see the implementation). // stuck raised (see the implementation).
// One-shot: hides the window the first time FMX shows it on a --tray
// launch, then unhooks itself.
procedure TrayStartupHideOnce(Sender: TObject);
procedure RestoreWindowFromTray; procedure RestoreWindowFromTray;
procedure WinStateMinimizeStart; procedure WinStateMinimizeStart;
procedure WinStateMinimizeEnd; procedure WinStateMinimizeEnd;
@@ -206,15 +209,18 @@ procedure WinStateEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
procedure TMainForm.FormCreate(Sender: TObject); procedure TMainForm.FormCreate(Sender: TObject);
begin begin
// --tray (the "Start with Windows" registry entry): never let the window // --tray ("Start with Windows"): get the window off screen as early as
// become visible at all. Hiding it later — even via ForceQueue — only runs // possible. Visible := False alone is not enough — FMX re-shows the main
// once the message loop is free, i.e. AFTER the DB open, the HTTP server // form after the constructor returns — so OnActivate hides it again the
// start and the WebView2 init have finished, so the window sat on screen // instant it appears. That fires on the very first show, long before the
// for a second or two during the Windows logon. Setting Visible here, before // deferred MinimizeToTray below, which only runs once the message loop is
// any of that, means there is nothing to flash. MinimizeToTray still runs // free (i.e. after the DB open, the server start and the WebView2 init:
// below to add the tray icon and drop the taskbar entry. // the second the window used to sit on screen during the Windows logon).
if FindCmdLineSwitch('tray', True) then if FindCmdLineSwitch('tray', True) then
begin
Visible := False; Visible := False;
OnActivate := TrayStartupHideOnce;
end;
// The .fmx ships WindowState = wsMaximized, so that is the starting intent. // The .fmx ships WindowState = wsMaximized, so that is the starting intent.
// Wired here (not in the .fmx) to keep the designer file untouched. // Wired here (not in the .fmx) to keep the designer file untouched.
@@ -1592,6 +1598,17 @@ begin
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd; else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
end; end;
procedure TMainForm.TrayStartupHideOnce(Sender: TObject);
var
LHwnd: HWND;
begin
OnActivate := nil; // one shot — never fight the user afterwards
Visible := False;
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if LHwnd <> 0 then
ShowWindow(LHwnd, SW_HIDE);
end;
// Single entry point for every tray restore. ExecuteAutofill really minimises // Single entry point for every tray restore. ExecuteAutofill really minimises
// the window (SW_MINIMIZE) before typing, which raises the guard; if the window // the window (SW_MINIMIZE) before typing, which raises the guard; if the window
// is then HIDDEN to the tray instead of being un-minimised, MINIMIZEEND may // is then HIDDEN to the tray instead of being un-minimised, MINIMIZEEND may