From 993c34642afbd877fc5addafc93688859742d6fb Mon Sep 17 00:00:00 2001 From: Raul Avila Catala Date: Sun, 30 Aug 2026 00:44:07 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20IDefaultUiValues=20=E2=80=94=20sistema?= =?UTF-8?q?=20centralizado=20de=20defaults=20de=20UI=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementa contrato IDefaultUiValues en Core (internal) con WindowUiDefaults, ControlUiDefaults y FontSpec como records. CoreUiDefaults lee colores via GetSysColor y aporta diccionario IReadOnlyDictionary. Win32UiDefaults expone defaults tematicos (IButton 30px, ILabel 23px) y FontSpec.SystemTheme. Fix bug visual: ControlProcedures.GetDefaultFont ahora usa NONCLIENTMETRICS.lfMessageFont cacheado con DeleteObject y fallback CreateFontFromSpec DPI-aware, corrige leak de HFONT. ControlBase.MeasureText* reemplaza GetStockObject(17) por fallback tematico y soporte pre-HWND. Button.MeasureContent usa PreferredHeight del provider con escalado DPI. Provider registrado primero en Application.Initialize y MasterRouter maneja WM_SETTINGCHANGE/WM_DPICHANGED con InvalidateDefaultFont + broadcast WM_SETFONT. Closes #36 --- .../Core/Application.cs | 3 + src/EasyWindowsApplication/Core/Constants.cs | 8 ++ .../Core/ControlBase.cs | 18 +++-- .../Core/HandleRegistry.cs | 6 ++ .../Core/MasterRouter.cs | 30 +++++++ .../Core/UiDefaults/ControlUiDefaults.cs | 10 +++ .../Core/UiDefaults/CoreUiDefaults.cs | 52 ++++++++++++ .../UiDefaults/DefaultUiValuesExtensions.cs | 10 +++ .../Core/UiDefaults/FontSpec.cs | 15 ++++ .../Core/UiDefaults/IDefaultUiValues.cs | 18 +++++ .../Core/UiDefaults/UiDefaultsProvider.cs | 16 ++++ .../Core/UiDefaults/WindowUiDefaults.cs | 13 +++ .../Win32ControlsModule/Backend/Controls.cs | 23 ++++++ .../Win32ControlsModule/Backend/Procedures.cs | 72 ++++++++++++++++- .../Win32ControlsModule/Backend/Win32.cs | 81 +++++++++++++++++++ .../Backend/Win32UiDefaults.cs | 19 +++++ 16 files changed, 386 insertions(+), 8 deletions(-) create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/ControlUiDefaults.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/CoreUiDefaults.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/DefaultUiValuesExtensions.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/FontSpec.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/IDefaultUiValues.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/UiDefaultsProvider.cs create mode 100644 src/EasyWindowsApplication/Core/UiDefaults/WindowUiDefaults.cs create mode 100644 src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32UiDefaults.cs diff --git a/src/EasyWindowsApplication/Core/Application.cs b/src/EasyWindowsApplication/Core/Application.cs index 0089381..de0e00b 100644 --- a/src/EasyWindowsApplication/Core/Application.cs +++ b/src/EasyWindowsApplication/Core/Application.cs @@ -51,6 +51,9 @@ internal void AddWindow(WindowModel window) public void Initialize() { + // 1. Registrar defaults PRIMERO (antes de cualquier control o HFONT) + try { UiDefaultsProvider.Set(new Win32ControlsModule.Backend.Win32UiDefaults()); } catch { } + var icc = new INITCOMMONCONTROLSEX { dwSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), diff --git a/src/EasyWindowsApplication/Core/Constants.cs b/src/EasyWindowsApplication/Core/Constants.cs index 2eab93a..bc1b04e 100644 --- a/src/EasyWindowsApplication/Core/Constants.cs +++ b/src/EasyWindowsApplication/Core/Constants.cs @@ -92,6 +92,14 @@ internal static class WM internal const uint MOUSEWHEEL = 0x020A; internal const uint ENTERSIZEMOVE = 0x0231; internal const uint EXITSIZEMOVE = 0x0232; + internal const uint SETTINGCHANGE = 0x001A; + internal const uint DPICHANGED = 0x02E0; + } + + internal static class SPI + { + internal const uint GETNONCLIENTMETRICS = 0x0029; + internal const uint SETNONCLIENTMETRICS = 0x002A; } internal static class WMSIZE diff --git a/src/EasyWindowsApplication/Core/ControlBase.cs b/src/EasyWindowsApplication/Core/ControlBase.cs index 945648c..393144e 100644 --- a/src/EasyWindowsApplication/Core/ControlBase.cs +++ b/src/EasyWindowsApplication/Core/ControlBase.cs @@ -3,6 +3,7 @@ using EasyWindowsApplication.Core.LayoutEngine; using EasyWindowsApplication.Share; +using EasyWindowsApplication.Win32ControlsModule.Backend; using EasyWindowsApplication.Win32ControlsModule.Frontend; using static EasyWindowsApplication.Core.Win32; @@ -210,14 +211,19 @@ protected string GetWindowText() } } + private static nint GetFallbackFont() + { + try { return ControlProcedures.GetDefaultFont(); } catch { return GetStockObject(17); } + } + protected (int Width, int Height) MeasureText(string text) { if (string.IsNullOrEmpty(text)) return (20, 24); nint hdc = GetDC(Hwnd); - nint hfont = SendMessageW(Hwnd, WM.GETFONT, 0, 0); - nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetStockObject(17)); + nint hfont = Hwnd != 0 ? SendMessageW(Hwnd, WM.GETFONT, 0, 0) : 0; + nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetFallbackFont()); RECT rect = default; DrawTextW(hdc, text, -1, ref rect, DT.CALCRECT | DT.SINGLELINE); SelectObject(hdc, oldFont); @@ -231,8 +237,8 @@ protected string GetWindowText() return (20, 24); nint hdc = GetDC(Hwnd); - nint hfont = SendMessageW(Hwnd, WM.GETFONT, 0, 0); - nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetStockObject(17)); + nint hfont = Hwnd != 0 ? SendMessageW(Hwnd, WM.GETFONT, 0, 0) : 0; + nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetFallbackFont()); GetTextExtentPoint32W(hdc, text, text.Length, out SIZE size); @@ -247,8 +253,8 @@ protected string GetWindowText() return (20, 24); nint hdc = GetDC(Hwnd); - nint hfont = SendMessageW(Hwnd, WM.GETFONT, 0, 0); - nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetStockObject(17)); + nint hfont = Hwnd != 0 ? SendMessageW(Hwnd, WM.GETFONT, 0, 0) : 0; + nint oldFont = SelectObject(hdc, hfont != 0 ? hfont : GetFallbackFont()); int totalWidth = 0; int maxHeight = 0; diff --git a/src/EasyWindowsApplication/Core/HandleRegistry.cs b/src/EasyWindowsApplication/Core/HandleRegistry.cs index 175c6ef..a684009 100644 --- a/src/EasyWindowsApplication/Core/HandleRegistry.cs +++ b/src/EasyWindowsApplication/Core/HandleRegistry.cs @@ -145,4 +145,10 @@ internal void UnregisterWindowByHwnd(nint hwnd) internal IBaseWindow? GetWindow(string name) => _windowByName.TryGetValue(name, out var w) ? w : null; + + internal IEnumerable AllControlHandles() + { + foreach (var kv in _hwndToControl) + yield return kv.Key; + } } diff --git a/src/EasyWindowsApplication/Core/MasterRouter.cs b/src/EasyWindowsApplication/Core/MasterRouter.cs index ce6eead..7f162f3 100644 --- a/src/EasyWindowsApplication/Core/MasterRouter.cs +++ b/src/EasyWindowsApplication/Core/MasterRouter.cs @@ -196,6 +196,36 @@ internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) return 0; } } + else if (msg == WM.SETTINGCHANGE) + { + // SPI_SETNONCLIENTMETRICS sent when system font/theme changes + if ((uint)wParam == SPI.SETNONCLIENTMETRICS) + { + try { Win32ControlsModule.Backend.ControlProcedures.InvalidateDefaultFont(); } catch { } + nint newFont = 0; + try { newFont = Win32ControlsModule.Backend.ControlProcedures.GetDefaultFont(); } catch { } + if (newFont != 0) + { + foreach (var ch in _registry.AllControlHandles()) + { + try { Win32.SendMessageW(ch, WM.SETFONT, newFont, 1); } catch { } + } + } + } + } + else if (msg == WM.DPICHANGED) + { + try { Win32ControlsModule.Backend.ControlProcedures.InvalidateDefaultFont(); } catch { } + nint newFont = 0; + try { newFont = Win32ControlsModule.Backend.ControlProcedures.GetDefaultFont(); } catch { } + if (newFont != 0) + { + foreach (var ch in _registry.AllControlHandles()) + { + try { Win32.SendMessageW(ch, WM.SETFONT, newFont, 1); } catch { } + } + } + } if (msg == WM.DESTROY) { diff --git a/src/EasyWindowsApplication/Core/UiDefaults/ControlUiDefaults.cs b/src/EasyWindowsApplication/Core/UiDefaults/ControlUiDefaults.cs new file mode 100644 index 0000000..0ff3ac6 --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/ControlUiDefaults.cs @@ -0,0 +1,10 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core; + +internal sealed record ControlUiDefaults +{ + public int? PreferredHeight { get; init; } + public Thickness? Padding { get; init; } + public Color? Background { get; init; } +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/CoreUiDefaults.cs b/src/EasyWindowsApplication/Core/UiDefaults/CoreUiDefaults.cs new file mode 100644 index 0000000..ac02d89 --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/CoreUiDefaults.cs @@ -0,0 +1,52 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core; + +internal abstract class CoreUiDefaults : IDefaultUiValues +{ + public virtual WindowUiDefaults MainWindow => new() + { + Width = 420, + Height = 280, + Position = WindowPositionOnScreen.Center, + Background = null + }; + + public virtual WindowUiDefaults AlternativeWindow => new() + { + Width = 400, + Height = 300, + Position = WindowPositionOnScreen.Center, + Background = null + }; + + public abstract FontSpec DefaultFont { get; } + + public virtual Color DefaultForeground + => ColorFromSysColor(Win32.GetSysColor(COLOR_WINDOWTEXT)); + + public virtual Color DefaultBackground + => ColorFromSysColor(Win32.GetSysColor(COLOR_WINDOW)); + + public virtual Thickness DefaultPadding => new(3); + public virtual int DefaultControlSpacing => 6; + public virtual bool EnableVisualStyles => true; + + public virtual IReadOnlyDictionary ControlDefaults + => _empty; + + private static readonly IReadOnlyDictionary _empty + = new Dictionary(); + + protected static Color ColorFromSysColor(int colorRef) + => Color.FromRgb( + (byte)(colorRef & 0xFF), + (byte)((colorRef >> 8) & 0xFF), + (byte)((colorRef >> 16) & 0xFF)); + + // SysColor indices (winuser.h) + private const int COLOR_WINDOW = 5; + private const int COLOR_WINDOWTEXT = 8; + private const int COLOR_BTNFACE = 15; + private const int COLOR_BTNTEXT = 18; +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/DefaultUiValuesExtensions.cs b/src/EasyWindowsApplication/Core/UiDefaults/DefaultUiValuesExtensions.cs new file mode 100644 index 0000000..1d7ada1 --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/DefaultUiValuesExtensions.cs @@ -0,0 +1,10 @@ +namespace EasyWindowsApplication.Core; + +internal static class DefaultUiValuesExtensions +{ + public static ControlUiDefaults? GetFor(this IDefaultUiValues d, Type t) + => d.ControlDefaults.TryGetValue(t, out var v) ? v : null; + + public static ControlUiDefaults? GetFor(this IDefaultUiValues d) + => d.GetFor(typeof(T)); +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/FontSpec.cs b/src/EasyWindowsApplication/Core/UiDefaults/FontSpec.cs new file mode 100644 index 0000000..7414c18 --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/FontSpec.cs @@ -0,0 +1,15 @@ +namespace EasyWindowsApplication.Core; + +internal enum FontWeight { Normal, Bold } +internal enum FontStyle { Regular, Italic } + +internal sealed record FontSpec +{ + public static readonly FontSpec SystemTheme = new() { IsSystemTheme = true }; + + public bool IsSystemTheme { get; init; } + public string Family { get; init; } = "Segoe UI"; + public float Size { get; init; } = 9f; + public FontWeight Weight { get; init; } = FontWeight.Normal; + public FontStyle Style { get; init; } = FontStyle.Regular; +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/IDefaultUiValues.cs b/src/EasyWindowsApplication/Core/UiDefaults/IDefaultUiValues.cs new file mode 100644 index 0000000..3dfacbf --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/IDefaultUiValues.cs @@ -0,0 +1,18 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core; + +internal interface IDefaultUiValues +{ + WindowUiDefaults MainWindow { get; } + WindowUiDefaults AlternativeWindow { get; } + + FontSpec DefaultFont { get; } + Color DefaultForeground { get; } + Color DefaultBackground { get; } + Thickness DefaultPadding { get; } + int DefaultControlSpacing { get; } + bool EnableVisualStyles { get; } + + IReadOnlyDictionary ControlDefaults { get; } +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/UiDefaultsProvider.cs b/src/EasyWindowsApplication/Core/UiDefaults/UiDefaultsProvider.cs new file mode 100644 index 0000000..c8fac77 --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/UiDefaultsProvider.cs @@ -0,0 +1,16 @@ +namespace EasyWindowsApplication.Core; + +internal static class UiDefaultsProvider +{ + private sealed class CoreFallbackDefaults : CoreUiDefaults + { + public override FontSpec DefaultFont => FontSpec.SystemTheme; + } + + private static IDefaultUiValues _current = new CoreFallbackDefaults(); + + public static IDefaultUiValues Current => _current; + + public static void Set(IDefaultUiValues defaults) + => _current = defaults ?? throw new ArgumentNullException(nameof(defaults)); +} diff --git a/src/EasyWindowsApplication/Core/UiDefaults/WindowUiDefaults.cs b/src/EasyWindowsApplication/Core/UiDefaults/WindowUiDefaults.cs new file mode 100644 index 0000000..ae548fc --- /dev/null +++ b/src/EasyWindowsApplication/Core/UiDefaults/WindowUiDefaults.cs @@ -0,0 +1,13 @@ +using EasyWindowsApplication.Share; + +namespace EasyWindowsApplication.Core; + +internal sealed record WindowUiDefaults +{ + public int Width { get; init; } = 420; + public int Height { get; init; } = 280; + public WindowPositionOnScreen Position { get; init; } = WindowPositionOnScreen.Center; + public Color? Background { get; init; } = null; + public string Title { get; init; } = ""; + public string Name { get; init; } = ""; +} diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs index c68ced6..3993daa 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs @@ -18,6 +18,29 @@ public string Text public bool Enabled { get; set; } = true; public void Click() => OnClick(() => { }); public void SetStyle(uint style, bool redraw = true) { } + + protected override (float Width, float Height) MeasureContent(float availableWidth, float availableHeight) + { + string text = GetWindowText(); + var (tw, th) = string.IsNullOrEmpty(text) ? (20, 24) : MeasureTextByGlyphs(text); + var defaults = UiDefaultsProvider.Current.GetFor(); + if (defaults == null) + defaults = UiDefaultsProvider.Current.GetFor