diff --git a/src/samples/Petzold/5th/Clock/Clock.csproj b/src/samples/Petzold/5th/Clock/Clock.csproj
new file mode 100644
index 0000000..6629cf4
--- /dev/null
+++ b/src/samples/Petzold/5th/Clock/Clock.csproj
@@ -0,0 +1,11 @@
+
+
+
+ WinExe
+ True
+ app1.manifest
+
+
+
+
+
\ No newline at end of file
diff --git a/src/samples/Petzold/5th/Clock/Program.cs b/src/samples/Petzold/5th/Clock/Program.cs
new file mode 100644
index 0000000..7f2b7fe
--- /dev/null
+++ b/src/samples/Petzold/5th/Clock/Program.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Jeremy W. Kuhne. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+// The GDI+ variant works, but is far from optimized. Creating and disposing a Graphics object every second
+// is clearly not optimal. Keeping things aligned with the sample to show the direct mapping.
+//
+#define GDIPLUS
+
+#if GDIPLUS
+using Windows.Win32.Graphics.GdiPlus;
+using GdiPlusPen = Windows.Win32.Graphics.GdiPlus.Pen;
+using GdiPlusBrush = Windows.Win32.Graphics.GdiPlus.Brush;
+#endif
+
+using Windows;
+using System.Drawing;
+using Windows.Win32.Foundation;
+using Windows.Win32;
+using Point = System.Drawing.Point;
+
+namespace Clock;
+
+///
+/// Sample from Programming Windows, 5th Edition.
+/// Original (c) Charles Petzold, 1998
+/// Figure 8-5, Pages 346-350.
+///
+internal static class Program
+{
+ [STAThread]
+ private static void Main() => Application.Run(new Clock("Analog Clock"));
+}
+
+internal class Clock : MainWindow
+{
+#if GDIPLUS
+ private readonly GdiPlusPen _blackPen = new(Color.Black);
+ private static readonly GdiPlusBrush s_blackBrush = new SolidBrush(Color.Black);
+ private readonly GdiPlusBrush _whiteBrush = new SolidBrush(Color.White);
+#endif
+
+ public Clock(string title) : base(title) { }
+
+ private void SetIsotropic(DeviceContext hdc)
+ {
+ hdc.SetMappingMode(MappingMode.Isotropic);
+ hdc.SetWindowExtents(new Size(1000, 1000));
+ hdc.SetViewportExtents(new Size(_clientSize.Width / 2, -_clientSize.Height / 2));
+ hdc.SetViewportOrigin(new Point(_clientSize.Width / 2, _clientSize.Height / 2));
+ }
+
+ private static void RotatePoint(Point[] pt, int iNum, int iAngle)
+ {
+ for (int i = 0; i < iNum; i++)
+ {
+ pt[i] = new Point
+ (
+ (int)(pt[i].X * Math.Cos(TWOPI * iAngle / 360) + pt[i].Y * Math.Sin(TWOPI * iAngle / 360)),
+ (int)(pt[i].Y * Math.Cos(TWOPI * iAngle / 360) - pt[i].X * Math.Sin(TWOPI * iAngle / 360))
+ );
+ }
+ }
+
+ private static void DrawClock(DeviceContext dc)
+ {
+ int iAngle;
+ Point[] pt = new Point[3];
+
+#if GDIPLUS
+ using var graphics = new Graphics(dc);
+ graphics.SetSmoothingMode(SmoothingMode.SmoothingModeHighQuality);
+#else
+ dc.SelectObject(StockBrush.Black);
+#endif
+ for (iAngle = 0; iAngle < 360; iAngle += 6)
+ {
+ pt[0].X = 0;
+ pt[0].Y = 900;
+ RotatePoint(pt, 1, iAngle);
+ pt[2].X = pt[2].Y = iAngle % 5 != 0 ? 33 : 100;
+ pt[0].X -= pt[2].X / 2;
+ pt[0].Y -= pt[2].Y / 2;
+ pt[1].X = pt[0].X + pt[2].X;
+ pt[1].Y = pt[0].Y + pt[2].Y;
+#if GDIPLUS
+ graphics.FillEllipse(s_blackBrush, pt[0].X, pt[0].Y, pt[1].X - pt[0].X, pt[1].Y - pt[0].Y);
+#else
+ dc.Ellipse(Rectangle.FromLTRB(pt[0].X, pt[0].Y, pt[1].X, pt[1].Y));
+#endif
+ }
+ }
+
+ private void DrawHands(DeviceContext dc, SYSTEMTIME time, bool erase = false, bool drawHourAndMinuteHands = true)
+ {
+ int[] handAngles =
+ [
+ (time.wHour * 30) % 360 + time.wMinute / 2,
+ time.wMinute * (360 / 60),
+ time.wSecond * (360 / 60)
+ ];
+
+ Point[][] handPoints =
+ [
+ [new(0, -150), new(100, 0), new(0, 600), new(-100, 0), new(0, -150)],
+ [new(0, -200), new(50, 0), new(0, 800), new(-50, 0), new(0, -200)],
+ [new(0, 0), new(0, 0), new(0, 0), new(0, 0), new(0, 800)]
+ ];
+
+#if GDIPLUS
+ using var graphics = new Graphics(dc);
+ if (erase)
+ {
+ graphics.FillEllipse(_whiteBrush, -830, -830, 1660, 1660);
+ return;
+ }
+
+ graphics.SetSmoothingMode(SmoothingMode.SmoothingModeHighQuality);
+#else
+ dc.SelectObject(erase ? StockPen.White : StockPen.Black);
+#endif
+
+ for (int i = drawHourAndMinuteHands ? 0 : 2; i < 3; i++)
+ {
+ RotatePoint(handPoints[i], 5, handAngles[i]);
+
+#if GDIPLUS
+ graphics.DrawLines(_blackPen, handPoints[i]);
+#else
+ dc.Polyline(handPoints[i]);
+#endif
+ }
+ }
+
+ private const int ID_TIMER = 1;
+ private const double TWOPI = Math.PI * 2;
+ private Size _clientSize;
+ private SYSTEMTIME _previousTime;
+
+ protected override LRESULT WindowProcedure(HWND window, MessageType message, WPARAM wParam, LPARAM lParam)
+ {
+ switch (message)
+ {
+ case MessageType.Create:
+ window.SetTimer(ID_TIMER, 1000);
+ Interop.GetLocalTime(out _previousTime);
+ return (LRESULT)0;
+ case MessageType.Size:
+ _clientSize = new Message.Size(wParam, lParam).NewSize;
+ return (LRESULT)0;
+ case MessageType.Timer:
+ Interop.GetLocalTime(out SYSTEMTIME time);
+ bool drawAllHands = time.wHour != _previousTime.wHour || time.wMinute != _previousTime.wMinute;
+ using (DeviceContext dc = window.GetDeviceContext())
+ {
+ SetIsotropic(dc);
+ DrawHands(dc, _previousTime, erase: true, drawAllHands);
+ DrawHands(dc, time);
+ }
+ _previousTime = time;
+ return (LRESULT)0;
+ case MessageType.Paint:
+ using (DeviceContext dc = window.BeginPaint())
+ {
+ SetIsotropic(dc);
+ DrawClock(dc);
+ DrawHands(dc, _previousTime);
+ }
+ return (LRESULT)0;
+ }
+
+ return base.WindowProcedure(window, message, wParam, lParam);
+ }
+}
diff --git a/src/samples/Petzold/5th/Clock/app1.manifest b/src/samples/Petzold/5th/Clock/app1.manifest
new file mode 100644
index 0000000..203d152
--- /dev/null
+++ b/src/samples/Petzold/5th/Clock/app1.manifest
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2
+ true
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/thirtytwo/DeviceContextExtensions.cs b/src/thirtytwo/DeviceContextExtensions.cs
index 2f74749..9e3a353 100644
--- a/src/thirtytwo/DeviceContextExtensions.cs
+++ b/src/thirtytwo/DeviceContextExtensions.cs
@@ -110,6 +110,20 @@ public static bool Polygon(this T context, ReadOnlySpan points)
}
}
+ public static bool Polyline(this T context, params Point[] points) where T : IHandle =>
+ Polyline(context, points.AsSpan());
+
+ public static bool Polyline(this T context, ReadOnlySpan points)
+ where T : IHandle
+ {
+ fixed (Point* p = points)
+ {
+ bool result = Interop.Polyline(context.Handle, p, points.Length);
+ GC.KeepAlive(context.Wrapper);
+ return result;
+ }
+ }
+
public static unsafe (int Height, uint LengthDrawn, Rectangle Bounds) DrawText(
this T context,
ReadOnlySpan text,
@@ -229,6 +243,71 @@ public static Bitmap CreateCompatibleBitmap(this T context, Size size) where
return Bitmap.Create(hbitmap, ownsHandle: true);
}
+ public static unsafe bool OffsetWindowOrigin(this T context, int x, int y) where T : IHandle
+ {
+ bool success = Interop.OffsetWindowOrgEx(context.Handle, x, y, null);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+
+ public static unsafe bool OffsetViewportOrigin(this T context, int x, int y) where T : IHandle
+ {
+ bool success = Interop.OffsetViewportOrgEx(context.Handle, x, y, null);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+
+ public static unsafe bool GetWindowExtents(this T context, out Size size) where T : IHandle
+ {
+ fixed (Size* s = &size)
+ {
+ bool success = Interop.GetWindowExtEx(context.Handle, (SIZE*)s);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+ }
+
+ ///
+ /// Sets the logical ("window") dimensions of the device context.
+ ///
+ public static unsafe bool SetWindowExtents(this T context, Size size) where T : IHandle
+ {
+ bool success = Interop.SetWindowExtEx(context.Handle, size.Width, size.Height, null);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+
+ public static unsafe bool GetViewportExtents(this T context, out Size size) where T : IHandle
+ {
+ fixed (Size* s = &size)
+ {
+ bool success = Interop.GetViewportExtEx(context.Handle, (SIZE*)s);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+ }
+
+ public static unsafe bool SetViewportExtents(this T context, Size size) where T : IHandle
+ {
+ bool success = Interop.SetViewportExtEx(context.Handle, size.Width, size.Height, null);
+ GC.KeepAlive(context.Wrapper);
+ return success;
+ }
+
+ public static MappingMode GetMappingMode(this T context) where T : IHandle
+ {
+ MappingMode result = (MappingMode)Interop.GetMapMode(context.Handle);
+ GC.KeepAlive(context.Wrapper);
+ return result;
+ }
+
+ public static MappingMode SetMappingMode(this T context, MappingMode mapMode) where T : IHandle
+ {
+ MappingMode result = (MappingMode)Interop.SetMapMode(context.Handle, (HDC_MAP_MODE)mapMode);
+ GC.KeepAlive(context.Wrapper);
+ return result;
+ }
+
public static unsafe Point GetViewportOrigin(this T context, out bool success)
where T : IHandle
{
diff --git a/src/thirtytwo/NativeMethods.txt b/src/thirtytwo/NativeMethods.txt
index 4db2435..48accd5 100644
--- a/src/thirtytwo/NativeMethods.txt
+++ b/src/thirtytwo/NativeMethods.txt
@@ -31,6 +31,7 @@ CreateEllipticRgn
CreateErrorInfo
CreateFont
CreateFontIndirect
+CreatePen
CreateRectRgn
CreateSolidBrush
CreateStdAccessibleObject
@@ -89,6 +90,18 @@ FONT_WEIGHT
FormatMessage
FrameRect
FVE_E_LOCKED_VOLUME
+GdipCreateFromHDC
+GdipCreatePen1
+GdipCreateSolidFill
+GdipDeleteBrush
+GdipDeleteGraphics
+GdipDeletePen
+GdipDrawLines
+GdipDrawLinesI
+GdipFillEllipse
+GdiplusShutdown
+GdiplusStartup
+GdipSetSmoothingMode
GET_MODULE_HANDLE_EX_FLAG*
GET_STOCK_OBJECT_FLAGS
GetActiveWindow
@@ -102,15 +115,19 @@ GetClipboardOwner
GetClipRgn
GetCurrentThread
GetCurrentThreadId
+GetCursorPos
GetDC
GetDeviceCaps
GetDialogBaseUnits
GetDialogBaseUnits
+GetDlgCtrlID
+GetDlgItem
GetDpiForWindow
GetFocus
GetGraphicsMode
GetIconInfo
GetIconInfoEx
+GetLocalTime
GetMapMode
GetMessage
GetModuleHandleEx
@@ -129,7 +146,11 @@ GetThreadLocale
GetUpdatedClipboardFormats
GetUpdateRgn
GetUserDefaultLCID
+GetViewportExtEx
GetViewportOrgEx
+GetWindowExtEx
+GetWindowLong
+GetWindowLongPtr
GetWindowRect
GetWindowRgn
GetWindowText
@@ -223,6 +244,8 @@ NtQuerySystemInformation
NTSTATUS
OBJ_TYPE
OBJECT_IDENTIFIER
+OffsetViewportOrgEx
+OffsetWindowOrgEx
OLECONTF
OLEIVERB*
OLEIVERB_*
@@ -233,6 +256,7 @@ PlaySound
POINTS
PolyBezier
Polygon
+Polyline
PostMessage
PostQuitMessage
PROPVARIANT
@@ -269,6 +293,7 @@ SetClassLongPtr
SetClipboardData
SetCoalescableTimer
SetCursor
+SetCursorPos
SetFocus
SetGraphicsMode
SetLayeredWindowAttributes
@@ -276,7 +301,9 @@ SetMapMode
SetPolyFillMode
SetROP2
SetThreadDpiAwarenessContext
+SetViewportExtEx
SetViewportOrgEx
+SetWindowExtEx
SetWindowLong
SetWindowRgn
SetWindowText
@@ -320,11 +347,4 @@ VIRTUAL_KEY
WIN32_ERROR
WINDOWPOS
WM_*
-XFORMCOORDS
-GetCursorPos
-SetCursorPos
-GetWindowLong
-GetWindowLongPtr
-GetDlgItem
-GetDlgCtrlID
-CreatePen
\ No newline at end of file
+XFORMCOORDS
\ No newline at end of file
diff --git a/src/thirtytwo/Support/IPointer.cs b/src/thirtytwo/Support/IPointer.cs
new file mode 100644
index 0000000..5a0f312
--- /dev/null
+++ b/src/thirtytwo/Support/IPointer.cs
@@ -0,0 +1,17 @@
+// 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.Support;
+
+///
+/// Used to indicate ownership of a native resource pointer.
+///
+///
+///
+/// This should never be put on a struct.
+///
+///
+public unsafe interface IPointer where TPointer : unmanaged
+{
+ TPointer* Pointer { get; }
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/ARGB.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/ARGB.cs
new file mode 100644
index 0000000..cb0a3ea
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/ARGB.cs
@@ -0,0 +1,51 @@
+// 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.Drawing;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+[StructLayout(LayoutKind.Explicit)]
+public readonly struct ARGB
+{
+ [FieldOffset(0)]
+ public readonly byte B;
+ [FieldOffset(1)]
+ public readonly byte G;
+ [FieldOffset(2)]
+ public readonly byte R;
+ [FieldOffset(3)]
+ public readonly byte A;
+
+ [FieldOffset(0)]
+ public readonly uint Value;
+
+ public ARGB(uint value)
+ {
+ Unsafe.SkipInit(out this);
+ Value = value;
+ }
+
+ public ARGB(byte red, byte green, byte blue)
+ : this(255, red, green, blue)
+ {
+ }
+
+ [SkipLocalsInit]
+ public ARGB(byte alpha, byte red, byte green, byte blue)
+ {
+ Unsafe.SkipInit(out this);
+ A = alpha;
+ R = red;
+ G = green;
+ B = blue;
+ }
+
+ public static implicit operator ARGB(COLORREF color) => new(color.R, color.G, color.B);
+ public static explicit operator COLORREF(ARGB color) => (COLORREF)((Color)color);
+ public static implicit operator ARGB(Color color) => new(color.A, color.R, color.G, color.B);
+ public static implicit operator Color(ARGB color) => Color.FromArgb((int)color.Value);
+ public static implicit operator uint(ARGB color) => color.Value;
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/Brush.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/Brush.cs
new file mode 100644
index 0000000..6fffa8c
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/Brush.cs
@@ -0,0 +1,26 @@
+// 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.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public unsafe class Brush : DisposableBase.Finalizable, IPointer
+{
+ private GpBrush* _pointer;
+
+ public GpBrush* Pointer => _pointer;
+
+ public Brush(GpBrush* pointer) => _pointer = pointer;
+
+ protected override void Dispose(bool disposing)
+ {
+ Status status = Interop.GdipDeleteBrush(_pointer);
+ if (disposing)
+ {
+ status.ThrowIfFailed();
+ }
+
+ _pointer = null;
+ }
+}
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlus.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlus.cs
new file mode 100644
index 0000000..5b9af5a
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlus.cs
@@ -0,0 +1,51 @@
+// 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;
+using Windows.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public static unsafe class GdiPlus
+{
+ internal static Session Init() => s_session;
+ private static readonly Session s_session = new();
+
+ [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
+ public static void Initialize() => Init();
+
+ public static nuint Startup(uint version = 2)
+ {
+ GdiplusStartupInput input = new() { GdiplusVersion = version };
+ GdiplusStartupOutput output;
+ nuint token;
+ ThrowIfFailed(Interop.GdiplusStartup(
+ &token,
+ &input,
+ &output));
+
+ return token;
+ }
+
+ public static void Shutdown(nuint token) => Interop.GdiplusShutdown(token);
+
+ public static void ThrowIfFailed(this Status status)
+ {
+ if (status != Status.Ok)
+ throw GetExceptionForStatus(status);
+ }
+
+ public static Exception GetExceptionForStatus(Status status)
+ {
+ switch (status)
+ {
+ case Status.Win32Error:
+ WIN32_ERROR error = Error.GetLastError();
+ if (error != WIN32_ERROR.ERROR_SUCCESS)
+ return error.GetException();
+ goto default;
+ default:
+ return new GdiPlusException(status);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlusException.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlusException.cs
new file mode 100644
index 0000000..53108b6
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/GdiPlusException.cs
@@ -0,0 +1,12 @@
+// 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.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public class GdiPlusException : ThirtyTwoException
+{
+ public GdiPlusException(Status status) : base(status.ToString()) => Status = status;
+ public Status Status { get; private set; }
+}
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/Graphics.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/Graphics.cs
new file mode 100644
index 0000000..35e511f
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/Graphics.cs
@@ -0,0 +1,34 @@
+// 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.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public unsafe class Graphics : DisposableBase.Finalizable, IPointer
+{
+ private GpGraphics* _pointer;
+
+ public GpGraphics* Pointer => _pointer;
+
+ public Graphics(GpGraphics* pointer) => _pointer = pointer;
+
+ public Graphics(HDC hdc)
+ {
+ GdiPlus.Init();
+ GpGraphics* pointer;
+ Interop.GdipCreateFromHDC(hdc, &pointer).ThrowIfFailed();
+ _pointer = pointer;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ Status status = Interop.GdipDeleteGraphics(_pointer);
+ if (disposing)
+ {
+ status.ThrowIfFailed();
+ }
+
+ _pointer = null;
+ }
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/GraphicsExtensions.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/GraphicsExtensions.cs
new file mode 100644
index 0000000..adef471
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/GraphicsExtensions.cs
@@ -0,0 +1,77 @@
+// 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.InteropServices;
+using Windows.Support;
+using Drawing = System.Drawing;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public static unsafe class GraphicsExtensions
+{
+ public static void SetSmoothingMode(this T graphics, SmoothingMode smoothingMode) where T : IPointer
+ {
+ Interop.GdipSetSmoothingMode(graphics.Pointer, smoothingMode).ThrowIfFailed();
+ GC.KeepAlive(graphics);
+ }
+
+ public static unsafe void DrawLines(this TGraphics graphics, TPen pen, ReadOnlySpan points)
+ where TGraphics : IPointer
+ where TPen : IPointer =>
+ DrawLines(graphics, pen, MemoryMarshal.Cast(points));
+
+ public static unsafe void DrawLines(this TGraphics graphics, TPen pen, ReadOnlySpan points)
+ where TGraphics : IPointer
+ where TPen : IPointer
+ {
+ fixed (Point* p = points)
+ {
+ Interop.GdipDrawLinesI(graphics.Pointer, pen.Pointer, p, points.Length).ThrowIfFailed();
+ }
+
+ GC.KeepAlive(graphics);
+ GC.KeepAlive(pen);
+ }
+
+ public static unsafe void DrawLines(this TGraphics graphics, TPen pen, ReadOnlySpan points)
+ where TGraphics : IPointer
+ where TPen : IPointer =>
+ DrawLines(graphics, pen, MemoryMarshal.Cast(points));
+
+ public static unsafe void DrawLines(this TGraphics graphics, TPen pen, ReadOnlySpan points)
+ where TGraphics : IPointer
+ where TPen : IPointer
+ {
+ fixed (PointF* p = points)
+ {
+ Interop.GdipDrawLines(graphics.Pointer, pen.Pointer, p, points.Length).ThrowIfFailed();
+ }
+
+ GC.KeepAlive(graphics);
+ GC.KeepAlive(pen);
+ }
+
+ public static void FillEllipse(this TGraphics graphics, TBrush brush, Drawing.Rectangle bounds)
+ where TGraphics : IPointer
+ where TBrush : IPointer
+ => FillEllipse(graphics, brush, (float)bounds.X, bounds.Y, bounds.Width, bounds.Height);
+
+ public static void FillEllipse(this TGraphics graphics, TBrush brush, Drawing.RectangleF bounds)
+ where TGraphics : IPointer
+ where TBrush : IPointer
+ => FillEllipse(graphics, brush, bounds.X, bounds.Y, bounds.Width, bounds.Height);
+
+ public static void FillEllipse(this TGraphics graphics, TBrush brush, int x, int y, int width, int height)
+ where TGraphics : IPointer
+ where TBrush : IPointer
+ => FillEllipse(graphics, brush, (float)x, y, width, height);
+
+ public static void FillEllipse(this TGraphics graphics, TBrush brush, float x, float y, float width, float height)
+ where TGraphics : IPointer
+ where TBrush : IPointer
+ {
+ Interop.GdipFillEllipse(graphics.Pointer, brush.Pointer, x, y, width, height).ThrowIfFailed();
+ GC.KeepAlive(graphics);
+ GC.KeepAlive(brush);
+ }
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/Pen.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/Pen.cs
new file mode 100644
index 0000000..772133c
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/Pen.cs
@@ -0,0 +1,32 @@
+// 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.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public unsafe class Pen : DisposableBase.Finalizable, IPointer
+{
+ private GpPen* _pointer;
+
+ public GpPen* Pointer => _pointer;
+
+ public Pen(ARGB color, float width = 1.0f)
+ {
+ GdiPlus.Init();
+ GpPen* pointer;
+ Interop.GdipCreatePen1(color, width, Unit.UnitPixel, &pointer).ThrowIfFailed();
+ _pointer = pointer;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ Status status = Interop.GdipDeletePen(_pointer);
+ if (disposing)
+ {
+ status.ThrowIfFailed();
+ }
+
+ _pointer = null;
+ }
+}
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs
new file mode 100644
index 0000000..4d3bd23
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs
@@ -0,0 +1,26 @@
+// 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;
+using Windows.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public class Session : IDisposable
+{
+ private UIntPtr _token;
+
+ public Session(uint version = 2)
+ {
+ _token = GdiPlus.Startup(version);
+ }
+
+ ~Session() => Dispose();
+
+ public void Dispose()
+ {
+ GC.SuppressFinalize(this);
+ GdiPlus.Shutdown(_token);
+ _token = UIntPtr.Zero;
+ }
+}
\ No newline at end of file
diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/SolidBrush.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/SolidBrush.cs
new file mode 100644
index 0000000..1597912
--- /dev/null
+++ b/src/thirtytwo/Win32/Graphics/GdiPlus/SolidBrush.cs
@@ -0,0 +1,22 @@
+// 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.Support;
+
+namespace Windows.Win32.Graphics.GdiPlus;
+
+public unsafe class SolidBrush : Brush, IPointer
+{
+ public new GpSolidFill* Pointer => (GpSolidFill*)base.Pointer;
+
+ public SolidBrush(ARGB color) : base(CreateBrush(color))
+ {
+ }
+
+ private static GpBrush* CreateBrush(ARGB color)
+ {
+ GpBrush* brush;
+ Interop.GdipCreateSolidFill(color, (GpSolidFill**)&brush).ThrowIfFailed();
+ return brush;
+ }
+}
diff --git a/src/thirtytwo/WrapperEnums/MappingMode.cs b/src/thirtytwo/WrapperEnums/MappingMode.cs
new file mode 100644
index 0000000..717c335
--- /dev/null
+++ b/src/thirtytwo/WrapperEnums/MappingMode.cs
@@ -0,0 +1,65 @@
+// 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;
+
+///
+/// Page-space to device-space transformation.
+///
+///
+///
+///
+/// Mapping Modes and Translations
+///
+///
+///
+public enum MappingMode
+{
+ ///
+ /// Pixel mapping. Each unit in page space is one pixel in device space.
+ /// X increases left to right. Y increases from top to bottom. (MM_TEXT)
+ ///
+ Text = HDC_MAP_MODE.MM_TEXT,
+
+ ///
+ /// Each unit in page space is 0.1 mm in device space.
+ /// X increases left to right. Y increases from bottom to top. (MM_LOMETRIC)
+ ///
+ LowMetric = HDC_MAP_MODE.MM_LOMETRIC,
+
+ ///
+ /// Each unit in page space is 0.01 mm in device space.
+ /// X increases left to right. Y increases from bottom to top. (MM_HIMETRIC)
+ ///
+ HighMetric = HDC_MAP_MODE.MM_HIMETRIC,
+
+ ///
+ /// Each unit in page space is 0.01 inch in device space.
+ /// X increases left to right. Y increases from bottom to top. (MM_LOENGLISH)
+ ///
+ LowEnglish = HDC_MAP_MODE.MM_LOENGLISH,
+
+ ///
+ /// Each unit in page space is 0.001 inch in device space.
+ /// X increases left to right. Y increases from bottom to top. (MM_HIENGLISH)
+ ///
+ HighEnglish = HDC_MAP_MODE.MM_HIENGLISH,
+
+ ///
+ /// Each unit in page space is a twip in device space (1/1440 inch).
+ /// X increases left to right. Y increases from bottom to top. (MM_TWIPS)
+ ///
+ Twips = HDC_MAP_MODE.MM_TWIPS,
+
+ ///
+ /// Application defined via SetWindow/ViewPortExtents.
+ /// Both axis are equally scaled. (MM_ISOTROPIC)
+ ///
+ Isotropic = HDC_MAP_MODE.MM_ISOTROPIC,
+
+ ///
+ /// Application defined via SetWindow/ViewPortExtents.
+ /// Axis may not be equally scaled. (MM_ANISOTROPIC)
+ ///
+ Anisotropic = HDC_MAP_MODE.MM_ANISOTROPIC
+}
\ No newline at end of file
diff --git a/src/thirtytwo/thirtytwo.csproj b/src/thirtytwo/thirtytwo.csproj
index d471a4d..9fd524d 100644
--- a/src/thirtytwo/thirtytwo.csproj
+++ b/src/thirtytwo/thirtytwo.csproj
@@ -18,4 +18,8 @@
+
+
+
+
diff --git a/thirtytwo.sln b/thirtytwo.sln
index 0d3e66e..d11c483 100644
--- a/thirtytwo.sln
+++ b/thirtytwo.sln
@@ -51,6 +51,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blokout2", "src\samples\Pet
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BtnLook", "src\samples\Petzold\5th\BtnLook\BtnLook.csproj", "{22E8534C-628A-4A9C-9EDB-7BE4DAC12544}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Checker", "src\samples\Petzold\5th\Checker1\Checker.csproj", "{7DB41BCA-E497-4EE4-9A2F-A529E5306ABC}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clock", "src\samples\Petzold\5th\Clock\Clock.csproj", "{D4EA97FD-45D3-4A26-843F-63427F0F1BFE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@@ -125,6 +129,14 @@ Global
{22E8534C-628A-4A9C-9EDB-7BE4DAC12544}.Debug|x64.Build.0 = Debug|x64
{22E8534C-628A-4A9C-9EDB-7BE4DAC12544}.Release|x64.ActiveCfg = Release|x64
{22E8534C-628A-4A9C-9EDB-7BE4DAC12544}.Release|x64.Build.0 = Release|x64
+ {7DB41BCA-E497-4EE4-9A2F-A529E5306ABC}.Debug|x64.ActiveCfg = Debug|x64
+ {7DB41BCA-E497-4EE4-9A2F-A529E5306ABC}.Debug|x64.Build.0 = Debug|x64
+ {7DB41BCA-E497-4EE4-9A2F-A529E5306ABC}.Release|x64.ActiveCfg = Release|x64
+ {7DB41BCA-E497-4EE4-9A2F-A529E5306ABC}.Release|x64.Build.0 = Release|x64
+ {D4EA97FD-45D3-4A26-843F-63427F0F1BFE}.Debug|x64.ActiveCfg = Debug|x64
+ {D4EA97FD-45D3-4A26-843F-63427F0F1BFE}.Debug|x64.Build.0 = Debug|x64
+ {D4EA97FD-45D3-4A26-843F-63427F0F1BFE}.Release|x64.ActiveCfg = Release|x64
+ {D4EA97FD-45D3-4A26-843F-63427F0F1BFE}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -146,6 +158,8 @@ Global
{751D8704-4AF9-46E4-AD24-B43DD011ED11} = {13110246-EBE1-441B-B721-B0614D62B13B}
{F52C0159-AFFE-431D-BC46-1AA6C8381400} = {13110246-EBE1-441B-B721-B0614D62B13B}
{22E8534C-628A-4A9C-9EDB-7BE4DAC12544} = {13110246-EBE1-441B-B721-B0614D62B13B}
+ {7DB41BCA-E497-4EE4-9A2F-A529E5306ABC} = {13110246-EBE1-441B-B721-B0614D62B13B}
+ {D4EA97FD-45D3-4A26-843F-63427F0F1BFE} = {13110246-EBE1-441B-B721-B0614D62B13B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3761BFC9-DBEF-4186-BB8B-BC0D84ED9AE5}