Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow a win32 window's restored size to be defined even when it is maximised or minimised #14470

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,18 +105,58 @@ public enum SizeCommand

public enum ShowWindowCommand
{
/// <summary>
/// Hides the window and activates another window.
/// </summary>
Hide = 0,
/// <summary>
/// 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.
/// </summary>
Normal = 1,
/// <summary>
/// Activates the window and displays it as a minimized window.
/// </summary>
ShowMinimized = 2,
/// <summary>
/// Activates the window and displays it as a maximized window.
/// </summary>
Maximize = 3,
ShowMaximized = 3,
/// <inheritdoc cref="Maximize"/>
ShowMaximized = Maximize,
/// <summary>
/// Displays a window in its most recent size and position. This value is similar to <see cref="Normal"/>, except that the window is not activated.
/// </summary>
ShowNoActivate = 4,
/// <summary>
/// Activates the window and displays it in its current size and position.
/// </summary>
Show = 5,
/// <summary>
/// Minimizes the specified window and activates the next top-level window in the Z order.
/// </summary>
Minimize = 6,
/// <summary>
/// Displays the window as a minimized window. This value is similar to <see cref="ShowMinimized"/>, except the window is not activated.
/// </summary>
ShowMinNoActive = 7,
/// <summary>
/// Displays the window in its current size and position. This value is similar to <see cref="Show"/>, except that the window is not activated.
/// </summary>
ShowNA = 8,
/// <summary>
/// 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.
/// </summary>
Restore = 9,
/// <summary>
/// Sets the show state based on the <see cref="ShowWindowCommand"/> value specified in the STARTUPINFO structure passed to the CreateProcess function
/// by the program that started the application.
/// </summary>
ShowDefault = 10,
/// <summary>
/// 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.
/// </summary>
ForceMinimize = 11
}

Expand Down Expand Up @@ -1160,6 +1200,9 @@ public static extern int SetDIBitsToDevice(IntPtr hdc, int XDest, int YDest,
[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);

Expand Down Expand Up @@ -1287,7 +1330,7 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle)
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);
Expand Down Expand Up @@ -1374,6 +1417,8 @@ public static extern IntPtr CreateIconFromResourceEx(byte* pbIconBits, uint cbIc
[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();
Expand Down Expand Up @@ -2240,6 +2285,14 @@ public struct TRACKMOUSEEVENT
public int dwHoverTime;
}

[Flags]
public enum WindowPlacementFlags : uint
{
SetMinPosition = 0x0001,
RestoreToMaximized = 0x0002,
AsyncWindowPlacement = 0x0004,
}

[StructLayout(LayoutKind.Sequential)]
public struct WINDOWPLACEMENT
{
Expand All @@ -2254,7 +2307,7 @@ public struct WINDOWPLACEMENT
/// <summary>
/// Specifies flags that control the position of the minimized window and the method by which the window is restored.
/// </summary>
public int Flags;
public WindowPlacementFlags Flags;

/// <summary>
/// The current show state of the window.
Expand Down
4 changes: 4 additions & 0 deletions src/Windows/Avalonia.Win32/PlatformConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ internal static class PlatformConstants
public const string CursorHandleType = "HCURSOR";

public static readonly Version Windows10 = new Version(10, 0);
/// <summary>
/// Windows 10 Anniversary Update
/// </summary>
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);
Expand Down
24 changes: 13 additions & 11 deletions src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
{
_dpi = (uint)wParam >> 16;
var newDisplayRect = Marshal.PtrToStructure<RECT>(lParam);
_scaling = _dpi / 96.0;
_scaling = _dpi / StandardDpi;
RefreshIcon();
ScalingChanged?.Invoke(_scaling);

Expand Down Expand Up @@ -613,14 +613,6 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
{
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,
Expand All @@ -629,10 +621,20 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
_ => 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;
Expand Down
90 changes: 71 additions & 19 deletions src/Windows/Avalonia.Win32/WindowImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
using Avalonia.Threading;
using static Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl;
using static Avalonia.Controls.Win32Properties;
using Avalonia.Logging;

namespace Avalonia.Win32
{
Expand All @@ -54,6 +55,11 @@ internal partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindow
{ WindowEdge.West, HitTestValues.HTLEFT }
};

/// <summary>
/// The Windows DPI which equates to a <see cref="RenderScaling"/> of 1.0.
/// </summary>
public const double StandardDpi = 96;

private SavedWindowInfo _savedWindowInfo;
private bool _isFullScreenActive;
private bool _isClientAreaExtended;
Expand Down Expand Up @@ -287,8 +293,7 @@ public WindowState WindowState
return WindowState.FullScreen;
}

var placement = default(WINDOWPLACEMENT);
GetWindowPlacement(_hwnd, ref placement);
GetWindowPlacement(_hwnd, out var placement);

return placement.ShowCmd switch
{
Expand Down Expand Up @@ -559,29 +564,59 @@ public void SetMinMaxSize(Size minSize, Size maxSize)

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still can't resize a fullscreen window, but I don't think this will ever be supported for the reasons outlined in the comment here. This is not a regression, it's the same behaviour as on master.

{
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()
Expand Down Expand Up @@ -913,7 +948,7 @@ private void CreateWindow()
out _dpi,
out _) == 0)
{
_scaling = _dpi / 96.0;
_scaling = _dpi / StandardDpi;
}
}
}
Expand Down Expand Up @@ -1473,6 +1508,23 @@ private static void EnableCloseButton(IntPtr hwnd)
MF_BYCOMMAND | MF_ENABLED);
}

private RECT ClientRectToWindowRect(RECT clientRect, WindowStyles? styleOverride = null, WindowStyles? extendedStyleOverride = null)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are several other uses of AdjustWindowRectEx which could be redirected to this method. These other locations currently don't account for per-window DPI, so using this method may fix some bugs. But I decided not to make unnecessary changes in this PR.

{
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)
{
Expand Down