diff --git a/CHANGELOG.md b/CHANGELOG.md index b235a3a..dcf2afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,13 @@ Тихий** (тишина). Поменялись местами и рисунок, и анимация (покачивание листа ↔ мерцание звёзд), и акцент ячейки в панели (зелёный ↔ сизый) — в панели, OSD и трее. Первопричина была в двусмысленной строке спецификации иконок «лист (эко/тихий)» — она переписана однозначно (XIC-14). +- **Окно настроек не переживало смену разрешения или масштаба экрана**: размеры шрифтов + оставались от старого DPI и расходились с разметкой — заметнее всего обрезался пояснительный + текст под названиями опций (высота карточки считается под него один раз при постройке). + Частый сценарий — удалённое подключение, которое подгоняет разрешение под клиента. Теперь окно + пересобирается под новый масштаб: ловим и `WM_DPICHANGED` (как это давно делают панель и + «Монитор»), и смену разрешения через `DisplaySettingsChanged`. Заодно шрифт окна переведён на + `ScaledFonts` — раньше он один задавался в пунктах мимо общего масштабирования (XIC-15). ## [0.7.0] — 2026-07-28 diff --git a/CLAUDE.md b/CLAUDE.md index 8dd1d1c..416c638 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -145,7 +145,8 @@ WMI-событий). `Program.cs`: single-instance mutex → DI-контейне `FlyoutForm` + `FlyoutPalette` (общая база флайаутов: borderless tool-window, Region, Esc; палитра — единственный источник тёмных цветов флайаутов), `FormChrome` (DWM-тёмный заголовок + WM_SETREDRAW), `ModeUi` (режим → ключ локализации / вид OSD / акцент), - `SettingsForm` (хост окна настроек: хром, навигация, пересборка на каждый показ), + `SettingsForm` (хост окна настроек: хром, навигация, пересборка на каждый показ — а также на + смену темы, DPI и разрешения экрана: шрифты и `Sc()` снимаются в момент постройки), `SettingsActions` (сумка колбэков в контроллер; поля `required` — забытый mount ловится компилятором, CS9035), `ToggleSwitch` (рисованный Win11-тумблер), `ScaledFonts` (**шрифты только отсюда** — пиксельные под DeviceDpi, иначе после смены diff --git a/src/Ui/Settings/AboutTab.cs b/src/Ui/Settings/AboutTab.cs index 4557085..1b37f2d 100644 --- a/src/Ui/Settings/AboutTab.cs +++ b/src/Ui/Settings/AboutTab.cs @@ -22,7 +22,7 @@ public AboutTab(SettingsToolkit ui) : base(ui) ico.Region = new Region(Draw.Rounded(new RectangleF(0, 0, ui.Sc(60), ui.Sc(60)), ui.Sc(12))); hero.Controls.Add(ico); var name = new Label { Text = "XiControl", Font = ui.NameFont, AutoSize = true, ForeColor = ui.T.Text, BackColor = Color.Transparent, Location = new Point(ui.Sc(74), ui.Sc(10)) }; - var ver = new Label { Text = $"{Loc.T("settings.version")} {AppVersion()} · GPLv3", Tag = "dim", AutoSize = true, ForeColor = ui.T.Text2, BackColor = Color.Transparent, Location = new Point(ui.Sc(74), ui.Sc(40)) }; + var ver = new Label { Text = $"{Loc.T("settings.version")} {AppVersion()} · GPLv3", Tag = "dim", AutoSize = true, ForeColor = ui.T.Text2, BackColor = Color.Transparent, Font = ui.NoteFont, Location = new Point(ui.Sc(74), ui.Sc(40)) }; hero.Controls.Add(name); hero.Controls.Add(ver); Controls.Add(hero); diff --git a/src/Ui/Settings/SettingsToolkit.cs b/src/Ui/Settings/SettingsToolkit.cs index ea4495e..ca34bef 100644 --- a/src/Ui/Settings/SettingsToolkit.cs +++ b/src/Ui/Settings/SettingsToolkit.cs @@ -136,8 +136,9 @@ public void AddKv(Panel p, string key, string val) { var row = new Panel { Width = RowW, Height = Sc(34), BackColor = T.WinBg, Margin = new Padding(0) }; row.Paint += (_, e) => { using var pen = new Pen(T.Border); e.Graphics.DrawLine(pen, 0, row.Height - 1, RowW, row.Height - 1); }; - row.Controls.Add(new Label { Text = Loc.T(key), Tag = "dim", AutoSize = true, ForeColor = T.Text2, BackColor = Color.Transparent, Location = new Point(Sc(2), Sc(9)) }); - row.Controls.Add(new Label { Text = val, AutoSize = true, ForeColor = T.Text, BackColor = Color.Transparent, Location = new Point(Sc(180), Sc(9)) }); + // Font задаём явно: наследованный ambient не пересчитывался бы вместе с Sc() при смене DPI + row.Controls.Add(new Label { Text = Loc.T(key), Tag = "dim", AutoSize = true, ForeColor = T.Text2, BackColor = Color.Transparent, Font = NoteFont, Location = new Point(Sc(2), Sc(9)) }); + row.Controls.Add(new Label { Text = val, AutoSize = true, ForeColor = T.Text, BackColor = Color.Transparent, Font = NoteFont, Location = new Point(Sc(180), Sc(9)) }); p.Controls.Add(row); } diff --git a/src/Ui/SettingsForm.cs b/src/Ui/SettingsForm.cs index bb7b6da..03d1e38 100644 --- a/src/Ui/SettingsForm.cs +++ b/src/Ui/SettingsForm.cs @@ -1,5 +1,7 @@ -using XiControl.Config; +using Microsoft.Win32; +using XiControl.Config; using XiControl.Localization; +using XiControl.SystemIntegration; using XiControl.Ui.Settings; namespace XiControl.Ui; @@ -26,6 +28,10 @@ public sealed class SettingsForm : Form private readonly List _panes = []; private int _tab; + // смена разрешения/масштаба сыплется пачкой событий — гасим дребезг и пересобираем один раз + // (шов IAppTimer тут не нужен: окно юнит-тестами не покрывается) + private readonly UiTimer _rescale = new() { Interval = 250 }; + // MifsClient сюда сознательно не передаётся: окно железо не трогает — все «умные» // операции идут через колбэки SettingsActions в TrayApp/AppController. public SettingsForm(AppConfig cfg, SettingsActions act) @@ -40,16 +46,33 @@ public SettingsForm(AppConfig cfg, SettingsActions act) StartPosition = FormStartPosition.Manual; // позицию считаем сами в Popup (всегда центр) KeyPreview = true; DoubleBuffered = true; - Font = new Font("Segoe UI", 9f); + // масштабируем себя сами (Sc + ScaledFonts от DeviceDpi) — иначе WinForms домасштабирует + // поверх наших размеров, и геометрия разъедется вдвойне + AutoScaleMode = AutoScaleMode.None; try { Icon = Icon.ExtractAssociatedIcon(Environment.ProcessPath!); } catch { /* иконки нет — не критично */ } _ = Handle; // форсируем хэндл (нужен DeviceDpi) _ui = new SettingsToolkit(this, SettingsTheme.Load()); ClientSize = new Size(_ui.Sc(824), _ui.Sc(700)); + // разрешение могло смениться и без смены DPI (удалённый рабочий стол подгоняет его + // под клиента) — WM_DPICHANGED тогда не приходит, а пересобраться всё равно надо + _rescale.Tick += () => { _rescale.Stop(); if (Visible) Rescale(); }; + SystemEvents.DisplaySettingsChanged += OnDisplaySettingsChanged; + BuildAll(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + SystemEvents.DisplaySettingsChanged -= OnDisplaySettingsChanged; + _rescale.Dispose(); + } + base.Dispose(disposing); + } + /// /// Открыть окно. Содержимое пересобирается при каждом открытии: настройки могли смениться /// из трея/панели, пока окно было спрятано (авто-герцовка и т.п.), тема — тоже. Пересборка @@ -58,18 +81,50 @@ public SettingsForm(AppConfig cfg, SettingsActions act) public void Popup() { BuildAll(); - // высота — чтобы вкладки влезали без прокрутки, но не выше рабочей области экрана; - // окно каждый раз открывается по центру экрана с курсором - var wa = Screen.FromPoint(Cursor.Position).WorkingArea; - int h = Math.Min(_ui.Sc(700), wa.Height - _ui.Sc(80)); - if (ClientSize.Height != h) ClientSize = new Size(_ui.Sc(824), h); - Location = new Point(wa.Left + (wa.Width - Width) / 2, wa.Top + (wa.Height - Height) / 2); + FitToScreen(Screen.FromPoint(Cursor.Position)); // открываемся по центру экрана с курсором Show(); if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal; Activate(); BringToFront(); } + /// Разместить окно по центру экрана: высота — чтобы вкладки влезали без прокрутки, + /// но не выше рабочей области (она меняется вместе с разрешением). + private void FitToScreen(Screen screen) + { + var wa = screen.WorkingArea; + var size = new Size(_ui.Sc(824), Math.Min(_ui.Sc(700), wa.Height - _ui.Sc(80))); + if (ClientSize != size) ClientSize = size; + Location = new Point(wa.Left + (wa.Width - Width) / 2, wa.Top + (wa.Height - Height) / 2); + } + + /// Пересобрать окно под текущий DPI/разрешение. Sc() и шрифты у нас пиксельные и + /// снимаются в момент постройки: без пересборки текст остаётся в старом масштабе и расходится + /// с разметкой — заметнее всего на пояснениях, высота карточки под них считается один раз + /// по DescFont (SettingsToolkit.AddRow). Механика та же, что у смены темы: пересборка дешёвая. + private void Rescale() + { + BuildAll(); + FitToScreen(Screen.FromControl(this)); + } + + /// Масштаб сменился или окно переехало на монитор с другим DPI — пересобрать + /// (для флайаутов это же делает FlyoutForm.OnDpiRescaled). + protected override void OnDpiChanged(DpiChangedEventArgs e) + { + base.OnDpiChanged(e); + Rescale(); + } + + // Сменилось разрешение/раскладка мониторов. Событие приходит из системного потока и пачкой — + // маршалим в UI и гасим дребезг. Спрятанное окно не трогаем: его пересоберёт следующий Popup. + private void OnDisplaySettingsChanged(object? sender, EventArgs e) + { + if (!IsHandleCreated || IsDisposed || !Visible) return; + try { BeginInvoke(new Action(() => { _rescale.Stop(); _rescale.Start(); })); } + catch (ObjectDisposedException) { /* окно закрылось, пока событие шло к нам */ } + } + protected override void OnFormClosing(FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) @@ -139,6 +194,10 @@ private void BuildAllCore() _panes.Clear(); _factories.Clear(); + // ambient-шрифт формы — тоже из ScaledFonts: контролы без явного Font наследуют его, + // а пунктовый шрифт при смене DPI разъезжается с нашими пиксельными + Font = _ui.NoteFont; + _host = new Panel { Dock = DockStyle.Fill, BackColor = _ui.T.WinBg, Tag = "host" }; Controls.Add(_host);