diff --git a/src/samples/DirectDraw/Direct2dDemo/Direct2dDemo.csproj b/src/samples/DirectDraw/Direct2dDemo/Direct2dDemo.csproj new file mode 100644 index 0000000..6888128 --- /dev/null +++ b/src/samples/DirectDraw/Direct2dDemo/Direct2dDemo.csproj @@ -0,0 +1,11 @@ + + + + WinExe + + + + + + + diff --git a/src/samples/DirectDraw/Direct2dDemo/Program.cs b/src/samples/DirectDraw/Direct2dDemo/Program.cs new file mode 100644 index 0000000..c4808c7 --- /dev/null +++ b/src/samples/DirectDraw/Direct2dDemo/Program.cs @@ -0,0 +1,84 @@ +// 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.Numerics; +using Windows; +using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Direct2D; + +namespace Direct2dDemo; + +internal class Program +{ + [STAThread] + private static void Main() => Application.Run(new Direct2dDemo()); + + private class Direct2dDemo : MainWindow + { + private SolidColorBrush? _lightSlateGrayBrush; + private SolidColorBrush? _cornflowerBlueBrush; + + public Direct2dDemo() : base(title: "Simple Direct2D Application", features: Features.EnableDirect2d) + { + } + + protected override void RenderTargetCreated(HwndRenderTarget renderTarget) + { + _lightSlateGrayBrush?.Dispose(); + _cornflowerBlueBrush?.Dispose(); + _lightSlateGrayBrush = renderTarget.CreateSolidColorBrush(Color.LightSlateGray); + _cornflowerBlueBrush = renderTarget.CreateSolidColorBrush(Color.CornflowerBlue); + base.RenderTargetCreated(renderTarget); + } + + protected override LRESULT WindowProcedure(HWND window, MessageType message, WPARAM wParam, LPARAM lParam) + { + switch (message) + { + case MessageType.Paint: + if (IsDirect2dEnabled(out var renderTarget)) + { + renderTarget.SetTransform(Matrix3x2.Identity); + renderTarget.Clear(Color.White); + + SizeF size = renderTarget.Size(); + + for (int x = 0; x < size.Width; x += 10) + { + renderTarget.DrawLine( + new(x, 0), new(x, size.Height), + _lightSlateGrayBrush!, + 0.5f); + } + + for (int y = 0; y < size.Height; y += 10) + { + renderTarget.DrawLine( + new(0, y), new(size.Width, y), + _lightSlateGrayBrush!, + 0.5f); + } + + RectangleF rectangle1 = RectangleF.FromLTRB( + size.Width / 2 - 50, + size.Height / 2 - 50, + size.Width / 2 + 50, + size.Height / 2 + 50); + + RectangleF rectangle2 = RectangleF.FromLTRB( + size.Width / 2 - 100, + size.Height / 2 - 100, + size.Width / 2 + 100, + size.Height / 2 + 100); + + renderTarget.FillRectangle(rectangle1, _lightSlateGrayBrush!); + renderTarget.DrawRectangle(rectangle2, _cornflowerBlueBrush!); + } + return (LRESULT)0; + } + + return base.WindowProcedure(window, message, wParam, lParam); + } + } +} diff --git a/src/thirtytwo/MainWindow.cs b/src/thirtytwo/MainWindow.cs index e6d43ed..d478ba9 100644 --- a/src/thirtytwo/MainWindow.cs +++ b/src/thirtytwo/MainWindow.cs @@ -18,7 +18,8 @@ public MainWindow( WindowClass? windowClass = default, nint parameters = default, HMENU menuHandle = default, - HBRUSH backgroundBrush = default) : base( + HBRUSH backgroundBrush = default, + Features features = default) : base( bounds, title, style, @@ -27,6 +28,7 @@ public MainWindow( windowClass, parameters, menuHandle, - backgroundBrush) + backgroundBrush, + features) { } } \ No newline at end of file diff --git a/src/thirtytwo/NativeMethods.txt b/src/thirtytwo/NativeMethods.txt index 48accd5..f7522bf 100644 --- a/src/thirtytwo/NativeMethods.txt +++ b/src/thirtytwo/NativeMethods.txt @@ -347,4 +347,7 @@ VIRTUAL_KEY WIN32_ERROR WINDOWPOS WM_* -XFORMCOORDS \ No newline at end of file +XFORMCOORDS +ID2D1Factory +D2D1CreateFactory +D2DERR_RECREATE_TARGET \ No newline at end of file diff --git a/src/thirtytwo/Support/EnumExtensions.cs b/src/thirtytwo/Support/EnumExtensions.cs new file mode 100644 index 0000000..d27b157 --- /dev/null +++ b/src/thirtytwo/Support/EnumExtensions.cs @@ -0,0 +1,82 @@ +// 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 System; + +internal static class EnumExtensions +{ + /// + /// Returns true if the given flag or flags are set. + /// + /// + /// Simple wrapper for that gives you better intellisense. + /// + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static unsafe bool AreFlagsSet(this T value, T flags) where T : unmanaged, Enum => value.HasFlag(flags); + + /// + /// Sets the given flag or flags. + /// + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static unsafe void SetFlags(this ref T value, T flags) where T : unmanaged, Enum + { + fixed (T* v = &value) + { + // Note that the non-relevant if clauses will be omitted by the JIT so these become one statement. + if (sizeof(T) == sizeof(byte)) + { + *(byte*)v |= *(byte*)&flags; + } + else if (sizeof(T) == sizeof(short)) + { + *(short*)v |= *(short*)&flags; + } + else if (sizeof(T) == sizeof(int)) + { + *(int*)v |= *(int*)&flags; + } + else if (sizeof(T) == sizeof(long)) + { + *(long*)v |= *(long*)&flags; + } + else + { + throw new InvalidOperationException(); + } + } + } + + [SkipLocalsInit] + [MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)] + public static unsafe void ClearFlags(this ref T value, T flags) where T : unmanaged, Enum + { + fixed (T* v = &value) + { + // Note that the non-relevant if clauses will be omitted by the JIT so these become one statement. + if (sizeof(T) == sizeof(byte)) + { + *(byte*)v &= (byte)~*(byte*)&flags; + } + else if (sizeof(T) == sizeof(short)) + { + *(short*)v &= (short)~*(short*)&flags; + } + else if (sizeof(T) == sizeof(int)) + { + *(int*)v &= ~*(int*)&flags; + } + else if (sizeof(T) == sizeof(long)) + { + *(long*)v &= ~*(long*)&flags; + } + else + { + throw new InvalidOperationException(); + } + } + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Brush.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Brush.cs new file mode 100644 index 0000000..38368e6 --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Brush.cs @@ -0,0 +1,19 @@ +// 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.Direct2D; + +public unsafe class Brush : Resource, IPointer +{ + public unsafe new ID2D1Brush* Pointer { get; private set; } + + public Brush(ID2D1Brush* brush) : base((ID2D1Resource*)brush) => Pointer = brush; + + protected override void Dispose(bool disposing) + { + Pointer = null; + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D1_COLOR_F.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D1_COLOR_F.cs new file mode 100644 index 0000000..01d845c --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D1_COLOR_F.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Windows.Win32.Graphics.Direct2D.Common; + +public partial struct D2D1_COLOR_F +{ + public D2D1_COLOR_F(float r, float g, float b, float a) + { + this.r = r; + this.g = g; + this.b = b; + this.a = a; + } + + public static explicit operator D2D1_COLOR_F(Color value) => + new(value.R / 255.0f, value.G / 255.0f, value.B / 255.0f, value.A / 255.0f); +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_RECT_F.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_RECT_F.cs new file mode 100644 index 0000000..819aed7 --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_RECT_F.cs @@ -0,0 +1,20 @@ +// 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; + +namespace Windows.Win32.Graphics.Direct2D.Common; + +public partial struct D2D_RECT_F +{ + public D2D_RECT_F(float left, float top, float right, float bottom) + { + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + } + + public static implicit operator D2D_RECT_F(RectangleF value) => + new(value.Left, value.Top, value.Right, value.Bottom); +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_SIZE_U.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_SIZE_U.cs new file mode 100644 index 0000000..1d1bb2f --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Common/D2D_SIZE_U.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Windows.Win32.Graphics.Direct2D.Common; + +public partial struct D2D_SIZE_U +{ + public D2D_SIZE_U(uint width, uint height) + { + this.width = width; + this.height = height; + } + + public static explicit operator D2D_SIZE_U(Size value) => + new(checked((uint)value.Width), checked((uint)value.Height)); +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Factory.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Factory.cs new file mode 100644 index 0000000..2303f6c --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Factory.cs @@ -0,0 +1,42 @@ +// 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; +using Windows.Win32.System.Com; + +namespace Windows.Win32.Graphics.Direct2D; + +public unsafe class Factory : DisposableBase.Finalizable, IPointer +{ + private readonly AgileComPointer _factory; + + public unsafe ID2D1Factory* Pointer { get; private set; } + + public Factory( + D2D1_FACTORY_TYPE factoryType = D2D1_FACTORY_TYPE.D2D1_FACTORY_TYPE_SINGLE_THREADED, + D2D1_DEBUG_LEVEL factoryOptions = D2D1_DEBUG_LEVEL.D2D1_DEBUG_LEVEL_NONE) + { + ID2D1Factory* factory; + Interop.D2D1CreateFactory( + factoryType, + IID.Get(), + (D2D1_FACTORY_OPTIONS*)&factoryOptions, + (void**)&factory).ThrowOnFailure(); + + Pointer = factory; + + // Ensure that this can be disposed on the finalizer thread by giving the "last" ref count + // to an agile pointer. + _factory = new AgileComPointer(factory, takeOwnership: true); + } + + protected override void Dispose(bool disposing) + { + Pointer = null; + + if (disposing) + { + _factory.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/HwndRenderTarget.cs b/src/thirtytwo/Win32/Graphics/Direct2D/HwndRenderTarget.cs new file mode 100644 index 0000000..33aec23 --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/HwndRenderTarget.cs @@ -0,0 +1,62 @@ +// 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 Windows.Support; +using Windows.Win32.Graphics.Direct2D.Common; + +namespace Windows.Win32.Graphics.Direct2D; + +public unsafe class HwndRenderTarget : RenderTarget, IPointer +{ + public new ID2D1HwndRenderTarget* Pointer { get; private set; } + + public HwndRenderTarget(ID2D1HwndRenderTarget* renderTarget) + : base((ID2D1RenderTarget*)renderTarget) + { + Pointer = renderTarget; + } + + public static HwndRenderTarget CreateForWindow( + TFactory factory, + TWindow window, + Size size) + where TFactory : IPointer + where TWindow : IHandle => + CreateForWindow( + factory, + window, + new D2D_SIZE_U() { width = checked((uint)size.Width), height = checked((uint)size.Height) }); + + public static HwndRenderTarget CreateForWindow( + TFactory factory, + TWindow window, + D2D_SIZE_U size) + where TFactory : IPointer + where TWindow : IHandle + { + D2D1_RENDER_TARGET_PROPERTIES properties = default; + D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProperties = new() + { + hwnd = window.Handle, + pixelSize = size + }; + + ID2D1HwndRenderTarget* renderTarget; + factory.Pointer->CreateHwndRenderTarget( + &properties, + &hwndProperties, + &renderTarget).ThrowOnFailure(); + + GC.KeepAlive(factory); + GC.KeepAlive(window.Wrapper); + + return new HwndRenderTarget(renderTarget); + } + + protected override void Dispose(bool disposing) + { + Pointer = null; + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/ID2D1RenderTarget.cs b/src/thirtytwo/Win32/Graphics/Direct2D/ID2D1RenderTarget.cs new file mode 100644 index 0000000..8927a71 --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/ID2D1RenderTarget.cs @@ -0,0 +1,16 @@ +// 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.Win32.Graphics.Direct2D.Common; + +namespace Windows.Win32.Graphics.Direct2D; + +public unsafe partial struct ID2D1RenderTarget +{ + public D2D_SIZE_F GetSizeHack() + { + D2D_SIZE_F result; + return *((delegate* unmanaged)lpVtbl[53])((ID2D1RenderTarget*)Unsafe.AsPointer(ref this), &result); + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/RenderTarget.cs b/src/thirtytwo/Win32/Graphics/Direct2D/RenderTarget.cs new file mode 100644 index 0000000..5cd8d4d --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/RenderTarget.cs @@ -0,0 +1,19 @@ +// 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.Direct2D; + +public unsafe class RenderTarget : Resource, IPointer +{ + public unsafe new ID2D1RenderTarget* Pointer { get; private set; } + + public RenderTarget(ID2D1RenderTarget* renderTarget) : base((ID2D1Resource*)renderTarget) => Pointer = renderTarget; + + protected override void Dispose(bool disposing) + { + Pointer = null; + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/RenderTargetExtensions.cs b/src/thirtytwo/Win32/Graphics/Direct2D/RenderTargetExtensions.cs new file mode 100644 index 0000000..f69b11a --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/RenderTargetExtensions.cs @@ -0,0 +1,95 @@ +// 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.Numerics; +using Windows.Support; +using Windows.Win32.Graphics.Direct2D.Common; + +namespace Windows.Win32.Graphics.Direct2D; + +public static unsafe class RenderTargetExtensions +{ + public static void Resize(this T target, Size size) + where T : IPointer + { + target.Pointer->Resize((D2D_SIZE_U)size).ThrowOnFailure(); + GC.KeepAlive(target); + } + + public static void BeginDraw(this T target) where T : IPointer + { + target.Pointer->BeginDraw(); + GC.KeepAlive(target); + } + + public static void EndDraw(this T target, out bool recreateTarget) + where T : IPointer + { + HRESULT result = target.Pointer->EndDraw(null, null); + if (result == HRESULT.D2DERR_RECREATE_TARGET) + { + recreateTarget = true; + } + else + { + result.ThrowOnFailure(); + recreateTarget = false; + } + + GC.KeepAlive(target); + } + + public static SolidColorBrush CreateSolidColorBrush(this T target, Color color) + where T : IPointer + { + ID2D1SolidColorBrush* solidColorBrush; + D2D1_COLOR_F colorf = (D2D1_COLOR_F)color; + target.Pointer->CreateSolidColorBrush(&colorf, null, &solidColorBrush).ThrowOnFailure(); + GC.KeepAlive(target); + return new SolidColorBrush(solidColorBrush); + } + + public static void SetTransform(this T target, Matrix3x2 transform) where T : IPointer + { + target.Pointer->SetTransform((D2D_MATRIX_3X2_F*)&transform); + GC.KeepAlive(target); + } + + public static void Clear(this T target, Color color) where T : IPointer + { + D2D1_COLOR_F colorf = (D2D1_COLOR_F)color; + target.Pointer->Clear(&colorf); + GC.KeepAlive(target); + } + + public static void DrawLine(this T target, PointF point0, PointF point1, Brush brush, float strokeWidth = 1.0f) + where T : IPointer + { + target.Pointer->DrawLine(*(D2D_POINT_2F*)&point0, *(D2D_POINT_2F*)&point1, brush.Pointer, strokeWidth, null); + GC.KeepAlive(target); + } + + public static void FillRectangle(this T target, RectangleF rect, Brush brush) + where T : IPointer + { + D2D_RECT_F rectf = (D2D_RECT_F)rect; + target.Pointer->FillRectangle(&rectf, brush.Pointer); + GC.KeepAlive(target); + } + + public static void DrawRectangle(this T target, RectangleF rect, Brush brush, float strokeWidth = 1.0f) + where T : IPointer + { + D2D_RECT_F rectf = (D2D_RECT_F)rect; + target.Pointer->DrawRectangle(&rectf, brush.Pointer, strokeWidth, null); + GC.KeepAlive(target); + } + + public static SizeF Size(this T target) where T : IPointer + { + D2D_SIZE_F size = target.Pointer->GetSizeHack(); + GC.KeepAlive(target); + return *(SizeF*)&size; + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/Resource.cs b/src/thirtytwo/Win32/Graphics/Direct2D/Resource.cs new file mode 100644 index 0000000..669fc3f --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/Resource.cs @@ -0,0 +1,33 @@ +// 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; +using Windows.Win32.System.Com; + +namespace Windows.Win32.Graphics.Direct2D; + +public unsafe class Resource : DisposableBase.Finalizable, IPointer +{ + private readonly AgileComPointer _resource; + + public unsafe ID2D1Resource* Pointer { get; private set; } + + public Resource(ID2D1Resource* resource) + { + Pointer = resource; + + // Ensure that this can be disposed on the finalizer thread by giving the "last" ref count + // to an agile pointer. + _resource = new AgileComPointer(resource, takeOwnership: true); + } + + protected override void Dispose(bool disposing) + { + Pointer = null; + + if (disposing) + { + _resource.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/Direct2D/SolidColorBrush.cs b/src/thirtytwo/Win32/Graphics/Direct2D/SolidColorBrush.cs new file mode 100644 index 0000000..1d097f1 --- /dev/null +++ b/src/thirtytwo/Win32/Graphics/Direct2D/SolidColorBrush.cs @@ -0,0 +1,19 @@ +// 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.Direct2D; + +public unsafe class SolidColorBrush : Brush, IPointer +{ + public unsafe new ID2D1SolidColorBrush* Pointer { get; private set; } + + public SolidColorBrush(ID2D1SolidColorBrush* brush) : base((ID2D1Brush*)brush) => Pointer = brush; + + protected override void Dispose(bool disposing) + { + Pointer = null; + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs b/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs index 4d3bd23..9220dc8 100644 --- a/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs +++ b/src/thirtytwo/Win32/Graphics/GdiPlus/Session.cs @@ -1,9 +1,6 @@ // 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 diff --git a/src/thirtytwo/Window.cs b/src/thirtytwo/Window.cs index 12a374a..6497aa5 100644 --- a/src/thirtytwo/Window.cs +++ b/src/thirtytwo/Window.cs @@ -6,20 +6,32 @@ using System.Numerics; using Windows.Components; using Windows.Support; +using Windows.Win32.Graphics.Direct2D; namespace Windows; public unsafe class Window : ComponentBase, IHandle, ILayoutHandler { + private static readonly object s_lock = new(); + private static readonly ConcurrentDictionary> s_windows = new(); + private static readonly WindowClass s_defaultWindowClass = new(className: $"DefaultWindowClass_{Guid.NewGuid()}"); + protected static Factory? Direct2dFactory { get; private set; } + public static Rectangle DefaultBounds { get; } + = new(Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT); + + // Default fonts for each DPI + private static readonly ConcurrentDictionary s_defaultFonts = new(); + internal static WNDPROC DefaultWindowProcedure { get; } = GetDefaultWindowProcedure(); + // High precision metric units are .01mm each private const int HiMetricUnitsPerInch = 2540; + private readonly object _lock = new(); + // Stash the delegate to keep it from being collected private readonly WindowProcedure _windowProcedure; private readonly WNDPROC _priorWindowProcedure; private readonly WindowClass _windowClass; - private static readonly object s_lock = new(); - private readonly object _lock = new(); private bool _destroyed; private HWND _handle; @@ -30,28 +42,40 @@ public unsafe class Window : ComponentBase, IHandle, ILayoutHandler // https://devblogs.microsoft.com/oldnewthing/20080912-00/?p=20893 private HFONT _font; + private HFONT _lastCreatedFont; private readonly HBRUSH _backgroundBrush; - private static readonly WindowClass s_defaultWindowClass = new(className: $"DefaultWindowClass_{Guid.NewGuid()}"); - internal static WNDPROC DefaultWindowProcedure { get; } = GetDefaultWindowProcedure(); + protected HwndRenderTarget? Direct2dRenderTarget { get; private set; } private string? _text; private uint _lastDpi; - private static readonly ConcurrentDictionary> s_windows = new(); - private HFONT _lastCreatedFont; + private readonly Features _features; - // Default fonts for each DPI - private static readonly ConcurrentDictionary s_defaultFonts = new(); + [MemberNotNullWhen(true, nameof(Direct2dRenderTarget))] + protected bool IsDirect2dEnabled() + { + bool enabled = _features.AreFlagsSet(Features.EnableDirect2d); + if (enabled && Direct2dRenderTarget is null) + { + UpdateRenderTarget(Handle, this.GetClientRectangle().Size); + } - public static Rectangle DefaultBounds { get; } - = new(Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT, Interop.CW_USEDEFAULT); + return enabled; + } - /// - /// The window handle. This will be after the window is destroyed. - /// - public HWND Handle => _handle; + [MemberNotNullWhen(true, nameof(Direct2dRenderTarget))] + protected bool IsDirect2dEnabled([NotNullWhen(true)] out HwndRenderTarget? renderTarget) + { + renderTarget = Direct2dRenderTarget; + return IsDirect2dEnabled(); + } + +/// +/// The window handle. This will be after the window is destroyed. +/// +public HWND Handle => _handle; public event WindowsMessageEvent? MessageHandler; @@ -64,7 +88,8 @@ public Window( WindowClass? windowClass = default, nint parameters = default, HMENU menuHandle = default, - HBRUSH backgroundBrush = default) + HBRUSH backgroundBrush = default, + Features features = default) { _windowClass = windowClass ?? s_defaultWindowClass; @@ -74,6 +99,12 @@ public Window( } _text = text; + _features = features; + + if (_features.AreFlagsSet(Features.EnableDirect2d)) + { + Direct2dFactory ??= new(); + } try { @@ -160,6 +191,32 @@ public void SetFont(string typeFace, int pointSize) this.SetFontHandle(_lastCreatedFont); } + [MemberNotNull(nameof(Direct2dRenderTarget))] + private void UpdateRenderTarget(HWND window, Size size) + { + if (Direct2dFactory is null) + { + throw new InvalidOperationException("Should never call UpdateRenderTarget without a factory."); + } + + if (Direct2dRenderTarget is null) + { + Direct2dRenderTarget = HwndRenderTarget.CreateForWindow(Direct2dFactory, window, size); + RenderTargetCreated(Direct2dRenderTarget); + } + else + { + Direct2dRenderTarget.Resize(size); + } + } + + /// + /// Called whenever the Direct2D render target has been created or recreated. + /// + protected virtual void RenderTargetCreated(HwndRenderTarget renderTarget) + { + } + private LRESULT InitializationWindowProcedure(HWND window, uint message, WPARAM wParam, LPARAM lParam) { if (Handle.IsNull) @@ -175,6 +232,43 @@ private LRESULT InitializationWindowProcedure(HWND window, uint message, WPARAM private LRESULT WindowProcedureInternal(HWND window, uint message, WPARAM wParam, LPARAM lParam) { + // What is the difference between WM_DESTROY and WM_NCDESTROY? + // https://devblogs.microsoft.com/oldnewthing/20050726-00/?p=34803 + + switch (message) + { + case Interop.WM_NCDESTROY: + lock (_lock) + { + // This should be the final message. Track that we've been destroyed so we know we don't have + // to manually clean up. + + bool success = s_windows.TryRemove(Handle, out _); + Debug.Assert(success); + _handle = default; + _destroyed = true; + } + + break; + + case Interop.WM_SIZE: + // Check the flag directly here so we don't create then resize. + if (_features.AreFlagsSet(Features.EnableDirect2d)) + { + UpdateRenderTarget(window, new Size(lParam.LOWORD, lParam.HIWORD)); + } + + break; + + case Interop.WM_PAINT: + if (IsDirect2dEnabled(out var renderTarget)) + { + renderTarget.BeginDraw(); + } + + break; + } + if (MessageHandler is { } handlers) { foreach (var handler in handlers.GetInvocationList().OfType()) @@ -187,25 +281,19 @@ private LRESULT WindowProcedureInternal(HWND window, uint message, WPARAM wParam } } - // What is the difference between WM_DESTROY and WM_NCDESTROY? - // https://devblogs.microsoft.com/oldnewthing/20050726-00/?p=34803 + LRESULT windProcResult = WindowProcedure(window, (MessageType)message, wParam, lParam); - if ((MessageType)message == MessageType.NonClientDestroy) + if (message == Interop.WM_PAINT && IsDirect2dEnabled()) { - lock (_lock) + Direct2dRenderTarget.EndDraw(out bool recreateTarget); + if (recreateTarget) { - // This should be the final message. Track that we've been destroyed so we know we don't have - // to manually clean up. - - bool success = s_windows.TryRemove(Handle, out _); - Debug.Assert(success); - _handle = default; - _destroyed = true; + Direct2dRenderTarget.Dispose(); + Direct2dRenderTarget = null; + UpdateRenderTarget(window, this.GetClientRectangle().Size); } } - LRESULT windProcResult = WindowProcedure(window, (MessageType)message, wParam, lParam); - // Ensure we're not collected while we're processing a message. GC.KeepAlive(this); return windProcResult; @@ -232,6 +320,12 @@ protected virtual LRESULT WindowProcedure(HWND window, MessageType message, WPAR return (LRESULT)1; } + if (IsDirect2dEnabled()) + { + // Having the HDC erased will cause flicker, so say we handled it. + return (LRESULT)1; + } + break; case MessageType.GetFont: @@ -442,4 +536,13 @@ private static WNDPROC GetDefaultWindowProcedure() Debug.Assert(!address.IsNull); return (WNDPROC)(void*)address.Value; } + + [Flags] + public enum Features + { + /// + /// Set this flag to enable Direct2D rendering. + /// + EnableDirect2d = 0b0000_0000_0000_0000_0000_0000_0000_0001, + } } \ No newline at end of file diff --git a/thirtytwo.sln b/thirtytwo.sln index d11c483..5dd6b4e 100644 --- a/thirtytwo.sln +++ b/thirtytwo.sln @@ -55,6 +55,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Checker", "src\samples\Petz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Clock", "src\samples\Petzold\5th\Clock\Clock.csproj", "{D4EA97FD-45D3-4A26-843F-63427F0F1BFE}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DirectDraw", "DirectDraw", "{7107A105-51E2-46EB-859D-E2DD2FD51839}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Direct2dDemo", "src\samples\DirectDraw\Direct2dDemo\Direct2dDemo.csproj", "{1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -137,6 +141,10 @@ Global {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 + {1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A}.Debug|x64.ActiveCfg = Debug|x64 + {1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A}.Debug|x64.Build.0 = Debug|x64 + {1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A}.Release|x64.ActiveCfg = Release|x64 + {1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -160,6 +168,8 @@ Global {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} + {7107A105-51E2-46EB-859D-E2DD2FD51839} = {C059056C-D771-4CF5-A93F-AA7140FF7827} + {1EC566E8-C337-42FF-9BA4-DB66BFAE5B8A} = {7107A105-51E2-46EB-859D-E2DD2FD51839} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3761BFC9-DBEF-4186-BB8B-BC0D84ED9AE5}