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
|
uses
|
||||||
System.SysUtils, // FindCmdLineSwitch
|
System.SysUtils, // FindCmdLineSwitch
|
||||||
|
|||||||
@@ -204,6 +204,7 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<Icon_MainIcon>PMServer_Icon1.ico</Icon_MainIcon>
|
<Icon_MainIcon>PMServer_Icon1.ico</Icon_MainIcon>
|
||||||
<UWP_DelphiLogo44>app.png</UWP_DelphiLogo44>
|
<UWP_DelphiLogo44>app.png</UWP_DelphiLogo44>
|
||||||
<UWP_DelphiLogo150>app.png</UWP_DelphiLogo150>
|
<UWP_DelphiLogo150>app.png</UWP_DelphiLogo150>
|
||||||
|
<Debugger_RunParams>-tray</Debugger_RunParams>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<DelphiCompile Include="$(MainSource)">
|
<DelphiCompile Include="$(MainSource)">
|
||||||
|
|||||||
@@ -127,10 +127,4 @@ 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
|
||||||
|
|||||||
+26
-145
@@ -64,7 +64,6 @@ 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);
|
||||||
@@ -72,7 +71,6 @@ 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
|
||||||
@@ -86,11 +84,6 @@ type
|
|||||||
// restore's resize is indistinguishable from a user gesture by placement
|
// restore's resize is indistinguishable from a user gesture by placement
|
||||||
// flags or foreground ownership — both were measured and rejected).
|
// flags or foreground ownership — both were measured and rejected).
|
||||||
FWinEventHook: THandle;
|
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
|
// True from "minimise starts" until just after the restore settles, so the
|
||||||
// resizes FMX fires while restoring are not recorded as user intent.
|
// resizes FMX fires while restoring are not recorded as user intent.
|
||||||
FIgnoreResizes: Boolean;
|
FIgnoreResizes: Boolean;
|
||||||
@@ -180,10 +173,6 @@ type
|
|||||||
// stuck raised (see the implementation).
|
// stuck raised (see the implementation).
|
||||||
// One-shot: hides the window the first time FMX shows it on a --tray
|
// One-shot: hides the window the first time FMX shows it on a --tray
|
||||||
// launch, then unhooks itself.
|
// 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 RestoreWindowFromTray;
|
||||||
procedure WinStateMinimizeStart;
|
procedure WinStateMinimizeStart;
|
||||||
procedure WinStateMinimizeEnd;
|
procedure WinStateMinimizeEnd;
|
||||||
@@ -210,11 +199,6 @@ function MaskAccessToken(const AUrl: string): string; forward;
|
|||||||
|
|
||||||
// ---- WinEvent hook plumbing (see the maximised-state block further down) --
|
// ---- WinEvent hook plumbing (see the maximised-state block further down) --
|
||||||
const
|
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_MINIMIZESTART = $0016;
|
||||||
EVENT_SYSTEM_MINIMIZEEND = $0017;
|
EVENT_SYSTEM_MINIMIZEEND = $0017;
|
||||||
WINEVENT_OUTOFCONTEXT = $0000;
|
WINEVENT_OUTOFCONTEXT = $0000;
|
||||||
@@ -226,66 +210,39 @@ function SetWinEventHook(eventMin, eventMax: DWORD; hmodWinEventProc: HMODULE;
|
|||||||
function UnhookWinEvent(hWinEventHook: THandle): BOOL;
|
function UnhookWinEvent(hWinEventHook: THandle): BOOL;
|
||||||
stdcall; external 'user32.dll' name 'UnhookWinEvent';
|
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;
|
procedure WinStateEventProc(hWinEventHook: THandle; AEvent: DWORD; AWnd: HWND;
|
||||||
idObject, idChild: LongInt; idEventThread, dwmsEventTime: DWORD); stdcall; forward;
|
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;
|
procedure StartTrayProxyHider;
|
||||||
begin
|
begin
|
||||||
GTrayStartupPending := True;
|
// ApplicationHWND (FMX.Platform.Win) IS the taskbar proxy — no hunting for a
|
||||||
TThread.CreateAnonymousThread(
|
// TFMAppClass window, and it is valid this early, before the form exists.
|
||||||
procedure
|
ShowWindow(ApplicationHWND, SW_HIDE);
|
||||||
var
|
// Hiding alone does not reliably drop the button: the taskbar had already
|
||||||
LWnd: HWND;
|
// drawn it. Clearing WS_EX_APPWINDOW and setting WS_EX_TOOLWINDOW does —
|
||||||
LTicks: Integer;
|
// a tool window is never listed. Undone by TrayProxyShowInTaskbar before any
|
||||||
begin
|
// restore, otherwise the app would come back with no taskbar button at all.
|
||||||
LTicks := 0;
|
SetWindowLong(ApplicationHWND, GWL_EXSTYLE,
|
||||||
// 5 ms cadence, 20 s ceiling. Stops as soon as the tray startup marks
|
(GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_APPWINDOW))
|
||||||
// itself done, so a user restoring from the tray is never fought.
|
or WS_EX_TOOLWINDOW);
|
||||||
while GTrayStartupPending and (LTicks < 4000) do
|
end;
|
||||||
begin
|
|
||||||
LWnd := FindOurFMXAppWindow;
|
// Give the proxy its normal styles back. Takes effect on the next show, which
|
||||||
if LWnd <> 0 then
|
// is exactly what RestoreFromTray does right after.
|
||||||
ShowWindow(LWnd, SW_HIDE); // unconditional: the button can be up
|
procedure TrayProxyShowInTaskbar;
|
||||||
Sleep(5); // before WS_VISIBLE reads as set
|
begin
|
||||||
Inc(LTicks);
|
SetWindowLong(ApplicationHWND, GWL_EXSTYLE,
|
||||||
end;
|
(GetWindowLong(ApplicationHWND, GWL_EXSTYLE) and (not WS_EX_TOOLWINDOW))
|
||||||
end).Start;
|
or WS_EX_APPWINDOW);
|
||||||
end;
|
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"): the taskbar proxy was already dealt with in
|
||||||
// possible. Visible := False alone is not enough — FMX re-shows the main
|
// the .dpr (StartTrayProxyHider) — it is shown before FormCreate is even
|
||||||
// form after the constructor returns — so OnActivate hides it again the
|
// reached. This just keeps the form itself off screen.
|
||||||
// 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).
|
|
||||||
if FindCmdLineSwitch('tray', True) then
|
if FindCmdLineSwitch('tray', True) then
|
||||||
begin
|
|
||||||
Visible := False;
|
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.
|
// 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.
|
||||||
@@ -325,9 +282,6 @@ begin
|
|||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
WebBrowser.Parent := Self;
|
WebBrowser.Parent := Self;
|
||||||
WebBrowser.Align := TAlignLayout.Client;
|
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 := TPMHTTPServer.Create;
|
||||||
FServer.OnLog := LogLine;
|
FServer.OnLog := LogLine;
|
||||||
@@ -404,21 +358,13 @@ begin
|
|||||||
// it's noise when Windows itself put us in the tray (the user
|
// it's noise when Windows itself put us in the tray (the user
|
||||||
// didn't actively minimise). Manual launches still see it once.
|
// didn't actively minimise). Manual launches still see it once.
|
||||||
FBridge.BalloonShown := True;
|
FBridge.BalloonShown := True;
|
||||||
TrayHideNow('hide#3 end-of-FormCreate');
|
|
||||||
TThread.ForceQueue(nil,
|
TThread.ForceQueue(nil,
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
FBridge.MinimizeToTray;
|
FBridge.MinimizeToTray;
|
||||||
// Hidden now — safe to pay the startup cost.
|
// Hidden now — safe to pay the startup cost.
|
||||||
btnStartClick(Nil);
|
btnStartClick(Nil);
|
||||||
GTrayStartupPending := False;
|
LogLine('Launched with --tray, started hidden (balloon suppressed)');
|
||||||
if FShowEventHook <> 0 then
|
|
||||||
begin
|
|
||||||
UnhookWinEvent(FShowEventHook);
|
|
||||||
FShowEventHook := 0;
|
|
||||||
end;
|
|
||||||
LogLine('[tray] deferred MinimizeToTray ran at ' +
|
|
||||||
FormatDateTime('hh:nn:ss.zzz', Now));
|
|
||||||
end);
|
end);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
@@ -430,11 +376,6 @@ begin
|
|||||||
UnhookWinEvent(FWinEventHook);
|
UnhookWinEvent(FWinEventHook);
|
||||||
FWinEventHook := 0;
|
FWinEventHook := 0;
|
||||||
end;
|
end;
|
||||||
if FShowEventHook <> 0 then
|
|
||||||
begin
|
|
||||||
UnhookWinEvent(FShowEventHook);
|
|
||||||
FShowEventHook := 0;
|
|
||||||
end;
|
|
||||||
if Assigned(FFileSaveChunks) then
|
if Assigned(FFileSaveChunks) then
|
||||||
begin
|
begin
|
||||||
for var LSB in FFileSaveChunks.Values do LSB.Free;
|
for var LSB in FFileSaveChunks.Values do LSB.Free;
|
||||||
@@ -1692,69 +1633,6 @@ begin
|
|||||||
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
|
else if AEvent = EVENT_SYSTEM_MINIMIZEEND then MainForm.WinStateMinimizeEnd;
|
||||||
end;
|
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
|
// 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
|
||||||
@@ -1764,6 +1642,9 @@ end;
|
|||||||
procedure TMainForm.RestoreWindowFromTray;
|
procedure TMainForm.RestoreWindowFromTray;
|
||||||
begin
|
begin
|
||||||
FIgnoreResizes := False;
|
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;
|
FBridge.RestoreFromTray;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user