Fix window geometry save/restore: DPI scaling, position drift, maximized state

- LFM: Position = poDesigned (was poScreenCenter, overriding our restore)
- RestoreWindowBounds: remove position scaling (L,T invariant across DPI);
  scale size correctly per backend: LCLWin32 uses CurDPI/SavedDPI (physical px),
  Qt6/GTK/Cocoa use SavedDPI/CurDPI (logical px, inverse); fix min-size
  inflation on Qt6 (was MulDiv(900,DPI,96) which exceeds logical screen width
  at 150%+ scale, forcing window to 0,0)
- SaveWindowBounds moved from FormDestroy to FormClose while window is still
  stable; add guard for bogus RestoredLeft/Top values on Qt6 when maximized
- RestoreWindowBounds stores FRestoreL/T and re-applies position in AfterShowTick
  (200ms after show) when WM frame extents are already known to Qt, fixing the
  systematic right/down drift caused by unknown frame extents at FormCreate time
- Save and restore WindowState (maximized flag); skip AfterShowTick position
  re-apply when window is maximized

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 11:56:56 +03:00
co-authored by Claude Sonnet 4.6
parent 1eecd974fc
commit 9028a8dac0
3 changed files with 70 additions and 22 deletions
+52 -6
View File
@@ -343,6 +343,8 @@ type
FMeterTimer: TTimer;
FSpectrumTimer: TTimer;
FAfterShowTimer:TTimer; // однократный таймер для пост-инициализации
FRestoreL, FRestoreT: Integer; // позиция для отложенного восстановления
FHasPendingRestore: Boolean;
// ---- Top panel (toolbar + VFO group + S-meter) ----
PanelToolbar: TPanel;
@@ -933,10 +935,11 @@ end;
procedure TMainForm.RestoreWindowBounds;
var
L, T, Wd, Ht, SavedDPI, CurDPI: Integer;
Maximized: Boolean;
R, WA: TRect;
Mon: TMonitor;
begin
FSettings.LoadWindowBounds(L, T, Wd, Ht, SavedDPI);
FSettings.LoadWindowBounds(L, T, Wd, Ht, SavedDPI, Maximized);
if (Wd > 400) and (Ht > 300) then
begin
CurDPI := CurrentScreenDPI;
@@ -944,14 +947,32 @@ begin
if SavedDPI <> CurDPI then
begin
L := MulDiv(L, CurDPI, SavedDPI);
T := MulDiv(T, CurDPI, SavedDPI);
// Позицию не масштабируем: на LCLWin32 координаты физические и не зависят
// от DPI-фактора; на Qt6/GTK/Cocoa — логические (device-independent), тоже
// инвариантны. Выход за рабочую область поймает EnsureRange ниже.
//
// Размер масштабируем, чтобы окно выглядело одинаково при смене DPI:
// LCLWin32: физические пиксели → Wd × CurDPI/SavedDPI
// Qt6/GTK/Cocoa: логические пиксели → Wd × SavedDPI/CurDPI (обратное)
{$IFDEF LCLWin32}
Wd := MulDiv(Wd, CurDPI, SavedDPI);
Ht := MulDiv(Ht, CurDPI, SavedDPI);
{$ELSE}
Wd := MulDiv(Wd, SavedDPI, CurDPI);
Ht := MulDiv(Ht, SavedDPI, CurDPI);
{$ENDIF}
end;
// Минимальный размер:
// LCLWin32 — в физических пикселях, масштабируется с DPI.
// Qt6/GTK/Cocoa — в логических пикселях, device-independent, фиксированный.
{$IFDEF LCLWin32}
Wd := Max(MulDiv(900, CurDPI, 96), Wd);
Ht := Max(MulDiv(560, CurDPI, 96), Ht);
{$ELSE}
Wd := Max(900, Wd);
Ht := Max(560, Ht);
{$ENDIF}
R := Bounds(L, T, Wd, Ht);
Mon := Screen.MonitorFromRect(R);
if Mon <> nil then WA := Mon.WorkareaRect
@@ -962,15 +983,27 @@ begin
L := EnsureRange(L, WA.Left, Max(WA.Left, WA.Right - Wd));
T := EnsureRange(T, WA.Top, Max(WA.Top, WA.Bottom - Ht));
Position := poDesigned;
SetBounds(L, T, Wd, Ht);
if Maximized then
WindowState := wsMaximized
else
begin
// На Qt6/X11 frame extents ещё не известны Qt во время FormCreate,
// поэтому move() может поставить контент туда, куда мы хотели рамку.
// Повторно применим позицию в AfterShowTick, когда WM уже декорировал окно.
FRestoreL := L;
FRestoreT := T;
FHasPendingRestore := True;
end;
end;
end;
procedure TMainForm.SaveWindowBounds;
var
L, T, Wd, Ht, DPI: Integer;
Maximized: Boolean;
begin
Maximized := WindowState = wsMaximized;
if WindowState = wsNormal then
begin
L := Left; T := Top; Wd := Width; Ht := Height;
@@ -981,9 +1014,12 @@ begin
Wd := RestoredWidth; Ht := RestoredHeight;
end;
// RestoredLeft/Top могут вернуть мусор на Qt6 при максимизации;
// ставим разумные запасные значения
if (L < -32000) or (T < -32000) then begin L := 80; T := 80; end;
if (Wd <= 400) or (Ht <= 300) then Exit;
DPI := CurrentScreenDPI;
FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI);
FSettings.SaveWindowBounds(L, T, Wd, Ht, DPI, Maximized);
end;
function TMainForm.MakeBandSettings: TBandSettings;
@@ -1497,7 +1533,6 @@ begin
end;
// Размер окна сохраняем всегда (не зависит от подключения)
FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate);
SaveWindowBounds;
FSettings.Save;
FSettings.Free;
FWebServer.Stop;
@@ -1509,6 +1544,9 @@ end;
procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
// Сохраняем позицию здесь — окно ещё полностью видимо и стабильно.
// В FormDestroy Qt-виджет может быть уже в нестабильном состоянии.
SaveWindowBounds;
// Останавливаем трансивер как при нажатии STOP
if FNetwork.Connected then
begin
@@ -6968,6 +7006,14 @@ begin
// Вызывается один раз через 200 мс после старта формы
FAfterShowTimer.Enabled := False;
// Повторно применяем сохранённую позицию: к этому моменту WM уже декорировал
// окно и Qt знает frame extents, поэтому move() ставит рамку точно.
if FHasPendingRestore then
begin
SetBounds(FRestoreL, FRestoreT, Width, Height);
FHasPendingRestore := False;
end;
try
FAudioOut.Open;
except