Windows Presentation Foundation (WPF)提供了用于创建应用程序的丰富环境。 但是,当你对 Win32 代码进行大量投资时,在 WPF 应用程序中至少重用部分代码而不是完全重写代码可能更有效。 WPF 提供了在 WPF 页面上托管 Win32 窗口的简单机制。
本主题将引导你了解应用程序在 WPF 示例中托管 Win32 列表框控件。 此常规过程可以扩展到托管任何 Win32 窗口。
要求
本主题假定基本熟悉 WPF 和 Windows API 编程。 有关 WPF 编程的基本简介,请参阅 入门。 有关 Windows API 编程的简介,请参阅有关该主题的任何书籍,特别是 Charles Petzold 的 Windows 编程 。
由于本主题附带的示例在 C# 中实现,因此它利用平台调用服务(PInvoke)来访问 Windows API。 对 PInvoke 的熟悉很有帮助,但不是必需的。
注释
本主题包含关联示例中的许多代码示例。 但是,为了提高可读性,它不包括完整的示例代码。 可以从 WPF 示例中托管 Win32 ListBox 控件获取或查看完整代码。
基本过程
本部分概述了在 WPF 页面上托管 Win32 窗口的基本过程。 其余部分将详细介绍每个步骤。
基本托管过程为:
实现 WPF 页以托管窗口。 一种方法是创建一个 Border 元素来保留托管窗口的页面部分。
实现一个类来承载继承自 HwndHost的控件。
在该类中,重写 HwndHost 类成员 BuildWindowCore。
将托管窗口创建为包含 WPF 页面的窗口的子窗口。 尽管传统的 WPF 编程不需要显式使用它,但宿主页是一个具有句柄的窗口(HWND)。 您通过BuildWindowCore方法的
hwndParent
参数接收页面 HWND。 托管窗口应作为此 HWND 的子窗口创建。创建主机窗口后,返回托管窗口的 HWND。 如果要托管一个或多个 Win32 控件,通常会创建一个宿主窗口作为 HWND 的子窗口,并使这些控件成为该宿主窗口的子级。 将控件包装在宿主窗口中为 WPF 页面提供了一种简单的方法,可以从控件接收通知,以处理跨 HWND 边界的某些特定的 Win32 通知问题。
处理发送到主机窗口的选定消息,例如来自子控件的通知。 可通过两种方式来执行此操作。
如果希望 WPF 处理消息,请在后置代码中处理 HwndHost 类 MessageHook 事件。 对于托管窗口收到的每条消息,都会发生此事件。 如果选择此选项,则仍必须重写 WndProc,但只需要最少的实现。
重写HwndHost的DestroyWindowCore和WndProc方法。 必须重写这些方法才能满足 HwndHost 协定,但可能只需要提供最少的实现。
在代码隐藏文件中,创建控件宿主类的实例,并将其设置为要承载窗口的 Border 元素的子级。
通过向其发送Microsoft Windows 消息以及处理其子窗口中的消息(例如控件发送的通知)来与托管窗口通信。
实现页面布局
承载 ListBox 控件的 WPF 页面的布局由两个区域组成。 页面左侧托管了多个 WPF 控件,这些控件提供了用户界面(UI),使您可以操作 Win32 控件。 页面右上角有托管的 ListBox 控件的正方形区域。
实现此布局的代码非常简单。 根元素是具有两个 DockPanel 子元素的元素。 第一个 Border 是承载 ListBox 控件的元素。 它占据页面右上角的 200x200 正方形。 第二个 StackPanel 元素包含一组 WPF 控件,这些控件显示信息,并允许通过设置公开的互操作属性来操作 ListBox 控件。 对于作为子元素 StackPanel的每个元素,请参阅各种元素的参考材料,这些元素用于详细了解这些元素是什么或它们的作用,这些元素在下面的示例代码中列出,但此处不会解释(基本互作模型不需要其中任何元素,它们提供它们以向示例添加一些交互性)。
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://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. 构造函数采用两个参数,高度和宽度,对应于承载 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;
}
Public Class ControlHost
Inherits HwndHost
Private hwndControl As IntPtr
Private hwndHost As IntPtr
Private hostHeight, hostWidth As Integer
Public Sub New(ByVal height As Double, ByVal width As Double)
hostHeight = CInt(height)
hostWidth = CInt(width)
End Sub
还有一组常量。 这些常量主要取自 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;
Friend Const WS_CHILD As Integer = &H40000000, WS_VISIBLE As Integer = &H10000000, LBS_NOTIFY As Integer = &H00000001, HOST_ID As Integer = &H00000002, LISTBOX_ID As Integer = &H00000001, WS_VSCROLL As Integer = &H00200000, WS_BORDER As Integer = &H00800000
重写 BuildWindowCore 以创建 Microsoft Win32 窗口
重写此方法以创建由页面托管的 Win32 窗口,并在窗口和页面之间建立连接。 由于此示例涉及托管 ListBox 控件,因此会创建两个窗口。 第一个窗口实际上是 WPF 页面托管的窗口。 该窗口的子窗口中创建了 ListBox 控件。
此方法的原因是简化从控件接收通知的过程。 该 HwndHost 类允许你处理发送到其托管窗口的消息。 如果直接托管 Win32 控件,则会收到发送到该控件的内部消息循环的消息。 可以显示控件并向其发送消息,但不会收到控件发送到其父窗口的通知。 这意味着,除了其他方面,你无法检测用户何时与控件交互。 相反,请创建一个宿主窗口,并将控件设为该窗口的子控件。 这样,你可以处理主机窗口的消息,包括控件发送给它的通知。 为方便起见,由于主机窗口只是控件的简单包装器,因此包将称为 ListBox 控件。
创建主机窗口和 ListBox 控件
可以通过创建和注册窗口类等,使用 PInvoke 为控件创建主机窗口。 但是,一种更简单的方法是使用预定义的“静态”窗口类创建窗口。 这提供了从控件接收通知所需的窗口过程,并且需要最少的编码。
控件的 HWND 通过只读属性公开,以便主机页可以使用它向控件发送消息。
public IntPtr hwndListBox
{
get { return hwndControl; }
}
Public ReadOnly Property hwndListBox() As IntPtr
Get
Return hwndControl
End Get
End Property
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,
hostWidth, hostHeight,
hwndParent.Handle,
(IntPtr)HOST_ID,
IntPtr.Zero,
0);
hwndControl = CreateWindowEx(0, "listbox", "",
WS_CHILD | WS_VISIBLE | LBS_NOTIFY
| WS_VSCROLL | WS_BORDER,
0, 0,
hostWidth, hostHeight,
hwndHost,
(IntPtr) LISTBOX_ID,
IntPtr.Zero,
0);
return new HandleRef(this, hwndHost);
}
Protected Overrides Function BuildWindowCore(ByVal hwndParent As HandleRef) As HandleRef
hwndControl = IntPtr.Zero
hwndHost = IntPtr.Zero
hwndHost = CreateWindowEx(0, "static", "", WS_CHILD Or WS_VISIBLE, 0, 0, hostWidth, hostHeight, hwndParent.Handle, New IntPtr(HOST_ID), IntPtr.Zero, 0)
hwndControl = CreateWindowEx(0, "listbox", "", WS_CHILD Or WS_VISIBLE Or LBS_NOTIFY Or WS_VSCROLL Or WS_BORDER, 0, 0, hostWidth, hostHeight, hwndHost, New IntPtr(LISTBOX_ID), IntPtr.Zero, 0)
Return New HandleRef(Me, hwndHost)
End Function
//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);
'PInvoke declarations
<DllImport("user32.dll", EntryPoint := "CreateWindowEx", CharSet := CharSet.Unicode)>
Friend Shared Function CreateWindowEx(ByVal dwExStyle As Integer, ByVal lpszClassName As String, ByVal lpszWindowName As String, ByVal style As Integer, ByVal x As Integer, ByVal y As Integer, ByVal width As Integer, ByVal height As Integer, ByVal hwndParent As IntPtr, ByVal hMenu As IntPtr, ByVal hInst As IntPtr, <MarshalAs(UnmanagedType.AsAny)> ByVal pvParam As Object) As IntPtr
End Function
实现 DestroyWindow 和 WndProc
此外 BuildWindowCore,你还必须重写WndProc和DestroyWindowCore方法以及HwndHost方法。 在此示例中,控件的消息由 MessageHook 事件处理程序处理,因此对 WndProc 和 DestroyWindowCore 的实现需求很少。 在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);
}
Protected Overrides Function WndProc(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr, ByRef handled As Boolean) As IntPtr
handled = False
Return IntPtr.Zero
End Function
Protected Overrides Sub DestroyWindowCore(ByVal hwnd As HandleRef)
DestroyWindow(hwnd.Handle)
End Sub
[DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)]
internal static extern bool DestroyWindow(IntPtr hwnd);
<DllImport("user32.dll", EntryPoint := "DestroyWindow", CharSet := CharSet.Unicode)>
Friend Shared Function DestroyWindow(ByVal hwnd As IntPtr) As Boolean
End Function
在页面上托管控件
若要在页面上托管控件,请先创建类的新实例 ControlHost
。 将包含控件 (ControlHostElement
) 的边框元素的高度和宽度传递给 ControlHost
构造函数。 这可确保 ListBox 的大小正确。 然后,通过将对象Child分配给ControlHost
主机Border的属性,在页面上托管控件。
该示例将处理程序附加到 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();
}
Partial Public Class HostWindow
Inherits Window
Private selectedItem As Integer
Private hwndListBox As IntPtr
Private listControl As ControlHost
Private app As Application
Private myWindow As Window
Private itemCount As Integer
Private Sub On_UIReady(ByVal sender As Object, ByVal e As EventArgs)
app = System.Windows.Application.Current
myWindow = app.MainWindow
myWindow.SizeToContent = SizeToContent.WidthAndHeight
listControl = New ControlHost(ControlHostElement.ActualHeight, ControlHostElement.ActualWidth)
ControlHostElement.Child = listControl
AddHandler listControl.MessageHook, AddressOf ControlMsgFilter
hwndListBox = listControl.hwndListBox
For i As Integer = 0 To 14 'populate listbox
Dim itemText As String = "Item" & i.ToString()
SendMessage(hwndListBox, LB_ADDSTRING, IntPtr.Zero, itemText)
Next i
itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero)
numItems.Text = "" & itemCount.ToString()
End Sub
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);
Private Function ControlMsgFilter(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr, ByRef handled As Boolean) As IntPtr
Dim textLength As Integer
handled = False
If msg = WM_COMMAND Then
Select Case CUInt(wParam.ToInt32()) >> 16 And &HFFFF '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)
Dim itemText As New StringBuilder()
SendMessage(hwndListBox, LB_GETTEXT, selectedItem, itemText)
selectedText.Text = itemText.ToString()
handled = True
End Select
End If
Return IntPtr.Zero
End Function
Friend Const LBN_SELCHANGE As Integer = &H1, WM_COMMAND As Integer = &H111, LB_GETCURSEL As Integer = &H188, LB_GETTEXTLEN As Integer = &H18A, LB_ADDSTRING As Integer = &H180, LB_GETTEXT As Integer = &H189, LB_DELETESTRING As Integer = &H182, LB_GETCOUNT As Integer = &H18B
<DllImport("user32.dll", EntryPoint:="SendMessage", CharSet:=CharSet.Unicode)>
Friend Shared Function SendMessage(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Integer
End Function
<DllImport("user32.dll", EntryPoint:="SendMessage", CharSet:=CharSet.Unicode)>
Friend Shared Function SendMessage(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As Integer, <MarshalAs(UnmanagedType.LPWStr)> ByVal lParam As StringBuilder) As Integer
End Function
<DllImport("user32.dll", EntryPoint:="SendMessage", CharSet:=CharSet.Unicode)>
Friend Shared Function SendMessage(ByVal hwnd As IntPtr, ByVal msg As Integer, ByVal wParam As IntPtr, ByVal lParam As String) As IntPtr
End Function
实现控件与页面之间的通信
通过向控件发送 Windows 消息来操控它。 控件通过向主机窗口发送通知来通知您用户何时与其交互。 托管 Win32 ListBox 控件的 WPF 实例包括一个用户界面,它提供了几个展示该功能如何实现的示例。
将项追加到列表中。
从列表中删除所选项
显示当前选中项的文本。
显示列表中的项数。
用户还可以单击列表框中的项目,就像常规 Win32 应用程序一样。 每次用户通过选择、添加或追加项来更改列表框的状态时,都会更新显示的数据。
若要追加项,请向列表框发送消息LB_ADDSTRING
。 若要删除项目,请发送 LB_GETCURSEL
以获取当前所选内容的索引,然后 LB_DELETESTRING
删除该项。 该示例还会发送 LB_GETCOUNT
,并使用返回的值更新显示物品数量。 这两个实例 SendMessage
使用了上一部分中讨论的 PInvoke 声明之一。
private void AppendText(object sender, EventArgs args)
{
if (!string.IsNullOrEmpty(txtAppend.Text))
{
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();
}
Private Sub AppendText(ByVal sender As Object, ByVal args As EventArgs)
If txtAppend.Text <> String.Empty Then
SendMessage(hwndListBox, LB_ADDSTRING, IntPtr.Zero, txtAppend.Text)
End If
itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero)
numItems.Text = "" & itemCount.ToString()
End Sub
Private Sub DeleteText(ByVal sender As Object, ByVal args As EventArgs)
selectedItem = SendMessage(listControl.hwndListBox, LB_GETCURSEL, IntPtr.Zero, IntPtr.Zero)
If selectedItem <> -1 Then 'check for selected item
SendMessage(hwndListBox, LB_DELETESTRING, New IntPtr(selectedItem), IntPtr.Zero)
End If
itemCount = SendMessage(hwndListBox, LB_GETCOUNT, IntPtr.Zero, IntPtr.Zero)
numItems.Text = "" & itemCount.ToString()
End Sub
当用户选择某个项或更改其选择时,控件会通过向其发送一WM_COMMAND
条消息来通知主机窗口,这会引发MessageHook页面的事件。 处理程序接收的信息与主机窗口的主窗口过程相同。 它还传递对布尔值的引用。 handled
将handled
设置为true
以指示消息已处理,无需进一步处理。
WM_COMMAND
是出于各种原因发送的,因此您必须检查通知 ID,以确定它是否是您希望处理的事件。 ID 包含在参数的高 wParam
字中。 此示例使用按位运算符提取 ID。 如果用户已做出或更改其选择,则 ID 将为 LBN_SELCHANGE
。
当收到LBN_SELCHANGE
时,该示例通过发送LB_GETCURSEL
消息给控件来获取选定项的索引。 若要获取文本,请先创建一个 StringBuilder。 然后,向控件发送LB_GETTEXT
消息。 将空 StringBuilder 对象作为 wParam
参数传递。
SendMessage
返回后,StringBuilder 将包含所选项的文本。 要使用 SendMessage
还需要一个额外的 PInvoke 声明。
最后,将handled
设置为true
以指示消息已处理。