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**.
|
- **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 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).
|
- **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).
|
**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é.
|
||||||
- **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é.
|
**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 ✅
|
### 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.MultiMon,
|
Winapi.Windows, Winapi.ShellAPI,
|
||||||
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,13 +87,6 @@ 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;
|
||||||
@@ -184,7 +177,6 @@ type
|
|||||||
procedure WinStateMinimizeStart;
|
procedure WinStateMinimizeStart;
|
||||||
procedure WinStateMinimizeEnd;
|
procedure WinStateMinimizeEnd;
|
||||||
procedure ApplyWantedWindowState;
|
procedure ApplyWantedWindowState;
|
||||||
procedure ReleaseNormalRect;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
var
|
var
|
||||||
@@ -1661,70 +1653,21 @@ 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
|
||||||
// Normally a no-op by now: the correction already ran from the restore's own
|
// Inline, not queued: by the time this hook fires Windows has already put the
|
||||||
// OnResize, which Windows delivers BEFORE this hook. Kept for the paths that
|
// window back at its normal size, and measurement showed nothing touches the
|
||||||
// reach here without a resize.
|
// state afterwards — the extra message-loop turn a ForceQueue costs was the
|
||||||
|
// whole flicker, one painted normal-sized frame before the correction landed.
|
||||||
ApplyWantedWindowState;
|
ApplyWantedWindowState;
|
||||||
// Net: whatever path we took, the real restored-size rectangle must never
|
// Second pass anyway, in case FMX sizes the window after us on some path.
|
||||||
// stay parked past the end of a restore.
|
// Idempotent: it only acts when the state still disagrees.
|
||||||
ReleaseNormalRect;
|
|
||||||
TThread.ForceQueue(nil,
|
TThread.ForceQueue(nil,
|
||||||
procedure
|
procedure
|
||||||
begin
|
begin
|
||||||
@@ -1734,35 +1677,21 @@ 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 = IsZoomed(LHwnd) then Exit;
|
if FWantMaximized and (not IsZoomed(LHwnd)) then
|
||||||
FApplyingWindowState := True;
|
begin
|
||||||
try
|
LogLine('[winstate] re-maximising after restore');
|
||||||
if FWantMaximized then
|
ShowWindow(LHwnd, SW_SHOWMAXIMIZED);
|
||||||
begin
|
end
|
||||||
LogLine('[winstate] re-maximising after restore');
|
else if (not FWantMaximized) and IsZoomed(LHwnd) then
|
||||||
ShowWindow(LHwnd, SW_SHOWMAXIMIZED);
|
begin
|
||||||
// Maximised again, so writing rcNormalPosition back is invisible here —
|
LogLine('[winstate] restoring to normal after restore');
|
||||||
// and it has to happen before the user can un-maximise.
|
ShowWindow(LHwnd, SW_RESTORE);
|
||||||
ReleaseNormalRect;
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
LogLine('[winstate] restoring to normal after restore');
|
|
||||||
ShowWindow(LHwnd, SW_RESTORE);
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
FApplyingWindowState := False;
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -1772,15 +1701,7 @@ procedure TMainForm.FormResizeTrackState(Sender: TObject);
|
|||||||
var
|
var
|
||||||
LHwnd: HWND;
|
LHwnd: HWND;
|
||||||
begin
|
begin
|
||||||
// A minimise / restore is in flight, so this resize is never a user gesture —
|
if FIgnoreResizes then Exit;
|
||||||
// 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