控件的父句柄使用 p/invoke 指向 WindowsFormsParkingWindow

Control's parent handle points to WindowsFormsParkingWindow using p/invoke

考虑以下 WinApi 功能单元测试:

public class WinApiTest
{
  [TestMethod]
  public void WinApiFindFormTest_SimpleNesting()
  {
    var form = new Form();
    form.Text = @"My form";

    var button = new Button();
    button.Text = @"My button";

    form.Controls.Add(button);
    //with below line commented out, the test fails
    form.Show();

    IntPtr actualParent = WinApiTest.FindParent(button.Handle);
    IntPtr expectedParent = form.Handle;

    //below 2 lines were added for debugging purposes, they are not part of test
    //and they don't affect test results
    Debug.WriteLine("Actual: " + WinApi.GetWindowTitle(actualParent));
    Debug.WriteLine("Expected: " + WinApi.GetWindowTitle(expectedParent));

    Assert.AreEqual(actualParent, expectedParent);
  }

  //this is a method being tested
  //please assume it's located in another class
  //I'm not trying to test winapi
  public static IntPtr FindParent(IntPtr child)
  {
    while (true)
    {
      IntPtr parent = WinApi.GetParent(child);
      if (parent == IntPtr.Zero)
      {
        return child;
      }
      child = parent;
    }
  }
}

问题是要让它工作,我必须显示表格,即做 form.Show(),否则,它会失败并显示以下输出:

Actual: WindowsFormsParkingWindow
Expected: My form
Exception thrown: 'Microsoft.VisualStudio.TestTools.UnitTesting.AssertFailedException' in Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll

我读到了这个神秘的 WindowsFormsParkingWindow,它似乎只有在没有指定父项的情况下才有意义。因此,所有没有父级的控件都位于此 window 下。然而,在我的例子中,button 被明确指定为 form 控件的一部分。

问题:有什么正确的方法可以让这个测试通过吗?我正在尝试测试 FindParent 方法。本着单元测试的真正精神,任何东西都不应该突然出现在用户面前。可以执行 ShowHide 序列,但我认为这是解决问题的一种相当 hack-ish 的方法。

下面提供了 WinApi class 的代码 - 它并没有给问题增加太多价值,但如果你绝对必须看到它,就在这里(主要部分来自 this answer on SO):

public class WinApi
{
  /// <summary>
  ///  Get window title for a given IntPtr handle.
  /// </summary>
  /// <param name="handle">Input handle.</param>
  /// <remarks>
  ///  Major portition of code for below class was used from here:
  ///  
  /// </remarks>
  public static string GetWindowTitle(IntPtr handle)
  {
    if (handle == IntPtr.Zero)
    {
      throw new ArgumentNullException(nameof(handle));
    }
    int length = WinApi.SendMessageGetTextLength(handle, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
    if (length > 0 && length < int.MaxValue)
    {
      length++; // room for EOS terminator
      StringBuilder windowTitle = new StringBuilder(length);
      WinApi.SendMessageGetText(handle, WM_GETTEXT, (IntPtr)windowTitle.Capacity, windowTitle);
      return windowTitle.ToString();
    }
    return String.Empty;
  }

  const int WM_GETTEXT = 0x000D;
  const int WM_GETTEXTLENGTH = 0x000E;

  [DllImport("User32.dll", EntryPoint = "SendMessage")]
  private static extern int SendMessageGetTextLength(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
  [DllImport("User32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
  private static extern IntPtr SendMessageGetText(IntPtr hWnd, int msg, IntPtr wParam, [Out] StringBuilder lParam);
  [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)]
  public static extern IntPtr GetParent(IntPtr hWnd);
}

当您访问 Handle 属性 时,需要创建 window。 Childwindows需要有一个parentwindow,如果parentwindow还没有创建,则child window 是用停车场 window 作为其 parent 创建的。只有在创建 parent window 时,child window 才会得到 re-parented.

IntPtr actualParent = WinApiTest.FindParent(button.Handle);
IntPtr expectedParent = form.Handle;

当您访问 button.Handle 时,按钮的 window 已创建,但由于表单的 window 尚未创建,停车 window 是 parent.处理此问题的最简单方法是确保表单的 window 在按钮的 window 之前创建。确保在调用按钮句柄上的 GetParent 之前引用 form.Handle,例如,在您的测试中,您可以颠倒赋值顺序:

IntPtr expectedParent = form.Handle;
IntPtr actualParent = WinApiTest.FindParent(button.Handle);

显然,您希望对这段代码进行注释,以便将来 reader 知道赋值顺序很重要。

我确实想知道为什么你觉得有必要进行这样的测试。我无法想象这种测试会揭示您代码中的错误。