Browse Source

Fix several issues with ExtendClientAreaToDecorationsHint on Windows (#20217)

* Add extended client area Win32 integration tests

* Fix Win32 extended client areas

* Run Win32 integration tests in pipelines

* Add extra tests and fixes when CanResize=false

* Use WM_GETMINMAXINFO to maximize captionless windows

* Use dotnet run on CI for IntegrationTests.Win32

* Address review
pull/20249/head
Julien Lebosquain 2 weeks ago
committed by GitHub
parent
commit
884051ed10
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      Avalonia.sln
  2. 10
      azure-pipelines-integrationtests.yml
  3. 20
      src/Avalonia.Controls/Chrome/TitleBar.cs
  4. 8
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  5. 116
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  6. 36
      src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs
  7. 269
      src/Windows/Avalonia.Win32/WindowImpl.cs
  8. 25
      tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj
  9. 224
      tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs
  10. 49
      tests/Avalonia.IntegrationTests.Win32/Infrastructure/AppManager.cs
  11. 10
      tests/Avalonia.IntegrationTests.Win32/Infrastructure/AvaloniaTestFramework.cs
  12. 51
      tests/Avalonia.IntegrationTests.Win32/Infrastructure/AvaloniaTestFrameworkExecutor.cs
  13. 29
      tests/Avalonia.IntegrationTests.Win32/Infrastructure/InvariantCultureScope.cs
  14. 5
      tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs
  15. 135
      tests/Avalonia.IntegrationTests.Win32/StandardWindowTests.cs
  16. 23
      tests/Avalonia.IntegrationTests.Win32/UnmanagedMethods.cs
  17. 55
      tests/Avalonia.IntegrationTests.Win32/WindowExtensions.cs

7
Avalonia.sln

@ -285,6 +285,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.Per
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.PerTest.UnitTests", "tests\Avalonia.Headless.XUnit.PerTest.UnitTests\Avalonia.Headless.XUnit.PerTest.UnitTests.csproj", "{26918642-829D-4FA2-B60A-BE8D83F4E063}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{11522B0D-BF31-42D5-8FC5-41E58F319AF9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -659,6 +661,10 @@ Global
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.Build.0 = Release|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -742,6 +748,7 @@ Global
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

10
azure-pipelines-integrationtests.yml

@ -87,10 +87,16 @@ jobs:
displayName: 'Build test project'
inputs:
command: 'build'
projects: 'tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
- task: DotNetCoreCLI@2
displayName: 'Run Win32 Integration Tests'
inputs:
command: 'run'
projects: 'tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj'
- task: VSTest@2
displayName: 'Run Integration Tests'
displayName: 'Run Appium Integration Tests'
inputs:
testAssemblyVer2: '**\bin\**\Avalonia.IntegrationTests.Appium.dll'
runSettingsFile: 'tests\Avalonia.IntegrationTests.Appium\record-video.runsettings'

20
src/Avalonia.Controls/Chrome/TitleBar.cs

@ -27,12 +27,19 @@ namespace Avalonia.Controls.Chrome
if (window.WindowState != WindowState.FullScreen)
{
Height = Math.Max(0, window.WindowDecorationMargin.Top);
if (_captionButtons != null)
{
_captionButtons.Height = Height;
}
var height = Math.Max(0, window.WindowDecorationMargin.Top);
Height = height;
_captionButtons?.Height = window.SystemDecorations == SystemDecorations.Full ? height : 0;
}
else
{
// Note: apparently the titlebar was supposed to be displayed when hovering the top of the screen,
// to mimic macOS behavior. This has been broken for years. It actually only partially works if the
// window is FullScreen right on startup, and only once. Any size change will then break it.
// Disable it for now.
// TODO: restore that behavior so that it works in all cases
Height = 0;
_captionButtons?.Height = 0;
}
IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false;
@ -79,6 +86,7 @@ namespace Avalonia.Controls.Chrome
PseudoClasses.Set(":normal", x == WindowState.Normal);
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
UpdateSize(window);
}),
window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
.Subscribe(_ => UpdateSize(window))

8
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -869,6 +869,14 @@ namespace Avalonia.Win32.Interop
DWMWCP_ROUNDSMALL
}
public enum DwmNCRenderingPolicy : uint
{
DWMNCRP_USEWINDOWSTYLE,
DWMNCRP_DISABLED,
DWMNCRP_ENABLED,
DWMNCRP_LAST
}
public enum MapVirtualKeyMapTypes : uint
{
MAPVK_VK_TO_VSC = 0x00,

116
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Automation.Peers;
using Avalonia.Controls;
@ -57,13 +58,79 @@ namespace Avalonia.Win32
return IntPtr.Zero;
}
case WindowsMessage.WM_NCCALCSIZE:
case WindowsMessage.WM_NCCALCSIZE when ToInt32(wParam) == 1:
{
if (ToInt32(wParam) == 1 && (_windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended))
if (_windowProperties.Decorations == SystemDecorations.None)
return IntPtr.Zero;
// When the client area is extended into the frame, we are still requesting the standard styles matching
// the wanted decorations (such as WS_CAPTION or WS_BORDER) along with window bounds larger than the client size.
// This allows the window to have the standard resize borders *outside* of the client area.
// The logic for this lies in the Resize() method.
//
// After this happens, WM_NCCALCSIZE provides us with a new window area matching those requested bounds.
// We need to adjust that area back to our preferred client area, keeping the resize borders around it.
//
// The same logic applies when the window gets maximized, the only difference being that Windows chose
// the final bounds instead of us.
if (_isClientAreaExtended)
{
GetWindowPlacement(hWnd, out var placement);
if (placement.ShowCmd == ShowWindowCommand.ShowMinimized)
break;
var paramsObj = Marshal.PtrToStructure<NCCALCSIZE_PARAMS>(lParam);
ref var rect = ref paramsObj.rgrc[0];
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
var adjuster = CreateWindowRectAdjuster();
var borderThickness = new RECT();
// We told Windows we have a caption, but since we're actually extending into it, it should not be taken into account.
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
{
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
{
adjuster.Adjust(ref borderThickness, style & ~WindowStyles.WS_CAPTION | WindowStyles.WS_BORDER | WindowStyles.WS_THICKFRAME, 0);
}
else
{
adjuster.Adjust(ref borderThickness, style, 0);
var thinBorderThickness = new RECT();
adjuster.Adjust(ref thinBorderThickness, style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME) | WindowStyles.WS_BORDER, 0);
borderThickness.top = thinBorderThickness.top;
}
}
else if (style.HasAllFlags(WindowStyles.WS_BORDER))
{
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
{
adjuster.Adjust(ref borderThickness, style, 0);
}
else
{
adjuster.Adjust(ref borderThickness, style, 0);
var thinBorderThickness = new RECT();
adjuster.Adjust(ref thinBorderThickness, style & ~WindowStyles.WS_THICKFRAME, 0);
borderThickness.top = thinBorderThickness.top;
}
}
else
{
adjuster.Adjust(ref borderThickness, style, 0);
}
rect.left -= borderThickness.left;
rect.top -= borderThickness.top;
rect.right -= borderThickness.right;
rect.bottom -= borderThickness.bottom;
Marshal.StructureToPtr(paramsObj, lParam, false);
return IntPtr.Zero;
}
break;
}
@ -699,11 +766,6 @@ namespace Avalonia.Win32
UpdateWindowProperties(newWindowProperties);
if (windowState == WindowState.Maximized)
{
MaximizeWithoutCoveringTaskbar();
}
WindowStateChanged?.Invoke(windowState);
if (_isClientAreaExtended)
@ -739,6 +801,44 @@ namespace Avalonia.Win32
_maxTrackSize = mmi.ptMaxTrackSize;
// A window without a caption (i.e. None and BorderOnly decorations) maximizes to the whole screen
// by default. Adjust that to the screen's working area instead.
var style = GetStyle();
if (!style.HasAllFlags(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))
{
var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
if (screen?.WorkingArea is { } workingArea)
{
var x = workingArea.X;
var y = workingArea.Y;
var cx = workingArea.Width;
var cy = workingArea.Height;
var adjuster = CreateWindowRectAdjuster();
var borderThickness = new RECT();
var adjustedStyle = style & ~WindowStyles.WS_CAPTION;
if (style.HasAllFlags(WindowStyles.WS_BORDER))
adjustedStyle |= WindowStyles.WS_BORDER;
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
adjustedStyle |= WindowStyles.WS_THICKFRAME;
adjuster.Adjust(ref borderThickness, adjustedStyle, 0);
x += borderThickness.left;
y += borderThickness.top;
cx += -borderThickness.left + borderThickness.right;
cy += -borderThickness.top + borderThickness.bottom;
mmi.ptMaxPosition.X = x;
mmi.ptMaxPosition.Y = y;
mmi.ptMaxSize.X = cx;
mmi.ptMaxSize.Y = cy;
}
}
if (_minSize.Width > 0)
{
mmi.ptMinTrackSize.X =

36
src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs

@ -18,36 +18,24 @@ namespace Avalonia.Win32
// Get the window rectangle.
GetWindowRect(hWnd, out var rcWindow);
var scaling = (uint)(RenderScaling * StandardDpi);
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
// Get the frame rectangle, adjusted for the style without a caption.
var rcFrame = new RECT();
var borderThickness = new RECT();
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
{
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
rcFrame.top = (int)(rcFrame.top * relativeScaling);
rcFrame.right = (int)(rcFrame.right * relativeScaling);
rcFrame.left = (int)(rcFrame.left * relativeScaling);
rcFrame.bottom = (int)(rcFrame.bottom * relativeScaling);
AdjustWindowRectEx(ref borderThickness, (uint)GetStyle(), false, 0);
borderThickness.top = (int)(borderThickness.top * relativeScaling);
borderThickness.right = (int)(borderThickness.right * relativeScaling);
borderThickness.left = (int)(borderThickness.left * relativeScaling);
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
}
else
var isMaximized = GetWindowPlacement(hWnd, out var placement) && placement.ShowCmd == ShowWindowCommand.ShowMaximized;
if (!isMaximized)
{
AdjustWindowRectExForDpi(ref rcFrame, WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION, false, 0, scaling);
AdjustWindowRectExForDpi(ref borderThickness, GetStyle(), false, 0, scaling);
}
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
if (style.HasAllFlags(WindowStyles.WS_THICKFRAME))
{
var adjuster = CreateWindowRectAdjuster();
adjuster.Adjust(ref rcFrame, style & ~WindowStyles.WS_CAPTION, 0);
adjuster.Adjust(ref borderThickness, style, 0);
borderThickness.left *= -1;
borderThickness.top *= -1;
borderThickness.left *= -1;
borderThickness.top *= -1;
}
}
if (_extendTitleBarHint >= 0)
{

269
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Avalonia.Collections.Pooled;
using Avalonia.Controls;
@ -61,7 +62,6 @@ namespace Avalonia.Win32
private bool _isFullScreenActive;
private bool _isClientAreaExtended;
private Thickness _extendedMargins;
private Thickness _offScreenMargin;
private double _extendTitleBarHint = -1;
private WindowResizeReason _resizeReason;
private MOUSEMOVEPOINT _lastWmMousePoint;
@ -150,6 +150,8 @@ namespace Avalonia.Win32
_wmPointerEnabled = Win32Platform.WindowsVersion >= PlatformConstants.Windows8;
Screen = Win32Platform.Instance.Screen;
CreateWindow();
_framebuffer = new FramebufferManager(_hwnd);
@ -173,7 +175,6 @@ namespace Avalonia.Win32
}
}
Screen = Win32Platform.Instance.Screen;
_storageProvider = new Win32StorageProvider(this);
_inputPane = WindowsInputPane.TryCreate(this);
_nativeControlHost = new Win32NativeControlHost(this, !UseRedirectionBitmap);
@ -240,8 +241,6 @@ namespace Avalonia.Win32
}
}
private double PrimaryScreenRenderScaling => Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1;
private ICompositionEffectsSurface? CompositionEffectsSurface => _glSurface as ICompositionEffectsSurface;
private bool UseRedirectionBitmap { get; }
@ -573,10 +572,11 @@ namespace Avalonia.Win32
return;
}
if (_lastWindowState == WindowState.FullScreen)
if (_lastWindowState == WindowState.FullScreen && _isFullScreenActive)
{
// 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.
// (If the fullscreen mode isn't yet active, continue normally so that our normal window size gets saved.)
Logger.TryGet(LogEventLevel.Warning, LogArea.Win32Platform)?.Log(this, "Ignoring resize event on fullscreen window.");
return;
}
@ -591,7 +591,19 @@ namespace Avalonia.Win32
bottom = requestedClientHeight
};
var requestedWindowRect = _isClientAreaExtended ? requestedClientRect : ClientRectToWindowRect(requestedClientRect);
var requestedWindowRect = ClientRectToWindowRect(requestedClientRect);
if (_isClientAreaExtended)
{
// We told Windows we have a caption, but since we're actually extending into it,
// it should be excluded from the final window bounds.
if (_windowProperties.Decorations != SystemDecorations.None)
{
var borderOnlyRect = ClientRectToWindowRect(requestedClientRect, WindowStyles.WS_BORDER);
requestedWindowRect.top = borderOnlyRect.top;
}
}
var windowWidth = requestedWindowRect.Width;
var windowHeight = requestedWindowRect.Height;
@ -1048,29 +1060,53 @@ namespace Avalonia.Win32
{
if (fullscreen)
{
GetWindowRect(_hwnd, out var windowRect);
_savedWindowInfo.WindowRect = windowRect;
var current = GetStyle();
var currentEx = GetExtendedStyle();
Screen? screen;
GetWindowPlacement(_hwnd, out var placement);
var isMinimized = placement.ShowCmd == ShowWindowCommand.ShowMinimized;
RECT windowRect;
// When minimized, we can't use GetWindowRect since the window is actually way outside the screen.
// Instead, fall back to WINDOWPLACEMENT.NormalPosition (which is in working area coordinates).
if (isMinimized)
{
windowRect = placement.NormalPosition;
screen = Screen.ScreenFromRect(windowRect.ToPixelRect());
if (screen?.WorkingArea is { } workingArea)
{
windowRect.left += workingArea.X;
windowRect.top += workingArea.Y;
windowRect.right += workingArea.X;
windowRect.bottom += workingArea.Y;
}
}
else
{
GetWindowRect(_hwnd, out windowRect);
screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
}
_savedWindowInfo.WindowRect = windowRect;
_savedWindowInfo.Style = current;
_savedWindowInfo.ExStyle = currentEx;
// Set new window style and size.
SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false);
SetStyle(current & ~WindowStyles.WS_OVERLAPPEDWINDOW, false);
SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false);
// On expand, if we're given a window_rect, grow to it, otherwise do
// not resize.
var screen = Screen.ScreenFromHwnd(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
if (screen?.Bounds is { } window_rect)
if (screen?.Bounds is { } screenBounds)
{
_isFullScreenActive = true;
SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
window_rect.Width, window_rect.Height,
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
if (isMinimized)
UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Restore);
SetWindowPos(_hwnd, IntPtr.Zero, screenBounds.X, screenBounds.Y, screenBounds.Width, screenBounds.Height,
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
}
}
else
@ -1101,44 +1137,27 @@ namespace Avalonia.Win32
private MARGINS UpdateExtendMargins()
{
RECT borderThickness = new RECT();
RECT borderCaptionThickness = new RECT();
var scaling = (uint)(RenderScaling * StandardDpi);
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
{
AdjustWindowRectEx(ref borderCaptionThickness, (uint)GetStyle(), false, 0);
AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0);
var borderThickness = new RECT();
var borderCaptionThickness = new RECT();
var style = GetStyle();
borderCaptionThickness.top = (int)(borderCaptionThickness.top * relativeScaling);
borderCaptionThickness.right = (int)(borderCaptionThickness.right * relativeScaling);
borderCaptionThickness.left = (int)(borderCaptionThickness.left * relativeScaling);
borderCaptionThickness.bottom = (int)(borderCaptionThickness.bottom * relativeScaling);
borderThickness.top = (int)(borderThickness.top * relativeScaling);
borderThickness.right = (int)(borderThickness.right * relativeScaling);
borderThickness.left = (int)(borderThickness.left * relativeScaling);
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
}
else
{
AdjustWindowRectExForDpi(ref borderCaptionThickness, GetStyle(), false, 0, scaling);
AdjustWindowRectExForDpi(ref borderThickness, GetStyle() & ~WindowStyles.WS_CAPTION, false, 0, scaling);
}
var adjuster = CreateWindowRectAdjuster();
adjuster.Adjust(ref borderCaptionThickness, style, 0);
adjuster.Adjust(ref borderThickness, style & ~WindowStyles.WS_CAPTION, 0);
borderThickness.left *= -1;
borderThickness.top *= -1;
borderCaptionThickness.left *= -1;
borderCaptionThickness.top *= -1;
bool wantsTitleBar = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1;
if (!wantsTitleBar)
if (_extendChromeHints.HasAnyFlag(ExtendClientAreaChromeHints.SystemChrome | ExtendClientAreaChromeHints.PreferSystemChrome) &&
_windowProperties.Decorations == SystemDecorations.Full)
{
borderCaptionThickness.top = 1;
if (_extendTitleBarHint != -1)
borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);
}
else
borderCaptionThickness.top = borderThickness.top;
//using a default margin of 0 when using WinUiComp removes artefacts when resizing. See issue #8316
var defaultMargin = UseRedirectionBitmap ? 1 : 0;
@ -1148,22 +1167,15 @@ namespace Avalonia.Win32
margins.cxRightWidth = defaultMargin;
margins.cyBottomHeight = defaultMargin;
if (_extendTitleBarHint != -1)
{
borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling);
}
margins.cyTopHeight = _extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : defaultMargin;
if (WindowState == WindowState.Maximized)
{
_extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0);
_offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling);
}
else
{
_extendedMargins = new Thickness(0, (borderCaptionThickness.top) / RenderScaling, 0, 0);
_offScreenMargin = new Thickness();
}
return margins;
@ -1183,15 +1195,22 @@ namespace Avalonia.Win32
}
GetWindowRect(_hwnd, out var rcWindow);
if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
if (_isClientAreaExtended && WindowState != WindowState.FullScreen && GetStyle().HasAllFlags(WindowStyles.WS_BORDER))
{
var margins = UpdateExtendMargins();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
unsafe
// On Windows 11 21H2 and later, corners are configurable.
// When doing so, we need to make sure that DWM draws the non-client frame for that to work correctly.
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) {
SetWindowCornerPreference(DwmWindowCornerPreference.DWMWCP_ROUND);
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_ENABLED);
}
else
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
// On older versions, we need to disable painting the non-client area to avoid issues
// (alternatively, we could return 0 in WM_NCPAINT in this case).
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_DISABLED);
}
}
else
@ -1199,9 +1218,13 @@ namespace Avalonia.Win32
var margins = new MARGINS();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
_offScreenMargin = new Thickness();
_extendedMargins = new Thickness();
if (OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000))
SetWindowCornerPreference(DwmWindowCornerPreference.DWMWCP_DEFAULT);
SetNCRenderingPolicy(DwmNCRenderingPolicy.DWMNCRP_USEWINDOWSTYLE);
unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT;
@ -1229,6 +1252,12 @@ namespace Avalonia.Win32
ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended);
}
private unsafe void SetWindowCornerPreference(DwmWindowCornerPreference value)
=> DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(int));
private unsafe void SetNCRenderingPolicy(DwmNCRenderingPolicy value)
=> DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_NCRENDERING_POLICY, &value, sizeof(int));
private void ShowWindow(WindowState state, bool activate)
{
if (_isClientAreaExtended)
@ -1275,11 +1304,6 @@ namespace Avalonia.Win32
UnmanagedMethods.ShowWindow(_hwnd, command.Value);
}
if (state == WindowState.Maximized)
{
MaximizeWithoutCoveringTaskbar();
}
if (!Design.IsDesignMode && activate)
{
SetFocus(_hwnd);
@ -1322,33 +1346,6 @@ namespace Avalonia.Win32
}
}
private void MaximizeWithoutCoveringTaskbar()
{
var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
if (screen?.WorkingArea is { } workingArea)
{
var x = workingArea.X;
var y = workingArea.Y;
var cx = workingArea.Width;
var cy = workingArea.Height;
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
if (!style.HasFlag(WindowStyles.WS_THICKFRAME))
{
// When calling SetWindowPos on a maximized window it automatically adjusts
// for "hidden" borders which are placed offscreen, EVEN IF THE WINDOW HAS
// NO BORDERS, meaning that the window is placed wrong when we have CanResize
// == false. Account for this here.
var borderThickness = BorderThickness;
x -= (int)borderThickness.Left;
cx += (int)borderThickness.Left + (int)borderThickness.Right;
cy += (int)borderThickness.Bottom;
}
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW | SetWindowPosFlags.SWP_FRAMECHANGED);
}
}
private WindowStyles GetWindowStateStyles()
{
return GetStyle() & WindowStateMask;
@ -1462,7 +1459,7 @@ namespace Avalonia.Win32
exStyle &= ~WindowStyles.WS_EX_APPWINDOW;
}
WindowStyles style = WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_OVERLAPPEDWINDOW | WindowStyles.WS_CLIPSIBLINGS;
var style = WindowStyles.WS_CLIPCHILDREN | WindowStyles.WS_CLIPSIBLINGS;
if (this is EmbeddedWindowImpl)
style |= WindowStyles.WS_CHILD;
@ -1470,40 +1467,26 @@ namespace Avalonia.Win32
if (IsWindowVisible(_hwnd))
style |= WindowStyles.WS_VISIBLE;
if (newProperties.IsResizable || newProperties.WindowState == WindowState.Maximized)
style |= WindowStyles.WS_THICKFRAME;
else
style &= ~WindowStyles.WS_THICKFRAME;
switch (newProperties.Decorations)
{
case SystemDecorations.Full:
style |= WindowStyles.WS_BORDER | WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU;
if (newProperties.IsMinimizable)
style |= WindowStyles.WS_MINIMIZEBOX;
else
style &= ~WindowStyles.WS_MINIMIZEBOX;
if (newProperties.IsMinimizable)
style |= WindowStyles.WS_MINIMIZEBOX;
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
style |= WindowStyles.WS_MAXIMIZEBOX;
else
style &= ~WindowStyles.WS_MAXIMIZEBOX;
if (newProperties.IsMaximizable || (newProperties.WindowState == WindowState.Maximized && newProperties.IsResizable))
style |= WindowStyles.WS_MAXIMIZEBOX;
const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_BORDER | WindowStyles.WS_SYSMENU;
break;
if (newProperties.Decorations == SystemDecorations.Full)
{
style |= fullDecorationFlags;
case SystemDecorations.BorderOnly:
style |= WindowStyles.WS_BORDER;
break;
}
else
{
style &= ~(fullDecorationFlags | WindowStyles.WS_THICKFRAME);
if (newProperties.Decorations == SystemDecorations.BorderOnly && newProperties.WindowState != WindowState.Maximized && newProperties.IsResizable)
{
style |= WindowStyles.WS_THICKFRAME | WindowStyles.WS_BORDER;
}
else if(newProperties.WindowState == WindowState.Maximized && _isClientAreaExtended)
{
style |= WindowStyles.WS_THICKFRAME;
}
}
if (newProperties.Decorations != SystemDecorations.None && newProperties.IsResizable)
style |= WindowStyles.WS_THICKFRAME;
var windowStates = GetWindowStateStyles();
style &= ~WindowStateMask;
@ -1577,14 +1560,8 @@ namespace Avalonia.Win32
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());
}
var adjuster = CreateWindowRectAdjuster();
adjuster.Adjust(ref clientRect, style, extendedStyle);
return clientRect;
}
@ -1681,7 +1658,7 @@ namespace Avalonia.Win32
public Thickness ExtendedMargins => _extendedMargins;
/// <inheritdoc/>
public Thickness OffScreenMargin => _offScreenMargin;
public Thickness OffScreenMargin => default;
/// <inheritdoc/>
public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0);
@ -1692,6 +1669,9 @@ namespace Avalonia.Win32
/// <inheritdoc/>
public CustomWndProcHookCallback? WndProcHookCallback { get; set; }
private WindowRectAdjuster CreateWindowRectAdjuster()
=> new(this);
private ResizeReasonScope SetResizeReason(WindowResizeReason reason)
{
var old = _resizeReason;
@ -1704,7 +1684,7 @@ namespace Avalonia.Win32
public WindowStyles Style { get; set; }
public WindowStyles ExStyle { get; set; }
public RECT WindowRect { get; set; }
};
}
protected struct WindowProperties
{
@ -1748,5 +1728,38 @@ namespace Avalonia.Win32
public int Time;
public PixelPoint Pt;
}
private struct WindowRectAdjuster
{
private static readonly bool s_hasAdjustWindowRectExForDpi = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 14393);
private readonly double _relativeScaling;
private readonly uint _dpi;
public WindowRectAdjuster(WindowImpl owner)
{
if (s_hasAdjustWindowRectExForDpi)
_dpi = (uint)(owner.RenderScaling * StandardDpi);
else
{
var primaryScaling = owner.Screen.AllScreens.FirstOrDefault(screen => screen.IsPrimary)?.Scaling ?? 1;
_relativeScaling = owner.RenderScaling / primaryScaling;
}
}
public void Adjust(ref RECT rect, WindowStyles style, WindowStyles exStyle)
{
if (s_hasAdjustWindowRectExForDpi)
AdjustWindowRectExForDpi(ref rect, style, false, exStyle, _dpi);
else
{
AdjustWindowRectEx(ref rect, (uint)style, false, (uint)exStyle);
rect.top = (int)(rect.top * _relativeScaling);
rect.right = (int)(rect.right * _relativeScaling);
rect.left = (int)(rect.left * _relativeScaling);
rect.bottom = (int)(rect.bottom * _relativeScaling);
}
}
}
}
}

25
tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFramework>$(AvsCurrentTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../src/Skia/Avalonia.Skia/Avalonia.Skia.csproj" />
<ProjectReference Include="../../src/Windows/Avalonia.Win32/Avalonia.Win32.csproj" />
<ProjectReference Include="../../src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="xunit.v3" Version="3.2.1" />
<PackageReference Include="xunit.v3.extensibility.core" Version="3.2.1" />
</ItemGroup>
<Import Project="../../build/SharedVersion.props" />
<Import Project="../../build/ReferenceCoreLibraries.props" />
<Import Project="../../build/BuildTargets.targets" />
</Project>

224
tests/Avalonia.IntegrationTests.Win32/ExtendClientAreaWindowTests.cs

@ -0,0 +1,224 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Chrome;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.IntegrationTests.Win32;
public abstract class ExtendClientAreaWindowTests : IDisposable
{
private const double ClientWidth = 200;
private const double ClientHeight = 200;
private Window? _window;
private Window Window
{
get
{
Assert.NotNull(_window);
return _window;
}
}
protected abstract SystemDecorations Decorations { get; }
public static MatrixTheoryData<bool, WindowState> States
=> new([true, false], Enum.GetValues<WindowState>());
private async Task InitWindowAsync(WindowState state, bool canResize)
{
Assert.Null(_window);
_window = new Window
{
CanResize = canResize,
WindowState = state,
SystemDecorations = Decorations,
ExtendClientAreaToDecorationsHint = true,
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome,
Width = ClientWidth,
Height = ClientHeight,
WindowStartupLocation = WindowStartupLocation.Manual,
Position = new PixelPoint(50, 50),
Content = new Border
{
Background = Brushes.DodgerBlue,
BorderBrush = Brushes.Yellow,
BorderThickness = new Thickness(1)
}
};
_window.Show();
await Window.WhenLoadedAsync();
}
[Theory]
[MemberData(nameof(States))]
public async Task Normal_State_Respects_Client_Size(bool canResize, WindowState initialState)
{
await InitWindowAsync(initialState, canResize);
if (initialState != WindowState.Normal)
Window.WindowState = WindowState.Normal;
// The client size should have been kept
var expected = PixelSize.FromSize(new Size(ClientWidth, ClientHeight), Window.RenderScaling);
var clientSize = Window.GetWin32ClientSize();
Assert.Equal(expected, clientSize);
VerifyNormalState(canResize);
}
protected abstract void VerifyNormalState(bool canResize);
[Theory]
[MemberData(nameof(States))]
public async Task Maximized_State_Fills_Screen_Working_Area(bool canResize, WindowState initialState)
{
await InitWindowAsync(initialState, canResize);
if (initialState != WindowState.Maximized)
Window.WindowState = WindowState.Maximized;
// The client size should match the screen working area
var clientSize = Window.GetWin32ClientSize();
var screenWorkingArea = Window.GetScreen().WorkingArea;
Assert.Equal(screenWorkingArea.Size, clientSize);
VerifyMaximizedState();
}
protected abstract void VerifyMaximizedState();
[Theory]
[MemberData(nameof(States))]
public async Task FullScreen_State_Fills_Screen(bool canResize, WindowState initialState)
{
await InitWindowAsync(initialState, canResize);
if (initialState != WindowState.FullScreen)
Window.WindowState = WindowState.FullScreen;
// The client size should match the screen bounds
var clientSize = Window.GetWin32ClientSize();
var screenBounds = Window.GetScreen().Bounds;
Assert.Equal(screenBounds.Width, clientSize.Width);
Assert.Equal(screenBounds.Height, clientSize.Height);
// The window size should also match the screen bounds
var windowBounds = Window.GetWin32WindowBounds();
Assert.Equal(screenBounds, windowBounds);
// And no visible title bar
AssertNoTitleBar();
}
protected void AssertHasBorder()
{
var clientSize = Window.GetWin32ClientSize();
var windowBounds = Window.GetWin32WindowBounds();
Assert.NotEqual(clientSize.Width, windowBounds.Width);
Assert.NotEqual(clientSize.Height, windowBounds.Height);
}
protected void AssertNoBorder()
{
var clientSize = Window.GetWin32ClientSize();
var windowBounds = Window.GetWin32WindowBounds();
Assert.Equal(clientSize.Width, windowBounds.Width);
Assert.Equal(clientSize.Height, windowBounds.Height);
}
protected (double TitleBarHeight, double ButtonsHeight) GetTitleBarInfo()
{
var titleBar = Window.GetVisualDescendants().OfType<TitleBar>().FirstOrDefault();
Assert.NotNull(titleBar);
var buttons = titleBar.GetVisualDescendants().OfType<CaptionButtons>().FirstOrDefault();
Assert.NotNull(buttons);
return (titleBar.Height, buttons.Height);
}
private void AssertNoTitleBar()
{
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
Assert.Equal(0, titleBarHeight);
Assert.Equal(0, buttonsHeight);
}
public void Dispose()
=> _window?.Close();
public sealed class DecorationsFull : ExtendClientAreaWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.Full;
protected override void VerifyNormalState(bool canResize)
{
AssertHasBorder();
AssertLargeTitleBarWithButtons();
}
protected override void VerifyMaximizedState()
=> AssertLargeTitleBarWithButtons();
private void AssertLargeTitleBarWithButtons()
{
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
Assert.True(titleBarHeight > 20);
Assert.True(buttonsHeight > 20);
}
}
public sealed class DecorationsBorderOnly : ExtendClientAreaWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.BorderOnly;
protected override void VerifyNormalState(bool canResize)
{
AssertHasBorder();
if (canResize)
AssertSmallTitleBarWithoutButtons();
else
AssertNoTitleBar();
}
protected override void VerifyMaximizedState()
=> AssertNoTitleBar();
private void AssertSmallTitleBarWithoutButtons()
{
var (titleBarHeight, buttonsHeight) = GetTitleBarInfo();
Assert.True(titleBarHeight < 10);
Assert.NotEqual(0, titleBarHeight);
Assert.Equal(0, buttonsHeight);
}
}
public sealed class DecorationsNone : ExtendClientAreaWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.None;
protected override void VerifyNormalState(bool canResize)
{
AssertNoBorder();
AssertNoTitleBar();
}
protected override void VerifyMaximizedState()
=> AssertNoTitleBar();
}
}

49
tests/Avalonia.IntegrationTests.Win32/Infrastructure/AppManager.cs

@ -0,0 +1,49 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Themes.Fluent;
using Avalonia.Threading;
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
internal static class AppManager
{
private static readonly Lazy<Task<Dispatcher>> s_initTask = new(CreateUIThread, LazyThreadSafetyMode.ExecutionAndPublication);
private static readonly CancellationTokenSource s_cancellation = new();
public static void Stop()
=> s_cancellation.Cancel();
private static Task<Dispatcher> CreateUIThread()
{
var tcs = new TaskCompletionSource<Dispatcher>();
var uiThread = new Thread(() =>
{
var appBuilder = AppBuilder
.Configure<Application>()
.UseWin32()
.UseSkia()
.SetupWithoutStarting();
appBuilder.Instance!.Styles.Add(new FluentTheme());
// Ensure that Dispatcher.UIThread is initialized on this thread
var dispatcher = Dispatcher.UIThread;
dispatcher.VerifyAccess();
tcs.TrySetResult(dispatcher);
dispatcher.MainLoop(s_cancellation.Token);
})
{
Name = "UI Thread"
};
uiThread.Start();
return tcs.Task;
}
public static Task<Dispatcher> EnsureAppInitializedAsync()
=> s_initTask.Value;
}

10
tests/Avalonia.IntegrationTests.Win32/Infrastructure/AvaloniaTestFramework.cs

@ -0,0 +1,10 @@
using System.Reflection;
using Xunit.v3;
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
internal sealed class AvaloniaTestFramework : XunitTestFramework
{
protected override ITestFrameworkExecutor CreateExecutor(Assembly assembly)
=> new AvaloniaTestFrameworkExecutor(new XunitTestAssembly(assembly, null, assembly.GetName().Version));
}

51
tests/Avalonia.IntegrationTests.Win32/Infrastructure/AvaloniaTestFrameworkExecutor.cs

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Xunit.Internal;
using Xunit.Sdk;
using Xunit.v3;
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
internal sealed class AvaloniaTestFrameworkExecutor(IXunitTestAssembly testAssembly)
: ITestFrameworkExecutor
{
public async ValueTask RunTestCases(
IReadOnlyCollection<ITestCase> testCases,
IMessageSink executionMessageSink,
ITestFrameworkExecutionOptions executionOptions,
CancellationToken? cancellationToken = null)
{
var seed = executionOptions.Seed() ?? testAssembly.ModuleVersionID.GetHashCode();
Randomizer.Seed = seed == int.MinValue ? int.MaxValue : Math.Abs(seed);
var executor = new XunitTestFrameworkExecutor(testAssembly);
var dispatcher = await AppManager.EnsureAppInitializedAsync().ConfigureAwait(false);
try
{
await dispatcher
.InvokeAsync(async () =>
{
using (new PreserveWorkingFolder(testAssembly))
using (new InvariantCultureScope())
{
await executor
.RunTestCases(
testCases.Cast<IXunitTestCase>().ToArray(),
executionMessageSink,
executionOptions,
cancellationToken.GetValueOrDefault())
.ConfigureAwait(false);
}
})
.ConfigureAwait(false);
}
finally
{
AppManager.Stop();
}
}
}

29
tests/Avalonia.IntegrationTests.Win32/Infrastructure/InvariantCultureScope.cs

@ -0,0 +1,29 @@
using System;
using System.Globalization;
namespace Avalonia.IntegrationTests.Win32.Infrastructure;
internal sealed class InvariantCultureScope : IDisposable
{
private readonly CultureInfo? _previousCulture;
private readonly CultureInfo? _previousUICulture;
public InvariantCultureScope()
{
_previousCulture = CultureInfo.CurrentCulture;
_previousUICulture = CultureInfo.CurrentUICulture;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
CultureInfo.CurrentUICulture = CultureInfo.InvariantCulture;
}
public void Dispose()
{
if (_previousCulture is not null)
CultureInfo.CurrentCulture = _previousCulture;
if (_previousUICulture is not null)
CultureInfo.CurrentUICulture = _previousUICulture;
}
}

5
tests/Avalonia.IntegrationTests.Win32/Properties/AssemblyInfo.cs

@ -0,0 +1,5 @@
using Avalonia.IntegrationTests.Win32.Infrastructure;
using Xunit;
[assembly: TestFramework(typeof(AvaloniaTestFramework))]
[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)]

135
tests/Avalonia.IntegrationTests.Win32/StandardWindowTests.cs

@ -0,0 +1,135 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Chrome;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.IntegrationTests.Win32;
public abstract class StandardWindowTests : IDisposable
{
private const double ClientWidth = 200;
private const double ClientHeight = 200;
private Window? _window;
private Window Window
{
get
{
Assert.NotNull(_window);
return _window;
}
}
protected abstract SystemDecorations Decorations { get; }
protected abstract bool HasCaption { get; }
public static MatrixTheoryData<bool, WindowState> States
=> new([true, false], Enum.GetValues<WindowState>());
private async Task InitWindowAsync(WindowState state, bool canResize)
{
Assert.Null(_window);
_window = new Window
{
CanResize = canResize,
WindowState = state,
SystemDecorations = Decorations,
ExtendClientAreaToDecorationsHint = false,
Width = ClientWidth,
Height = ClientHeight,
WindowStartupLocation = WindowStartupLocation.Manual,
Position = new PixelPoint(50, 50),
Content = new Border
{
Background = Brushes.DodgerBlue,
BorderBrush = Brushes.Yellow,
BorderThickness = new Thickness(1)
}
};
_window.Show();
await Window.WhenLoadedAsync();
}
[Theory]
[MemberData(nameof(States))]
public async Task Maximized_State_Fills_Screen_Working_Area(bool canResize, WindowState initialState)
{
await InitWindowAsync(initialState, canResize);
if (initialState != WindowState.Maximized)
Window.WindowState = WindowState.Maximized;
// The client size should match the screen working area
var clientSize = Window.GetWin32ClientSize();
var screenWorkingArea = Window.GetScreen().WorkingArea;
if (HasCaption)
{
Assert.Equal(screenWorkingArea.Size.Width, clientSize.Width);
Assert.True(clientSize.Height < screenWorkingArea.Size.Height);
}
else
Assert.Equal(screenWorkingArea.Size, clientSize);
}
[Theory]
[MemberData(nameof(States))]
public async Task FullScreen_State_Fills_Screen(bool canResize, WindowState initialState)
{
await InitWindowAsync(initialState, canResize);
if (initialState != WindowState.FullScreen)
Window.WindowState = WindowState.FullScreen;
// The client size should match the screen bounds
var clientSize = Window.GetWin32ClientSize();
var screenBounds = Window.GetScreen().Bounds;
Assert.Equal(screenBounds.Width, clientSize.Width);
Assert.Equal(screenBounds.Height, clientSize.Height);
// The window size should also match the screen bounds
var windowBounds = Window.GetWin32WindowBounds();
Assert.Equal(screenBounds, windowBounds);
}
public void Dispose()
=> _window?.Close();
public sealed class DecorationsFull : StandardWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.Full;
protected override bool HasCaption
=> true;
}
public sealed class DecorationsBorderOnly : StandardWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.BorderOnly;
protected override bool HasCaption
=> false;
}
public sealed class DecorationsNone : StandardWindowTests
{
protected override SystemDecorations Decorations
=> SystemDecorations.None;
protected override bool HasCaption
=> false;
}
}

23
tests/Avalonia.IntegrationTests.Win32/UnmanagedMethods.cs

@ -0,0 +1,23 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.IntegrationTests.Win32;
internal static partial class UnmanagedMethods
{
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetClientRect(IntPtr hwnd, out RECT lpRect);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
public struct RECT
{
public int left;
public int top;
public int right;
public int bottom;
}
}

55
tests/Avalonia.IntegrationTests.Win32/WindowExtensions.cs

@ -0,0 +1,55 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.IntegrationTests.Win32;
internal static class WindowExtensions
{
public static PixelRect ToPixelRect(this UnmanagedMethods.RECT rect)
=> new(new PixelPoint(rect.left, rect.top), new PixelPoint(rect.right, rect.bottom));
public static Task WhenLoadedAsync(this Window window)
{
if (window.IsLoaded)
return Task.CompletedTask;
var tcs = new TaskCompletionSource();
window.Loaded += OnLoaded;
return tcs.Task;
void OnLoaded(object? sender, RoutedEventArgs e)
{
window.Loaded -= OnLoaded;
tcs.TrySetResult();
}
}
public static Screen GetScreen(this Window window)
{
var screen = window.Screens.ScreenFromWindow(window);
Assert.NotNull(screen);
return screen;
}
public static PixelSize GetWin32ClientSize(this Window window)
{
var platformHandle = window.TryGetPlatformHandle();
Assert.NotNull(platformHandle);
Assert.True(UnmanagedMethods.GetClientRect(platformHandle.Handle, out var rect));
return rect.ToPixelRect().Size;
}
public static PixelRect GetWin32WindowBounds(this Window window)
{
var platformHandle = window.TryGetPlatformHandle();
Assert.NotNull(platformHandle);
Assert.True(UnmanagedMethods.GetWindowRect(platformHandle.Handle, out var rect));
return rect.ToPixelRect();
}
}
Loading…
Cancel
Save