diff --git a/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs b/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs new file mode 100644 index 00000000000..04ac0383e24 --- /dev/null +++ b/src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Platform; +using static Avalonia.Controls.Platform.Win32SpecificOptions; + +namespace Avalonia.Controls.Platform +{ + [PrivateApi] + public interface IWin32OptionsTopLevelImpl : ITopLevelImpl + { + /// + /// Gets or sets a callback to set the window styles. + /// + public CustomWindowStylesCallback? WindowStylesCallback { get; set; } + + /// + /// Gets or sets a custom callback for the window's WndProc + /// + public CustomWndProcHookCallback? WndProcHookCallback { get; set; } + } +} diff --git a/src/Avalonia.Controls/Platform/Win32SpecificOptions.cs b/src/Avalonia.Controls/Platform/Win32SpecificOptions.cs new file mode 100644 index 00000000000..53eb911e2e2 --- /dev/null +++ b/src/Avalonia.Controls/Platform/Win32SpecificOptions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Metadata; +using Avalonia.Platform; +using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; + +namespace Avalonia.Controls.Platform +{ + public static class Win32SpecificOptions + { + public delegate (uint style, uint exStyle) CustomWindowStylesCallback(uint style, uint exStyle); + public delegate IntPtr CustomWndProcHookCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled); + + /// + /// Adds a callback to set the window's style. + /// + /// The window implementation + /// The callback + public static void AddWindowStylesCallback(TopLevel topLevel, CustomWindowStylesCallback? callback) + { + if (topLevel.PlatformImpl is IWin32OptionsTopLevelImpl toplevelImpl) + { + toplevelImpl.WindowStylesCallback += callback; + } + } + + /// + /// Removes a callback to set the window's style. + /// + /// The window implementation + /// The callback + public static void RemoveWindowStylesCallback(TopLevel topLevel, CustomWindowStylesCallback? callback) + { + if (topLevel.PlatformImpl is IWin32OptionsTopLevelImpl toplevelImpl) + { + toplevelImpl.WindowStylesCallback -= callback; + } + } + + /// + /// Adds a custom callback for the window's WndProc + /// + /// The window + /// The callback + public static void AddWndProcHookCallback(TopLevel topLevel, CustomWndProcHookCallback? callback) + { + if (topLevel.PlatformImpl is IWin32OptionsTopLevelImpl toplevelImpl) + { + toplevelImpl.WndProcHookCallback += callback; + } + } + + /// + /// Removes a custom callback for the window's WndProc + /// + /// The window + /// The callback + public static void RemoveWndProcHookCallback(TopLevel topLevel, CustomWndProcHookCallback? callback) + { + if (topLevel.PlatformImpl is IWin32OptionsTopLevelImpl toplevelImpl) + { + toplevelImpl.WndProcHookCallback -= callback; + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d521f1ba328..6dd491030d2 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -59,7 +59,7 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, case WindowsMessage.WM_NCCALCSIZE: { - if (ToInt32(wParam) == 1 && _windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended) + if (ToInt32(wParam) == 1 && (_windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended)) { return IntPtr.Zero; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index e92272c079c..26d0c3b58f9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.InteropServices; using Avalonia.Controls.Platform; namespace Avalonia.Win32 diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ca0e287fe4c..6062c6b762e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -26,13 +26,15 @@ using static Avalonia.Win32.Interop.UnmanagedMethods; using Avalonia.Input.Platform; using System.Diagnostics; +using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; +using static Avalonia.Controls.Platform.Win32SpecificOptions; namespace Avalonia.Win32 { /// /// Window implementation for Win32 platform. /// - internal partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo + internal partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo, IWin32OptionsTopLevelImpl { private static readonly List s_instances = new(); @@ -823,7 +825,7 @@ protected virtual IntPtr CreateWindowOverride(ushort atom) private void CreateWindow() { // Ensure that the delegate doesn't get garbage collected by storing it as a field. - _wndProcDelegate = WndProc; + _wndProcDelegate = WndProcMessageHandler; _className = $"Avalonia-{Guid.NewGuid().ToString()}"; @@ -876,6 +878,20 @@ private void CreateWindow() } } + private IntPtr WndProcMessageHandler(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + bool handled = false; + IntPtr ret = IntPtr.Zero; + + if (WndProcHookCallback is { } callback) + ret = callback(hWnd, msg, wParam, lParam, ref handled); + + if (handled) + return ret; + + return WndProc(hWnd, msg, wParam, lParam); + } + private void CreateDropTarget(IInputRoot inputRoot) { if (AvaloniaLocator.Current.GetService() is { } dragDropDevice) @@ -905,7 +921,7 @@ private void SetFullScreen(bool fullscreen) clientRect.right += windowRect.left; clientRect.top += windowRect.top; clientRect.bottom += windowRect.top; - + _savedWindowInfo.WindowRect = clientRect; var current = GetStyle(); @@ -1256,51 +1272,60 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh // according to the new values already. _windowProperties = newProperties; - if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges) + if (oldProperties.IsFullScreen == newProperties.IsFullScreen) { - var exStyle = GetExtendedStyle(); + var exStyle = WindowStyles.WS_EX_WINDOWEDGE | (_isUsingComposition ? WindowStyles.WS_EX_NOREDIRECTIONBITMAP : 0); - if (newProperties.ShowInTaskbar) + if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges) { - exStyle |= WindowStyles.WS_EX_APPWINDOW; - - if (_hiddenWindowIsParent) + if (newProperties.ShowInTaskbar) { - // Can't enable the taskbar icon by clearing the parent window unless the window - // is hidden. Hide the window and show it again with the same activation state - // when we've finished. Interestingly it seems to work fine the other way. - var shown = IsWindowVisible(_hwnd); - var activated = GetActiveWindow() == _hwnd; + exStyle |= WindowStyles.WS_EX_APPWINDOW; + + if (_hiddenWindowIsParent) + { + // Can't enable the taskbar icon by clearing the parent window unless the window + // is hidden. Hide the window and show it again with the same activation state + // when we've finished. Interestingly it seems to work fine the other way. + var shown = IsWindowVisible(_hwnd); + var activated = GetActiveWindow() == _hwnd; - if (shown) - Hide(); + if (shown) + Hide(); - _hiddenWindowIsParent = false; - SetParent(null); + _hiddenWindowIsParent = false; + SetParent(null); - if (shown) - Show(activated, false); + if (shown) + Show(activated, false); + } } - } - else - { - // To hide a non-owned window's taskbar icon we need to parent it to a hidden window. - if (_parent is null) + else { - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, OffscreenParentWindow.Handle); - _hiddenWindowIsParent = true; + // To hide a non-owned window's taskbar icon we need to parent it to a hidden window. + if (_parent is null) + { + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, OffscreenParentWindow.Handle); + _hiddenWindowIsParent = true; + } + + exStyle &= ~WindowStyles.WS_EX_APPWINDOW; } + } + if (newProperties.ShowInTaskbar) + { + exStyle |= WindowStyles.WS_EX_APPWINDOW; + } + else + { exStyle &= ~WindowStyles.WS_EX_APPWINDOW; } - SetExtendedStyle(exStyle); - } + WindowStyles style = WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_OVERLAPPEDWINDOW | WindowStyles.WS_CLIPSIBLINGS; - WindowStyles style; - if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) - { - style = GetStyle(); + if (IsWindowVisible(_hwnd)) + style |= WindowStyles.WS_VISIBLE; if (newProperties.IsResizable) { @@ -1313,18 +1338,6 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh style &= ~WindowStyles.WS_MAXIMIZEBOX; } - SetStyle(style); - } - - if (oldProperties.IsFullScreen != newProperties.IsFullScreen) - { - SetFullScreen(newProperties.IsFullScreen); - } - - if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) - { - style = GetStyle(); - const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; if (newProperties.Decorations == SystemDecorations.Full) @@ -1334,39 +1347,70 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh else { style &= ~fullDecorationFlags; + + if (newProperties.Decorations == SystemDecorations.BorderOnly) + { + style |= WindowStyles.WS_THICKFRAME | WindowStyles.WS_BORDER; + } } - SetStyle(style); + var windowStates = GetWindowStateStyles(); + style &= ~WindowStateMask; + style |= windowStates; - if (!_isFullScreenActive) + _savedWindowInfo.Style = style; + _savedWindowInfo.ExStyle = exStyle; + + if (WindowStylesCallback is { } callback) { - var margins = new MARGINS - { - cyBottomHeight = 0, - cxRightWidth = 0, - cxLeftWidth = 0, - cyTopHeight = 0 - }; + var (s, e) = callback((uint)style, (uint)exStyle); - DwmExtendFrameIntoClientArea(_hwnd, ref margins); + style = (WindowStyles)s; + exStyle = (WindowStyles)e; + } - GetClientRect(_hwnd, out var oldClientRect); - var oldClientRectOrigin = new POINT(); - ClientToScreen(_hwnd, ref oldClientRectOrigin); - oldClientRect.Offset(oldClientRectOrigin); + SetStyle(style); + SetExtendedStyle(exStyle); + } + else + SetFullScreen(newProperties.IsFullScreen); - var newRect = oldClientRect; + if (!_isFullScreenActive) + { + var style = GetStyle(); - if (newProperties.Decorations == SystemDecorations.Full) - { - AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); - } + var margin = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0; + + var margins = new MARGINS + { + cyBottomHeight = margin, + cxRightWidth = margin, + cxLeftWidth = margin, + cyTopHeight = margin + }; + + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + GetClientRect(_hwnd, out var oldClientRect); + var oldClientRectOrigin = new POINT(); + ClientToScreen(_hwnd, ref oldClientRectOrigin); + oldClientRect.Offset(oldClientRectOrigin); + + var newRect = oldClientRect; - SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | - SetWindowPosFlags.SWP_FRAMECHANGED); + if (newProperties.Decorations == SystemDecorations.Full) + { + AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); } + + SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); } + + // Ensure window state if decorations change + if (oldProperties.Decorations != newProperties.Decorations) + ShowWindow(WindowState, false); } private const int MF_BYCOMMAND = 0x0; @@ -1452,6 +1496,12 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) /// public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + /// + public CustomWindowStylesCallback? WindowStylesCallback { get; set; } + + /// + public CustomWndProcHookCallback? WndProcHookCallback { get; set; } + private ResizeReasonScope SetResizeReason(WindowResizeReason reason) { var old = _resizeReason;