From 8fe45494e8a89b371863e5f25fec35c1b08ac391 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Tue, 24 Mar 2020 22:26:53 -0700 Subject: [PATCH 1/2] Add a low level window class Adds WindowClass for testing windows messages. Adds InternalsVisibleTo so we can expose helpers for Interop testing from internal test utility. Ref to SWF in internal test utility should be removed. This is one step. Tests using AxHost should refactor AxHost helper methods into SWF.Primitives (don't move the whole AxHost down). Fix a bug in Cursor interop. --- .../InternalUtilitiesForTests.csproj | 1 + .../InternalsVisibleTo.cs | 12 ++ .../src/WindowClass.cs | 203 ++++++++++++++++++ .../src/Interop/Interop.RECT.cs | 12 +- .../src/Interop/User32/Interop.CS.cs | 2 + .../Interop/User32/Interop.CreateWindowExW.cs | 28 ++- .../src/Interop/User32/Interop.LoadCursorW.cs | 30 +-- .../Interop/User32/GetWindowTextTests.cs | 55 +++-- .../src/System/Windows/Forms/Cursor.cs | 2 +- .../src/System/Windows/Forms/NativeWindow.cs | 4 +- 10 files changed, 297 insertions(+), 52 deletions(-) create mode 100644 src/Common/tests/InternalUtilitiesForTests/InternalsVisibleTo.cs create mode 100644 src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs diff --git a/src/Common/tests/InternalUtilitiesForTests/InternalUtilitiesForTests.csproj b/src/Common/tests/InternalUtilitiesForTests/InternalUtilitiesForTests.csproj index 2ab244ba1b4..cb833482993 100644 --- a/src/Common/tests/InternalUtilitiesForTests/InternalUtilitiesForTests.csproj +++ b/src/Common/tests/InternalUtilitiesForTests/InternalUtilitiesForTests.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Common/tests/InternalUtilitiesForTests/InternalsVisibleTo.cs b/src/Common/tests/InternalUtilitiesForTests/InternalsVisibleTo.cs new file mode 100644 index 00000000000..eefe7821763 --- /dev/null +++ b/src/Common/tests/InternalUtilitiesForTests/InternalsVisibleTo.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// Awkward, but necessary to expose Interop based internals to other test libaries +[assembly: InternalsVisibleTo("System.Windows.Forms.Primitives.Tests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("System.Windows.Forms.Tests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("System.Windows.Forms.Design.Tests, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("WinformsControlsTest, PublicKey=00000000000000000400000000000000")] +[assembly: InternalsVisibleTo("MauiListViewTests, PublicKey=00000000000000000400000000000000")] diff --git a/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs b/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs new file mode 100644 index 00000000000..1e793c3ad43 --- /dev/null +++ b/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs @@ -0,0 +1,203 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using static Interop; + +namespace System +{ + internal class WindowClass + { + [DllImport(Libraries.User32, SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)] + public unsafe static extern IntPtr LoadIconW( + IntPtr hInstance, + IntPtr lpIconName); + + private const int CW_USEDEFAULT = unchecked((int)0x80000000); + private const uint IDI_APPLICATION = 32512; + private const uint IDC_ARROW = 32512; + private const int COLOR_WINDOW = 5; + + private static RECT DefaultBounds => new RECT(CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT); + + // Stash the delegate to keep it from being collected + private readonly User32.WNDPROC _windowProcedure; + private User32.WNDCLASS _wndClass; + private readonly string _className; + private readonly string _menuName; + + public ushort Atom { get; private set; } + public IntPtr MainWindow { get; private set; } + public IntPtr ModuleInstance { get; } + + /// + /// Constructor. + /// + /// Name, or default will be generated. + /// Module to associate with the window. The entry assembly is the default. + /// Use (IntPtr)(-1) for no background brush. + /// Use (IntPtr)(-1) for no icon. + /// Use (IntPtr)(-1) for no cursor. + /// Menu name, can not set with . + /// Menu id, can not set with . + public unsafe WindowClass( + string className = default, + IntPtr moduleInstance = default, + User32.CS classStyle = User32.CS.HREDRAW | User32.CS.VREDRAW, + IntPtr backgroundBrush = default, + IntPtr icon = default, + IntPtr cursor = default, + string menuName = null, + int menuId = 0, + int classExtraBytes = 0, + int windowExtraBytes = 0) + { + // Handle default values + className ??= Guid.NewGuid().ToString(); + + if (backgroundBrush == default) + { + backgroundBrush = User32.GetSysColorBrush(COLOR_WINDOW); + } + else if (backgroundBrush == (IntPtr)(-1)) + { + backgroundBrush = default; + } + + if (icon == default) + { + icon = LoadIconW(IntPtr.Zero, (IntPtr)IDI_APPLICATION); + } + else if (icon == (IntPtr)(-1)) + { + icon = default; + } + + if (cursor == default) + { + cursor = User32.LoadCursorW(IntPtr.Zero, User32.CursorResourceId.IDC_ARROW); + } + else if (cursor == (IntPtr)(-1)) + { + cursor = default; + } + + if (moduleInstance == IntPtr.Zero) + Marshal.GetHINSTANCE(Assembly.GetCallingAssembly().Modules.First()); + + if (menuId != 0 && menuName != null) + throw new ArgumentException($"Can't set both {nameof(menuName)} and {nameof(menuId)}."); + + _windowProcedure = WNDPROC; + ModuleInstance = moduleInstance; + + _className = className; + _menuName = menuName ?? string.Empty; + + _wndClass = new User32.WNDCLASS + { + style = classStyle, + lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_windowProcedure), + cbClsExtra = classExtraBytes, + cbWndExtra = windowExtraBytes, + hInstance = moduleInstance, + hIcon = icon, + hCursor = cursor, + hbrBackground = backgroundBrush, + lpszMenuName = (char*)menuId + }; + } + + public bool IsRegistered => Atom != 0; + + public unsafe WindowClass Register() + { + fixed (char* name = _className) + fixed (char* menuName = _menuName) + { + _wndClass.lpszClassName = name; + if (!string.IsNullOrEmpty(_menuName)) + _wndClass.lpszMenuName = menuName; + + ushort atom = User32.RegisterClassW(ref _wndClass); + if (atom == 0) + { + throw new Win32Exception(); + } + Atom = atom; + return this; + } + } + + public IntPtr CreateWindow( + string windowName = null, + User32.WS style = User32.WS.OVERLAPPED, + User32.WS_EX extendedStyle = default, + bool isMainWindow = false, + IntPtr parentWindow = default, + IntPtr parameters = default, + IntPtr menuHandle = default) + { + return CreateWindow( + DefaultBounds, + windowName, + style, + extendedStyle, + isMainWindow, + parentWindow, + parameters, + menuHandle); + } + + public unsafe IntPtr CreateWindow( + RECT bounds, + string windowName = null, + User32.WS style = User32.WS.OVERLAPPED, + User32.WS_EX extendedStyle = default, + bool isMainWindow = false, + IntPtr parentWindow = default, + IntPtr parameters = default, + IntPtr menuHandle = default) + { + if (!IsRegistered) + throw new ArgumentException("Window class must be registered before using."); + + IntPtr window = User32.CreateWindowExW( + dwExStyle: extendedStyle, + lpClassName: (char*)Atom, + lpWindowName: windowName, + dwStyle: style, + X: bounds.X, + Y: bounds.Y, + nWidth: bounds.Width, + nHeight: bounds.Height, + hWndParent: parentWindow, + hMenu: menuHandle, + hInst: IntPtr.Zero, + lpParam: parameters); + + if (isMainWindow) + MainWindow = window; + + return window; + } + + protected virtual IntPtr WNDPROC(IntPtr hWnd, User32.WM msg, IntPtr wParam, IntPtr lParam) + { + switch (msg) + { + case User32.WM.DESTROY: + if (hWnd == MainWindow) + User32.PostQuitMessage(0); + return (IntPtr)0; + } + + return User32.DefWindowProcW(hWnd, msg, wParam, lParam); + } + } +} diff --git a/src/System.Windows.Forms.Primitives/src/Interop/Interop.RECT.cs b/src/System.Windows.Forms.Primitives/src/Interop/Interop.RECT.cs index 170c179b2c7..a9df9aa129e 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/Interop.RECT.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/Interop.RECT.cs @@ -37,7 +37,17 @@ public static implicit operator Rectangle(RECT r) public static implicit operator RECT(Rectangle r) => new RECT(r); + public int X => left; + + public int Y => top; + + public int Width + => right - left; + + public int Height + => bottom - top; + public Size Size - => new Size(right - left, bottom - top); + => new Size(Width, Height); } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CS.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CS.cs index 4df92454d90..097234ce981 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CS.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CS.cs @@ -14,6 +14,8 @@ internal static partial class User32 [Flags] public enum CS : uint { + VREDRAW = 0x0001, + HREDRAW = 0x0002, DBLCLKS = 0x0008, DROPSHADOW = 0x00020000, SAVEBITS = 0x0800 diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CreateWindowExW.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CreateWindowExW.cs index 1204d35c3f9..279346d4e82 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CreateWindowExW.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.CreateWindowExW.cs @@ -10,11 +10,11 @@ internal static partial class Interop internal static partial class User32 { [DllImport(Libraries.User32, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern IntPtr CreateWindowExW( - int dwExStyle, - string lpClassName, + public unsafe static extern IntPtr CreateWindowExW( + WS_EX dwExStyle, + char* lpClassName, string lpWindowName, - int dwStyle, + WS dwStyle, int X, int Y, int nWidth, @@ -23,5 +23,25 @@ public static extern IntPtr CreateWindowExW( IntPtr hMenu, IntPtr hInst, [MarshalAs(UnmanagedType.AsAny)] object lpParam); + + public unsafe static IntPtr CreateWindowExW( + WS_EX dwExStyle, + string lpClassName, + string lpWindowName, + WS dwStyle, + int X, + int Y, + int nWidth, + int nHeight, + IntPtr hWndParent, + IntPtr hMenu, + IntPtr hInst, + object lpParam) + { + fixed(char* c = lpClassName) + { + return CreateWindowExW(dwExStyle, c, lpWindowName, dwStyle, X, Y, nWidth, nHeight, hWndParent, hMenu, hInst, lpParam); + } + } } } diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs index 993a092260c..00fe1b42da7 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs @@ -11,23 +11,23 @@ internal static partial class User32 { public static class CursorResourceId { - public const int IDC_ARROW = 32512; - public const int IDC_IBEAM = 32513; - public const int IDC_WAIT = 32514; - public const int IDC_CROSS = 32515; - public const int IDC_SIZEALL = 32646; - public const int IDC_SIZENWSE = 32642; - public const int IDC_SIZENESW = 32643; - public const int IDC_SIZEWE = 32644; - public const int IDC_SIZENS = 32645; - public const int IDC_UPARROW = 32516; - public const int IDC_NO = 32648; - public const int IDC_HAND = 32649; - public const int IDC_APPSTARTING = 32650; - public const int IDC_HELP = 32651; + public static IntPtr IDC_ARROW = (IntPtr)32512; + public static IntPtr IDC_IBEAM = (IntPtr)32513; + public static IntPtr IDC_WAIT = (IntPtr)32514; + public static IntPtr IDC_CROSS = (IntPtr)32515; + public static IntPtr IDC_SIZEALL = (IntPtr)32646; + public static IntPtr IDC_SIZENWSE = (IntPtr)32642; + public static IntPtr IDC_SIZENESW = (IntPtr)32643; + public static IntPtr IDC_SIZEWE = (IntPtr)32644; + public static IntPtr IDC_SIZENS = (IntPtr)32645; + public static IntPtr IDC_UPARROW = (IntPtr)32516; + public static IntPtr IDC_NO = (IntPtr)32648; + public static IntPtr IDC_HAND = (IntPtr)32649; + public static IntPtr IDC_APPSTARTING = (IntPtr)32650; + public static IntPtr IDC_HELP = (IntPtr)32651; } [DllImport(Libraries.User32, ExactSpelling = true)] - public static extern IntPtr LoadCursorW(IntPtr hInst, int iconId); + public static extern IntPtr LoadCursorW(IntPtr hInstance, IntPtr lpCursorName); } } diff --git a/src/System.Windows.Forms.Primitives/tests/Interop/User32/GetWindowTextTests.cs b/src/System.Windows.Forms.Primitives/tests/Interop/User32/GetWindowTextTests.cs index 1f40c99f426..87035132bbd 100644 --- a/src/System.Windows.Forms.Primitives/tests/Interop/User32/GetWindowTextTests.cs +++ b/src/System.Windows.Forms.Primitives/tests/Interop/User32/GetWindowTextTests.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Xunit; -using static Interop; using static Interop.User32; namespace System.Windows.Forms.Primitives.Tests.Interop.User32 @@ -29,26 +28,23 @@ private void CallGetWindowText(bool useBeforeGetTextLengthCallback) // Use a long string that exceeds the initial buffer size (16). string longText = new string('X', 50); - using var form = new ChangeWindowTextForm() - { - Text = shortText - }; - - // Creating the handle causes GetWindowText to be called, - // so do it before setting the delegates. - IntPtr formHandle = form.Handle; + var windowClass = new ChangeWindowTextClass(); + windowClass.Register(); + IntPtr windowHandle = windowClass.CreateWindow(shortText); - form.BeforeGetTextCallback = () => longText; + windowClass.BeforeGetTextCallback = () => longText; if (useBeforeGetTextLengthCallback) { - form.BeforeGetTextLengthCallback = () => shortText; + windowClass.BeforeGetTextLengthCallback = () => shortText; } - string result = GetWindowText(formHandle); + string result = GetWindowText(windowHandle); + DestroyWindow(windowHandle); + Assert.Equal(longText, result); } - private class ChangeWindowTextForm : Form + private class ChangeWindowTextClass : WindowClass { public Func BeforeGetTextLengthCallback { @@ -62,26 +58,27 @@ public Func BeforeGetTextCallback set; } - protected override void WndProc(ref Message m) + protected override IntPtr WNDPROC(IntPtr hWnd, WM msg, IntPtr wParam, IntPtr lParam) { - if (m.Msg == (int)WM.GETTEXTLENGTH) - { - string text = BeforeGetTextLengthCallback?.Invoke(); - if (text != null) - { - SetWindowTextW(m.HWnd, text); - } - } - else if (m.Msg == (int)WM.GETTEXT) + switch (msg) { - string text = BeforeGetTextCallback?.Invoke(); - if (text != null) - { - SetWindowTextW(m.HWnd, text); - } + case WM.GETTEXTLENGTH: + string text = BeforeGetTextLengthCallback?.Invoke(); + if (text != null) + { + SetWindowTextW(hWnd, text); + } + break; + case WM.GETTEXT: + text = BeforeGetTextCallback?.Invoke(); + if (text != null) + { + SetWindowTextW(hWnd, text); + } + break; } - base.WndProc(ref m); + return base.WNDPROC(hWnd, msg, wParam, lParam); } } } diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/Cursor.cs b/src/System.Windows.Forms/src/System/Windows/Forms/Cursor.cs index 1a141ab52bd..1943d9f0e90 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/Cursor.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/Cursor.cs @@ -40,7 +40,7 @@ internal Cursor(int nResourceId) // We don't delete stock cursors. _ownHandle = false; _resourceId = nResourceId; - _handle = User32.LoadCursorW(IntPtr.Zero, nResourceId); + _handle = User32.LoadCursorW(IntPtr.Zero, (IntPtr)nResourceId); } /// diff --git a/src/System.Windows.Forms/src/System/Windows/Forms/NativeWindow.cs b/src/System.Windows.Forms/src/System/Windows/Forms/NativeWindow.cs index 4a31d8525f7..24bb15f192c 100644 --- a/src/System.Windows.Forms/src/System/Windows/Forms/NativeWindow.cs +++ b/src/System.Windows.Forms/src/System/Windows/Forms/NativeWindow.cs @@ -454,10 +454,10 @@ public virtual void CreateHandle(CreateParams cp) } createResult = User32.CreateWindowExW( - cp.ExStyle, + (User32.WS_EX)cp.ExStyle, windowClass._windowClassName, cp.Caption, - cp.Style, + (User32.WS)cp.Style, cp.X, cp.Y, cp.Width, From fbfbcd58b419ccba78ecd153d9a6f18d59f05688 Mon Sep 17 00:00:00 2001 From: Jeremy Kuhne Date: Wed, 25 Mar 2020 10:48:07 -0700 Subject: [PATCH 2/2] Fix test breaks. --- .../src/WindowClass.cs | 2 +- .../src/Interop/User32/Interop.LoadCursorW.cs | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs b/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs index 1e793c3ad43..1e603fd05d4 100644 --- a/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs +++ b/src/Common/tests/InternalUtilitiesForTests/src/WindowClass.cs @@ -80,7 +80,7 @@ public unsafe WindowClass( if (cursor == default) { - cursor = User32.LoadCursorW(IntPtr.Zero, User32.CursorResourceId.IDC_ARROW); + cursor = User32.LoadCursorW(IntPtr.Zero, (IntPtr)User32.CursorResourceId.IDC_ARROW); } else if (cursor == (IntPtr)(-1)) { diff --git a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs index 00fe1b42da7..e95d26edf2d 100644 --- a/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs +++ b/src/System.Windows.Forms.Primitives/src/Interop/User32/Interop.LoadCursorW.cs @@ -9,22 +9,27 @@ internal static partial class Interop { internal static partial class User32 { + // The Cursor class has an IntPtr constructor that takes a handle + // to an existing cursor. The int constructor does the LoadCursorW + // call. To avoid accidental use of the IntPtr constructor this + // set of defines should be left as int, even though they ultimately + // need converted to IntPtr. public static class CursorResourceId { - public static IntPtr IDC_ARROW = (IntPtr)32512; - public static IntPtr IDC_IBEAM = (IntPtr)32513; - public static IntPtr IDC_WAIT = (IntPtr)32514; - public static IntPtr IDC_CROSS = (IntPtr)32515; - public static IntPtr IDC_SIZEALL = (IntPtr)32646; - public static IntPtr IDC_SIZENWSE = (IntPtr)32642; - public static IntPtr IDC_SIZENESW = (IntPtr)32643; - public static IntPtr IDC_SIZEWE = (IntPtr)32644; - public static IntPtr IDC_SIZENS = (IntPtr)32645; - public static IntPtr IDC_UPARROW = (IntPtr)32516; - public static IntPtr IDC_NO = (IntPtr)32648; - public static IntPtr IDC_HAND = (IntPtr)32649; - public static IntPtr IDC_APPSTARTING = (IntPtr)32650; - public static IntPtr IDC_HELP = (IntPtr)32651; + public const int IDC_ARROW = 32512; + public const int IDC_IBEAM = 32513; + public const int IDC_WAIT = 32514; + public const int IDC_CROSS = 32515; + public const int IDC_SIZEALL = 32646; + public const int IDC_SIZENWSE = 32642; + public const int IDC_SIZENESW = 32643; + public const int IDC_SIZEWE = 32644; + public const int IDC_SIZENS = 32645; + public const int IDC_UPARROW = 32516; + public const int IDC_NO = 32648; + public const int IDC_HAND = 32649; + public const int IDC_APPSTARTING = 32650; + public const int IDC_HELP = 32651; } [DllImport(Libraries.User32, ExactSpelling = true)]