diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ae6b88f565..7b132f3857 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -105,18 +105,58 @@ namespace Avalonia.Win32.Interop public enum ShowWindowCommand { + /// + /// Hides the window and activates another window. + /// Hide = 0, + /// + /// Activates and displays a window. If the window is minimized, maximized, or arranged, the system restores it to its original + /// size and position. An application should specify this flag when displaying the window for the first time. + /// Normal = 1, + /// + /// Activates the window and displays it as a minimized window. + /// ShowMinimized = 2, + /// + /// Activates the window and displays it as a maximized window. + /// Maximize = 3, - ShowMaximized = 3, + /// + ShowMaximized = Maximize, + /// + /// Displays a window in its most recent size and position. This value is similar to , except that the window is not activated. + /// ShowNoActivate = 4, + /// + /// Activates the window and displays it in its current size and position. + /// Show = 5, + /// + /// Minimizes the specified window and activates the next top-level window in the Z order. + /// Minimize = 6, + /// + /// Displays the window as a minimized window. This value is similar to , except the window is not activated. + /// ShowMinNoActive = 7, + /// + /// Displays the window in its current size and position. This value is similar to , except that the window is not activated. + /// ShowNA = 8, + /// + /// Activates and displays the window. If the window is minimized, maximized, or arranged, the system restores it to its original size and position. + /// An application should specify this flag when restoring a minimized window. + /// Restore = 9, + /// + /// Sets the show state based on the value specified in the STARTUPINFO structure passed to the CreateProcess function + /// by the program that started the application. + /// ShowDefault = 10, + /// + /// Minimizes a window, even if the thread that owns the window is not responding. This flag should only be used when minimizing windows from a different thread. + /// ForceMinimize = 11 } @@ -1160,6 +1200,9 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll", SetLastError = true)] public static extern bool AdjustWindowRectEx(ref RECT lpRect, uint dwStyle, bool bMenu, uint dwExStyle); + [DllImport("user32.dll", SetLastError = true)] + public static extern bool AdjustWindowRectExForDpi(ref RECT lpRect, WindowStyles dwStyle, bool bMenu, WindowStyles dwExStyle, uint dpi); + [DllImport("user32.dll")] public static extern IntPtr BeginPaint(IntPtr hwnd, out PAINTSTRUCT lpPaint); @@ -1287,7 +1330,7 @@ namespace Avalonia.Win32.Interop public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); [DllImport("user32.dll", SetLastError = true)] - public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + public static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect); @@ -1374,6 +1417,8 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, SetWindowPosFlags uFlags); [DllImport("user32.dll")] + public static extern bool SetWindowPlacement(IntPtr hWnd, in WINDOWPLACEMENT windowPlacement); + [DllImport("user32.dll")] public static extern bool SetFocus(IntPtr hWnd); [DllImport("user32.dll")] public static extern IntPtr GetFocus(); @@ -2240,6 +2285,14 @@ namespace Avalonia.Win32.Interop public int dwHoverTime; } + [Flags] + public enum WindowPlacementFlags : uint + { + SetMinPosition = 0x0001, + RestoreToMaximized = 0x0002, + AsyncWindowPlacement = 0x0004, + } + [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { @@ -2254,7 +2307,7 @@ namespace Avalonia.Win32.Interop /// /// Specifies flags that control the position of the minimized window and the method by which the window is restored. /// - public int Flags; + public WindowPlacementFlags Flags; /// /// The current show state of the window. diff --git a/src/Windows/Avalonia.Win32/PlatformConstants.cs b/src/Windows/Avalonia.Win32/PlatformConstants.cs index c638314c4d..9f5f152199 100644 --- a/src/Windows/Avalonia.Win32/PlatformConstants.cs +++ b/src/Windows/Avalonia.Win32/PlatformConstants.cs @@ -8,6 +8,10 @@ namespace Avalonia.Win32 public const string CursorHandleType = "HCURSOR"; public static readonly Version Windows10 = new Version(10, 0); + /// + /// Windows 10 Anniversary Update + /// + public static readonly Version Windows10_1607 = new Version(10, 0, 1607); public static readonly Version Windows8 = new Version(6, 2); public static readonly Version Windows8_1 = new Version(6, 3); public static readonly Version Windows7 = new Version(6, 1); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index d164b7ca73..0070581e1d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -131,7 +131,7 @@ namespace Avalonia.Win32 { _dpi = (uint)wParam >> 16; var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = _dpi / 96.0; + _scaling = _dpi / StandardDpi; RefreshIcon(); ScalingChanged?.Invoke(_scaling); @@ -613,14 +613,6 @@ namespace Avalonia.Win32 { var size = (SizeCommand)wParam; - if (Resized != null && - (size == SizeCommand.Restored || - size == SizeCommand.Maximized)) - { - var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / RenderScaling, _resizeReason); - } - var windowState = size switch { SizeCommand.Maximized => WindowState.Maximized, @@ -629,10 +621,20 @@ namespace Avalonia.Win32 _ => WindowState.Normal, }; - if (windowState != _lastWindowState) + var stateChanged = windowState != _lastWindowState; + _lastWindowState = windowState; + + if (Resized != null && + (size == SizeCommand.Restored || + size == SizeCommand.Maximized)) { - _lastWindowState = windowState; + var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); + Resized(clientSize / RenderScaling, _resizeReason); + } + + if (stateChanged) + { var newWindowProperties = _windowProperties; newWindowProperties.WindowState = windowState; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f2b536dc71..fe65a881c6 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -28,6 +28,7 @@ using Avalonia.Platform.Storage.FileIO; using Avalonia.Threading; using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl; using static Avalonia.Controls.Platform.Win32SpecificOptions; +using Avalonia.Logging; namespace Avalonia.Win32 { @@ -54,6 +55,11 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + /// + /// The Windows DPI which equates to a of 1.0. + /// + public const double StandardDpi = 96; + private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; private bool _isClientAreaExtended; @@ -287,8 +293,7 @@ namespace Avalonia.Win32 return WindowState.FullScreen; } - var placement = default(WINDOWPLACEMENT); - GetWindowPlacement(_hwnd, ref placement); + GetWindowPlacement(_hwnd, out var placement); return placement.ShowCmd switch { @@ -559,29 +564,59 @@ namespace Avalonia.Win32 public void Resize(Size value, WindowResizeReason reason) { - if (WindowState != WindowState.Normal) - return; - int requestedClientWidth = (int)(value.Width * RenderScaling); int requestedClientHeight = (int)(value.Height * RenderScaling); - GetClientRect(_hwnd, out var clientRect); + GetClientRect(_hwnd, out var currentClientRect); + if (currentClientRect.Width == requestedClientWidth && currentClientRect.Height == requestedClientHeight) + { + // Don't update our window position if the client size is already correct. This leads to Windows updating our + // "normal position" (i.e. restored bounds) to match our maximised or areo snap size, which is incorrect behaviour. + // We only want to proceed with this method if the new size is coming from Avalonia. + return; + } - // do comparison after scaling to avoid rounding issues - if (requestedClientWidth != clientRect.Width || requestedClientHeight != clientRect.Height) + if (_lastWindowState == WindowState.FullScreen) { - GetWindowRect(_hwnd, out var windowRect); + // Fullscreen mode is really a restored window without a frame filling the whole monitor. + // It doesn't make sense to resize the window in this state, so ignore this request. + Logger.TryGet(LogEventLevel.Warning, LogArea.Win32Platform)?.Log(this, "Ignoring resize event on fullscreen window."); + return; + } - using var scope = SetResizeReason(reason); - SetWindowPos( - _hwnd, - IntPtr.Zero, - 0, - 0, - requestedClientWidth + (_isClientAreaExtended ? 0 : windowRect.Width - clientRect.Width), - requestedClientHeight + (_isClientAreaExtended ? 0 : windowRect.Height - clientRect.Height), - SetWindowPosFlags.SWP_RESIZE); + GetWindowPlacement(_hwnd, out var windowPlacement); + + var clientScreenOrigin = new POINT(); + ClientToScreen(_hwnd, ref clientScreenOrigin); + + var requestedClientRect = new RECT + { + left = clientScreenOrigin.X, + right = clientScreenOrigin.X + requestedClientWidth, + + top = clientScreenOrigin.Y, + bottom = clientScreenOrigin.Y + requestedClientHeight, + }; + + var requestedWindowRect = _isClientAreaExtended ? requestedClientRect : ClientRectToWindowRect(requestedClientRect); + + if (requestedWindowRect.Width == windowPlacement.NormalPosition.Width && requestedWindowRect.Height == windowPlacement.NormalPosition.Height) + { + return; } + + windowPlacement.NormalPosition = requestedWindowRect; + + windowPlacement.ShowCmd = _lastWindowState switch + { + WindowState.Minimized => ShowWindowCommand.ShowMinNoActive, + WindowState.Maximized => ShowWindowCommand.ShowMaximized, + WindowState.Normal => ShowWindowCommand.ShowNoActivate, + _ => throw new NotImplementedException(), + }; + + using var scope = SetResizeReason(reason); + SetWindowPlacement(_hwnd, in windowPlacement); } public void Activate() @@ -913,7 +948,7 @@ namespace Avalonia.Win32 out _dpi, out _) == 0) { - _scaling = _dpi / 96.0; + _scaling = _dpi / StandardDpi; } } } @@ -1473,6 +1508,23 @@ namespace Avalonia.Win32 MF_BYCOMMAND | MF_ENABLED); } + private RECT ClientRectToWindowRect(RECT clientRect, WindowStyles? styleOverride = null, WindowStyles? extendedStyleOverride = null) + { + var style = styleOverride ?? GetStyle(); + var extendedStyle = extendedStyleOverride ?? GetExtendedStyle(); + + var result = Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607 + ? AdjustWindowRectEx(ref clientRect, (uint)style, false, (uint)extendedStyle) + : AdjustWindowRectExForDpi(ref clientRect, style, false, extendedStyle, (uint)(RenderScaling * StandardDpi)); + + if (!result) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + + return clientRect; + } + #if USE_MANAGED_DRAG private Point ScreenToClient(Point point) {