fix(winstate): park the restored-size rect so the FMX restore has nothing to grow
FMX drops the maximised state when Windows minimises the form externally — reproducible in an empty FMX app. Windows then picks rcNormalPosition as the restore target before it animates, so every reactive correction measured (queued, inline in MINIMIZEEND, inside the restore's own WM_SIZE) landed after the frame was already growing: the state ended correct and the flash remained. Widen rcNormalPosition to the monitor work area while the window is down instead. FMX still restores to normal, except normal is now full-screen, so nothing grows; ReleaseNormalRect writes the real rectangle back once the window is maximised again, keeping the user's own restored size intact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,7 +19,7 @@ uses
|
||||
System.SysUtils, System.Classes, System.UITypes, System.NetEncoding,
|
||||
System.StrUtils, System.Generics.Collections, System.IOUtils, System.JSON,
|
||||
System.Net.HttpClient, System.Net.URLClient,
|
||||
Winapi.Windows, Winapi.ShellAPI,
|
||||
Winapi.Windows, Winapi.ShellAPI, Winapi.MultiMon,
|
||||
FMX.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls,
|
||||
FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types,
|
||||
FMX.Dialogs, FMX.DialogService,
|
||||
@@ -87,6 +87,13 @@ type
|
||||
// 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;
|
||||
// Re-entrance guard: ApplyWantedWindowState runs from OnResize and its own
|
||||
// ShowWindow fires another OnResize.
|
||||
FApplyingWindowState: Boolean;
|
||||
// The window's real restored-size rectangle, parked while we temporarily
|
||||
// widen it to the monitor's work area (see WinStateMinimizeStart).
|
||||
FSavedNormalRect: TRect;
|
||||
FNormalRectHijacked: Boolean;
|
||||
// Set from activate until the queued correction runs, so the restore's
|
||||
// own resize is not mistaken for the user resizing the window.
|
||||
FRestoring: Boolean;
|
||||
@@ -177,6 +184,7 @@ type
|
||||
procedure WinStateMinimizeStart;
|
||||
procedure WinStateMinimizeEnd;
|
||||
procedure ApplyWantedWindowState;
|
||||
procedure ReleaseNormalRect;
|
||||
end;
|
||||
|
||||
var
|
||||
@@ -1653,21 +1661,70 @@ end;
|
||||
|
||||
// The window is going down: from here until the restore settles, every resize
|
||||
// FMX fires is part of that transition, never a user gesture.
|
||||
// FMX loses the maximised state whenever Windows minimises the form from the
|
||||
// outside — reproducible in an empty FMX app, so there is nothing to fix on our
|
||||
// side of it. The window comes back at its RESTORED size and DWM animates the
|
||||
// frame growing towards that rectangle, which is the flash; every correction we
|
||||
// can run (OnResize, MINIMIZEEND) lands after that decision is made.
|
||||
//
|
||||
// So instead of fighting the restore, make the rectangle it restores to
|
||||
// harmless: park the real one and widen it to the monitor's work area while the
|
||||
// window is down. FMX still restores "to normal", except normal is now
|
||||
// full-screen — nothing visibly grows — and the OnResize correction promotes it
|
||||
// to a true maximised window right after. ApplyWantedWindowState puts the real
|
||||
// rectangle back, so un-maximising later still lands on the user's own size.
|
||||
procedure TMainForm.WinStateMinimizeStart;
|
||||
var
|
||||
LHwnd: HWND;
|
||||
LPlace: TWindowPlacement;
|
||||
LMonitor: TMonitorInfo;
|
||||
begin
|
||||
FIgnoreResizes := True;
|
||||
if FNormalRectHijacked or (not FWantMaximized) then Exit;
|
||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||
if LHwnd = 0 then Exit;
|
||||
LPlace := Default(TWindowPlacement);
|
||||
LPlace.length := SizeOf(LPlace);
|
||||
if not GetWindowPlacement(LHwnd, @LPlace) then Exit;
|
||||
LMonitor := Default(TMonitorInfo);
|
||||
LMonitor.cbSize := SizeOf(LMonitor);
|
||||
if not GetMonitorInfo(MonitorFromWindow(LHwnd, MONITOR_DEFAULTTONEAREST), @LMonitor) then Exit;
|
||||
FSavedNormalRect := LPlace.rcNormalPosition;
|
||||
FNormalRectHijacked := True;
|
||||
LPlace.rcNormalPosition := LMonitor.rcWork;
|
||||
// The window is maximised right now, so touching only rcNormalPosition moves
|
||||
// nothing on screen.
|
||||
SetWindowPlacement(LHwnd, @LPlace);
|
||||
end;
|
||||
|
||||
// Undo the hijack above. Safe to call whenever: it is a no-op unless the real
|
||||
// rectangle is currently parked.
|
||||
procedure TMainForm.ReleaseNormalRect;
|
||||
var
|
||||
LHwnd: HWND;
|
||||
LPlace: TWindowPlacement;
|
||||
begin
|
||||
if not FNormalRectHijacked then Exit;
|
||||
FNormalRectHijacked := False;
|
||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||
if LHwnd = 0 then Exit;
|
||||
LPlace := Default(TWindowPlacement);
|
||||
LPlace.length := SizeOf(LPlace);
|
||||
if not GetWindowPlacement(LHwnd, @LPlace) then Exit;
|
||||
LPlace.rcNormalPosition := FSavedNormalRect;
|
||||
SetWindowPlacement(LHwnd, @LPlace);
|
||||
end;
|
||||
|
||||
// Back up: re-apply what the user had.
|
||||
procedure TMainForm.WinStateMinimizeEnd;
|
||||
begin
|
||||
// Inline, not queued: by the time this hook fires Windows has already put the
|
||||
// window back at its normal size, and measurement showed nothing touches the
|
||||
// state afterwards — the extra message-loop turn a ForceQueue costs was the
|
||||
// whole flicker, one painted normal-sized frame before the correction landed.
|
||||
// Normally a no-op by now: the correction already ran from the restore's own
|
||||
// OnResize, which Windows delivers BEFORE this hook. Kept for the paths that
|
||||
// reach here without a resize.
|
||||
ApplyWantedWindowState;
|
||||
// Second pass anyway, in case FMX sizes the window after us on some path.
|
||||
// Idempotent: it only acts when the state still disagrees.
|
||||
// Net: whatever path we took, the real restored-size rectangle must never
|
||||
// stay parked past the end of a restore.
|
||||
ReleaseNormalRect;
|
||||
TThread.ForceQueue(nil,
|
||||
procedure
|
||||
begin
|
||||
@@ -1677,21 +1734,35 @@ begin
|
||||
end);
|
||||
end;
|
||||
|
||||
// Re-entrant by nature: it is called from OnResize and its own ShowWindow fires
|
||||
// another OnResize. FApplyingWindowState keeps that one pass deep.
|
||||
procedure TMainForm.ApplyWantedWindowState;
|
||||
var
|
||||
LHwnd: HWND;
|
||||
begin
|
||||
if FApplyingWindowState then Exit;
|
||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||
// Iconic means the window is genuinely down (minimise-to-tray, autofill):
|
||||
// nothing to correct, and forcing a state here would fight the hide.
|
||||
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
||||
if FWantMaximized and (not IsZoomed(LHwnd)) then
|
||||
begin
|
||||
LogLine('[winstate] re-maximising after restore');
|
||||
ShowWindow(LHwnd, SW_SHOWMAXIMIZED);
|
||||
end
|
||||
else if (not FWantMaximized) and IsZoomed(LHwnd) then
|
||||
begin
|
||||
LogLine('[winstate] restoring to normal after restore');
|
||||
ShowWindow(LHwnd, SW_RESTORE);
|
||||
if FWantMaximized = IsZoomed(LHwnd) then Exit;
|
||||
FApplyingWindowState := True;
|
||||
try
|
||||
if FWantMaximized then
|
||||
begin
|
||||
LogLine('[winstate] re-maximising after restore');
|
||||
ShowWindow(LHwnd, SW_SHOWMAXIMIZED);
|
||||
// Maximised again, so writing rcNormalPosition back is invisible here —
|
||||
// and it has to happen before the user can un-maximise.
|
||||
ReleaseNormalRect;
|
||||
end
|
||||
else
|
||||
begin
|
||||
LogLine('[winstate] restoring to normal after restore');
|
||||
ShowWindow(LHwnd, SW_RESTORE);
|
||||
end;
|
||||
finally
|
||||
FApplyingWindowState := False;
|
||||
end;
|
||||
end;
|
||||
|
||||
@@ -1701,7 +1772,15 @@ procedure TMainForm.FormResizeTrackState(Sender: TObject);
|
||||
var
|
||||
LHwnd: HWND;
|
||||
begin
|
||||
if FIgnoreResizes then Exit;
|
||||
// A minimise / restore is in flight, so this resize is never a user gesture —
|
||||
// but it IS where Windows applies the normal-sized restore, one message before
|
||||
// MINIMIZEEND is delivered. Correcting from the hook therefore always paints a
|
||||
// normal frame first; this is the earliest point we get.
|
||||
if FIgnoreResizes then
|
||||
begin
|
||||
ApplyWantedWindowState;
|
||||
Exit;
|
||||
end;
|
||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
||||
if FWantMaximized <> IsZoomed(LHwnd) then
|
||||
|
||||
Reference in New Issue
Block a user