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:
+2
-2
@@ -340,8 +340,8 @@ L'app démarre maximisée (`WindowState = wsMaximized` dans la `.fmx`).
|
||||
- **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 (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é.
|
||||
**Statut : non corrigé, assumé.** Une cinquième piste a été tentée puis **annulée** : garer `rcNormalPosition` et le remplacer par `rcWork` du moniteur pendant que la fenêtre est en bas, pour que « restaurer en normal » vaille déjà plein écran. Ça supprimait bien le flash, mais tout `SetWindowPlacement` sur une fenêtre en cours de minimisation — ou déjà iconique — laissait **un moignon de fenêtre dans un coin de l'écran**, y compris en différant l'écriture jusqu'à `IsIconic`. Un artefact visible permanent est pire que l'animation. Reverté.
|
||||
**Ce qui reste** : l'état final est correct (la fenêtre revient bien maximisée), seule l'animation de restauration est laide. Ne pas rouvrir sans une vraie raison — la seule route restante est de sous-classer le `WndProc` de la fenêtre FMX pour corriger le rectangle dans `WM_WINDOWPOSCHANGING`, ce qui insère du code dans la boucle de messages d'une fenêtre qu'on ne contrôle pas, avec le tray, l'autofill et le démarrage `-tray` en aval.
|
||||
|
||||
### I2 · normal survit aussi ✅
|
||||
- **ACTION** : dé-maximiser la fenêtre → `Win+D` → revenir.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user