演练:在 Windows Presentation Foundation 应用程序中承载简单 Wind32 控件

更新:2007 年 11 月

Windows Presentation Foundation (WPF) 提供用于创建应用程序的丰富环境。但是,如果您对 <token>TLA#tla_win32</token> 代码的投入较大,那么更有效的办法是在您的 <token>TLA2#tla_winclient</token> 应用程序中至少重用这些代码的一部分,而不是完全重新编写应用程序。WPF 提供了一种用于在 WPF 页中承载 Win32 窗口的简单机制。

本教程引导您完成创建应用程序在 Windows Presentation Foundation 中承载 Win32 ListBox 控件的示例的过程,该应用程序承载了一个 Win32 列表框控件。可以对该一般性过程进行扩展以承载任何 Win32 窗口。

本主题包括下列各节。

  • 要求
  • 基本过程
  • 实现页布局
  • 实现类以承载 Microsoft Win32 控件
  • 在页上承载控件
  • 在控件和页之间实现通信
  • 相关主题

要求

本教程假定您基本熟悉 WPF 和 Win32 编程。有关 WPF 编程的基本介绍,请参见 入门 (WPF)。有关 Win32 编程的介绍,请参见关于此主题的众多书籍中的任何一本,特别推荐由 Charles Petzold 编写的 Programming Windows(《Windows 编程》)。

因为本教程附带的示例是用 C# 实现的,所以该示例利用平台调用服务 (PInvoke) 来访问 Win32 API。稍微熟悉一下 PInvoke 是有用的,但不是必需的。

说明:

本教程包含关联示例中的一些代码示例。但是,出于可读性目的,本教程并不包含完整的示例代码。您可以在在 Windows Presentation Foundation 中承载 Win32 ListBox 控件的示例中获得或查看完整代码。

基本过程

本节概述在 WPF 页中承载 Win32 窗口的基本过程。其余各节将详细讨论每个步骤。

基本承载过程是:

  1. 实现一个用来承载窗口的 WPF 页。一种方法是创建一个 Border 元素,以便为所承载的窗口保留该页的一部分。

  2. 实现一个从 HwndHost 继承的类以承载控件。

  3. 在该类中,重写 HwndHost 类成员 BuildWindowCore

  4. 将所承载的窗口创建为包含 WPF 页的窗口的子窗口。尽管传统 WPF 编程不需要显式利用宿主页,但需要指出的是,宿主页是一个带有句柄 (HWND) 的窗口。可以通过 BuildWindowCore 方法的 hwndParent 参数接收页 HWND。应该将所承载的窗口创建为此 HWND 的子窗口。

  5. 在创建宿主窗口之后,返回所承载的窗口的 HWND。如果要承载一个或多个 Win32 控件,通常需要将宿主窗口创建为该 HWND 的子窗口,并且使这些控件成为该宿主窗口的子窗口。通过将控件包装在宿主窗口中,可以提供一种让 WPF 页从这些控件接收通知的简单方式,该方式可以处理一些与跨 HWND 边界的通知有关的特定 Win32 问题。

  6. 处理发送到宿主窗口的选定消息,例如,来自子控件的通知。有两种方法可以实现此目的。

    • 如果您希望在宿主类中处理消息,请重写 HwndHost 类的 WndProc 方法。

    • 如果您希望让 WPF 处理消息,请在代码隐藏文件中处理 HwndHostMessageHook 事件。对于所承载的窗口收到的每个消息,都将发生此事件。如果您选择此选项,仍然必须重写 WndProc,但只需要最小实现。

  7. 重写 HwndHostDestroyWindowCoreWndProc 方法。必须重写这些方法以满足 HwndHost 协定的要求,但可能只需要提供最小实现。

  8. 在代码隐藏文件中,创建控件宿主类的一个实例,并使其成为用于承载窗口的 Border 元素的子元素。

  9. 通过向所承载的窗口发送 Microsoft Windows 消息以及处理来自其子窗口的消息(例如由控件发送的通知),与该窗口进行通信。

实现页布局

承载 ListBox 控件的 WPF 页的布局由两个区域组成。该页的左侧承载了几个 WPF 控件,这些控件提供了一个 用户界面 (UI) 以使您可以操作 Win32 控件。该页的右上角具有一个正方形区域,用于放置所承载的 ListBox 控件。

用于实现此布局的代码非常简单。根元素是一个 DockPanel,它具有两个子元素。第一个子元素是一个用于承载 ListBox 控件的 Border 元素。它在该页的右上角占据了一个大小为 200x200 的正方形。第二个子元素是一个 StackPanel 元素,它包含一组 WPF 控件,这些控件显示信息,并使您可以通过设置已公开的互操作属性来操作 ListBox 控件。对于 StackPanel 的每个子元素,请参见有关所使用的各种元素的参考资料,以了解有关这些元素是什么以及它们有哪些功能的详细信息。下面的示例代码中列出了这些元素,但这里不对其进行说明(基本互操作模型不需要它们中的任何一个,提供它们的目的是为该示例增加一些交互性)。

<Window
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  x:Class="WPF_Hosting_Win32_Control.HostWindow"
  Name="mainWindow"
  Loaded="On_UIReady">

  <DockPanel Background="LightGreen">
    <Border Name="ControlHostElement"
    Width="200"
    Height="200"
    HorizontalAlignment="Right"
    VerticalAlignment="Top"
    BorderBrush="LightGray"
    BorderThickness="3"
    DockPanel.Dock="Right"/>
    <StackPanel>
      <Label HorizontalAlignment="Center"
        Margin="0,10,0,0"
        FontSize="14"
        FontWeight="Bold">Control the Control</Label>
      <TextBlock Margin="10,10,10,10" >Selected Text: <TextBlock  Name="selectedText"/></TextBlock>
      <TextBlock Margin="10,10,10,10" >Number of Items: <TextBlock  Name="numItems"/></TextBlock>

      <Line X1="0" X2="200"
        Stroke="LightYellow"
        StrokeThickness="2"
        HorizontalAlignment="Center"
        Margin="0,20,0,0"/>

      <Label HorizontalAlignment="Center"
        Margin="10,10,10,10">Append an Item to the List</Label>
      <StackPanel Orientation="Horizontal">
        <Label HorizontalAlignment="Left"
          Margin="10,10,10,10">Item Text</Label>
        <TextBox HorizontalAlignment="Left"
          Name="txtAppend"
          Width="200"
          Margin="10,10,10,10"></TextBox>
      </StackPanel>

      <Button HorizontalAlignment="Left"
        Click="AppendText"
        Width="75"
        Margin="10,10,10,10">Append</Button>

      <Line X1="0" X2="200"
        Stroke="LightYellow"
        StrokeThickness="2"
        HorizontalAlignment="Center"
        Margin="0,20,0,0"/>

      <Label HorizontalAlignment="Center"
        Margin="10,10,10,10">Delete the Selected Item</Label>

      <Button Click="DeleteText"
        Width="125"
        Margin="10,10,10,10"
        HorizontalAlignment="Left">Delete</Button>
    </StackPanel>
  </DockPanel>
</Window>  

实现类以承载 Microsoft Win32 控件

本示例的核心是实际承载控件的类 — ControlHost.cs。它继承自 HwndHost。构造函数接受两个参数 — height 和 width,它们分别对应于承载 ListBox 控件的 Border 元素的高度和宽度。这些值将在以后用于确保控件的大小与 Border 元素相匹配。

public class ControlHost : HwndHost
{
  IntPtr hwndControl;
  IntPtr hwndHost;
  int hostHeight, hostWidth;

  public ControlHost(double height, double width)
  {
    hostHeight = (int)height;
    hostWidth = (int)width;
  }

还有一组常量。这些常量主要取自 Winuser.h,它们使您可以在调用 Win32 函数时使用传统名称。

internal const int
  WS_CHILD = 0x40000000,
  WS_VISIBLE = 0x10000000,
  LBS_NOTIFY = 0x00000001,
  HOST_ID = 0x00000002,
  LISTBOX_ID = 0x00000001,
  WS_VSCROLL = 0x00200000,
  WS_BORDER = 0x00800000;

重写 BuildWindowCore 以创建 Microsoft Win32 窗口

重写此方法以创建将由页承载的 Win32 窗口,并且在窗口和页之间建立连接。因为本示例涉及到承载 ListBox 控件,所以创建了两个窗口。第一个窗口是由 WPF 页实际承载的窗口。ListBox 控件被创建为该窗口的子窗口。

采用此方法的原因是为了简化从控件接收通知的过程。使用 HwndHost 类可以处理发送到它所承载的窗口的消息。如果直接承载 Win32 控件,您将收到被发送到该控件的内部消息循环的消息。您可以显示控件并向其发送消息,但您不会收到该控件发送到它的父窗口的通知。这意味着很多事情,其中一件事情便是您没有办法检测用户何时与该控件进行交互。可改为创建一个宿主窗口,并使控件成为该窗口的子窗口。这使您可以处理宿主窗口的消息,包括由控件发送给它的通知。既然宿主窗口只是控件的简单包装而已,那么为了方便起见,以后将把该包装称为 ListBox 控件。

创建宿主窗口和 ListBox 控件

使用 PInvoke 可以创建控件的宿主窗口,方法是创建并注册一个窗口类,然后执行其他相关操作。但是,一种简单得多的方法是用预定义的“静态”窗口类创建一个窗口。这将为您提供所需的窗口过程,以便从控件接收通知,并且只需完成最少的编码工作。

控件的 HWND 通过一个只读属性公开,以便宿主页可以使用它向控件发送消息。

public IntPtr hwndListBox
{
  get { return hwndControl; }
}

ListBox 控件被创建为宿主窗口的子窗口。这两个窗口的高度和宽度被设置为传递给构造函数的值,这在前面进行了讨论。这可以确保宿主窗口和控件的大小与页上的保留区域相同。 在创建窗口之后,该示例将返回一个 HandleRef 对象,其中包含宿主窗口的 HWND。

protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
  hwndControl = IntPtr.Zero;
  hwndHost = IntPtr.Zero;

  hwndHost = CreateWindowEx(0, "static", "",
                            WS_CHILD | WS_VISIBLE,
                            0, 0,
                            hostHeight, hostWidth,
                            hwndParent.Handle,
                            (IntPtr)HOST_ID,
                            IntPtr.Zero,
                            0);

  hwndControl = CreateWindowEx(0, "listbox", "",
                                WS_CHILD | WS_VISIBLE | LBS_NOTIFY
                                  | WS_VSCROLL | WS_BORDER,
                                0, 0,
                                hostHeight, hostWidth,
                                hwndHost,
                                (IntPtr) LISTBOX_ID,
                                IntPtr.Zero,
                                0);

  return new HandleRef(this, hwndHost);
}
//PInvoke declarations
[DllImport("user32.dll", EntryPoint = "CreateWindowEx", CharSet = CharSet.Unicode)]
internal static extern IntPtr CreateWindowEx(int dwExStyle,
                                              string lpszClassName,
                                              string lpszWindowName,
                                              int style,
                                              int x, int y,
                                              int width, int height,
                                              IntPtr hwndParent,
                                              IntPtr hMenu,
                                              IntPtr hInst,
                                              [MarshalAs(UnmanagedType.AsAny)] object pvParam);

实现 DestroyWindow 和 WndProc

除了 BuildWindowCore 以外,您还必须重写 HwndHostWndProcDestroyWindowCore 方法。在本示例中,控件的消息由 MessageHook 处理程序处理,因而 WndProcDestroyWindowCore 的实现是最小实现。对于 WndProc,请将 handled 设置为 false 以指示消息未处理,并且返回 0。对于 DestroyWindowCore,只需简单地销毁窗口。

protected override IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
  handled = false;
  return IntPtr.Zero;
}

protected override void DestroyWindowCore(HandleRef hwnd)
{
  DestroyWindow(hwnd.Handle);
}
[DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)]
internal static extern bool DestroyWindow(IntPtr hwnd);

在页上承载控件

若要在页上承载控件,首先需要创建 ControlHost 类的新实例。将包含该控件的边框元素 (ControlHostElement) 的高度和宽度传递给 ControlHost 构造函数。这可以确保 ListBox 具有正确的大小。然后,通过将 ControlHost 对象赋给宿主 BorderChild 属性,在页上承载该控件。

本示例将一个处理程序附加到 ControlHost 的 MessageHook 事件,以便从控件接收消息。对于发送到所承载的窗口的每个消息,都将引发此事件。在此情况下,这些消息是发送给包装了实际 ListBox 控件的窗口的消息,包括来自控件的通知。本示例调用 SendMessage 以便从控件获取信息以及修改它的内容。下一节将详细讨论页如何与控件进行通信。

说明:

请注意,SendMessage 有两个 PInvoke 声明。这是因为其中一个声明使用 wParam 参数传递字符串,而另一个声明使用该参数传递整数。对于每个签名,都需要一个单独的声明,以确保正确封送数据。

 public partial class HostWindow : Window
    {
    int selectedItem;
    IntPtr hwndListBox;
    ControlHost listControl;
    Application app;
    Window myWindow;
    int itemCount;

    private void On_UIReady(object sender, EventArgs e)
    {
      app = System.Windows.Application.Current;
      myWindow = app.MainWindow;
      myWindow.SizeToContent = SizeToContent.WidthAndHeight;
      listControl = new ControlHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth);
      ControlHostElement.Child = listControl;
      listControl.MessageHook += new HwndSourceHook(ControlMsgFilter);
      hwndListBox = listControl.hwndListBox;
      for (int i = 0; i < 15; i++) //populate listbox
      {
        string itemText = "Item" + i.ToString();
        SendMessage(hwndListBox, LB_ADDSTRING, IntPtr.Zero, itemText);
      }
      itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero);
      numItems.Text = "" +  itemCount.ToString();
    }
private IntPtr ControlMsgFilter(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
  int textLength;

  handled = false;
  if (msg == WM_COMMAND)
  {
    switch ((uint)wParam.ToInt32() >> 16 & 0xFFFF) //extract the HIWORD
    {
      case LBN_SELCHANGE : //Get the item text and display it
        selectedItem = SendMessage(listControl.hwndListBox, LB_GETCURSEL, IntPtr.Zero, IntPtr.Zero);
        textLength = SendMessage(listControl.hwndListBox, LB_GETTEXTLEN, IntPtr.Zero, IntPtr.Zero);
        StringBuilder itemText = new StringBuilder();
        SendMessage(hwndListBox, LB_GETTEXT, selectedItem, itemText);
        selectedText.Text = itemText.ToString();
        handled = true;
        break;
    }
  }
  return IntPtr.Zero;
}
internal const int
  LBN_SELCHANGE = 0x00000001,
  WM_COMMAND = 0x00000111,
  LB_GETCURSEL = 0x00000188,
  LB_GETTEXTLEN = 0x0000018A,
  LB_ADDSTRING = 0x00000180, 
  LB_GETTEXT = 0x00000189,
  LB_DELETESTRING = 0x00000182,
  LB_GETCOUNT = 0x0000018B;

[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Unicode)]
internal static extern int SendMessage(IntPtr hwnd,
                                       int msg,
                                       IntPtr wParam,
                                       IntPtr lParam);

[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Unicode)]
internal static extern int SendMessage(IntPtr hwnd,
                                       int msg,
                                       int wParam, 
                                       [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lParam);

[DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Unicode)]
internal static extern IntPtr SendMessage(IntPtr hwnd,
                                          int msg,
                                          IntPtr wParam,
                                          String lParam);

在控件和页之间实现通信

可以通过向控件发送 Windows 消息来操作控件。当用户与控件交互时,控件会通过向其宿主窗口发送通知来通知您。在 Windows Presentation Foundation 中承载 Win32 ListBox 控件的示例示例包含一个 UI,它提供了几个对这一机制的工作方式进行演示的示例。

  • 向列表中追加项。

  • 从列表中删除选定项。

  • 显示当前选定项的文本。

  • 显示列表中的项数。

用户还可以通过单击列表中的项来选择它,就像在传统 Win32 应用程序中一样。每当用户通过选择、添加或追加项来更改列表框的状态时,都将更新所显示的数据。

若要追加项,请向列表框发送一个 LB_ADDSTRING 消息。若要删除项,请发送 LB_GETCURSEL 以获取当前选定内容的索引,然后发送 LB_DELETESTRING 以删除该项。本示例还发送 LB_GETCOUNT,并且使用返回的值更新所显示的项数。这两个 SendMessage 实例都使用上一节中讨论的 PInvoke 声明之一。

private void AppendText(object sender, EventArgs args)
{
  if (txtAppend.Text != string.Empty)
  {
    SendMessage(hwndListBox, LB_ADDSTRING, IntPtr.Zero, txtAppend.Text);
  }
  itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero);
  numItems.Text = "" + itemCount.ToString();
}
private void DeleteText(object sender, EventArgs args)
{
  selectedItem = SendMessage(listControl.hwndListBox, LB_GETCURSEL, IntPtr.Zero, IntPtr.Zero);
  if (selectedItem != -1) //check for selected item
  {
    SendMessage(hwndListBox, LB_DELETESTRING, (IntPtr)selectedItem, IntPtr.Zero);
  }
  itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero);
  numItems.Text = "" + itemCount.ToString();
}

当选择项的用户更改其选择时,控件将通过向宿主窗口发送 WM_COMMAND 消息来通知该窗口,而该消息将为页面引发 MessageHook 事件。处理程序与宿主窗口的主窗口过程收到相同的信息。它还会传递对布尔值 handled 的引用。通过将 handled 设置为 true 可以指示您已经处理该消息并且无需进行其他处理。

发送 WM_COMMAND 的原因多种多样,因此您必须检查通知 ID 以确定它是否是您希望处理的事件。该 ID 包含在 wParam 参数的高位字中。因为 Microsoft .NET 不具有 HIWORD 宏,所以本示例使用按位运算符来提取该 ID。如果用户已经进行选择或更改选择,该 ID 将为 LBN_SELCHANGE。

在收到 LBN_SELCHANGE 之后,本示例将通过向控件发送一个 LB_GETCURSEL 消息来获取选定项的索引。若要获取文本,首先需要创建一个 StringBuilder。然后,向控件发送一个 LB_GETTEXT 消息。将空的 StringBuilder 对象作为 wParam 参数传递。当 SendMessage 返回时,StringBuilder 将包含选定项的文本。此处对 SendMessage 的使用还需要另外一个 PInvoke 声明。

最后,将 handled 设置为 true 以指示该消息已得到处理。下面的代码再次强调了 ControlMsgFilter 方法的使用,上述行为就是在此方法中实现的。

private IntPtr ControlMsgFilter(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
  int textLength;

  handled = false;
  if (msg == WM_COMMAND)
  {
    switch ((uint)wParam.ToInt32() >> 16 & 0xFFFF) //extract the HIWORD
    {
      case LBN_SELCHANGE : //Get the item text and display it
        selectedItem = SendMessage(listControl.hwndListBox, LB_GETCURSEL, IntPtr.Zero, IntPtr.Zero);
        textLength = SendMessage(listControl.hwndListBox, LB_GETTEXTLEN, IntPtr.Zero, IntPtr.Zero);
        StringBuilder itemText = new StringBuilder();
        SendMessage(hwndListBox, LB_GETTEXT, selectedItem, itemText);
        selectedText.Text = itemText.ToString();
        handled = true;
        break;
    }
  }
  return IntPtr.Zero;
}

请参见

概念

WPF 和 Win32 互操作概述

Windows Presentation Foundation 入门

参考

HwndHost