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:
+3
-1
@@ -339,7 +339,9 @@ L'app démarre maximisée (`WindowState = wsMaximized` dans la `.fmx`).
|
|||||||
- **ACTION** : fenêtre **maximisée** → `Win+D` (ou *Show desktop*) → revenir à l'app (barre des tâches, tray ou Alt+Tab).
|
- **ACTION** : fenêtre **maximisée** → `Win+D` (ou *Show desktop*) → revenir à l'app (barre des tâches, tray ou Alt+Tab).
|
||||||
- **ATTENDU** : elle revient **maximisée**, **sans passer par une frame en taille normale**.
|
- **ATTENDU** : elle revient **maximisée**, **sans passer par une frame en taille normale**.
|
||||||
- **Historique** : FMX oublie l'état maximisé quand Windows minimise la fenêtre de l'extérieur, et aucun de nos chemins de restore ne tourne pour un clic sur la barre des tâches.
|
- **Historique** : FMX oublie l'état maximisé quand Windows minimise la fenêtre de l'extérieur, et aucun de nos chemins de restore ne tourne pour un clic sur la barre des tâches.
|
||||||
- **Historique (flash)** : une fois l'état correct, il restait un scintillement — la fenêtre revenait normale puis se maximisait. La correction était postée via `ForceQueue` ; le log a montré que l'état ne changeait **pas** entre le hook et le tour de boucle suivant, donc la file ne protégeait d'aucune course et n'ajoutait que la frame visible. Elle s'exécute maintenant **en ligne** dans `MINIMIZEEND` (`ApplyWantedWindowState`), la passe différée restant en filet idempotent. Piste écartée au passage : poser `WPF_RESTORETOMAXIMIZED` — `GetWindowPlacement` rapporte déjà ce flag sur toute fenêtre maximisée, donc le stamp est un no-op.
|
- **Historique (flash)** : une fois l'état final correct, il restait un scintillement — le cadre apparaissait en taille normale puis grandissait. **C'est un bug FMX**, reproduit sur une application FMX vide : FMX perd le maximisé quand Windows minimise le formulaire de l'extérieur. Windows choisit alors `rcNormalPosition` comme cible **avant** d'animer, donc aucune correction réactive ne peut arriver à temps — trois ont été essayées et rejetées par la mesure : `ForceQueue` (un tour de boucle trop tard), appel inline dans `MINIMIZEEND` (le hook est délivré après le `WM_SIZE`), correction dans le `WM_SIZE` lui-même (l'état devient parfait, le flash reste — preuve que le problème n'était plus l'état). Écarté aussi : poser `WPF_RESTORETOMAXIMIZED`, que `GetWindowPlacement` rapporte déjà à 2 sur une fenêtre maximisée (no-op).
|
||||||
|
**Solution** : ne pas combattre la restauration, rendre sa cible inoffensive. `WinStateMinimizeStart` gare le vrai `rcNormalPosition` et le remplace par `rcWork` du moniteur ; FMX restaure toujours « en normal », mais normal vaut désormais plein écran, donc rien ne grandit. `ReleaseNormalRect` rend le rectangle réel une fois la fenêtre re-maximisée (invisible : elle est maximisée à ce moment).
|
||||||
|
- **Non-régression obligatoire** : après le retour, **dé-maximiser** doit rendre la taille d'origine de l'utilisateur, pas la zone de travail — sinon `ReleaseNormalRect` n'a pas tourné et le rectangle est resté garé.
|
||||||
|
|
||||||
### I2 · normal survit aussi ✅
|
### I2 · normal survit aussi ✅
|
||||||
- **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir.
|
- **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ uses
|
|||||||
System.SysUtils, System.Classes, System.UITypes, System.NetEncoding,
|
System.SysUtils, System.Classes, System.UITypes, System.NetEncoding,
|
||||||
System.StrUtils, System.Generics.Collections, System.IOUtils, System.JSON,
|
System.StrUtils, System.Generics.Collections, System.IOUtils, System.JSON,
|
||||||
System.Net.HttpClient, System.Net.URLClient,
|
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.Forms, FMX.Controls, FMX.Controls.Presentation, FMX.StdCtrls,
|
||||||
FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types,
|
FMX.Memo, FMX.Memo.Types, FMX.ScrollBox, FMX.Edit, FMX.Layouts, FMX.Types,
|
||||||
FMX.Dialogs, FMX.DialogService,
|
FMX.Dialogs, FMX.DialogService,
|
||||||
@@ -87,6 +87,13 @@ type
|
|||||||
// 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;
|
||||||
|
// 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
|
// Set from activate until the queued correction runs, so the restore's
|
||||||
// own resize is not mistaken for the user resizing the window.
|
// own resize is not mistaken for the user resizing the window.
|
||||||
FRestoring: Boolean;
|
FRestoring: Boolean;
|
||||||
@@ -177,6 +184,7 @@ type
|
|||||||
procedure WinStateMinimizeStart;
|
procedure WinStateMinimizeStart;
|
||||||
procedure WinStateMinimizeEnd;
|
procedure WinStateMinimizeEnd;
|
||||||
procedure ApplyWantedWindowState;
|
procedure ApplyWantedWindowState;
|
||||||
|
procedure ReleaseNormalRect;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
@@ -1653,21 +1661,70 @@ end;
|
|||||||
|
|
||||||
// The window is going down: from here until the restore settles, every resize
|
// 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 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;
|
procedure TMainForm.WinStateMinimizeStart;
|
||||||
|
var
|
||||||
|
LHwnd: HWND;
|
||||||
|
LPlace: TWindowPlacement;
|
||||||
|
LMonitor: TMonitorInfo;
|
||||||
begin
|
begin
|
||||||
FIgnoreResizes := True;
|
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;
|
end;
|
||||||
|
|
||||||
// Back up: re-apply what the user had.
|
// Back up: re-apply what the user had.
|
||||||
procedure TMainForm.WinStateMinimizeEnd;
|
procedure TMainForm.WinStateMinimizeEnd;
|
||||||
begin
|
begin
|
||||||
// Inline, not queued: by the time this hook fires Windows has already put the
|
// Normally a no-op by now: the correction already ran from the restore's own
|
||||||
// window back at its normal size, and measurement showed nothing touches the
|
// OnResize, which Windows delivers BEFORE this hook. Kept for the paths that
|
||||||
// state afterwards — the extra message-loop turn a ForceQueue costs was the
|
// reach here without a resize.
|
||||||
// whole flicker, one painted normal-sized frame before the correction landed.
|
|
||||||
ApplyWantedWindowState;
|
ApplyWantedWindowState;
|
||||||
// Second pass anyway, in case FMX sizes the window after us on some path.
|
// Net: whatever path we took, the real restored-size rectangle must never
|
||||||
// Idempotent: it only acts when the state still disagrees.
|
// stay parked past the end of a restore.
|
||||||
|
ReleaseNormalRect;
|
||||||
TThread.ForceQueue(nil,
|
TThread.ForceQueue(nil,
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
@@ -1677,22 +1734,36 @@ begin
|
|||||||
end);
|
end);
|
||||||
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;
|
procedure TMainForm.ApplyWantedWindowState;
|
||||||
var
|
var
|
||||||
LHwnd: HWND;
|
LHwnd: HWND;
|
||||||
begin
|
begin
|
||||||
|
if FApplyingWindowState then Exit;
|
||||||
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
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 (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
||||||
if FWantMaximized and (not IsZoomed(LHwnd)) then
|
if FWantMaximized = IsZoomed(LHwnd) then Exit;
|
||||||
|
FApplyingWindowState := True;
|
||||||
|
try
|
||||||
|
if FWantMaximized then
|
||||||
begin
|
begin
|
||||||
LogLine('[winstate] re-maximising after restore');
|
LogLine('[winstate] re-maximising after restore');
|
||||||
ShowWindow(LHwnd, SW_SHOWMAXIMIZED);
|
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
|
end
|
||||||
else if (not FWantMaximized) and IsZoomed(LHwnd) then
|
else
|
||||||
begin
|
begin
|
||||||
LogLine('[winstate] restoring to normal after restore');
|
LogLine('[winstate] restoring to normal after restore');
|
||||||
ShowWindow(LHwnd, SW_RESTORE);
|
ShowWindow(LHwnd, SW_RESTORE);
|
||||||
end;
|
end;
|
||||||
|
finally
|
||||||
|
FApplyingWindowState := False;
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Track the state the user settles on. Ignored entirely while a minimise /
|
// Track the state the user settles on. Ignored entirely while a minimise /
|
||||||
@@ -1701,7 +1772,15 @@ procedure TMainForm.FormResizeTrackState(Sender: TObject);
|
|||||||
var
|
var
|
||||||
LHwnd: HWND;
|
LHwnd: HWND;
|
||||||
begin
|
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;
|
LHwnd := WindowHandleToPlatform(Self.Handle).Wnd;
|
||||||
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
if (LHwnd = 0) or IsIconic(LHwnd) then Exit;
|
||||||
if FWantMaximized <> IsZoomed(LHwnd) then
|
if FWantMaximized <> IsZoomed(LHwnd) then
|
||||||
|
|||||||
Reference in New Issue
Block a user