Skip to content

Commit 53b5afb

Browse files
committed
Improved region creation code, high-dpi support and sample addition
1 parent 7f4cb87 commit 53b5afb

File tree

7 files changed

+286
-15
lines changed

7 files changed

+286
-15
lines changed

src/WinUIEx/NativeMethods.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,9 @@ MonitorFromPoint
6969
IDI_APPLICATION
7070
CreateRectRgn
7171
CreateRoundRectRgn
72+
CreateEllipticRgn
73+
CreatePolygonRgn
7274
CombineRgn
7375
SetWindowRgn
76+
GetWindowRgn
77+
GetWindowRgnBox

src/WinUIEx/Region.cs

Lines changed: 127 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Text;
55
using System.Threading.Tasks;
6+
using Microsoft.UI.Content;
67
using Windows.Foundation;
78
using Windows.Graphics;
89
using Windows.Win32;
@@ -21,9 +22,19 @@ private class RectRegion : Region
2122
{
2223
private readonly Rect _rect;
2324
public RectRegion(Rect rect) { _rect = rect; }
24-
internal override HRGN Create(double scalefactor)
25-
=> PInvoke.CreateRectRgn((int)(_rect.Left * scalefactor), (int)(_rect.Top * scalefactor),
26-
(int)(_rect.Right * scalefactor), (int)(_rect.Bottom * scalefactor));
25+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
26+
{
27+
var p1 = Convert(_rect.Left, _rect.Top, converter, screenLoc, scaleFactor);
28+
var p2 = Convert(_rect.Right, _rect.Bottom, converter, screenLoc, scaleFactor);
29+
return PInvoke.CreateRectRgn(p1.X, p1.Y, p2.X, p2.Y);
30+
}
31+
}
32+
private class RectInt32Region : Region
33+
{
34+
private readonly RectInt32 _rect;
35+
public RectInt32Region(RectInt32 rect) { _rect = rect; }
36+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
37+
=> PInvoke.CreateRectRgn(_rect.X, _rect.Y, _rect.X + _rect.Width, _rect.Y + _rect.Height);
2738
}
2839

2940
private class RoundRectRegion : Region
@@ -37,9 +48,70 @@ public RoundRectRegion(Rect rect, double w, double h)
3748
_w = w;
3849
_h = h;
3950
}
40-
internal override HRGN Create(double scalefactor)
41-
=> PInvoke.CreateRoundRectRgn((int)(_rect.Left * scalefactor), (int)(_rect.Top * scalefactor),
42-
(int)(_rect.Right * scalefactor), (int)(_rect.Bottom * scalefactor), (int)(_w * scalefactor), (int)(_h * scalefactor));
51+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
52+
{
53+
var p1 = Convert(_rect.Left, _rect.Top, converter, screenLoc, scaleFactor);
54+
var p2 = Convert(_rect.Right, _rect.Bottom, converter, screenLoc, scaleFactor);
55+
return PInvoke.CreateRoundRectRgn(p1.X, p1.Y, p2.X, p2.Y, (int)(_w * scaleFactor), (int)(_h * scaleFactor));
56+
}
57+
}
58+
59+
private class EllipticRegion : Region
60+
{
61+
private readonly double _x1;
62+
private readonly double _y1;
63+
private readonly double _x2;
64+
private readonly double _y2;
65+
public EllipticRegion(double x1, double y1, double x2, double y2)
66+
{
67+
_x1 = x1;
68+
_y1 = y1;
69+
_x2 = x2;
70+
_y2 = y2;
71+
}
72+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
73+
{
74+
var p1 = Convert(_x1, _y1, converter, screenLoc, scaleFactor);
75+
var p2 = Convert(_x2, _y2, converter, screenLoc, scaleFactor);
76+
return PInvoke.CreateEllipticRgn(p1.X, p1.Y, p2.X, p2.Y);
77+
}
78+
}
79+
80+
private class PolygonRegion : Region
81+
{
82+
private readonly IEnumerable<Point> _points;
83+
private readonly CREATE_POLYGON_RGN_MODE _mode;
84+
public PolygonRegion(IEnumerable<Point> points, CREATE_POLYGON_RGN_MODE mode)
85+
{
86+
_points = points;
87+
_mode = mode;
88+
}
89+
internal unsafe override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
90+
{
91+
var pint32 = converter.ConvertLocalToScreen(_points.Select(p => new Point(p.X * scaleFactor, p.Y * scaleFactor)).ToArray());
92+
var array = pint32.Select(p => new System.Drawing.Point(p.X - screenLoc.X, p.Y - screenLoc.Y)).ToArray();
93+
fixed (System.Drawing.Point* pArray = array)
94+
return PInvoke.CreatePolygonRgn(pArray, array.Length, _mode);
95+
}
96+
97+
}
98+
99+
private class CurrentRegion : Region
100+
{
101+
private nint _WindowHandle;
102+
public CurrentRegion(nint windowHandle)
103+
{
104+
_WindowHandle = windowHandle;
105+
}
106+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
107+
{
108+
var dest = PInvoke.CreateRectRgn(0, 0, 0, 0);
109+
//PInvoke.GetWindowRgnBox(new Windows.Win32.Foundation.HWND(_WindowHandle), dest);
110+
var type = PInvoke.GetWindowRgnBox(new Windows.Win32.Foundation.HWND(_WindowHandle), out var rect);
111+
return PInvoke.CreateRectRgn(rect.left, rect.top, rect.right, rect.bottom);
112+
// PInvoke.InvertRgn(dest, null);
113+
// return dest;
114+
}
43115
}
44116

45117
private class CombinedRegion : Region
@@ -54,10 +126,10 @@ public CombinedRegion(Region region1, Region region2, CombineMode mode)
54126
_mode = mode;
55127
}
56128

57-
internal override HRGN Create(double scaleFactor)
129+
internal override HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
58130
{
59-
var rgn1 = _region1.Create(scaleFactor);
60-
var rgn2 = _region2.Create(scaleFactor);
131+
var rgn1 = _region1.Create(converter, screenLoc, scaleFactor);
132+
var rgn2 = _region2.Create(converter, screenLoc, scaleFactor);
61133

62134
var dest = PInvoke.CreateRectRgn(0, 0, 0, 0);
63135
PInvoke.CombineRgn(dest, rgn1, rgn2, (RGN_COMBINE_MODE)_mode);
@@ -68,11 +140,18 @@ internal override HRGN Create(double scaleFactor)
68140
}
69141

70142
/// <summary>
71-
/// Creates a rectangular region.
143+
/// Creates a rectangular region in device independent units.
72144
/// </summary>
73145
/// <param name="rect">Rectangular region.</param>
74146
/// <returns></returns>
75-
public static Region Create(Rect rect) => new RectRegion(rect);
147+
public static Region CreateRectangle(Rect rect) => new RectRegion(rect);
148+
149+
/// <summary>
150+
/// Creates a rectangular region in screen units.
151+
/// </summary>
152+
/// <param name="rect">Rectangular region.</param>
153+
/// <returns></returns>
154+
public static Region CreateRectangle(RectInt32 rect) => new RectInt32Region(rect);
76155

77156
/// <summary>
78157
/// Creates a rectangular region with rounded corners.
@@ -81,13 +160,48 @@ internal override HRGN Create(double scaleFactor)
81160
/// <param name="w">Specifies the width of the ellipse used to create the rounded corners.</param>
82161
/// <param name="h">Specifies the height of the ellipse used to create the rounded corners.</param>
83162
/// <returns></returns>
84-
public static Region Create(Rect rect, double w, double h) => new RoundRectRegion(rect, w, h);
163+
public static Region CreateRoundedRectangle(Rect rect, double w, double h) => new RoundRectRegion(rect, w, h);
164+
165+
/// <summary>
166+
/// Creates an elliptical region.
167+
/// </summary>
168+
/// <param name="x1">Specifies the x-coordinate in device independent units, of the upper-left corner of the bounding rectangle of the ellipse.</param>
169+
/// <param name="y1">Specifies the yx-coordinate in device independent units, of the upper-left corner of the bounding rectangle of the ellipse.</param>
170+
/// <param name="x2">Specifies the x-coordinate in device independent units, of the lower-right corner of the bounding rectangle of the ellipse.</param>
171+
/// <param name="y2">Specifies the y-coordinate in device independent units, of the lower-right corner of the bounding rectangle of the ellipse.</param>
172+
/// <returns></returns>
173+
public static Region CreateElliptic(double x1, double y1, double x2, double y2) => new EllipticRegion(x1, y1, x2, y2);
174+
175+
/// <summary>
176+
/// Creates a polygonal region using alternate mode (fills area between odd-numbered and even-numbered polygon sides on each scan line).
177+
/// </summary>
178+
/// <param name="points">The vertices of the polygon in device independent units. The polygon is presumed closed. Each vertex can be specified only once.</param>
179+
/// <returns></returns>
180+
public static Region CreatePolygon(IEnumerable<Point> points) => new PolygonRegion(points, CREATE_POLYGON_RGN_MODE.ALTERNATE);
181+
182+
/*
183+
/// <summary>
184+
/// Creates a region that represents the current visible region of a window.
185+
/// </summary>
186+
/// <param name="window">The window to obtain the region from</param>
187+
/// <returns></returns>
188+
public static Region GetWindowRegion(Microsoft.UI.Xaml.Window window) => new CurrentRegion(window.GetWindowHandle());*/
85189

86190
private Region()
87191
{
88192
}
89193

90-
internal abstract HRGN Create(double scalefactor);
194+
internal abstract HRGN Create(ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor);
195+
196+
197+
private protected static PointInt32 Convert(double x, double y, ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor) => Convert(new Point(x, y), converter, screenLoc, scaleFactor);
198+
199+
private protected static PointInt32 Convert(Point local, ContentCoordinateConverter converter, PointInt32 screenLoc, double scaleFactor)
200+
{
201+
local = new Point(local.X * scaleFactor, local.Y * scaleFactor);
202+
var p = converter.ConvertLocalToScreen(local);
203+
return new PointInt32(p.X - screenLoc.X, p.Y - screenLoc.Y);
204+
}
91205

92206
/// <summary>
93207
/// The Combine function combines two regions. The two regions are combined according to the specified mode.

src/WinUIEx/WindowExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,17 @@ public static void SetLayeredWindowAttributes(this Microsoft.UI.Xaml.Window wind
373373
/// <param name="region">The region to set on the window</param>
374374
public static void SetRegion(this Microsoft.UI.Xaml.Window window, Region? region)
375375
{
376-
PInvoke.SetWindowRgn(new Windows.Win32.Foundation.HWND(window.GetWindowHandle()),
377-
region?.Create(window.GetDpiForWindow() / 96d) ?? Windows.Win32.Graphics.Gdi.HRGN.Null, window.Visible);
376+
var converter = Microsoft.UI.Content.ContentCoordinateConverter.CreateForWindowId(window.AppWindow.Id);
377+
var screenLoc = window.AppWindow.Position;
378+
var rgn = region?.Create(converter, screenLoc, window.GetDpiForWindow() / 96d) ?? Windows.Win32.Graphics.Gdi.HRGN.Null;
379+
try
380+
{
381+
PInvoke.SetWindowRgn(new Windows.Win32.Foundation.HWND(window.GetWindowHandle()), rgn, window.Visible);
382+
}
383+
finally
384+
{
385+
PInvoke.DeleteObject(rgn);
386+
}
378387
}
379388
}
380389
}

src/WinUIExSample/HoleWindow.xaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Window
3+
x:Class="WinUIExSample.HoleWindow"
4+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:ex="using:WinUIEx"
7+
xmlns:local="using:WinUIExSample"
8+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
9+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
10+
Title="WinUIEx Regions"
11+
mc:Ignorable="d">
12+
<Grid Margin="0">
13+
<Grid.RowDefinitions>
14+
<RowDefinition Height="Auto"/>
15+
<RowDefinition Height="*"/>
16+
</Grid.RowDefinitions>
17+
<Border VerticalAlignment="Top" Background="CornflowerBlue">
18+
<StackPanel Orientation="Horizontal">
19+
<TextBlock Text="WinUIEx Regions" FontSize="24" FontFamily="Segoe UI" FontWeight="Light" Margin="10,5,0,5" Foreground="White" />
20+
<TextBlock Text="Click blue area to punch holes in the window" Margin="20,10,0,5" Foreground="White" VerticalAlignment="Center" />
21+
</StackPanel>
22+
</Border>
23+
24+
<Canvas Grid.Row="1">
25+
26+
<Border Width="200" Height="200" Canvas.Left="100" Canvas.Top="50" BorderThickness="2" BorderBrush="Red" >
27+
<Border Background="Blue" x:Name="RectangleArea" PointerPressed="RectangleArea_PointerPressed" />
28+
</Border>
29+
30+
<Grid Width="200" Height="100" Canvas.Left="350" Canvas.Top="75" >
31+
<Ellipse x:Name="EllipseArea" Fill="Blue" PointerPressed="EllipseArea_PointerPressed" Margin="1,1,1,1" />
32+
<Ellipse Stroke="Red" StrokeThickness="2" />
33+
</Grid>
34+
35+
<Border Width="200" Height="200" Canvas.Left="100" Canvas.Top="275" BorderThickness="2" BorderBrush="Red" CornerRadius="20" >
36+
<Border Background="Blue" x:Name="RoundedRectangleArea" PointerPressed="RoundedRectangleArea_PointerPressed" CornerRadius="18" />
37+
</Border>
38+
39+
<Grid Canvas.Left="350" Canvas.Top="275" >
40+
<Path Fill="Blue" Stroke="Red" StrokeThickness="2" Data="M100,10 L122.45,72.36 L188.17,72.36 L134.86,112.9 L154.45,177.64 L100,140 L45.55,177.64 L65.14,112.9 L11.83,72.36 L77.55,72.36 Z"
41+
Stretch="None" PointerPressed="StarArea_Pressed" />
42+
</Grid>
43+
44+
</Canvas>
45+
46+
<Button Content="Reset" Click="ButtonClear_Click" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="20" Grid.Row="1" />
47+
48+
</Grid>
49+
</Window>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Microsoft.UI.Windowing;
4+
using Microsoft.UI.Xaml;
5+
using Windows.Foundation;
6+
using WinUIEx;
7+
8+
namespace WinUIExSample
9+
{
10+
/// <summary>
11+
/// An empty page that can be used on its own or navigated to within a Frame.
12+
/// </summary>
13+
public sealed partial class HoleWindow : Window
14+
{
15+
private readonly WindowManager manager;
16+
public HoleWindow()
17+
{
18+
this.InitializeComponent();
19+
ExtendsContentIntoTitleBar = true;
20+
manager = WindowManager.Get(this);
21+
manager.Width = 800;
22+
manager.Height = 640;
23+
manager.IsResizable = false; //Disable resizing for simplicity - You need to update regions on resize otherwise
24+
manager.IsAlwaysOnTop = true;
25+
// ((OverlappedPresenter)AppWindow.Presenter).SetBorderAndTitleBar(false, false);
26+
this.Closed += HoleWindow_Closed;
27+
MainWindow.Closed += MainWindow_Closed;
28+
}
29+
30+
private void RectangleArea_PointerPressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
31+
{
32+
// Get the bounds of the rectangle relative to the window
33+
var bounds = RectangleArea.TransformToVisual(this.Content).TransformBounds(new Rect(0, 1, RectangleArea.ActualWidth, RectangleArea.ActualHeight));
34+
// Subtract the rectangle from the window region
35+
var regionWithHole = GetWindowRegion() - Region.CreateRectangle(bounds);
36+
this.SetRegion(regionWithHole);
37+
}
38+
39+
private void RoundedRectangleArea_PointerPressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
40+
{
41+
// Get the bounds of the rectangle relative to the window
42+
var bounds = RoundedRectangleArea.TransformToVisual(this.Content).TransformBounds(new Rect(1, 1, RoundedRectangleArea.ActualWidth, RoundedRectangleArea.ActualHeight));
43+
// Subtract the rectangle from the window region
44+
var regionWithHole = GetWindowRegion() - Region.CreateRoundedRectangle(bounds, 40, 40);
45+
this.SetRegion(regionWithHole);
46+
}
47+
48+
49+
private void EllipseArea_PointerPressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
50+
{
51+
// Get the bounds of the ellipse relative to the window
52+
var bounds = EllipseArea.TransformToVisual(this.Content).TransformBounds(new Rect(0, 1, EllipseArea.ActualWidth, EllipseArea.ActualHeight + 1));
53+
// Subtract the ellipse from the window region
54+
var regionWithHole = GetWindowRegion() - Region.CreateElliptic(bounds.X, bounds.Y, bounds.Width+bounds.X, bounds.Height + bounds.Y);
55+
this.SetRegion(regionWithHole);
56+
}
57+
58+
private void ButtonClear_Click(object sender, RoutedEventArgs e) => this.SetRegion(null);
59+
60+
private Region GetWindowRegion()
61+
{
62+
return Region.CreateRoundedRectangle(new Rect(0, 1, manager.Width - 16, manager.Height - 8), 8, 8);
63+
}
64+
65+
#region Cleanup: Ensure this window is closed when main window closes
66+
private void MainWindow_Closed(object sender, WindowEventArgs args) => this.Close();
67+
68+
public WindowEx MainWindow => ((App)Application.Current).MainWindow!;
69+
70+
private void HoleWindow_Closed(object sender, WindowEventArgs args) => MainWindow.Closed -= MainWindow_Closed;
71+
#endregion
72+
73+
private void StarArea_Pressed(object sender, Microsoft.UI.Xaml.Input.PointerRoutedEventArgs e)
74+
{
75+
var path = (Microsoft.UI.Xaml.Shapes.Path)sender;
76+
var transformer = path.TransformToVisual(this.Content);
77+
var geom = (Microsoft.UI.Xaml.Media.PathGeometry)path.Data;
78+
var figure = geom.Figures[0];
79+
List<Point> vertices = [transformer.TransformPoint(figure.StartPoint)];
80+
foreach(var segment in figure.Segments.OfType<Microsoft.UI.Xaml.Media.LineSegment>())
81+
{
82+
vertices.Add(transformer.TransformPoint(segment.Point));
83+
}
84+
// Subtract the polygon from the window region
85+
var regionWithHole = GetWindowRegion() - Region.CreatePolygon(vertices);
86+
this.SetRegion(regionWithHole);
87+
}
88+
}
89+
}

src/WinUIExSample/Pages/WindowDesign.xaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<ComboBoxItem>Animated</ComboBoxItem>
3131
<ComboBoxItem>Blur</ComboBoxItem>
3232
</ComboBox>
33+
<Button Content="Open Window Hole Punch Sample" Click="OpenHoleWindow_Click" />
3334
</StackPanel>
3435
</ScrollViewer>
3536
</Grid>

src/WinUIExSample/Pages/WindowDesign.xaml.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,10 @@ private partial class BlurredBackdrop : CompositionBrushBackdrop
106106
protected override Windows.UI.Composition.CompositionBrush CreateBrush(Windows.UI.Composition.Compositor compositor)
107107
=> compositor.CreateHostBackdropBrush();
108108
}
109+
110+
private void OpenHoleWindow_Click(object sender, RoutedEventArgs e)
111+
{
112+
new HoleWindow().Activate();
113+
}
109114
}
110115
}

0 commit comments

Comments
 (0)