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..1e603fd05d4 --- /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, (IntPtr)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..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,6 +9,11 @@ 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 const int IDC_ARROW = 32512; @@ -28,6 +33,6 @@ public static class CursorResourceId } [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,