Skip to content

Commit

Permalink
Port the Clover sample from WInterop
Browse files Browse the repository at this point in the history
  • Loading branch information
JeremyKuhne committed Feb 3, 2024
1 parent bd5eee7 commit f01809a
Show file tree
Hide file tree
Showing 13 changed files with 327 additions and 10 deletions.
10 changes: 10 additions & 0 deletions src/samples/Petzold/5th/Clover/Clover.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\thirtytwo\thirtytwo.csproj" />
</ItemGroup>
</Project>
79 changes: 79 additions & 0 deletions src/samples/Petzold/5th/Clover/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Windows;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Gdi;

namespace Clover;

/// <summary>
/// Sample from Programming Windows, 5th Edition.
/// Original (c) Charles Petzold, 1998
/// Figure 5-27, Pages 205-208.
/// </summary>
internal static class Program
{
[STAThread]
private static void Main() => Application.Run(new Clover(), "Draw a Clover");
}

internal class Clover : WindowClass
{
private int _cxClient, _cyClient;
private HRGN _hrgnClip;
private const double TWO_PI = Math.PI * 2;

public Clover() : base(backgroundBrush: StockBrush.White) { }

protected override LRESULT WindowProcedure(HWND window, MessageType message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case MessageType.Size:
{
_cxClient = lParam.LOWORD;
_cyClient = lParam.HIWORD;

using WaitCursorScope cursorScope = new();

_hrgnClip.Dispose();

using HRGN region0 = HRGN.FromEllipse(0, _cyClient / 3, _cxClient / 2, 2 * _cyClient / 3);
using HRGN region1 = HRGN.FromEllipse(_cxClient / 2, _cyClient / 3, _cxClient, 2 * _cyClient / 3);
using HRGN region2 = HRGN.FromEllipse(_cxClient / 3, 0, 2 * _cxClient / 3, _cyClient / 2);
using HRGN region3 = HRGN.FromEllipse(_cxClient / 3, _cyClient / 2, 2 * _cxClient / 3, _cyClient);
using HRGN region4 = HRGN.CombineRegion(region0, region1, RegionCombineMode.Or);
using HRGN region5 = HRGN.CombineRegion(region2, region3, RegionCombineMode.Or);
_hrgnClip = HRGN.CombineRegion(region4, region5, RegionCombineMode.Xor);

return (LRESULT)0;
}
case MessageType.Paint:
using (DeviceContext dc = window.BeginPaint())
{
dc.SetViewportOrigin(new(_cxClient / 2, _cyClient / 2));
dc.SelectClippingRegion(_hrgnClip);

double fRadius = Hypotenuse(_cxClient / 2.0, _cyClient / 2.0);

for (double fAngle = 0.0; fAngle < TWO_PI; fAngle += TWO_PI / 360)
{
dc.MoveTo(default);
dc.LineTo(
(int)(fRadius * Math.Cos(fAngle) + 0.5),
(int)(-fRadius * Math.Sin(fAngle) + 0.5));
}
}

return (LRESULT)0;
case MessageType.Destroy:
_hrgnClip.Dispose();
break;
}

return base.WindowProcedure(window, message, wParam, lParam);
}

private static double Hypotenuse(double x, double y) => Math.Sqrt(x * x + y * y);
}
53 changes: 51 additions & 2 deletions src/thirtytwo/DeviceContextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,8 +204,8 @@ public static DeviceContext CreateCompatibleDeviceContext<TDeviceContext>(this T
return DeviceContext.Create(hdc, ownsHandle: true);
}

public static Bitmap CreateCompatibleBitmap<TDeviceContext>(this TDeviceContext context, Size size)
where TDeviceContext : IHandle<HDC>
public static Bitmap CreateCompatibleBitmap<T>(this T context, Size size)
where T : IHandle<HDC>
{
HBITMAP hbitmap = Interop.CreateCompatibleBitmap(context.Handle, size.Width, size.Height);
if (hbitmap.IsNull)
Expand All @@ -216,4 +216,53 @@ public static Bitmap CreateCompatibleBitmap<TDeviceContext>(this TDeviceContext
GC.KeepAlive(context.Wrapper);
return Bitmap.Create(hbitmap, ownsHandle: true);
}

public static unsafe Point GetViewportOrigin<T>(this T context, out bool success)
where T : IHandle<HDC>
{
Point point;
success = Interop.GetViewportOrgEx(context.Handle, &point);
GC.KeepAlive(context.Wrapper);
return point;
}

public static unsafe bool SetViewportOrigin<T>(this T context, Point point)
where T : IHandle<HDC>
{
bool result = Interop.SetViewportOrgEx(context.Handle, point.X, point.Y, null);
GC.KeepAlive(context.Wrapper);
return result;
}

public static RegionType SelectClippingRegion<T>(this T context, HRGN region)
where T : IHandle<HDC>
{
RegionType type = (RegionType)Interop.SelectClipRgn(context.Handle, region);
GC.KeepAlive(context.Wrapper);
return type;
}

public static bool MoveTo<T>(this T context, Point point)
where T : IHandle<HDC>
=> context.MoveTo(point.X, point.Y);

public static bool MoveTo<T>(this T context, int x, int y)
where T : IHandle<HDC>
{
bool result = Interop.MoveToEx(context.Handle, x, y, null);
GC.KeepAlive(context.Wrapper);
return result;
}

public static bool LineTo<T>(this T context, Point point)
where T : IHandle<HDC>
=> context.LineTo(point.X, point.Y);

public static bool LineTo<T>(this T context, int x, int y)
where T : IHandle<HDC>
{
bool success = Interop.LineTo(context.Handle, x, y);
GC.KeepAlive(context.Wrapper);
return success;
}
}
17 changes: 14 additions & 3 deletions src/thirtytwo/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ CLIPBOARD_FORMAT
CloseClipboard
CoCreateInstance
CoGetClassObject
CombineRegion

Check warning on line 21 in src/thirtytwo/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build

Method, type or constant "CombineRegion" not found. Did you mean , "GdipCombineRegionRect", "GdipCombineRegionRectI" or "GdipCombineRegionPath" or "GdipCombineRegionRegion"?

Check warning on line 21 in src/thirtytwo/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build

Method, type or constant "CombineRegion" not found. Did you mean , "GdipCombineRegionRect", "GdipCombineRegionRectI" or "GdipCombineRegionPath" or "GdipCombineRegionRegion"?
CombineRgn
COMBOBOXINFO_BUTTON_STATE
CopyImage
Expand All @@ -27,6 +28,7 @@ CreateActCtx
CreateCompatibleBitmap
CreateCompatibleDC
CreateDispTypeInfo
CreateEllipticRgn
CreateErrorInfo
CreateFont
CreateFontIndirect
Expand Down Expand Up @@ -93,6 +95,7 @@ GetClientRect
GetClipboardData
GetClipboardFormatName
GetClipboardOwner
GetClipRgn
GetCurrentThread
GetCurrentThreadId
GetDC
Expand All @@ -119,6 +122,7 @@ GetThreadLocale
GetUpdatedClipboardFormats
GetUpdateRgn
GetUserDefaultLCID
GetViewportOrgEx
GetWindowRect
GetWindowRgn
GetWindowText
Expand All @@ -141,6 +145,7 @@ IClassFactory2
IConnectionPoint
IConnectionPointContainer
ICreateErrorInfo
IDC_*
IDI_*
IDispatch
IDispatchEx
Expand Down Expand Up @@ -184,6 +189,7 @@ KEY_INFORMATION_CLASS
KEY_NAME_INFORMATION
KF_*
LIBFLAGS
LineTo
LoadCursor
LoadIcon
LoadLibrary
Expand All @@ -197,6 +203,7 @@ MEMBERID_NIL
MessageBoxEx
METHODDATA
MODIFIERKEYS_FLAGS
MoveToEx
MoveWindow
MSGF_*
MulDiv
Expand All @@ -212,6 +219,7 @@ OpenClipboard
PARAMDATA
PeekMessage
POINTS
Polygon
PostMessage
PostQuitMessage
PROPVARIANT
Expand All @@ -233,23 +241,28 @@ SafeArrayGetVartype
SafeArrayLock
SafeArrayUnlock
ScreenToClient
SelectClipRgn
SelectObject
SELFLAG_*
SendMessage
SetActiveWindow
SetClipboardData
SetCursor
SetFocus
SetGraphicsMode
SetLayeredWindowAttributes
SetMapMode
SetPolyFillMode
SetThreadDpiAwarenessContext
SetViewportOrgEx
SetWindowLong
SetWindowRgn
SetWindowText
SetWorldTransform
SHCreateShellItem
SHDefExtractIcon
SHGetStockIconInfo
ShowCursor
ShowWindow
SHParseDisplayName
STATE_SYSTEM_*
Expand Down Expand Up @@ -285,6 +298,4 @@ VIRTUAL_KEY
WIN32_ERROR
WINDOWPOS
WM_*
XFORMCOORDS
SetPolyFillMode
Polygon
XFORMCOORDS
24 changes: 24 additions & 0 deletions src/thirtytwo/WaitCursorScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Windows;

/// <summary>
/// Temporarily sets the cursor to the wait cursor.
/// </summary>
public readonly ref struct WaitCursorScope
{
private readonly HCURSOR _cursor;

public WaitCursorScope()
{
_cursor = Interop.SetCursor(Interop.LoadCursor(default, Interop.IDC_WAIT));
_ = Interop.ShowCursor(true);
}

public void Dispose()
{
Interop.SetCursor(_cursor);
_ = Interop.ShowCursor(false);
}
}
47 changes: 43 additions & 4 deletions src/thirtytwo/Win32/Graphics/Gdi/HRGN.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,48 @@ public void Dispose()
Unsafe.AsRef(in this) = default;
}

public static HRGN FromRectangle(Rectangle rectangle)
=> Interop.CreateRectRgn(rectangle.X, rectangle.Y, rectangle.Right, rectangle.Bottom);
public static HRGN FromRectangle(Rectangle rectangle) =>
Interop.CreateRectRgn(rectangle.X, rectangle.Y, rectangle.Right, rectangle.Bottom);

public static HRGN CreateEmpty()
=> Interop.CreateRectRgn(0, 0, 0, 0);
public static HRGN FromRectangle(int x1, int y1, int x2, int y2) => Interop.CreateRectRgn(x1, y1, x2, y2);

public static HRGN FromEllipse(Rectangle bounds) =>
Interop.CreateEllipticRgn(bounds.X, bounds.Y, bounds.Right, bounds.Bottom);

public static HRGN FromEllipse(int x1, int y1, int x2, int y2) => Interop.CreateEllipticRgn(x1, y1, x2, y2);

public static HRGN CreateEmpty() => Interop.CreateRectRgn(0, 0, 0, 0);

public static HRGN CombineRegion(HRGN first, HRGN second, RegionCombineMode combineMode) =>
CombineRegion(first, second, combineMode, out _);

public static HRGN CombineRegion(HRGN first, HRGN second, RegionCombineMode combineMode, out RegionType type)
{
HRGN hrgn = CreateEmpty();
type = (RegionType)Interop.CombineRgn(hrgn, first, second, (RGN_COMBINE_MODE)combineMode);
if (type == RegionType.Error)
{
hrgn.Dispose();
}

return hrgn;
}

public static HRGN FromHdc(HDC hdc)
{
HRGN region = Interop.CreateRectRgn(0, 0, 0, 0);
int result = Interop.GetClipRgn(hdc, region);
Debug.Assert(result != -1, "GetClipRgn failed");

if (result == 1)
{
return region;
}
else
{
// No region, delete our temporary region
Interop.DeleteObject(region);
return default;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Windows.Win32.UI.WindowsAndMessaging;

public unsafe partial struct HCURSOR
{
public readonly struct SetScope : IDisposable
{
private readonly HCURSOR _previousCursor;
public SetScope(HCURSOR cursor) => _previousCursor = Interop.SetCursor(cursor);
public readonly void Dispose() => Interop.SetCursor(_previousCursor);
}
}
16 changes: 15 additions & 1 deletion src/thirtytwo/Win32/UI/WindowsAndMessaging/HCURSOR.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
// Copyright (c) Jeremy W. Kuhne. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Runtime.CompilerServices;

namespace Windows.Win32.UI.WindowsAndMessaging;

public unsafe partial struct HCURSOR
public unsafe partial struct HCURSOR : IDisposable
{
public static HCURSOR Invalid => new(-1);

public static implicit operator HCURSOR(CursorId id) => Interop.LoadCursor(default, (PCWSTR)(char*)(uint)id);

public SetScope SetCursorScope() => new(this);

public void Dispose()
{
if (!IsNull)
{
Interop.DestroyCursor(this);
}

Unsafe.AsRef(in this) = default;
}
}
2 changes: 2 additions & 0 deletions src/thirtytwo/WrapperEnums/CursorId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace Windows;

public enum CursorId : uint
{
// Can't use CsWin32 values here as they aren't constant

/// <summary>
/// Arrow cursor. (IDC_ARROW)
/// </summary>
Expand Down
Loading

0 comments on commit f01809a

Please sign in to comment.