From 26657f46568f2bf5b169ccd6e35fa9eaa47a838e Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Sat, 3 Feb 2024 22:26:19 -0800 Subject: [PATCH] Set up subclassing for common controls Subclass existing controls so we can get initial messages (such as WM_CREATE). Port BtnLook sample from WInterop. --- .../Petzold/5th/BtnLook/BtnLook.csproj | 11 ++ src/samples/Petzold/5th/BtnLook/Program.cs | 177 ++++++++++++++++++ src/samples/Petzold/5th/BtnLook/app.manifest | 74 ++++++++ src/thirtytwo/Controls/ButtonControl.cs | 4 +- src/thirtytwo/Controls/RichEditControl.cs | 4 +- src/thirtytwo/DeviceContextExtensions.cs | 57 ++++-- src/thirtytwo/NativeMethods.txt | 7 + .../Win32/UI/WindowsAndMessaging/WNDPROC.cs | 2 + src/thirtytwo/Window.cs | 51 +++-- src/thirtytwo/WindowClass.cs | 18 +- src/thirtytwo/WindowExtensions.cs | 15 ++ thirtytwo.sln | 7 + 12 files changed, 393 insertions(+), 34 deletions(-) create mode 100644 src/samples/Petzold/5th/BtnLook/BtnLook.csproj create mode 100644 src/samples/Petzold/5th/BtnLook/Program.cs create mode 100644 src/samples/Petzold/5th/BtnLook/app.manifest diff --git a/src/samples/Petzold/5th/BtnLook/BtnLook.csproj b/src/samples/Petzold/5th/BtnLook/BtnLook.csproj new file mode 100644 index 0000000..2541e9d --- /dev/null +++ b/src/samples/Petzold/5th/BtnLook/BtnLook.csproj @@ -0,0 +1,11 @@ + + + + WinExe + True + app.manifest + + + + + \ No newline at end of file diff --git a/src/samples/Petzold/5th/BtnLook/Program.cs b/src/samples/Petzold/5th/BtnLook/Program.cs new file mode 100644 index 0000000..05806df --- /dev/null +++ b/src/samples/Petzold/5th/BtnLook/Program.cs @@ -0,0 +1,177 @@ +// 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; +using Windows.Win32; +using Windows.Win32.Foundation; + +namespace OwnDraw; + +/// +/// Sample from Programming Windows, 5th Edition. +/// Original (c) Charles Petzold, 1998 +/// Figure 9-3, Pages 375-380. +/// +internal static class Program +{ + [STAThread] + private static void Main() => Application.Run(new OwnerDraw("Owner-Draw Button Demo")); + + private class OwnerDraw : MainWindow + { + private HWND _hwndSmaller, _hwndLarger; + private int _cxClient, _cyClient; + private int _btnWidth, _btnHeight; + private Size _baseUnits; + private const int ID_SMALLER = 1; + private const int ID_LARGER = 2; + + public OwnerDraw(string title) : base(title) + { + } + + protected override LRESULT WindowProcedure(HWND window, MessageType message, WPARAM wParam, LPARAM lParam) + { + switch (message) + { + case MessageType.Create: + int baseUnits = Interop.GetDialogBaseUnits(); + _baseUnits = new(baseUnits & 0xFFFF, baseUnits >> 16); + _btnWidth = _baseUnits.Width * 8; + _btnHeight = _baseUnits.Height * 4; + + // Create the owner-draw pushbuttons + _hwndSmaller = new ButtonControl( + style: WindowStyles.Child | WindowStyles.Visible, + buttonStyle: ButtonControl.Styles.OwnerDrawn, + parentWindow: this, + buttonId: ID_SMALLER); + _hwndLarger = new ButtonControl( + style: WindowStyles.Child | WindowStyles.Visible, + buttonStyle: ButtonControl.Styles.OwnerDrawn, + parentWindow: this, + buttonId: ID_LARGER); + + return (LRESULT)0; + + case MessageType.Size: + _cxClient = lParam.LOWORD; + _cyClient = lParam.HIWORD; + + // Move the buttons to the new center + _hwndSmaller.MoveWindow( + new Rectangle(_cxClient / 2 - 3 * _btnWidth / 2, _cyClient / 2 - _btnHeight / 2, _btnWidth, _btnHeight), + repaint: true); + _hwndLarger.MoveWindow( + new Rectangle(_cxClient / 2 + _btnWidth / 2, _cyClient / 2 - _btnHeight / 2, _btnWidth, _btnHeight), + repaint: true); + return (LRESULT)0; + + case MessageType.Command: + Rectangle rc = window.GetWindowRectangle(); + + // Make the window 10% smaller or larger + switch ((int)(uint)wParam) + { + case ID_SMALLER: + rc.Inflate(rc.Width / -10, rc.Height / -10); + break; + case ID_LARGER: + rc.Inflate(rc.Width / 10, rc.Height / 10); + break; + } + + window.MoveWindow(rc, repaint: true); + return (LRESULT)0; + + case MessageType.DrawItem: + + var drawItemMessage = new Message.DrawItem(lParam); + + // Fill area with white and frame it black + using (DeviceContext dc = drawItemMessage.DeviceContext) + { + Rectangle rect = drawItemMessage.ItemRectangle; + + dc.FillRectangle(rect, StockBrush.White); + dc.FrameRectangle(rect, StockBrush.Black); + + // Draw inward and outward black triangles + int cx = rect.Right - rect.Left; + int cy = rect.Bottom - rect.Top; + + Point[] pt = new Point[3]; + + switch ((int)drawItemMessage.ControlId) + { + case ID_SMALLER: + pt[0].X = 3 * cx / 8; pt[0].Y = 1 * cy / 8; + pt[1].X = 5 * cx / 8; pt[1].Y = 1 * cy / 8; + pt[2].X = 4 * cx / 8; pt[2].Y = 3 * cy / 8; + Triangle(dc, pt); + pt[0].X = 7 * cx / 8; pt[0].Y = 3 * cy / 8; + pt[1].X = 7 * cx / 8; pt[1].Y = 5 * cy / 8; + pt[2].X = 5 * cx / 8; pt[2].Y = 4 * cy / 8; + Triangle(dc, pt); + pt[0].X = 5 * cx / 8; pt[0].Y = 7 * cy / 8; + pt[1].X = 3 * cx / 8; pt[1].Y = 7 * cy / 8; + pt[2].X = 4 * cx / 8; pt[2].Y = 5 * cy / 8; + Triangle(dc, pt); + pt[0].X = 1 * cx / 8; pt[0].Y = 5 * cy / 8; + pt[1].X = 1 * cx / 8; pt[1].Y = 3 * cy / 8; + pt[2].X = 3 * cx / 8; pt[2].Y = 4 * cy / 8; + Triangle(dc, pt); + break; + case ID_LARGER: + pt[0].X = 5 * cx / 8; pt[0].Y = 3 * cy / 8; + pt[1].X = 3 * cx / 8; pt[1].Y = 3 * cy / 8; + pt[2].X = 4 * cx / 8; pt[2].Y = 1 * cy / 8; + Triangle(dc, pt); + pt[0].X = 5 * cx / 8; pt[0].Y = 5 * cy / 8; + pt[1].X = 5 * cx / 8; pt[1].Y = 3 * cy / 8; + pt[2].X = 7 * cx / 8; pt[2].Y = 4 * cy / 8; + Triangle(dc, pt); + pt[0].X = 3 * cx / 8; pt[0].Y = 5 * cy / 8; + pt[1].X = 5 * cx / 8; pt[1].Y = 5 * cy / 8; + pt[2].X = 4 * cx / 8; pt[2].Y = 7 * cy / 8; + Triangle(dc, pt); + pt[0].X = 3 * cx / 8; pt[0].Y = 3 * cy / 8; + pt[1].X = 3 * cx / 8; pt[1].Y = 5 * cy / 8; + pt[2].X = 1 * cx / 8; pt[2].Y = 4 * cy / 8; + Triangle(dc, pt); + break; + } + + // Invert the rectangle if the button is selected + if (drawItemMessage.ItemState.HasFlag(Message.DrawItem.States.Selected)) + { + dc.InvertRectangle(rect); + } + + if (drawItemMessage.ItemState.HasFlag(Message.DrawItem.States.Focus)) + { + rect = Rectangle.FromLTRB( + rect.Left + cx / 16, + rect.Top + cy / 16, + rect.Right - cx / 16, + rect.Bottom - cy / 16); + + dc.DrawFocusRectangle(rect); + } + } + + return (LRESULT)0; + } + + static void Triangle(DeviceContext dc, Point[] pt) + { + dc.SelectObject(StockBrush.Black); + dc.Polygon(pt); + dc.SelectObject(StockBrush.White); + } + + return base.WindowProcedure(window, message, wParam, lParam); + } + } +} diff --git a/src/samples/Petzold/5th/BtnLook/app.manifest b/src/samples/Petzold/5th/BtnLook/app.manifest new file mode 100644 index 0000000..ed4b574 --- /dev/null +++ b/src/samples/Petzold/5th/BtnLook/app.manifest @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/thirtytwo/Controls/ButtonControl.cs b/src/thirtytwo/Controls/ButtonControl.cs index be43d37..6dc6027 100644 --- a/src/thirtytwo/Controls/ButtonControl.cs +++ b/src/thirtytwo/Controls/ButtonControl.cs @@ -15,6 +15,7 @@ public ButtonControl( Styles buttonStyle = Styles.PushButton, WindowStyles style = WindowStyles.Overlapped | WindowStyles.Child | WindowStyles.Visible, ExtendedWindowStyles extendedStyle = ExtendedWindowStyles.Default, + int buttonId = default, Window? parentWindow = default, nint parameters = default) : base( bounds, @@ -23,7 +24,8 @@ public ButtonControl( extendedStyle, parentWindow, s_buttonClass, - parameters) + parameters, + (HMENU)buttonId) { } } \ No newline at end of file diff --git a/src/thirtytwo/Controls/RichEditControl.cs b/src/thirtytwo/Controls/RichEditControl.cs index 7377f35..dd6ff48 100644 --- a/src/thirtytwo/Controls/RichEditControl.cs +++ b/src/thirtytwo/Controls/RichEditControl.cs @@ -8,7 +8,7 @@ namespace Windows; public partial class RichEditControl : EditBase { - private static readonly WindowClass s_richEditClass = new("RICHEDIT50W"); + private static readonly WindowClass s_richEditClass; static RichEditControl() { @@ -17,6 +17,8 @@ static RichEditControl() { Error.ThrowLastError(); } + + s_richEditClass = new("RICHEDIT50W"); } public RichEditControl( diff --git a/src/thirtytwo/DeviceContextExtensions.cs b/src/thirtytwo/DeviceContextExtensions.cs index c6db3a5..2f74749 100644 --- a/src/thirtytwo/DeviceContextExtensions.cs +++ b/src/thirtytwo/DeviceContextExtensions.cs @@ -274,6 +274,29 @@ public static bool LineTo(this T context, int x, int y) where T : IHandle(this T context, Rectangle rectangle) where T : IHandle => + context.Ellipse(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom); + + public static bool Ellipse(this T context, int left, int top, int right, int bottom) where T : IHandle + { + bool success = Interop.Ellipse(context.Handle, left, top, right, bottom); + GC.KeepAlive(context.Wrapper); + return success; + } + + public static bool PolyBezier(this T context, params Point[] points) where T : IHandle => + PolyBezier(context, points.AsSpan()); + + public static bool PolyBezier(this T context, ReadOnlySpan points) where T : IHandle + { + fixed (Point* p = points) + { + bool success = Interop.PolyBezier(context.Handle, p, (uint)points.Length); + GC.KeepAlive(context.Wrapper); + return success; + } + } + public static bool Rectangle(this T context, Rectangle rectangle) where T : IHandle => context.Rectangle(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom); @@ -296,34 +319,34 @@ public static bool RoundRectangle(this T context, int left, int top, int righ return success; } - public static bool Ellipse(this T context, Rectangle rectangle) where T : IHandle => - context.Ellipse(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Bottom); - - public static bool Ellipse(this T context, int left, int top, int right, int bottom) where T : IHandle + public static bool FillRectangle(this T context, Rectangle rectangle, HBRUSH hbrush) where T : IHandle { - bool success = Interop.Ellipse(context.Handle, left, top, right, bottom); + RECT rect = rectangle; + bool success = (BOOL)Interop.FillRect(context.Handle, &rect, hbrush); GC.KeepAlive(context.Wrapper); return success; } - public static unsafe bool PolyBezier(this T context, params Point[] points) where T : IHandle => - PolyBezier(context, points.AsSpan()); + public static bool FrameRectangle(this T context, Rectangle rectangle, HBRUSH brush) where T : IHandle + { + RECT rect = rectangle; + bool success = (BOOL)Interop.FrameRect(context.Handle, &rect, brush); + GC.KeepAlive(context.Wrapper); + return success; + } - public static unsafe bool PolyBezier(this T context, ReadOnlySpan points) where T : IHandle + public static bool InvertRectangle(this T context, Rectangle rectangle) where T : IHandle { - fixed (Point* p = points) - { - bool success = Interop.PolyBezier(context.Handle, p, (uint)points.Length); - GC.KeepAlive(context.Wrapper); - return success; - } + RECT rect = rectangle; + bool success = Interop.InvertRect(context.Handle, &rect); + GC.KeepAlive(context.Wrapper); + return success; } - public static bool FillRectangle(this T context, Rectangle rectangle, HBRUSH hbrush) - where T : IHandle + public static bool DrawFocusRectangle(this T context, Rectangle rectangle) where T : IHandle { RECT rect = rectangle; - bool success = (BOOL)Interop.FillRect(context.Handle, &rect, hbrush); + bool success = Interop.DrawFocusRect(context.Handle, &rect); GC.KeepAlive(context.Wrapper); return success; } diff --git a/src/thirtytwo/NativeMethods.txt b/src/thirtytwo/NativeMethods.txt index 935a7e3..67ae622 100644 --- a/src/thirtytwo/NativeMethods.txt +++ b/src/thirtytwo/NativeMethods.txt @@ -56,6 +56,7 @@ DISPID_PROPERTYPUT DISPID_STARTENUM DISPID_UNKNOWN DPI_AWARENESS_CONTEXT_* +DrawFocusRect DrawIconEx DRAWITEMSTRUCT DrawTextEx @@ -103,6 +104,8 @@ GetCurrentThread GetCurrentThreadId GetDC GetDeviceCaps +GetDialogBaseUnits +GetDialogBaseUnits GetDpiForWindow GetFocus GetGraphicsMode @@ -168,6 +171,7 @@ InitVariantFromDoubleArray INTERFACEDATA INVALID_HANDLE_VALUE InvalidateRect +InvertRect IOleClientSite IOleContainer IOleControlSite @@ -203,6 +207,7 @@ LocalFree LOGFONTW LPtoDP LsaNtStatusToWinError +MapDialogRect MapWindowPoints MEMBERID_NIL MessageBeep @@ -259,6 +264,8 @@ SELFLAG_* SendMessage SetActiveWindow SetCapture +SetClassLong +SetClassLongPtr SetClipboardData SetCoalescableTimer SetCursor diff --git a/src/thirtytwo/Win32/UI/WindowsAndMessaging/WNDPROC.cs b/src/thirtytwo/Win32/UI/WindowsAndMessaging/WNDPROC.cs index 87ebd86..e17753c 100644 --- a/src/thirtytwo/Win32/UI/WindowsAndMessaging/WNDPROC.cs +++ b/src/thirtytwo/Win32/UI/WindowsAndMessaging/WNDPROC.cs @@ -16,4 +16,6 @@ public static implicit operator WNDPROC(delegate* unmanaged[Stdcall] new((delegate* unmanaged[Stdcall])value); + public static explicit operator WNDPROC(nuint value) + => new((delegate* unmanaged[Stdcall])value); } \ No newline at end of file diff --git a/src/thirtytwo/Window.cs b/src/thirtytwo/Window.cs index 96ab5be..12a374a 100644 --- a/src/thirtytwo/Window.cs +++ b/src/thirtytwo/Window.cs @@ -21,6 +21,7 @@ public unsafe class Window : ComponentBase, IHandle, ILayoutHandler private static readonly object s_lock = new(); private readonly object _lock = new(); private bool _destroyed; + private HWND _handle; // When I send a WM_GETFONT message to a window, why don't I get a font? // https://devblogs.microsoft.com/oldnewthing/20140724-00/?p=413 @@ -50,7 +51,7 @@ public unsafe class Window : ComponentBase, IHandle, ILayoutHandler /// /// The window handle. This will be after the window is destroyed. /// - public HWND Handle { get; private set; } + public HWND Handle => _handle; public event WindowsMessageEvent? MessageHandler; @@ -72,22 +73,31 @@ public Window( bounds = DefaultBounds; } + _text = text; + + try + { + _handle = _windowClass.CreateWindow( + bounds, + text, + style, + extendedStyle, + parentWindow?.Handle ?? default, + parameters, + menuHandle, + InitializationWindowProcedure); + } + catch + { + // Make sure we don't leave a window handle around if we fail to create the window. + _handle = default; + throw; + } + // Need to set our Window Procedure to get messages before we set // the font (which sends a message to do so). _windowProcedure = WindowProcedureInternal; - _text = text; - - Handle = _windowClass.CreateWindow( - bounds, - text, - style, - extendedStyle, - parentWindow?.Handle ?? default, - parameters, - menuHandle, - _windowProcedure); - _backgroundBrush = backgroundBrush; s_windows[Handle] = new(this); @@ -150,6 +160,19 @@ public void SetFont(string typeFace, int pointSize) this.SetFontHandle(_lastCreatedFont); } + private LRESULT InitializationWindowProcedure(HWND window, uint message, WPARAM wParam, LPARAM lParam) + { + if (Handle.IsNull) + { + // In the middle of CreateWindow, set our handle so that the "this" pointer is valid for use. + // This enables things such as parenting children during WM_CREATE. + + _handle = window; + } + + return WindowProcedureInternal(window, message, wParam, lParam); + } + private LRESULT WindowProcedureInternal(HWND window, uint message, WPARAM wParam, LPARAM lParam) { if (MessageHandler is { } handlers) @@ -176,7 +199,7 @@ private LRESULT WindowProcedureInternal(HWND window, uint message, WPARAM wParam bool success = s_windows.TryRemove(Handle, out _); Debug.Assert(success); - Handle = default; + _handle = default; _destroyed = true; } } diff --git a/src/thirtytwo/WindowClass.cs b/src/thirtytwo/WindowClass.cs index ead0373..ea7e9fa 100644 --- a/src/thirtytwo/WindowClass.cs +++ b/src/thirtytwo/WindowClass.cs @@ -2,12 +2,14 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System.Drawing; +using System.Runtime.InteropServices; using Windows.Support; namespace Windows; public unsafe partial class WindowClass : DisposableBase.Finalizable { + private readonly WNDPROC _priorClassProcedure; // Stash the delegate to keep it from being collected private readonly WindowProcedure _windowProcedure; private readonly string _className; @@ -144,6 +146,18 @@ public WindowClass(string registeredClassName) _windowProcedure = WindowProcedureInternal; _className = registeredClassName; ModuleInstance = HMODULE.Null; + + // We need to subclass the preexisting window class to get class messages first and during construction. + HWND window = CreateWindow(); + try + { + _priorClassProcedure = (WNDPROC)window.GetClassLong(GET_CLASS_LONG_INDEX.GCL_WNDPROC); + window.SetClassLong(GET_CLASS_LONG_INDEX.GCL_WNDPROC, Marshal.GetFunctionPointerForDelegate(_windowProcedure)); + } + finally + { + Interop.DestroyWindow(window); + } } public bool IsRegistered => Atom.IsValid || ModuleInstance == HMODULE.Null; @@ -286,7 +300,9 @@ or MessageType.NonClientCalculateSize } protected virtual LRESULT WindowProcedure(HWND window, MessageType message, WPARAM wParam, LPARAM lParam) => - Interop.DefWindowProc(window, (uint)message, wParam, lParam); + _priorClassProcedure.IsNull + ? Interop.DefWindowProc(window, (uint)message, wParam, lParam) + : Interop.CallWindowProc(_priorClassProcedure, window, (uint)message, wParam, lParam); protected override void Dispose(bool disposing) { diff --git a/src/thirtytwo/WindowExtensions.cs b/src/thirtytwo/WindowExtensions.cs index 3aacbde..5010c60 100644 --- a/src/thirtytwo/WindowExtensions.cs +++ b/src/thirtytwo/WindowExtensions.cs @@ -137,6 +137,21 @@ public static nuint GetClassLong(this T window, GET_CLASS_LONG_INDEX index) w return result; } + /// + public static nuint SetClassLong(this T window, GET_CLASS_LONG_INDEX index, nint value) where T : IHandle + { + nuint result = Environment.Is64BitProcess + ? Interop.SetClassLongPtr(window.Handle, index, value) + : Interop.SetClassLong(window.Handle, index, (int)value); + + if (result == 0) + { + Error.ThrowIfLastErrorNot(WIN32_ERROR.ERROR_SUCCESS); + } + + return result; + } + /// public static nint SetWindowLong(this T window, WINDOW_LONG_PTR_INDEX index, nint value) where T : IHandle diff --git a/thirtytwo.sln b/thirtytwo.sln index 4dc4f72..0d3e66e 100644 --- a/thirtytwo.sln +++ b/thirtytwo.sln @@ -49,6 +49,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bezier", "src\samples\Petzo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blokout2", "src\samples\Petzold\5th\Blokout2\Blokout2.csproj", "{F52C0159-AFFE-431D-BC46-1AA6C8381400}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BtnLook", "src\samples\Petzold\5th\BtnLook\BtnLook.csproj", "{22E8534C-628A-4A9C-9EDB-7BE4DAC12544}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -119,6 +121,10 @@ Global {F52C0159-AFFE-431D-BC46-1AA6C8381400}.Debug|x64.Build.0 = Debug|x64 {F52C0159-AFFE-431D-BC46-1AA6C8381400}.Release|x64.ActiveCfg = Release|x64 {F52C0159-AFFE-431D-BC46-1AA6C8381400}.Release|x64.Build.0 = Release|x64 + {22E8534C-628A-4A9C-9EDB-7BE4DAC12544}.Debug|x64.ActiveCfg = Debug|x64 + {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 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -139,6 +145,7 @@ Global {1EABEAF9-CEE7-4415-98B9-466982727848} = {13110246-EBE1-441B-B721-B0614D62B13B} {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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3761BFC9-DBEF-4186-BB8B-BC0D84ED9AE5}