revert(winstate): drop the parked restored-size rect, it left a stub window

Widening rcNormalPosition while the window was down did remove the restore
flash, but SetWindowPlacement left a small stub frame in a corner of the screen
whenever the window was maximised — during the minimise animation and equally
once the window was already iconic. A permanent visible artefact is worse than
an ugly animation, so this goes back to the validated behaviour: correct final
state, flash still present.

The flash itself is an FMX bug, reproducible in an empty FMX app. Rationale and
the five rejected approaches are recorded in TEST_SCENARIOS.md I1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-27 20:51:28 +01:00
parent 17e817f8ab
commit e98acabbbd
2 changed files with 19 additions and 98 deletions
+17 -96
View File
@@ -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.MultiMon,
Winapi.Windows, Winapi.ShellAPI,
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,13 +87,6 @@ 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;
@@ -184,7 +177,6 @@ type
procedure WinStateMinimizeStart;
procedure WinStateMinimizeEnd;
procedure ApplyWantedWindowState;
procedure ReleaseNormalRect;
end;
var
@@ -1661,70 +1653,21 @@ 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
// 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.
// 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.
ApplyWantedWindowState;
// Net: whatever path we took, the real restored-size rectangle must never
// stay parked past the end of a restore.
ReleaseNormalRect;
// Second pass anyway, in case FMX sizes the window after us on some path.
// Idempotent: it only acts when the state still disagrees.
TThread.ForceQueue(nil,
procedure
begin
@@ -1734,35 +1677,21 @@ 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 = 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;
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);
end;
end;
@@ -1772,15 +1701,7 @@ procedure TMainForm.FormResizeTrackState(Sender: TObject);
var
LHwnd: HWND;
begin
// 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;
if FIgnoreResizes then Exit;
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
if FWantMaximized <> IsZoomed(LHwnd) then