From 5236f933d78a35c3563e69aa8565abb4e42c4754 Mon Sep 17 00:00:00 2001 From: Raul Avila Catala Date: Thu, 27 Aug 2026 04:53:59 -0600 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20Framework=20Win32=20no=20funcional?= =?UTF-8?q?=20=E2=80=94=20fases=200-4=20(ComCtl32,=20factory,=20pipeline,?= =?UTF-8?q?=20Window=20API,=20scroll)=20(#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fase 0: app.manifest ComCtl32 v6 + PerMonitorV2 + InitCommonControlsEx Fase 1: INativeHandleFactory + Win32NativeHandleFactory + HandleRegistry WeakReference + MasterRouter WM_DESTROY cleanup Fase 2: Window.MaterializeContent + LayoutEngine DeferWindowPos batching + Controls Text sync Fase 3: Window propiedades conectadas + WM_SIZE/MOVE/CLOSE/ACTIVATE + Center/Maximize + re-layout Fase 4: Window Scroll (WS_VSCROLL, SetScrollInfo, WM_HSCROLL/VSCROLL/MOUSEWHEEL, ScrollTo, Vertical().AutoHide()) --- .../Common/ControlActivatorRegistry.cs | 51 ++ .../Common/INativeHandleFactory.cs | 13 + .../Core/Application.cs | 35 ++ src/EasyWindowsApplication/Core/Constants.cs | 72 +++ .../Core/ControlBase.cs | 9 +- src/EasyWindowsApplication/Core/Entities.cs | 16 + .../Core/HandleRegistry.cs | 78 ++- .../Core/LayoutEngine/LayoutEngine.cs | 36 +- .../Core/LayoutEngine/WindowConfigBuilder.cs | 8 +- .../Core/LayoutModels.cs | 1 + .../Core/MasterRouter.cs | 147 +++++- src/EasyWindowsApplication/Core/Win32.cs | 36 ++ .../Core/Windowing/AlternativeWindowImpl.cs | 173 ++++++- .../Core/Windowing/WindowImpl.cs | 480 +++++++++++++++++- .../EasyWindowsApplication.csproj | 1 + .../Share/CancelEventArgs.cs | 6 + .../Share/IBaseWindow.cs | 6 + src/EasyWindowsApplication/Share/IWindow.cs | 13 + .../Share/WindowMovedEventArgs.cs | 8 + .../Share/WindowResizedEventArgs.cs | 8 + .../Share/WindowResizingEventArgs.cs | 8 + .../Share/WindowsScrollExtensions.cs | 49 ++ .../Win32ControlsModule/Backend/Controls.cs | 22 +- .../Backend/Win32ControlActivator.cs | 6 + .../Backend/Win32NativeHandleFactory.cs | 60 +++ src/EasyWindowsApplication/app.manifest | 21 + 26 files changed, 1325 insertions(+), 38 deletions(-) create mode 100644 src/EasyWindowsApplication/Common/INativeHandleFactory.cs create mode 100644 src/EasyWindowsApplication/Share/CancelEventArgs.cs create mode 100644 src/EasyWindowsApplication/Share/WindowMovedEventArgs.cs create mode 100644 src/EasyWindowsApplication/Share/WindowResizedEventArgs.cs create mode 100644 src/EasyWindowsApplication/Share/WindowResizingEventArgs.cs create mode 100644 src/EasyWindowsApplication/Share/WindowsScrollExtensions.cs create mode 100644 src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32NativeHandleFactory.cs create mode 100644 src/EasyWindowsApplication/app.manifest diff --git a/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs b/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs index dbf9314..e29e40a 100644 --- a/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs +++ b/src/EasyWindowsApplication/Common/ControlActivatorRegistry.cs @@ -1,8 +1,12 @@ +using System.Diagnostics.CodeAnalysis; +using EasyWindowsApplication.Win32ControlsModule.Frontend; + namespace EasyWindowsApplication.Common; internal sealed partial class ControlActivatorRegistry { private readonly Dictionary> _factories = new(); + private readonly Dictionary _handleFactories = new(); private static bool _inited; private static readonly object _initLock = new(); @@ -11,6 +15,11 @@ internal void Register(Func factory) where T : class _factories[typeof(T)] = () => factory()!; } + internal void RegisterFactory(INativeHandleFactory factory) where T : class + { + _handleFactories[typeof(T)] = factory; + } + internal T Create() where T : class { if (_factories.TryGetValue(typeof(T), out var f)) @@ -25,6 +34,48 @@ internal object CreateFor(Type type) throw new InvalidOperationException($"No control implementation registered for '{type.FullName}'."); } + internal INativeHandleFactory? TryGetFactory(Type type) + => _handleFactories.TryGetValue(type, out var f) ? f : null; + + internal INativeHandleFactory CreateFactory() where T : class + { + if (_handleFactories.TryGetValue(typeof(T), out var f)) + return f; + throw new InvalidOperationException($"No handle factory registered for '{typeof(T).FullName}'. Did you forget to register Win32NativeHandleFactory?"); + } + + internal INativeHandleFactory CreateFactoryFor(Type type) + { + if (_handleFactories.TryGetValue(type, out var f)) + return f; + throw new InvalidOperationException($"No handle factory registered for '{type.FullName}'."); + } + + internal bool TryGetFactoryForControl(IControl control, out INativeHandleFactory? factory) + { + + var t = control.GetType(); + if (_handleFactories.TryGetValue(t, out factory)) + return true; +#pragma warning disable IL2075 // trimming: GetInterfaces usado solo para lookup de factories registradas (AOT: factories registradas vía source generator) + foreach (var iface in t.GetInterfaces()) + { + if (_handleFactories.TryGetValue(iface, out factory)) + return true; + } +#pragma warning restore IL2075 + // fallback por base types + var bt = t.BaseType; + while (bt != null && bt != typeof(object)) + { + if (_handleFactories.TryGetValue(bt, out factory)) + return true; + bt = bt.BaseType; + } + factory = null; + return false; + } + internal static ControlActivatorRegistry Shared { get; } = new(); internal static void EnsureInitialized() diff --git a/src/EasyWindowsApplication/Common/INativeHandleFactory.cs b/src/EasyWindowsApplication/Common/INativeHandleFactory.cs new file mode 100644 index 0000000..fba8368 --- /dev/null +++ b/src/EasyWindowsApplication/Common/INativeHandleFactory.cs @@ -0,0 +1,13 @@ +using EasyWindowsApplication.Core; +using EasyWindowsApplication.Win32ControlsModule.Frontend; + +namespace EasyWindowsApplication.Common; + +internal interface INativeHandleFactory +{ + /// + /// Crea el HWND nativo para el control y lo asigna a control.Hwnd. + /// Debe registrar el control en HandleRegistry (hwnd + name). + /// + nint CreateHandle(nint parentHwnd, IControl control, HandleRegistry registry); +} diff --git a/src/EasyWindowsApplication/Core/Application.cs b/src/EasyWindowsApplication/Core/Application.cs index ebee4eb..57f5501 100644 --- a/src/EasyWindowsApplication/Core/Application.cs +++ b/src/EasyWindowsApplication/Core/Application.cs @@ -52,6 +52,13 @@ internal void AddWindow(WindowModel window) public void Initialize() { + var icc = new INITCOMMONCONTROLSEX + { + dwSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), + dwICC = ICC.STANDARD_CLASSES + }; + Win32.InitCommonControlsEx(ref icc); + ControlActivatorRegistry.EnsureInitialized(); _router = new MasterRouter(_registry); @@ -80,7 +87,13 @@ private void RegisterMain(WindowModel window) } _registry.RegisterWindow(win); + // Fase 2: materializar contenido (creación de HWNDs hijos + layout batching sin flicker) + win.MaterializeContent(window, _registry, _router); + // Si Position == Center, centrar antes de mostrar (multi-monitor correcto) + if (window.Position == WindowPositionOnScreen.Center) + win.Center(); win.Show(); + win.RaiseLoaded(); } private void RegisterAlternative(WindowModel window) @@ -93,6 +106,28 @@ private void RegisterAlternative(WindowModel window) _router.RegisterWindowBackgroundBrush(hwnd, brush); } _registry.RegisterWindow(win); + // Fase 2: materializar contenido para AlternativeWindow también + win.MaterializeContent(window, _registry, _router); + if (window.Position == WindowPositionOnScreen.Center) + { + // Center para AlternativeWindow (usa misma lógica) + nint monitor = Win32.MonitorFromWindow(hwnd, MONITOR.DEFAULTTONEAREST); + if (monitor == 0) monitor = Win32.MonitorFromWindow(hwnd, MONITOR.DEFAULTTOPRIMARY); + if (monitor != 0) + { + MONITORINFO mi = new() { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf() }; + if (Win32.GetMonitorInfoW(monitor, ref mi)) + { + Win32.GetWindowRect(hwnd, out RECT wr); + int winW = wr.Right - wr.Left; + int winH = wr.Bottom - wr.Top; + int x = mi.rcWork.Left + ((mi.rcWork.Right - mi.rcWork.Left - winW) / 2); + int y = mi.rcWork.Top + ((mi.rcWork.Bottom - mi.rcWork.Top - winH) / 2); + Win32.SetWindowPos(hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); + } + } + } win.Show(); + win.RaiseLoaded(); } } diff --git a/src/EasyWindowsApplication/Core/Constants.cs b/src/EasyWindowsApplication/Core/Constants.cs index dd7eb01..a209bef 100644 --- a/src/EasyWindowsApplication/Core/Constants.cs +++ b/src/EasyWindowsApplication/Core/Constants.cs @@ -87,6 +87,31 @@ internal static class WM internal const uint HSCROLL = 0x0114; internal const uint VSCROLL = 0x0115; internal const uint MOUSEWHEEL = 0x020A; + internal const uint ENTERSIZEMOVE = 0x0231; + internal const uint EXITSIZEMOVE = 0x0232; + } + + internal static class WMSIZE + { + internal const int RESTORED = 0; + internal const int MINIMIZED = 1; + internal const int MAXIMIZED = 2; + internal const int MAXSHOW = 3; + internal const int MAXHIDE = 4; + } + + internal static class WA + { + internal const int INACTIVE = 0; + internal const int ACTIVE = 1; + internal const int CLICKACTIVE = 2; + } + + internal static class MONITOR + { + internal const uint DEFAULTTONULL = 0x00000000; + internal const uint DEFAULTTOPRIMARY = 0x00000001; + internal const uint DEFAULTTONEAREST = 0x00000002; } internal static class DT @@ -133,7 +158,18 @@ internal static class EN internal static class SW { + internal const int HIDE = 0; internal const int SHOWNORMAL = 1; + internal const int SHOWMINIMIZED = 2; + internal const int SHOWMAXIMIZED = 3; + internal const int SHOWNOACTIVATE = 4; + internal const int SHOW = 5; + internal const int MINIMIZE = 6; + internal const int SHOWMINNOACTIVE = 7; + internal const int SHOWNORMAL2 = 8; + internal const int RESTORE = 9; + internal const int SHOWDEFAULT = 10; + internal const int MAXIMIZE = 3; } internal static class GWL @@ -142,3 +178,39 @@ internal static class GWL internal const int EXSTYLE = -20; internal const int ID = -12; } + + internal static class ICC + { + internal const uint LISTVIEW_CLASSES = 0x00000001; + internal const uint TREEVIEW_CLASSES = 0x00000002; + internal const uint BAR_CLASSES = 0x00000004; + internal const uint TAB_CLASSES = 0x00000008; + internal const uint UPDOWN_CLASS = 0x00000010; + internal const uint PROGRESS_CLASS = 0x00000020; + internal const uint HOTKEY_CLASS = 0x00000040; + internal const uint ANIMATE_CLASS = 0x00000080; + internal const uint WIN95_CLASSES = 0x000000FF; + internal const uint DATE_CLASSES = 0x00000100; + internal const uint USEREX_CLASSES = 0x00000200; + internal const uint COOL_CLASSES = 0x00000400; + internal const uint INTERNET_CLASSES = 0x00000800; + internal const uint PAGESCROLLER_CLASS = 0x00001000; + internal const uint NATIVEFNTCTL_CLASS = 0x00002000; + internal const uint STANDARD_CLASSES = 0x00004000; + internal const uint LINK_CLASS = 0x00008000; + } + + internal static class SWP + { + internal const uint NOSIZE = 0x0001; + internal const uint NOMOVE = 0x0002; + internal const uint NOZORDER = 0x0004; + internal const uint NOREDRAW = 0x0008; + internal const uint NOACTIVATE = 0x0010; + internal const uint FRAMECHANGED = 0x0020; + internal const uint SHOWWINDOW = 0x0040; + internal const uint HIDEWINDOW = 0x0080; + internal const uint NOCOPYBITS = 0x0100; + internal const uint NOOWNERZORDER = 0x0200; + internal const uint NOSENDCHANGING = 0x0400; + } diff --git a/src/EasyWindowsApplication/Core/ControlBase.cs b/src/EasyWindowsApplication/Core/ControlBase.cs index 0f33e1c..945648c 100644 --- a/src/EasyWindowsApplication/Core/ControlBase.cs +++ b/src/EasyWindowsApplication/Core/ControlBase.cs @@ -16,6 +16,13 @@ internal ControlBase() { } public nint Hwnd { get; internal set; } public string Name { get; set; } = ""; + // IBaseWindow lifecycle events (requeridos por IBaseWindow pero no usados en controles) + public event EventHandler? Loaded { add { } remove { } } + public event EventHandler? Closing { add { } remove { } } + public event EventHandler? Closed { add { } remove { } } + public event EventHandler? Activated { add { } remove { } } + public event EventHandler? Deactivated { add { } remove { } } + private float _x, _y, _w, _h; internal event Action? InternalClick; @@ -142,7 +149,7 @@ internal void DestroyBackgroundBrush() float ILayoutable.MeasuredHeight => _measuredH; private float _measuredW, _measuredH; - private float _arrangedX, _arrangedY, _arrangedW, _arrangedH; + internal float _arrangedX, _arrangedY, _arrangedW, _arrangedH; void ILayoutable.Measure(float availableWidth, float availableHeight) { diff --git a/src/EasyWindowsApplication/Core/Entities.cs b/src/EasyWindowsApplication/Core/Entities.cs index e6e84c1..85c06ee 100644 --- a/src/EasyWindowsApplication/Core/Entities.cs +++ b/src/EasyWindowsApplication/Core/Entities.cs @@ -97,6 +97,22 @@ internal struct SCROLLINFO public int nTrackPos; } +[StructLayout(LayoutKind.Sequential)] +internal struct INITCOMMONCONTROLSEX +{ + public uint dwSize; + public uint dwICC; +} + +[StructLayout(LayoutKind.Sequential)] +internal struct MONITORINFO +{ + public uint cbSize; + public RECT rcMonitor; + public RECT rcWork; + public uint dwFlags; +} + [StructLayout(LayoutKind.Sequential)] internal struct DRAWITEMSTRUCT { diff --git a/src/EasyWindowsApplication/Core/HandleRegistry.cs b/src/EasyWindowsApplication/Core/HandleRegistry.cs index d9c8a63..004bb39 100644 --- a/src/EasyWindowsApplication/Core/HandleRegistry.cs +++ b/src/EasyWindowsApplication/Core/HandleRegistry.cs @@ -12,28 +12,94 @@ internal sealed class HandleRegistry internal static void UnregisterRouter(nint hwnd) => _routersByHwnd.TryRemove(hwnd, out _); internal static MasterRouter? GetRouter(nint hwnd) => _routersByHwnd.TryGetValue(hwnd, out var r) ? r : null; - private readonly Dictionary _hwndToControl = new(); - private readonly Dictionary _nameToControl = new(StringComparer.Ordinal); + private readonly Dictionary> _hwndToControl = new(); + private readonly Dictionary> _nameToControl = new(StringComparer.Ordinal); private readonly Dictionary _windowByName = new(StringComparer.Ordinal); + private readonly Dictionary _hwndToWindow = new(); internal void Register(nint hwnd, IControl control) { - _hwndToControl[hwnd] = control; + _hwndToControl[hwnd] = new WeakReference(control); if (!string.IsNullOrEmpty(control.Name)) - _nameToControl[control.Name] = control; + _nameToControl[control.Name] = new WeakReference(control); + } + + internal void Unregister(nint hwnd) + { + if (_hwndToControl.TryGetValue(hwnd, out var weak)) + { + if (weak.TryGetTarget(out var ctrl) && !string.IsNullOrEmpty(ctrl.Name)) + { + // Solo elimina la entrada de nombre si apunta al mismo control + if (_nameToControl.TryGetValue(ctrl.Name, out var nameWeak) + && nameWeak.TryGetTarget(out var nameCtrl) + && ReferenceEquals(nameCtrl, ctrl)) + { + _nameToControl.Remove(ctrl.Name); + } + } + _hwndToControl.Remove(hwnd); + } } internal void RegisterWindow(IBaseWindow window) { if (!string.IsNullOrEmpty(window.Name)) _windowByName[window.Name] = window; + if (window.Hwnd != 0) + _hwndToWindow[window.Hwnd] = window; + } + + internal void UnregisterWindow(string name) + { + if (!string.IsNullOrEmpty(name)) + { + if (_windowByName.TryGetValue(name, out var w)) + { + _windowByName.Remove(name); + if (w.Hwnd != 0) _hwndToWindow.Remove(w.Hwnd); + } + } } + internal void UnregisterWindowByHwnd(nint hwnd) + { + _hwndToWindow.Remove(hwnd); + // también limpiar por nombre si existe + string? keyToRemove = null; + foreach (var kv in _windowByName) + if (kv.Value.Hwnd == hwnd) { keyToRemove = kv.Key; break; } + if (keyToRemove != null) _windowByName.Remove(keyToRemove); + } + + internal IBaseWindow? GetWindowByHwnd(nint hwnd) + => _hwndToWindow.TryGetValue(hwnd, out var w) ? w : null; + + internal T? GetWindowByHwnd(nint hwnd) where T : class, IBaseWindow + => GetWindowByHwnd(hwnd) as T; + internal IControl? GetByHwnd(nint hwnd) - => _hwndToControl.TryGetValue(hwnd, out var c) ? c : null; + { + if (_hwndToControl.TryGetValue(hwnd, out var weak)) + { + if (weak.TryGetTarget(out var c)) + return c; + // Entrada muerta: limpieza perezosa + _hwndToControl.Remove(hwnd); + } + return null; + } internal IControl? GetByName(string name) - => _nameToControl.TryGetValue(name, out var c) ? c : null; + { + if (_nameToControl.TryGetValue(name, out var weak)) + { + if (weak.TryGetTarget(out var c)) + return c; + _nameToControl.Remove(name); + } + return null; + } internal T? GetByName(string name) where T : class, IControl => GetByName(name) as T; diff --git a/src/EasyWindowsApplication/Core/LayoutEngine/LayoutEngine.cs b/src/EasyWindowsApplication/Core/LayoutEngine/LayoutEngine.cs index e6b629a..0fa734c 100644 --- a/src/EasyWindowsApplication/Core/LayoutEngine/LayoutEngine.cs +++ b/src/EasyWindowsApplication/Core/LayoutEngine/LayoutEngine.cs @@ -1,4 +1,5 @@ -using EasyWindowsApplication.Share; +using EasyWindowsApplication.Core; +using EasyWindowsApplication.Share; namespace EasyWindowsApplication.Core.LayoutEngine; @@ -23,6 +24,39 @@ public void Execute( _strategy.Measure(children, availableWidth, availableHeight); _strategy.Arrange(children, availableWidth, availableHeight, spacing, padding); + // Batching Win32: cero flickering (DeferWindowPos) + nint hdwp = Win32.BeginDeferWindowPos(children.Count); + bool batching = hdwp != 0; + + if (batching) + { + foreach (var child in children) + { + if (child is ControlBase cb && cb.Hwnd != 0) + { + nint next = Win32.DeferWindowPos(hdwp, cb.Hwnd, 0, + (int)cb._arrangedX, (int)cb._arrangedY, (int)cb._arrangedW, (int)cb._arrangedH, + SWP.NOZORDER | SWP.NOACTIVATE); + if (next != 0) hdwp = next; + else batching = false; // fallback a Render individual si falla + } + else + { + // No es ControlBase (ej. contenedor lógico) → Render directo + child.Render(); + } + } + + if (batching) + { + Win32.EndDeferWindowPos(hdwp); + return; + } + // Si falló batching, EndDefer y fallback + if (hdwp != 0) Win32.EndDeferWindowPos(hdwp); + } + + // Fallback sin batching foreach (var child in children) child.Render(); } diff --git a/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs b/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs index f1f5251..b56594d 100644 --- a/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs +++ b/src/EasyWindowsApplication/Core/LayoutEngine/WindowConfigBuilder.cs @@ -40,7 +40,13 @@ public IWindowConfig Background(Color color) return this; } - public IWindowConfig Scroll(Action configure) => this; + public IWindowConfig Scroll(Action configure) + { + var cfg = new WindowsScroll(); + configure(cfg); + _model.ScrollConfig = cfg; + return this; + } public IWindowContentConfig Content(Action configure) where TLayout : IStackLayout => RegisterContent(configure); diff --git a/src/EasyWindowsApplication/Core/LayoutModels.cs b/src/EasyWindowsApplication/Core/LayoutModels.cs index ebfb15b..afea273 100644 --- a/src/EasyWindowsApplication/Core/LayoutModels.cs +++ b/src/EasyWindowsApplication/Core/LayoutModels.cs @@ -16,6 +16,7 @@ internal sealed class WindowModel internal Color? Background { get; set; } internal bool IsAlternative { get; set; } internal IContentModel? Content { get; set; } + internal WindowsScroll? ScrollConfig { get; set; } } internal sealed class ContentModel : IContentModel diff --git a/src/EasyWindowsApplication/Core/MasterRouter.cs b/src/EasyWindowsApplication/Core/MasterRouter.cs index 7eef885..30cf595 100644 --- a/src/EasyWindowsApplication/Core/MasterRouter.cs +++ b/src/EasyWindowsApplication/Core/MasterRouter.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using EasyWindowsApplication.Common; +using EasyWindowsApplication.Core.Windowing; using EasyWindowsApplication.Share; using EasyWindowsApplication.Win32ControlsModule.Frontend; @@ -11,6 +12,7 @@ internal sealed class MasterRouter private readonly Dictionary<(nint Hwnd, uint Msg), Win32MessageHandler> _handlers = new(); private readonly HandleRegistry _registry; private nint _mainHwnd; + private bool _isResizing; private readonly Dictionary _windowBrushes = new(); private readonly Dictionary _controlBrushes = new(); @@ -84,10 +86,149 @@ internal static nint WndProcTrampoline(nint hwnd, uint msg, nint wParam, nint lP internal nint WndProc(nint hwnd, uint msg, nint wParam, nint lParam) { - if (msg == WM.DESTROY && hwnd == _mainHwnd) + // ── Window lifecycle & resize routing (Fase 3) ── + if (msg == WM.CLOSE) { - Win32.PostQuitMessage(0); - return 0; + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + bool cancel = false; + if (win is WindowImpl wi) cancel = wi.RaiseClosing(); + else if (win is AlternativeWindowImpl aw) cancel = aw.RaiseClosing(); + if (cancel) return 0; + } + } + + if (msg == WM.ENTERSIZEMOVE) + { + _isResizing = true; + } + else if (msg == WM.EXITSIZEMOVE) + { + _isResizing = false; + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + Win32.GetClientRect(hwnd, out RECT rc); + int w = rc.Right - rc.Left; + int h = rc.Bottom - rc.Top; + if (win is WindowImpl wi) wi.RaiseResized(w, h); + } + } + else if (msg == WM.SIZE) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + int wParamType = (int)wParam; + if (wParamType != WMSIZE.MINIMIZED) + { + int w = (int)Win32Helpers.LOWORD(lParam); + int h = (int)Win32Helpers.HIWORD(lParam); + if (_isResizing) + { + if (win is WindowImpl wi) wi.RaiseResizing(w, h); + } + else + { + if (win is WindowImpl wi) wi.RaiseResized(w, h); + } + } + } + } + else if (msg == WM.MOVE) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + int x = (int)(short)Win32Helpers.LOWORD(lParam); + int y = (int)(short)Win32Helpers.HIWORD(lParam); + if (win is WindowImpl wi) wi.RaiseMoved(x, y); + } + } + else if (msg == WM.ACTIVATE) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + int state = (int)Win32Helpers.LOWORD(wParam); + if (state == WA.INACTIVE) + { + if (win is WindowImpl wi) wi.RaiseDeactivated(); + else if (win is AlternativeWindowImpl aw) aw.RaiseDeactivated(); + } + else + { + if (win is WindowImpl wi) wi.RaiseActivated(); + else if (win is AlternativeWindowImpl aw) aw.RaiseActivated(); + } + } + } + else if (msg == WM.HSCROLL) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win is WindowImpl wi) + { + int request = (int)Win32Helpers.LOWORD(wParam); + int pos = (int)Win32Helpers.HIWORD(wParam); + wi.HandleHScroll(request, pos); + return 0; + } + } + else if (msg == WM.VSCROLL) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win is WindowImpl wi) + { + int request = (int)Win32Helpers.LOWORD(wParam); + int pos = (int)Win32Helpers.HIWORD(wParam); + wi.HandleVScroll(request, pos); + return 0; + } + } + else if (msg == WM.MOUSEWHEEL) + { + var win = _registry.GetWindowByHwnd(hwnd); + if (win is WindowImpl wi) + { + short delta = (short)Win32Helpers.HIWORD(wParam); + wi.HandleMouseWheel(delta); + return 0; + } + } + + if (msg == WM.DESTROY) + { + // Disparar Closed antes de limpieza + var win = _registry.GetWindowByHwnd(hwnd); + if (win != null) + { + if (win is WindowImpl wi) wi.RaiseClosed(); + else if (win is AlternativeWindowImpl aw) aw.RaiseClosed(); + } + + // Limpieza determinística del registry (Fase 1: evita memory leak) + _registry.Unregister(hwnd); + _registry.UnregisterWindowByHwnd(hwnd); + HandleRegistry.UnregisterRouter(hwnd); + _windowBrushes.Remove(hwnd); + _controlBrushes.Remove(hwnd); + _layoutGroupBackgrounds.Remove(hwnd); + _scrollOffsets.Remove(hwnd); + // Limpia handlers asociados a este hwnd (evita leak de delegates) + if (_handlers.Count > 0) + { + // Copia claves para evitar modificar durante enumeración + var toRemove = new List<(nint Hwnd, uint Msg)>(); + foreach (var key in _handlers.Keys) + if (key.Hwnd == hwnd) toRemove.Add(key); + foreach (var key in toRemove) _handlers.Remove(key); + } + if (hwnd == _mainHwnd) + { + Win32.PostQuitMessage(0); + return 0; + } } // Handle WM_ERASEBKGND — paint window background, then LayoutGroup backgrounds on top diff --git a/src/EasyWindowsApplication/Core/Win32.cs b/src/EasyWindowsApplication/Core/Win32.cs index cd324c3..7612392 100644 --- a/src/EasyWindowsApplication/Core/Win32.cs +++ b/src/EasyWindowsApplication/Core/Win32.cs @@ -173,4 +173,40 @@ internal static partial class Win32 [LibraryImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static partial bool ScreenToClient(nint hWnd, ref POINT lpPoint); + + // ── ComCtl32 ── + [LibraryImport("comctl32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool InitCommonControlsEx(ref INITCOMMONCONTROLSEX lpInitCtrls); + + // ── DeferWindowPos (batching anti-flicker) ── + [LibraryImport("user32.dll", SetLastError = true)] + internal static partial nint BeginDeferWindowPos(int nNumWindows); + + [LibraryImport("user32.dll", SetLastError = true)] + internal static partial nint DeferWindowPos(nint hWinPosInfo, nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool EndDeferWindowPos(nint hWinPosInfo); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags); + + [LibraryImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetForegroundWindow(nint hWnd); + + [LibraryImport("user32.dll")] + internal static partial nint MonitorFromWindow(nint hwnd, uint dwFlags); + + [LibraryImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetMonitorInfoW(nint hMonitor, ref MONITORINFO lpmi); + + // SetWindowTextW con string (para Title) + [LibraryImport("user32.dll", EntryPoint = "SetWindowTextW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetWindowText(nint hWnd, string lpString); } diff --git a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs index 9be6eac..4490f92 100644 --- a/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/AlternativeWindowImpl.cs @@ -1,4 +1,7 @@ -using EasyWindowsApplication.Share; +using EasyWindowsApplication.Common; +using EasyWindowsApplication.Core; +using EasyWindowsApplication.Core.LayoutEngine; +using EasyWindowsApplication.Share; namespace EasyWindowsApplication.Core.Windowing; @@ -7,23 +10,169 @@ internal sealed class AlternativeWindowImpl : IAlternativeWindow public nint Hwnd { get; } public string Name { get; } public nint OwnerHwnd { get; } - public string Title { get; set; } - public int Width { get; set; } - public int Height { get; set; } - public WindowPositionOnScreen PositionMode { get; set; } + private string _title; + public string Title + { + get => _title; + set + { + _title = value ?? ""; + if (Hwnd != 0) EasyWindowsApplication.Core.Win32.SetWindowText(Hwnd, _title); + } + } + private int _width; + public int Width + { + get => _width; + set + { + if (_width == value) return; + _width = value; + if (Hwnd != 0) + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + } + } + private int _height; + public int Height + { + get => _height; + set + { + if (_height == value) return; + _height = value; + if (Hwnd != 0) + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, _width, _height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + } + } + private WindowPositionOnScreen _positionMode; + public WindowPositionOnScreen PositionMode + { + get => _positionMode; + set + { + if (_positionMode == value) return; + _positionMode = value; + if (Hwnd != 0 && _positionMode == WindowPositionOnScreen.Center) + { + // Reuse Center logic similar to WindowImpl + nint monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); + if (monitor == 0) monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); + if (monitor != 0) + { + MONITORINFO mi = new() { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf() }; + if (EasyWindowsApplication.Core.Win32.GetMonitorInfoW(monitor, ref mi)) + { + EasyWindowsApplication.Core.Win32.GetWindowRect(Hwnd, out RECT wr); + int winW = wr.Right - wr.Left; + int winH = wr.Bottom - wr.Top; + int workW = mi.rcWork.Right - mi.rcWork.Left; + int workH = mi.rcWork.Bottom - mi.rcWork.Top; + int x = mi.rcWork.Left + (workW - winW) / 2; + int y = mi.rcWork.Top + (workH - winH) / 2; + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); + } + } + } + } + } + + private List? _materializedChildren; + private ContentModel? _contentModel; + + public event EventHandler? Loaded; + public event EventHandler? Closing; + public event EventHandler? Closed; + public event EventHandler? Activated; + public event EventHandler? Deactivated; internal AlternativeWindowImpl(nint hwnd, nint ownerHwnd, string name, string title, int width, int height, WindowPositionOnScreen position) { Hwnd = hwnd; OwnerHwnd = ownerHwnd; Name = name; - Title = title; - Width = width; - Height = height; - PositionMode = position; + _title = title ?? ""; + _width = width; + _height = height; + _positionMode = position; } - public void Show() => Win32.ShowWindow(Hwnd, 5); - public void Hide() => Win32.ShowWindow(Hwnd, 0); - public void Close() => Win32.DestroyWindow(Hwnd); + public void Show() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.SHOW); + public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); + public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + + internal void RaiseLoaded() => Loaded?.Invoke(this, EventArgs.Empty); + internal bool RaiseClosing() + { + var args = new CancelEventArgs(); + Closing?.Invoke(this, args); + return args.Cancel; + } + internal void RaiseClosed() => Closed?.Invoke(this, EventArgs.Empty); + internal void RaiseActivated() => Activated?.Invoke(this, EventArgs.Empty); + internal void RaiseDeactivated() => Deactivated?.Invoke(this, EventArgs.Empty); + + internal void MaterializeContent(WindowModel window, HandleRegistry registry, MasterRouter router) + { + if (window.Content is not ContentModel content) return; + if (content.Children.Count == 0) return; + + var layoutables = new List(); + MaterializeChildren(content, Hwnd, registry, router, layoutables); + + if (layoutables.Count == 0) return; + + Win32.GetClientRect(Hwnd, out RECT rect); + float availW = rect.Right - rect.Left; + float availH = rect.Bottom - rect.Top; + if (availW <= 0) availW = window.Width; + if (availH <= 0) availH = window.Height; + + var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + engine.Execute(layoutables, availW, availH, content.Spacing, content.Padding); + + _materializedChildren = layoutables; + _contentModel = content; + } + + private static void MaterializeChildren(ContentModel model, nint parentHwnd, HandleRegistry registry, MasterRouter router, List outLayoutables) + { + foreach (var vmBase in model.Children) + { + if (vmBase is not ViewModel vm) continue; + var control = vm.Control; + if (control is null) continue; + + if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(control, out var factory) || factory is null) + throw new InvalidOperationException($"No handle factory registered for control '{control.GetType().Name}' (name='{control.Name}')."); + + nint hwnd = factory.CreateHandle(parentHwnd, control, registry); + if (hwnd == 0) + throw new InvalidOperationException($"CreateHandle failed for control '{control.Name}' ({control.GetType().Name})"); + + if (control is ControlBase cb) + { + cb.Router = router; + cb.Registry = registry; + nint brush = cb.GetOrCreateBackgroundBrush(); + if (brush != 0) router.RegisterControlBrush(hwnd, brush); + } + + outLayoutables.Add((ILayoutable)control); + + if (vm.SubContent is ContentModel sub) + { + var subLayoutables = new List(); + MaterializeChildren(sub, hwnd, registry, router, subLayoutables); + if (control is UserControl uc) + { + foreach (var subChild in subLayoutables) uc.Children.Add(subChild); + uc.Spacing = sub.Spacing; + } + else if (subLayoutables.Count > 0) + { + outLayoutables.AddRange(subLayoutables); + } + } + } + } } diff --git a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs index f34f90b..33a6ffc 100644 --- a/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs +++ b/src/EasyWindowsApplication/Core/Windowing/WindowImpl.cs @@ -1,4 +1,7 @@ -using EasyWindowsApplication.Share; +using EasyWindowsApplication.Common; +using EasyWindowsApplication.Core; +using EasyWindowsApplication.Core.LayoutEngine; +using EasyWindowsApplication.Share; namespace EasyWindowsApplication.Core.Windowing; @@ -6,22 +9,475 @@ internal sealed class WindowImpl : IWindow { public nint Hwnd { get; } public string Name { get; } - public string Title { get; set; } - public float Width { get; set; } - public float Height { get; set; } - public WindowPositionOnScreen PositionMode { get; set; } + private string _title; + public string Title + { + get => _title; + set + { + _title = value ?? ""; + if (Hwnd != 0) EasyWindowsApplication.Core.Win32.SetWindowText(Hwnd, _title); + } + } + + private float _width; + public float Width + { + get => _width; + set + { + if (_width == value) return; + _width = value; + if (Hwnd != 0) + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + } + } + + private float _height; + public float Height + { + get => _height; + set + { + if (_height == value) return; + _height = value; + if (Hwnd != 0) + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, (int)_width, (int)_height, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE); + } + } + + private WindowPositionOnScreen _positionMode; + public WindowPositionOnScreen PositionMode + { + get => _positionMode; + set + { + if (_positionMode == value) return; + _positionMode = value; + if (Hwnd != 0 && _positionMode == WindowPositionOnScreen.Center) + Center(); + } + } + + private List? _materializedChildren; + private ContentModel? _contentModel; + + // Scroll state (Fase 4) + private WindowsScroll? _scrollConfig; + private int _scrollX; + private int _scrollY; + private int _maxScrollX; + private int _maxScrollY; + private int _clientW; + private int _clientH; + + public (int X, int Y) ScrollOffset => (_scrollX, _scrollY); + + public event EventHandler? Loaded; + public event EventHandler? Closing; + public event EventHandler? Closed; + public event EventHandler? Activated; + public event EventHandler? Deactivated; + public event EventHandler? Resizing; + public event EventHandler? Resized; + public event EventHandler? Moved; internal WindowImpl(nint hwnd, string name, string title, float width, float height, WindowPositionOnScreen position) { Hwnd = hwnd; Name = name; - Title = title; - Width = width; - Height = height; - PositionMode = position; + _title = title ?? ""; + _width = width; + _height = height; + _positionMode = position; } - public void Show() => Win32.ShowWindow(Hwnd, 5); - public void Hide() => Win32.ShowWindow(Hwnd, 0); - public void Close() => Win32.DestroyWindow(Hwnd); + public void Show() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.SHOW); + public void Hide() => EasyWindowsApplication.Core.Windowing.Win32.ShowWindow(Hwnd, SW.HIDE); + public void Close() => EasyWindowsApplication.Core.Windowing.Win32.DestroyWindow(Hwnd); + + public void Center() + { + if (Hwnd == 0) return; + nint monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTONEAREST); + if (monitor == 0) monitor = EasyWindowsApplication.Core.Win32.MonitorFromWindow(Hwnd, MONITOR.DEFAULTTOPRIMARY); + if (monitor == 0) return; + + MONITORINFO mi = new() { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf() }; + if (!EasyWindowsApplication.Core.Win32.GetMonitorInfoW(monitor, ref mi)) return; + + EasyWindowsApplication.Core.Win32.GetWindowRect(Hwnd, out RECT wr); + int winW = wr.Right - wr.Left; + int winH = wr.Bottom - wr.Top; + + int workW = mi.rcWork.Right - mi.rcWork.Left; + int workH = mi.rcWork.Bottom - mi.rcWork.Top; + + int x = mi.rcWork.Left + (workW - winW) / 2; + int y = mi.rcWork.Top + (workH - winH) / 2; + + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, x, y, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOSIZE); + } + + public void Maximize() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.MAXIMIZE); + public void Minimize() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.MINIMIZE); + public void Restore() => EasyWindowsApplication.Core.Win32.ShowWindow(Hwnd, SW.RESTORE); + public void Focus() => EasyWindowsApplication.Core.Win32.SetForegroundWindow(Hwnd); + + internal void RaiseLoaded() => Loaded?.Invoke(this, EventArgs.Empty); + internal bool RaiseClosing() + { + var args = new CancelEventArgs(); + Closing?.Invoke(this, args); + return args.Cancel; + } + internal void RaiseClosed() => Closed?.Invoke(this, EventArgs.Empty); + internal void RaiseActivated() => Activated?.Invoke(this, EventArgs.Empty); + internal void RaiseDeactivated() => Deactivated?.Invoke(this, EventArgs.Empty); + public void ScrollTo(int x, int y) + { + if (Hwnd == 0) return; + int newX = Math.Clamp(x, 0, Math.Max(0, _maxScrollX)); + int newY = Math.Clamp(y, 0, Math.Max(0, _maxScrollY)); + if (newX == _scrollX && newY == _scrollY) return; + _scrollX = newX; + _scrollY = newY; + + // Actualizar SCROLLINFO + if (_scrollConfig != null) + { + if (_maxScrollY > 0 || _scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) + { + var siV = new SCROLLINFO { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), fMask = SIF.POS, nPos = _scrollY }; + EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 1, ref siV, true); + } + if (_maxScrollX > 0 || _scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) + { + var siH = new SCROLLINFO { cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), fMask = SIF.POS, nPos = _scrollX }; + EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 0, ref siH, true); + } + } + + // Notificar a MasterRouter para offsets de background + // Se busca router vía HandleRegistry (si está registrado) + var router = HandleRegistry.GetRouter(Hwnd); + router?.SetScrollOffset(Hwnd, _scrollX, _scrollY); + + ApplyScrollOffset(); + } + + private void ApplyScrollOffset() + { + if (_materializedChildren == null || _materializedChildren.Count == 0) return; + nint hdwp = EasyWindowsApplication.Core.Win32.BeginDeferWindowPos(_materializedChildren.Count); + bool batching = hdwp != 0; + if (batching) + { + foreach (var child in _materializedChildren) + { + if (child is ControlBase cb && cb.Hwnd != 0) + { + int x = (int)cb._arrangedX - _scrollX; + int y = (int)cb._arrangedY - _scrollY; + nint next = EasyWindowsApplication.Core.Win32.DeferWindowPos(hdwp, cb.Hwnd, 0, x, y, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); + if (next != 0) hdwp = next; + else batching = false; + } + } + if (batching) + { + EasyWindowsApplication.Core.Win32.EndDeferWindowPos(hdwp); + return; + } + if (hdwp != 0) EasyWindowsApplication.Core.Win32.EndDeferWindowPos(hdwp); + } + foreach (var child in _materializedChildren) + { + if (child is ControlBase cb && cb.Hwnd != 0) + { + EasyWindowsApplication.Core.Win32.SetWindowPos(cb.Hwnd, 0, (int)cb._arrangedX - _scrollX, (int)cb._arrangedY - _scrollY, (int)cb._arrangedW, (int)cb._arrangedH, SWP.NOZORDER | SWP.NOACTIVATE); + } + } + } + + internal void HandleVScroll(int request, int thumbPos) + { + if (_scrollConfig == null) return; + int newPos = _scrollY; + switch (request) + { + case 0: // SB_LINEUP + newPos = _scrollY - 20; break; + case 1: // SB_LINEDOWN + newPos = _scrollY + 20; break; + case 2: // SB_PAGEUP + newPos = _scrollY - _clientH; break; + case 3: // SB_PAGEDOWN + newPos = _scrollY + _clientH; break; + case 4: // SB_THUMBPOSITION + case 5: // SB_THUMBTRACK + newPos = thumbPos; break; + case 6: // SB_TOP + newPos = 0; break; + case 7: // SB_BOTTOM + newPos = _maxScrollY; break; + case 8: // SB_ENDSCROLL + return; + default: return; + } + ScrollTo(_scrollX, newPos); + } + + internal void HandleHScroll(int request, int thumbPos) + { + if (_scrollConfig == null) return; + int newPos = _scrollX; + switch (request) + { + case 0: // SB_LINELEFT + newPos = _scrollX - 20; break; + case 1: // SB_LINERIGHT + newPos = _scrollX + 20; break; + case 2: // SB_PAGELEFT + newPos = _scrollX - _clientW; break; + case 3: // SB_PAGERIGHT + newPos = _scrollX + _clientW; break; + case 4: + case 5: + newPos = thumbPos; break; + case 6: + newPos = 0; break; + case 7: + newPos = _maxScrollX; break; + case 8: + return; + default: return; + } + ScrollTo(newPos, _scrollY); + } + + internal void HandleMouseWheel(int delta) + { + if (_scrollConfig == null) return; + // Rueda vertical por defecto; si orientación es Horizontal, scrollear horizontal + bool isHorizontal = _scrollConfig.Orientation == ScrollOrientation.Horizontal; + if (isHorizontal) + { + int newX = _scrollX - delta * 30 / 120; + ScrollTo(newX, _scrollY); + } + else + { + int newY = _scrollY - delta * 30 / 120; + ScrollTo(_scrollX, newY); + } + } + + private void ConfigureScrollbars() + { + if (_scrollConfig == null || Hwnd == 0) return; + + // Calcular tamaños totales del contenido + int totalH = 0; + int totalW = 0; + if (_materializedChildren != null && _materializedChildren.Count > 0) + { + // Altura total para VerticalStack + foreach (var c in _materializedChildren) + { + totalH += (int)(c.MeasuredHeight + c.Margin.Top + c.Margin.Bottom); + totalW = Math.Max(totalW, (int)(c.MeasuredWidth + c.Margin.Left + c.Margin.Right)); + } + if (_materializedChildren.Count > 1) totalH += (int)(_contentModel!.Spacing * (_materializedChildren.Count - 1)); + totalH += (int)(_contentModel!.Padding.Top + _contentModel.Padding.Bottom); + totalW += (int)(_contentModel.Padding.Left + _contentModel.Padding.Right); + } + + bool wantsV = _scrollConfig.Orientation == ScrollOrientation.Vertical || _scrollConfig.Orientation == ScrollOrientation.Both; + bool wantsH = _scrollConfig.Orientation == ScrollOrientation.Horizontal || _scrollConfig.Orientation == ScrollOrientation.Both; + + bool needV = false, needH = false; + if (wantsV) + { + needV = totalH > _clientH; + if (_scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Never) needV = false; + else if (_scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) needV = true; + } + if (wantsH) + { + needH = totalW > _clientW; + if (_scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Never) needH = false; + else if (_scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) needH = true; + } + + // Activar estilos WS_HSCROLL/VSCROLL + int style = EasyWindowsApplication.Core.Win32.GetWindowLongW(Hwnd, GWL.STYLE); + int newStyle = style; + if (needV) newStyle |= (int)WS.VSCROLL; else newStyle &= ~(int)WS.VSCROLL; + if (needH) newStyle |= (int)WS.HSCROLL; else newStyle &= ~(int)WS.HSCROLL; + if (newStyle != style) + { + EasyWindowsApplication.Core.Win32.SetWindowLongW(Hwnd, GWL.STYLE, newStyle); + EasyWindowsApplication.Core.Win32.SetWindowPos(Hwnd, 0, 0, 0, 0, 0, SWP.NOZORDER | SWP.NOACTIVATE | SWP.NOMOVE | SWP.NOSIZE | SWP.FRAMECHANGED); + } + + _maxScrollY = Math.Max(0, totalH - _clientH); + _maxScrollX = Math.Max(0, totalW - _clientW); + + // Clampear offset actual + _scrollX = Math.Clamp(_scrollX, 0, _maxScrollX); + _scrollY = Math.Clamp(_scrollY, 0, _maxScrollY); + + // Configurar SCROLLINFO + if (needV || _scrollConfig.VerticalScrollBarVisibility == ScrollBarVisibility.Always) + { + var si = new SCROLLINFO + { + cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), + fMask = SIF.RANGE | SIF.PAGE | SIF.POS, + nMin = 0, + nMax = Math.Max(0, totalH - 1), + nPage = (uint)Math.Max(0, _clientH), + nPos = _scrollY + }; + EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 1, ref si, true); + } + if (needH || _scrollConfig.HorizontalScrollBarVisibility == ScrollBarVisibility.Always) + { + var si = new SCROLLINFO + { + cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), + fMask = SIF.RANGE | SIF.PAGE | SIF.POS, + nMin = 0, + nMax = Math.Max(0, totalW - 1), + nPage = (uint)Math.Max(0, _clientW), + nPos = _scrollX + }; + EasyWindowsApplication.Core.Win32.SetScrollInfo(Hwnd, 0, ref si, true); + } + + // Sincronizar con MasterRouter para dibujado + var router = HandleRegistry.GetRouter(Hwnd); + router?.SetScrollOffset(Hwnd, _scrollX, _scrollY); + + // Aplicar offset a hijos si hay scroll + if (_scrollX != 0 || _scrollY != 0) ApplyScrollOffset(); + } + + internal void RaiseResizing(int w, int h) => Resizing?.Invoke(this, new WindowResizingEventArgs(w, h)); + internal void RaiseResized(int w, int h) + { + // Actualizar backing fields para Width/Height (evitar recursión SetWindowPos) + _width = w; + _height = h; + _clientW = w; + _clientH = h; + + if (_materializedChildren != null && _contentModel != null && _materializedChildren.Count > 0) + { + var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + engine.Execute(_materializedChildren, w, h, _contentModel.Spacing, _contentModel.Padding); + } + + // Reconfigurar scrollbars tras resize (puede cambiar necesidad de scroll) + if (_scrollConfig != null) ConfigureScrollbars(); + + Resized?.Invoke(this, new WindowResizedEventArgs(w, h)); + } + internal void RaiseMoved(int x, int y) => Moved?.Invoke(this, new WindowMovedEventArgs(x, y)); + + internal void MaterializeContent(WindowModel window, HandleRegistry registry, MasterRouter router) + { + if (window.Content is not ContentModel content) return; + if (content.Children.Count == 0) return; + + var layoutables = new List(); + MaterializeChildren(content, Hwnd, registry, router, layoutables); + + if (layoutables.Count == 0) return; + + // Calcular área disponible (client rect) + Win32.GetClientRect(Hwnd, out RECT rect); + float availW = rect.Right - rect.Left; + float availH = rect.Bottom - rect.Top; + if (availW <= 0) availW = window.Width; + if (availH <= 0) availH = window.Height; + + var engine = new EasyWindowsApplication.Core.LayoutEngine.LayoutEngine(new VerticalStackLayoutStrategy()); + engine.Execute(layoutables, availW, availH, content.Spacing, content.Padding); + + _materializedChildren = layoutables; + _contentModel = content; + _clientW = (int)availW; + _clientH = (int)availH; + + // Fase 4: Scroll + if (window.ScrollConfig != null) + { + _scrollConfig = window.ScrollConfig; + _scrollX = (int)window.ScrollConfig.ScrollX; + _scrollY = (int)window.ScrollConfig.ScrollY; + ConfigureScrollbars(); + // Si hay offset inicial, aplicar + if (_scrollX != 0 || _scrollY != 0) ScrollTo(_scrollX, _scrollY); + } + } + + private static void MaterializeChildren(ContentModel model, nint parentHwnd, HandleRegistry registry, MasterRouter router, List outLayoutables) + { + foreach (var vmBase in model.Children) + { + if (vmBase is not ViewModel vm) continue; + var control = vm.Control; + if (control is null) continue; + + // Resolver factory por tipo concreto/interfaz + if (!ControlActivatorRegistry.Shared.TryGetFactoryForControl(control, out var factory) || factory is null) + throw new InvalidOperationException($"No handle factory registered for control '{control.GetType().Name}' (name='{control.Name}'). Registre un INativeHandleFactory para ese tipo."); + + nint hwnd = factory.CreateHandle(parentHwnd, control, registry); + if (hwnd == 0) + throw new InvalidOperationException($"CreateHandle failed for control '{control.Name}' ({control.GetType().Name})"); + + if (control is ControlBase cb) + { + cb.Router = router; + cb.Registry = registry; + + // Registrar brush si tiene Background + nint brush = cb.GetOrCreateBackgroundBrush(); + if (brush != 0) + router.RegisterControlBrush(hwnd, brush); + } + + outLayoutables.Add((ILayoutable)control); + + // Recursión para SubContent (contenedores anidados) + if (vm.SubContent is ContentModel sub) + { + // Los hijos de SubContent son hijos visuales de 'control' + // Se materializan bajo hwnd del control padre, pero NO se añaden al layout del window + // El layout interno será gestionado por el control contenedor (ej. UserControl.PostRender) en Fase 2 deja batch simple para ellos + var subLayoutables = new List(); + MaterializeChildren(sub, hwnd, registry, router, subLayoutables); + + // Si el control es UserControl, inyectar hijos y hacer layout inmediato del contenedor + if (control is UserControl uc) + { + foreach (var subChild in subLayoutables) + uc.Children.Add(subChild); + uc.Spacing = sub.Spacing; + // Padding del UserControl ya viene de View.Padding, pero también aplicar el Padding del sub-content si existe + // Layout interno: se hará en PostRender, pero podemos hacer un primer layout usando GetClientRect del hwnd recién creado + // (opcional, PostRender lo repetirá al primer Render) + } + else if (subLayoutables.Count > 0) + { + // Para controles no contenedor, tratar sub-hijos como hermanos adicionales bajo la ventana + // (fallback) — no debería ocurrir en uso normal + outLayoutables.AddRange(subLayoutables); + } + } + } + } } diff --git a/src/EasyWindowsApplication/EasyWindowsApplication.csproj b/src/EasyWindowsApplication/EasyWindowsApplication.csproj index 3cfa231..afd85f8 100644 --- a/src/EasyWindowsApplication/EasyWindowsApplication.csproj +++ b/src/EasyWindowsApplication/EasyWindowsApplication.csproj @@ -5,6 +5,7 @@ enable enable True + app.manifest True EasyWindowsApplication diff --git a/src/EasyWindowsApplication/Share/CancelEventArgs.cs b/src/EasyWindowsApplication/Share/CancelEventArgs.cs new file mode 100644 index 0000000..a9cc7b7 --- /dev/null +++ b/src/EasyWindowsApplication/Share/CancelEventArgs.cs @@ -0,0 +1,6 @@ +namespace EasyWindowsApplication.Share; + +public sealed class CancelEventArgs : EventArgs +{ + public bool Cancel { get; set; } +} diff --git a/src/EasyWindowsApplication/Share/IBaseWindow.cs b/src/EasyWindowsApplication/Share/IBaseWindow.cs index 76f5705..435b2b6 100644 --- a/src/EasyWindowsApplication/Share/IBaseWindow.cs +++ b/src/EasyWindowsApplication/Share/IBaseWindow.cs @@ -7,4 +7,10 @@ public interface IBaseWindow void Show(); void Hide(); void Close(); + + event EventHandler? Loaded; + event EventHandler? Closing; + event EventHandler? Closed; + event EventHandler? Activated; + event EventHandler? Deactivated; } diff --git a/src/EasyWindowsApplication/Share/IWindow.cs b/src/EasyWindowsApplication/Share/IWindow.cs index f1e543c..4aa5dc0 100644 --- a/src/EasyWindowsApplication/Share/IWindow.cs +++ b/src/EasyWindowsApplication/Share/IWindow.cs @@ -7,4 +7,17 @@ public interface IWindow : IBaseWindow float Width { get; set; } float Height { get; set; } WindowPositionOnScreen PositionMode { get; set; } + + event EventHandler? Resizing; + event EventHandler? Resized; + event EventHandler? Moved; + + void Center(); + void Maximize(); + void Minimize(); + void Restore(); + void Focus(); + + (int X, int Y) ScrollOffset { get; } + void ScrollTo(int x, int y); } diff --git a/src/EasyWindowsApplication/Share/WindowMovedEventArgs.cs b/src/EasyWindowsApplication/Share/WindowMovedEventArgs.cs new file mode 100644 index 0000000..a8e794d --- /dev/null +++ b/src/EasyWindowsApplication/Share/WindowMovedEventArgs.cs @@ -0,0 +1,8 @@ +namespace EasyWindowsApplication.Share; + +public sealed class WindowMovedEventArgs : EventArgs +{ + public int X { get; } + public int Y { get; } + public WindowMovedEventArgs(int x, int y) { X = x; Y = y; } +} diff --git a/src/EasyWindowsApplication/Share/WindowResizedEventArgs.cs b/src/EasyWindowsApplication/Share/WindowResizedEventArgs.cs new file mode 100644 index 0000000..28823b5 --- /dev/null +++ b/src/EasyWindowsApplication/Share/WindowResizedEventArgs.cs @@ -0,0 +1,8 @@ +namespace EasyWindowsApplication.Share; + +public sealed class WindowResizedEventArgs : EventArgs +{ + public int Width { get; } + public int Height { get; } + public WindowResizedEventArgs(int width, int height) { Width = width; Height = height; } +} diff --git a/src/EasyWindowsApplication/Share/WindowResizingEventArgs.cs b/src/EasyWindowsApplication/Share/WindowResizingEventArgs.cs new file mode 100644 index 0000000..5a14147 --- /dev/null +++ b/src/EasyWindowsApplication/Share/WindowResizingEventArgs.cs @@ -0,0 +1,8 @@ +namespace EasyWindowsApplication.Share; + +public sealed class WindowResizingEventArgs : EventArgs +{ + public int Width { get; } + public int Height { get; } + public WindowResizingEventArgs(int width, int height) { Width = width; Height = height; } +} diff --git a/src/EasyWindowsApplication/Share/WindowsScrollExtensions.cs b/src/EasyWindowsApplication/Share/WindowsScrollExtensions.cs new file mode 100644 index 0000000..5de68a1 --- /dev/null +++ b/src/EasyWindowsApplication/Share/WindowsScrollExtensions.cs @@ -0,0 +1,49 @@ +namespace EasyWindowsApplication.Share; + +public static class WindowsScrollExtensions +{ + public static IWindowsScrollConfig Vertical(this IWindowsScrollConfig config) + { + config.Orientation(ScrollOrientation.Vertical); + return config; + } + + public static IWindowsScrollConfig Horizontal(this IWindowsScrollConfig config) + { + config.Orientation(ScrollOrientation.Horizontal); + return config; + } + + public static IWindowsScrollConfig Both(this IWindowsScrollConfig config) + { + config.Orientation(ScrollOrientation.Both); + return config; + } + + public static IWindowsScrollConfig Neither(this IWindowsScrollConfig config) + { + config.Orientation(ScrollOrientation.Neither); + return config; + } + + public static IWindowsScrollConfig AutoHide(this IWindowsScrollConfig config) + { + config.VerticalScrollBarVisibility(ScrollBarVisibility.Default); + config.HorizontalScrollBarVisibility(ScrollBarVisibility.Default); + return config; + } + + public static IWindowsScrollConfig Always(this IWindowsScrollConfig config) + { + config.VerticalScrollBarVisibility(ScrollBarVisibility.Always); + config.HorizontalScrollBarVisibility(ScrollBarVisibility.Always); + return config; + } + + public static IWindowsScrollConfig Never(this IWindowsScrollConfig config) + { + config.VerticalScrollBarVisibility(ScrollBarVisibility.Never); + config.HorizontalScrollBarVisibility(ScrollBarVisibility.Never); + return config; + } +} diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs index bc0fa9b..c68ced6 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Controls.cs @@ -5,7 +5,16 @@ namespace EasyWindowsApplication.Win32ControlsModule.Backend; public sealed class Button : ControlBase, IButton { - public string Text { get; set; } = ""; + private string _text = ""; + public string Text + { + get => _text; + set + { + _text = value ?? ""; + if (Hwnd != 0) ControlProcedures.SetWindowText(Hwnd, _text); + } + } public bool Enabled { get; set; } = true; public void Click() => OnClick(() => { }); public void SetStyle(uint style, bool redraw = true) { } @@ -13,5 +22,14 @@ public void SetStyle(uint style, bool redraw = true) { } public sealed class Label : ControlBase, ILabel { - public string Text { get; set; } = ""; + private string _text = ""; + public string Text + { + get => _text; + set + { + _text = value ?? ""; + if (Hwnd != 0) ControlProcedures.SetWindowText(Hwnd, _text); + } + } } diff --git a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32ControlActivator.cs b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32ControlActivator.cs index e529d03..528915a 100644 --- a/src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32ControlActivator.cs +++ b/src/EasyWindowsApplication/Win32ControlsModule/Backend/Win32ControlActivator.cs @@ -12,5 +12,11 @@ public void RegisterActivators(ControlActivatorRegistry registry) // Register concrete types as well for direct resolution if needed registry.Register