diff --git a/MainForm.lfm b/MainForm.lfm index 617c630..5f1454b 100644 --- a/MainForm.lfm +++ b/MainForm.lfm @@ -8,7 +8,7 @@ object MainForm: TMainForm Font.Color = clSilver Font.Height = -13 Font.Name = 'Courier New' - Position = poScreenCenter + Position = poDesigned LCLVersion = '4.6.0.0' OnClose = FormClose OnCreate = FormCreate diff --git a/MainForm.pas b/MainForm.pas index 9d86d7a..5ca3a0c 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -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 diff --git a/Settings.pas b/Settings.pas index 69f61c0..cb8a2c9 100644 --- a/Settings.pas +++ b/Settings.pas @@ -282,8 +282,8 @@ type class procedure DefaultGlobal(out G: TGlobalSettings); class procedure DefaultTX(out T: TTXSettings); // Размер окна — не привязан к MAC, хранится в корне JSON - procedure SaveWindowBounds(L, T, W, H, DPI: Integer); - procedure LoadWindowBounds(out L, T, W, H, DPI: Integer); + procedure SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); + procedure LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean); procedure SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer); function LoadStartupPreview(out VfoA, VfoB: Double; out SampleRate: Integer): Boolean; procedure SaveAudioBufferSize(BufferSize: Integer); @@ -757,28 +757,30 @@ begin B.FMStepIdx := EnsureRange(JI(O,'fmstep_idx', B.FMStepIdx), 0, 3); end; -procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer); +procedure TSettingsManager.SaveWindowBounds(L, T, W, H, DPI: Integer; Maximized: Boolean); var O: TJSONObject; begin O := EnsureObj(FRoot, 'window'); - JW(O, 'left', L); - JW(O, 'top', T); - JW(O, 'width', W); - JW(O, 'height', H); - JW(O, 'dpi', DPI); + JW(O, 'left', L); + JW(O, 'top', T); + JW(O, 'width', W); + JW(O, 'height', H); + JW(O, 'dpi', DPI); + JW(O, 'maximized', Maximized); end; -procedure TSettingsManager.LoadWindowBounds(out L, T, W, H, DPI: Integer); +procedure TSettingsManager.LoadWindowBounds(out L, T, W, H, DPI: Integer; out Maximized: Boolean); var O: TJSONObject; begin - L := 80; T := 80; W := 1400; H := 900; DPI := 0; + L := 80; T := 80; W := 1400; H := 900; DPI := 0; Maximized := False; if FRoot.Find('window') = nil then Exit; O := EnsureObj(FRoot, 'window'); - L := JI(O, 'left', 80); - T := JI(O, 'top', 80); - W := JI(O, 'width', 1400); - H := JI(O, 'height', 900); - DPI := JI(O, 'dpi', 0); + L := JI(O, 'left', 80); + T := JI(O, 'top', 80); + W := JI(O, 'width', 1400); + H := JI(O, 'height', 900); + DPI := JI(O, 'dpi', 0); + Maximized := JB(O, 'maximized', False); end; procedure TSettingsManager.SaveStartupPreview(VfoA, VfoB: Double; SampleRate: Integer);