Skip to content

Commit 884051e

Browse files
authored
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
1 parent d39cb62 commit 884051e

17 files changed

+904
-168
lines changed

Avalonia.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.XUnit.Per
285285
EndProject
286286
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}"
287287
EndProject
288+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.IntegrationTests.Win32", "tests\Avalonia.IntegrationTests.Win32\Avalonia.IntegrationTests.Win32.csproj", "{11522B0D-BF31-42D5-8FC5-41E58F319AF9}"
289+
EndProject
288290
Global
289291
GlobalSection(SolutionConfigurationPlatforms) = preSolution
290292
Debug|Any CPU = Debug|Any CPU
@@ -659,6 +661,10 @@ Global
659661
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Debug|Any CPU.Build.0 = Debug|Any CPU
660662
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.ActiveCfg = Release|Any CPU
661663
{26918642-829D-4FA2-B60A-BE8D83F4E063}.Release|Any CPU.Build.0 = Release|Any CPU
664+
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
665+
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Debug|Any CPU.Build.0 = Debug|Any CPU
666+
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.ActiveCfg = Release|Any CPU
667+
{11522B0D-BF31-42D5-8FC5-41E58F319AF9}.Release|Any CPU.Build.0 = Release|Any CPU
662668
EndGlobalSection
663669
GlobalSection(SolutionProperties) = preSolution
664670
HideSolutionNode = FALSE
@@ -742,6 +748,7 @@ Global
742748
{09EC467F-0F25-4E6F-A836-2BAEC8F6AB0C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
743749
{342D2657-2F84-493C-B74B-9D2CAE5D9DAB} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
744750
{26918642-829D-4FA2-B60A-BE8D83F4E063} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
751+
{11522B0D-BF31-42D5-8FC5-41E58F319AF9} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
745752
EndGlobalSection
746753
GlobalSection(ExtensibilityGlobals) = postSolution
747754
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

azure-pipelines-integrationtests.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,16 @@ jobs:
8787
displayName: 'Build test project'
8888
inputs:
8989
command: 'build'
90-
projects: 'tests\Avalonia.IntegrationTests.Appium\Avalonia.IntegrationTests.Appium.csproj'
90+
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
91+
92+
- task: DotNetCoreCLI@2
93+
displayName: 'Run Win32 Integration Tests'
94+
inputs:
95+
command: 'run'
96+
projects: 'tests/Avalonia.IntegrationTests.Win32/Avalonia.IntegrationTests.Win32.csproj'
9197

9298
- task: VSTest@2
93-
displayName: 'Run Integration Tests'
99+
displayName: 'Run Appium Integration Tests'
94100
inputs:
95101
testAssemblyVer2: '**\bin\**\Avalonia.IntegrationTests.Appium.dll'
96102
runSettingsFile: 'tests\Avalonia.IntegrationTests.Appium\record-video.runsettings'

src/Avalonia.Controls/Chrome/TitleBar.cs

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@ private void UpdateSize(Window window)
2727

2828
if (window.WindowState != WindowState.FullScreen)
2929
{
30-
Height = Math.Max(0, window.WindowDecorationMargin.Top);
31-
32-
if (_captionButtons != null)
33-
{
34-
_captionButtons.Height = Height;
35-
}
30+
var height = Math.Max(0, window.WindowDecorationMargin.Top);
31+
Height = height;
32+
_captionButtons?.Height = window.SystemDecorations == SystemDecorations.Full ? height : 0;
33+
}
34+
else
35+
{
36+
// Note: apparently the titlebar was supposed to be displayed when hovering the top of the screen,
37+
// to mimic macOS behavior. This has been broken for years. It actually only partially works if the
38+
// window is FullScreen right on startup, and only once. Any size change will then break it.
39+
// Disable it for now.
40+
// TODO: restore that behavior so that it works in all cases
41+
Height = 0;
42+
_captionButtons?.Height = 0;
3643
}
3744

3845
IsVisible = window.PlatformImpl?.NeedsManagedDecorations ?? false;
@@ -79,6 +86,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
7986
PseudoClasses.Set(":normal", x == WindowState.Normal);
8087
PseudoClasses.Set(":maximized", x == WindowState.Maximized);
8188
PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen);
89+
UpdateSize(window);
8290
}),
8391
window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty)
8492
.Subscribe(_ => UpdateSize(window))

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,14 @@ public enum DwmWindowCornerPreference : uint
869869
DWMWCP_ROUNDSMALL
870870
}
871871

872+
public enum DwmNCRenderingPolicy : uint
873+
{
874+
DWMNCRP_USEWINDOWSTYLE,
875+
DWMNCRP_DISABLED,
876+
DWMNCRP_ENABLED,
877+
DWMNCRP_LAST
878+
}
879+
872880
public enum MapVirtualKeyMapTypes : uint
873881
{
874882
MAPVK_VK_TO_VSC = 0x00,

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

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.Diagnostics.CodeAnalysis;
44
using System.Linq;
5+
using System.Runtime.CompilerServices;
56
using System.Runtime.InteropServices;
67
using Avalonia.Automation.Peers;
78
using Avalonia.Controls;
@@ -57,13 +58,79 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
5758
return IntPtr.Zero;
5859
}
5960

60-
case WindowsMessage.WM_NCCALCSIZE:
61+
case WindowsMessage.WM_NCCALCSIZE when ToInt32(wParam) == 1:
6162
{
62-
if (ToInt32(wParam) == 1 && (_windowProperties.Decorations == SystemDecorations.None || _isClientAreaExtended))
63+
if (_windowProperties.Decorations == SystemDecorations.None)
64+
return IntPtr.Zero;
65+
66+
// When the client area is extended into the frame, we are still requesting the standard styles matching
67+
// the wanted decorations (such as WS_CAPTION or WS_BORDER) along with window bounds larger than the client size.
68+
// This allows the window to have the standard resize borders *outside* of the client area.
69+
// The logic for this lies in the Resize() method.
70+
//
71+
// After this happens, WM_NCCALCSIZE provides us with a new window area matching those requested bounds.
72+
// We need to adjust that area back to our preferred client area, keeping the resize borders around it.
73+
//
74+
// The same logic applies when the window gets maximized, the only difference being that Windows chose
75+
// the final bounds instead of us.
76+
if (_isClientAreaExtended)
6377
{
78+
GetWindowPlacement(hWnd, out var placement);
79+
if (placement.ShowCmd == ShowWindowCommand.ShowMinimized)
80+
break;
81+
82+
var paramsObj = Marshal.PtrToStructure<NCCALCSIZE_PARAMS>(lParam);
83+
ref var rect = ref paramsObj.rgrc[0];
84+
85+
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
86+
var adjuster = CreateWindowRectAdjuster();
87+
var borderThickness = new RECT();
88+
89+
// We told Windows we have a caption, but since we're actually extending into it, it should not be taken into account.
90+
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
91+
{
92+
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
93+
{
94+
adjuster.Adjust(ref borderThickness, style & ~WindowStyles.WS_CAPTION | WindowStyles.WS_BORDER | WindowStyles.WS_THICKFRAME, 0);
95+
}
96+
else
97+
{
98+
adjuster.Adjust(ref borderThickness, style, 0);
99+
100+
var thinBorderThickness = new RECT();
101+
adjuster.Adjust(ref thinBorderThickness, style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME) | WindowStyles.WS_BORDER, 0);
102+
borderThickness.top = thinBorderThickness.top;
103+
}
104+
}
105+
else if (style.HasAllFlags(WindowStyles.WS_BORDER))
106+
{
107+
if (placement.ShowCmd == ShowWindowCommand.ShowMaximized)
108+
{
109+
adjuster.Adjust(ref borderThickness, style, 0);
110+
}
111+
else
112+
{
113+
adjuster.Adjust(ref borderThickness, style, 0);
114+
115+
var thinBorderThickness = new RECT();
116+
adjuster.Adjust(ref thinBorderThickness, style & ~WindowStyles.WS_THICKFRAME, 0);
117+
borderThickness.top = thinBorderThickness.top;
118+
}
119+
}
120+
else
121+
{
122+
adjuster.Adjust(ref borderThickness, style, 0);
123+
}
124+
125+
rect.left -= borderThickness.left;
126+
rect.top -= borderThickness.top;
127+
rect.right -= borderThickness.right;
128+
rect.bottom -= borderThickness.bottom;
129+
130+
Marshal.StructureToPtr(paramsObj, lParam, false);
131+
64132
return IntPtr.Zero;
65133
}
66-
67134
break;
68135
}
69136

@@ -699,11 +766,6 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
699766

700767
UpdateWindowProperties(newWindowProperties);
701768

702-
if (windowState == WindowState.Maximized)
703-
{
704-
MaximizeWithoutCoveringTaskbar();
705-
}
706-
707769
WindowStateChanged?.Invoke(windowState);
708770

709771
if (_isClientAreaExtended)
@@ -739,6 +801,44 @@ protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam,
739801

740802
_maxTrackSize = mmi.ptMaxTrackSize;
741803

804+
// A window without a caption (i.e. None and BorderOnly decorations) maximizes to the whole screen
805+
// by default. Adjust that to the screen's working area instead.
806+
var style = GetStyle();
807+
if (!style.HasAllFlags(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))
808+
{
809+
var screen = Screen.ScreenFromHwnd(Hwnd, MONITOR.MONITOR_DEFAULTTONEAREST);
810+
if (screen?.WorkingArea is { } workingArea)
811+
{
812+
var x = workingArea.X;
813+
var y = workingArea.Y;
814+
var cx = workingArea.Width;
815+
var cy = workingArea.Height;
816+
817+
var adjuster = CreateWindowRectAdjuster();
818+
var borderThickness = new RECT();
819+
820+
var adjustedStyle = style & ~WindowStyles.WS_CAPTION;
821+
822+
if (style.HasAllFlags(WindowStyles.WS_BORDER))
823+
adjustedStyle |= WindowStyles.WS_BORDER;
824+
825+
if (style.HasAllFlags(WindowStyles.WS_CAPTION))
826+
adjustedStyle |= WindowStyles.WS_THICKFRAME;
827+
828+
adjuster.Adjust(ref borderThickness, adjustedStyle, 0);
829+
830+
x += borderThickness.left;
831+
y += borderThickness.top;
832+
cx += -borderThickness.left + borderThickness.right;
833+
cy += -borderThickness.top + borderThickness.bottom;
834+
835+
mmi.ptMaxPosition.X = x;
836+
mmi.ptMaxPosition.Y = y;
837+
mmi.ptMaxSize.X = cx;
838+
mmi.ptMaxSize.Y = cy;
839+
}
840+
}
841+
742842
if (_minSize.Width > 0)
743843
{
744844
mmi.ptMinTrackSize.X =

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

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,24 @@ private HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam)
1818
// Get the window rectangle.
1919
GetWindowRect(hWnd, out var rcWindow);
2020

21-
var scaling = (uint)(RenderScaling * StandardDpi);
22-
var relativeScaling = RenderScaling / PrimaryScreenRenderScaling;
23-
2421
// Get the frame rectangle, adjusted for the style without a caption.
2522
var rcFrame = new RECT();
2623
var borderThickness = new RECT();
27-
if (Win32Platform.WindowsVersion < PlatformConstants.Windows10_1607)
28-
{
29-
AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0);
30-
31-
rcFrame.top = (int)(rcFrame.top * relativeScaling);
32-
rcFrame.right = (int)(rcFrame.right * relativeScaling);
33-
rcFrame.left = (int)(rcFrame.left * relativeScaling);
34-
rcFrame.bottom = (int)(rcFrame.bottom * relativeScaling);
3524

36-
AdjustWindowRectEx(ref borderThickness, (uint)GetStyle(), false, 0);
37-
38-
borderThickness.top = (int)(borderThickness.top * relativeScaling);
39-
borderThickness.right = (int)(borderThickness.right * relativeScaling);
40-
borderThickness.left = (int)(borderThickness.left * relativeScaling);
41-
borderThickness.bottom = (int)(borderThickness.bottom * relativeScaling);
42-
}
43-
else
25+
var isMaximized = GetWindowPlacement(hWnd, out var placement) && placement.ShowCmd == ShowWindowCommand.ShowMaximized;
26+
if (!isMaximized)
4427
{
45-
AdjustWindowRectExForDpi(ref rcFrame, WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION, false, 0, scaling);
46-
AdjustWindowRectExForDpi(ref borderThickness, GetStyle(), false, 0, scaling);
47-
}
28+
var style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
29+
if (style.HasAllFlags(WindowStyles.WS_THICKFRAME))
30+
{
31+
var adjuster = CreateWindowRectAdjuster();
32+
adjuster.Adjust(ref rcFrame, style & ~WindowStyles.WS_CAPTION, 0);
33+
adjuster.Adjust(ref borderThickness, style, 0);
4834

49-
borderThickness.left *= -1;
50-
borderThickness.top *= -1;
35+
borderThickness.left *= -1;
36+
borderThickness.top *= -1;
37+
}
38+
}
5139

5240
if (_extendTitleBarHint >= 0)
5341
{

0 commit comments

Comments
 (0)