fix(startup): use ApplicationHWND + WS_EX_TOOLWINDOW, restore it on show
Adopts the approach that actually works: ApplicationHWND (FMX.Platform.Win) IS the taskbar proxy, so there is nothing to hunt for and it is valid before the form exists. And hiding it was never enough — the taskbar had already drawn the button; clearing WS_EX_APPWINDOW and setting WS_EX_TOOLWINDOW is what removes it, since a tool window is never listed. That style is permanent, which is why the button never came back after a restore. TrayProxyShowInTaskbar puts it back, called from RestoreWindowFromTray — the single choke point every tray restore already goes through — so it lands before RestoreFromTray shows the windows and the style takes effect on that show. Removes the scaffolding this chase accumulated and that the real fix makes pointless: the EVENT_OBJECT_SHOW hook, the polling worker, FindOurFMXAppWindow, TrayHideNow with its four probes, the one-shot OnActivate hide, and the [tray] tracing. ~4.8 KB of dead code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
program PMServer;
|
||||
program PMServer;
|
||||
|
||||
uses
|
||||
System.SysUtils, // FindCmdLineSwitch
|
||||
|
||||
@@ -204,6 +204,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
||||
<Icon_MainIcon>PMServer_Icon1.ico</Icon_MainIcon>
|
||||
<UWP_DelphiLogo44>app.png</UWP_DelphiLogo44>
|
||||
<UWP_DelphiLogo150>app.png</UWP_DelphiLogo150>
|
||||
<Debugger_RunParams>-tray</Debugger_RunParams>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<DelphiCompile Include="$(MainSource)">
|
||||
|
||||
@@ -127,10 +127,4 @@ object MainForm: TMainForm
|
||||
Left = 424
|
||||
Top = 240
|
||||
end
|
||||
object Timer1: TTimer
|
||||
Interval = 1
|
||||
OnTimer = Timer1Timer
|
||||
Left = 568
|
||||
Top = 184
|
||||
end
|
||||
end
|
||||
|
||||
+25
-144
@@ -64,7 +64,6 @@ type
|
||||
Memo: TMemo;
|
||||
Splitter: TSplitter;
|
||||
PopupMenu1: TPopupMenu;
|
||||
Timer1: TTimer;
|
||||
procedure FormCreate(Sender: TObject);
|
||||
procedure FormDestroy(Sender: TObject);
|
||||
procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
|
||||
@@ -72,7 +71,6 @@ 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
|
||||
@@ -86,11 +84,6 @@ type
|
||||
// restore's resize is indistinguishable from a user gesture by placement
|
||||
// flags or foreground ownership — both were measured and rejected).
|
||||
FWinEventHook: THandle;
|
||||
// --tray only: catches EVENT_OBJECT_SHOW so any top-level window of ours
|
||||
// is hidden the instant Windows shows it. The FMX taskbar proxy is created
|
||||
// partway through FormCreate — it does not exist at the first hide points
|
||||
// and was therefore visible for the best part of a second.
|
||||
FShowEventHook: THandle;
|
||||
// True from "minimise starts" until just after the restore settles, so the
|
||||
// resizes FMX fires while restoring are not recorded as user intent.
|
||||
FIgnoreResizes: Boolean;
|
||||
@@ -180,10 +173,6 @@ type
|
||||
// stuck raised (see the implementation).
|
||||
// One-shot: hides the window the first time FMX shows it on a --tray
|
||||
// launch, then unhooks itself.
|
||||
// Hides the form AND the FMX taskbar proxy right now, logging whether the
|
||||
// window was actually visible — that is what pins down when FMX shows it.
|
||||
procedure TrayHideNow(const ATag: string);
|
||||
procedure TrayStartupHideOnce(Sender: TObject);
|
||||
procedure RestoreWindowFromTray;
|
||||
procedure WinStateMinimizeStart;
|
||||
procedure WinStateMinimizeEnd;
|
||||
@@ -210,11 +199,6 @@ function MaskAccessToken(const AUrl: string): string; forward;
|
||||
|
||||
// ---- WinEvent hook plumbing (see the maximised-state block further down) --
|
||||
const
|
||||
EVENT_OBJECT_SHOW = $8002;
|
||||
// Synchronous delivery, in our own process. OUTOFCONTEXT would queue the
|
||||
// callback until the thread pumps messages — which does not happen during
|
||||
// FormCreate, exactly when we need to react.
|
||||
WINEVENT_INCONTEXT = $0004;
|
||||
EVENT_SYSTEM_MINIMIZESTART = $0016;
|
||||
EVENT_SYSTEM_MINIMIZEEND = $0017;
|
||||
WINEVENT_OUTOFCONTEXT = $0000;
|
||||
@@ -226,66 +210,39 @@ function SetWinEventHook(eventMin, eventMax: DWORD; hmodWinEventProc: HMODULE;
|
||||
function UnhookWinEvent(hWinEventHook: THandle): BOOL;
|
||||
stdcall; external 'user32.dll' name 'UnhookWinEvent';
|
||||
|
||||
// Defined lower down, next to the window-state logic it serves.
|
||||
// Unit-level, NOT a form field: Application.CreateForm only assigns MainForm
|
||||
// after the constructor returns, so during FormCreate the global is still nil
|
||||
// and the callback could not reach the form at all.
|
||||
var
|
||||
GTrayStartupPending: Boolean = False;
|
||||
|
||||
procedure WinStateEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
||||
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);
|
||||
// ApplicationHWND (FMX.Platform.Win) IS the taskbar proxy — no hunting for a
|
||||
// TFMAppClass window, and it is valid this early, before the form exists.
|
||||
ShowWindow(ApplicationHWND, SW_HIDE);
|
||||
// Hiding alone does not reliably drop the button: the taskbar had already
|
||||
// drawn it. Clearing WS_EX_APPWINDOW and setting WS_EX_TOOLWINDOW does —
|
||||
// a tool window is never listed. Undone by TrayProxyShowInTaskbar before any
|
||||
// restore, otherwise the app would come back with no taskbar button at all.
|
||||
SetWindowLong(ApplicationHWND, GWL_EXSTYLE,
|
||||
(GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW))
|
||||
or WS_EX_TOOLWINDOW);
|
||||
end;
|
||||
end).Start;
|
||||
|
||||
// Give the proxy its normal styles back. Takes effect on the next show, which
|
||||
// is exactly what RestoreFromTray does right after.
|
||||
procedure TrayProxyShowInTaskbar;
|
||||
begin
|
||||
SetWindowLong(ApplicationHWND, GWL_EXSTYLE,
|
||||
(GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_TOOLWINDOW))
|
||||
or WS_EX_APPWINDOW);
|
||||
end;
|
||||
|
||||
procedure TMainForm.FormCreate(Sender: TObject);
|
||||
begin
|
||||
// --tray ("Start with Windows"): get the window off screen as early as
|
||||
// possible. Visible := False alone is not enough — FMX re-shows the main
|
||||
// form after the constructor returns — so OnActivate hides it again the
|
||||
// instant it appears. That fires on the very first show, long before the
|
||||
// deferred MinimizeToTray below, which only runs once the message loop is
|
||||
// free (i.e. after the DB open, the server start and the WebView2 init:
|
||||
// the second the window used to sit on screen during the Windows logon).
|
||||
// --tray ("Start with Windows"): the taskbar proxy was already dealt with in
|
||||
// the .dpr (StartTrayProxyHider) — it is shown before FormCreate is even
|
||||
// reached. This just keeps the form itself off screen.
|
||||
if FindCmdLineSwitch('tray', True) then
|
||||
begin
|
||||
Visible := False;
|
||||
OnActivate := TrayStartupHideOnce;
|
||||
GTrayStartupPending := True;
|
||||
FShowEventHook := SetWinEventHook(
|
||||
EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, 0,
|
||||
@TrayShowEventProc, GetCurrentProcessId, 0, WINEVENT_INCONTEXT);
|
||||
LogLine('[tray] FormCreate begin at ' + FormatDateTime('hh:nn:ss.zzz', Now) +
|
||||
' show-hook=' + BoolToStr(FShowEventHook <> 0, True));
|
||||
// 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;
|
||||
|
||||
// The .fmx ships WindowState = wsMaximized, so that is the starting intent.
|
||||
// Wired here (not in the .fmx) to keep the designer file untouched.
|
||||
@@ -325,9 +282,6 @@ begin
|
||||
{$ENDIF}
|
||||
WebBrowser.Parent := Self;
|
||||
WebBrowser.Align := TAlignLayout.Client;
|
||||
// Parenting forces the form's window handle to exist, and that is the point
|
||||
// FMX may show it — hide again right away.
|
||||
if FindCmdLineSwitch('tray', True) then TrayHideNow('hide#2 after-browser-parent');
|
||||
|
||||
FServer := TPMHTTPServer.Create;
|
||||
FServer.OnLog := LogLine;
|
||||
@@ -404,21 +358,13 @@ begin
|
||||
// it's noise when Windows itself put us in the tray (the user
|
||||
// didn't actively minimise). Manual launches still see it once.
|
||||
FBridge.BalloonShown := True;
|
||||
TrayHideNow('hide#3 end-of-FormCreate');
|
||||
TThread.ForceQueue(nil,
|
||||
procedure
|
||||
begin
|
||||
FBridge.MinimizeToTray;
|
||||
// Hidden now — safe to pay the startup cost.
|
||||
btnStartClick(Nil);
|
||||
GTrayStartupPending := False;
|
||||
if FShowEventHook <> 0 then
|
||||
begin
|
||||
UnhookWinEvent(FShowEventHook);
|
||||
FShowEventHook := 0;
|
||||
end;
|
||||
LogLine('[tray] deferred MinimizeToTray ran at ' +
|
||||
FormatDateTime('hh:nn:ss.zzz', Now));
|
||||
LogLine('Launched with --tray, started hidden (balloon suppressed)');
|
||||
end);
|
||||
end;
|
||||
end;
|
||||
@@ -430,11 +376,6 @@ begin
|
||||
UnhookWinEvent(FWinEventHook);
|
||||
FWinEventHook := 0;
|
||||
end;
|
||||
if FShowEventHook <> 0 then
|
||||
begin
|
||||
UnhookWinEvent(FShowEventHook);
|
||||
FShowEventHook := 0;
|
||||
end;
|
||||
if Assigned(FFileSaveChunks) then
|
||||
begin
|
||||
for var LSB in FFileSaveChunks.Values do LSB.Free;
|
||||
@@ -1692,69 +1633,6 @@ begin
|
||||
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
|
||||
end;
|
||||
|
||||
// While a --tray launch is still starting up, every top-level window our
|
||||
// process shows gets hidden again immediately. Scoped to our process by the
|
||||
// hook itself; WebView2 runs in separate processes so its windows never reach
|
||||
// here. Torn down as soon as startup finishes.
|
||||
// Our FMX taskbar proxy (TFMAppClass), found by class name and filtered on our
|
||||
// own process id — the same lookup TPMBridge does, duplicated here because it
|
||||
// must work from a worker thread before FBridge even exists.
|
||||
function FindOurFMXAppWindow: HWND;
|
||||
var
|
||||
LPid: DWORD;
|
||||
begin
|
||||
Result := FindWindowEx(0, 0, 'TFMAppClass', nil);
|
||||
while Result <> 0 do
|
||||
begin
|
||||
LPid := 0;
|
||||
GetWindowThreadProcessId(Result, LPid);
|
||||
if LPid = GetCurrentProcessId then Exit;
|
||||
Result := FindWindowEx(0, Result, 'TFMAppClass', nil);
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TrayShowEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall;
|
||||
begin
|
||||
if not GTrayStartupPending then Exit;
|
||||
if (idObject <> OBJID_WINDOW) or (AWnd = 0) then Exit;
|
||||
if GetParent(AWnd) <> 0 then Exit; // top-level windows only
|
||||
ShowWindow(AWnd, SW_HIDE);
|
||||
end;
|
||||
|
||||
pprocedure TMainForm.Timer1Timer(Sender: TObject);
|
||||
begin
|
||||
|
||||
end;
|
||||
|
||||
rocedure TMainForm.TrayHideNow(const ATag: string);
|
||||
var
|
||||
LHwnd, LAppHwnd: HWND;
|
||||
LWasVisible: Boolean;
|
||||
begin
|
||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||
LWasVisible := (LHwnd <> 0) and IsWindowVisible(LHwnd);
|
||||
if LHwnd <> 0 then ShowWindow(LHwnd, SW_HIDE);
|
||||
LAppHwnd := 0;
|
||||
if Assigned(FBridge) then LAppHwnd := FBridge.FindFMXAppWindow;
|
||||
if LAppHwnd <> 0 then ShowWindow(LAppHwnd, SW_HIDE);
|
||||
LogLine(Format('[tray] %s at %s — form_visible_was=%s hwnd=%d app=%d',
|
||||
[ATag, FormatDateTime('hh:nn:ss.zzz', Now),
|
||||
BoolToStr(LWasVisible, True), LHwnd, LAppHwnd]));
|
||||
end;
|
||||
|
||||
procedure TMainForm.TrayStartupHideOnce(Sender: TObject);
|
||||
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
|
||||
// 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
|
||||
@@ -1764,6 +1642,9 @@ end;
|
||||
procedure TMainForm.RestoreWindowFromTray;
|
||||
begin
|
||||
FIgnoreResizes := False;
|
||||
// A --tray launch turned the proxy into a tool window to keep it off the
|
||||
// taskbar during startup; put it back before showing anything.
|
||||
TrayProxyShowInTaskbar;
|
||||
FBridge.RestoreFromTray;
|
||||
end;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user