연습: Windows Presentation Foundation 응용 프로그램에서 간단한 Win32 컨트롤 호스팅
업데이트: 2007년 11월
WPF(Windows Presentation Foundation)에서는 응용 프로그램을 만들기 위한 다양한 환경을 제공합니다. 하지만 Win32 코드에 많은 투자를 한 경우에는 코드를 완전히 다시 작성하기보다는 이 코드의 최소한 일부라도 WPF 응용 프로그램에 재사용하는 것이 더 효율적일 수 있습니다. WPF에서는 WPF 페이지에 Win32 창을 호스팅하기 위한 간단한 메커니즘을 제공합니다.
이 자습서에서는 Win32 목록 상자 컨트롤을 호스팅하는 응용 프로그램인 Windows Presentation Foundation에서 Win32 ListBox 컨트롤 호스팅 샘플을 예로 들어 설명합니다. 이러한 일반 절차를 확장하여 원하는 Win32 창을 호스팅할 수 있습니다.
이 항목에는 다음 단원이 포함되어 있습니다.
- 요구 사항
- 기본 절차
- 페이지 레이아웃 구현
- Microsoft Win32 컨트롤을 호스팅할 클래스 구현
- 페이지에 컨트롤 호스팅
- 컨트롤과 페이지 간의 통신 구현
- 관련 항목
요구 사항
이 자습서에서는 사용자가 WPF 및 Win32 프로그래밍의 기본 사항에 대해 잘 알고 있다고 가정합니다. WPF 프로그래밍에 대한 소개는 시작(WPF)을 참조하십시오. Win32 프로그래밍에 대한 소개는 관련된 수많은 서적이 있지만, 특히 Charles Petzold의 Programming Windows가 도움이 될 것입니다.
이 자습서와 함께 제공되는 샘플은 C#에서 구현되기 때문에 PInvoke(Platform Invocation Services)를 사용하여 Win32API에 액세스합니다. PInvoke에 대해 알고 있으면 도움이 되겠지만 알지 못해도 상관없습니다.
참고
이 자습서에는 관련 샘플에 있는 많은 코드 예제가 포함되어 있지만 편의상 전체 샘플 코드는 제공하지 않습니다. 전체 코드는 Windows Presentation Foundation에서 Win32 ListBox 컨트롤 호스팅 샘플에서 볼 수 있습니다.
기본 절차
이 단원에서는 Win32 창을 WPF 페이지에 호스팅하기 위한 기본 절차를 개괄적으로 설명합니다. 나머지 단원에서는 각 단계를 자세히 안내합니다.
기본 호스팅 절차는 다음과 같습니다.
창을 호스팅할 WPF 페이지를 구현합니다. 이를 위한 한 가지 방법은 Border 요소를 만들어서 호스팅되는 창을 위한 페이지 부분을 확보하는 것입니다.
HwndHost에서 상속하는 컨트롤을 호스팅할 클래스를 구현합니다.
이 클래스에서 HwndHost 클래스 멤버 BuildWindowCore를 재정의합니다.
호스팅되는 창을 WPF 페이지가 포함된 창의 자식으로 만듭니다. 일반적인 WPF 프로그래밍에서는 이를 명시적으로 사용할 필요가 없지만 호스팅 페이지는 핸들(HWND)이 있는 창입니다. 페이지 HWND는 BuildWindowCore 메서드의 hwndParent 매개 변수를 통해 수신됩니다. 따라서 호스팅되는 창을 이 HWND의 자식으로 만들어야 합니다.
호스트 창을 만든 뒤에는 호스팅되는 창의 HWND를 반환합니다. 하나 이상의 Win32 컨트롤을 호스팅하려면 일반적으로 호스트 창을 HWND의 자식으로 만들고 컨트롤을 해당 호스트 창의 자식으로 만듭니다. 컨트롤을 호스트 창에 래핑하면 간단하게 WPF 페이지가 컨트롤에서 알림을 수신하도록 할 수 있습니다. 이 페이지는 HWND 경계를 넘어 전송되는 알림과 관련된 몇 가지 특정 Win32 문제를 처리합니다.
자식 컨트롤에서 전송되는 알림과 같은 호스트 창으로 전송된 선택된 메시지를 처리합니다. 이렇게 하는 데는 두 가지 방법이 있습니다.
호스팅 클래스에서 메시지를 처리하려는 경우에는 HwndHost 클래스의 WndProc 메서드를 재정의합니다.
WPF가 메시지를 처리하도록 하려면 HwndHost 클래스의 MessageHook 이벤트를 코드 숨김에서 처리합니다. 호스팅된 창이 수신하는 모든 메시지에 대해 이 이벤트가 발생합니다. 이 옵션을 선택한 경우에도 WndProc를 재정의해야 하지만 최소한의 구현만 필요합니다.
HwndHost의 DestroyWindowCore 및 WndProc 메서드를 재정의합니다. HwndHost 계약을 충족하려면 이러한 메서드를 재정의해야 하지만 최소한의 구현만 제공하면 됩니다.
코드 숨김 파일에서 컨트롤 호스팅 클래스의 인스턴스를 만들고 이를 창을 호스팅할 Border 요소의 자식으로 만듭니다.
호스팅된 창에 Microsoft Windows 메시지를 보내고 컨트롤에서 보낸 알림과 같이 자식 창에서 전송된 메시지를 처리함으로써 호스팅된 창과 통신합니다.
페이지 레이아웃 구현
ListBox 컨트롤을 호스팅하는 WPF 페이지의 레이아웃은 두 영역으로 구성됩니다. 페이지의 왼쪽에서는 Win32 컨트롤을 조작하는 데 사용할 수 있는 UI(사용자 인터페이스)를 제공하는 몇 개의 WPF 컨트롤을 호스팅합니다. 페이지의 오른쪽 위에서는 호스팅되는 ListBox 컨트롤을 위한 사각형 영역이 있습니다.
이 레이아웃을 구현하기 위한 코드는 꽤 간단합니다. 루트 요소는 두 개의 자식 요소가 있는 DockPanel입니다. 첫 번째는 ListBox 컨트롤을 호스팅하는 Border 요소입니다. 이는 페이지 오른쪽 위에 200x200 크기의 사각형으로 표시됩니다. 두 번째는 정보를 표시하며 노출된 상호 운영 속성을 설정하여 ListBox 컨트롤을 조작할 수 있도록 하는 WPF 컨트롤 집합이 들어 있는 StackPanel 요소입니다. 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에서 상속합니다. 생성자는 ListBox 컨트롤을 호스팅하는 Border 요소의 높이와 너비에 해당하는 height 및 width의 두 매개 변수를 사용합니다. 이들 값은 나중에 컨트롤 크기가 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를 사용하여 창 클래스를 만들고 등록하는 등의 작업을 통해 컨트롤에 대한 호스트 창을 만들 수 있습니다. 하지만 미리 정의된 "static" 창 클래스로 창을 만드는 방법이 훨씬 더 간단합니다. 이렇게 하면 컨트롤에서 알림을 수신하기 위해 필요한 창 프로시저를 사용할 수 있으며 작성해야 할 코드도 최소화됩니다.
컨트롤의 HWND는 읽기 전용 속성을 통해 노출되며 호스트 페이지는 이를 사용하여 컨트롤에 메시지를 보낼 수 있습니다.
public IntPtr hwndListBox
{
get { return hwndControl; }
}
ListBox 컨트롤은 호스트 창의 자식으로 만들어집니다. 두 창의 높이와 너비는 위에서 설명한 생성자에 전달되는 값으로 설정됩니다. 이를 통해 호스트 창과 컨트롤의 크기가 페이지의 예약된 영역과 같아집니다. 창을 만든 뒤 샘플에서는 호스트 창의 HWND가 포함된 HandleRef 개체를 반환합니다.
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 이외에도 HwndHost의 WndProc 및 DestroyWindowCore 메서드도 재정의해야 합니다. 이 예제에서는 컨트롤에 대한 메시지가 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);
}
[DllImport("user32.dll", EntryPoint = "DestroyWindow", CharSet = CharSet.Unicode)]
internal static extern bool DestroyWindow(IntPtr hwnd);
페이지에 컨트롤 호스팅
페이지에 컨트롤을 호스팅하려면 먼저 ControlHost 클래스의 새 인스턴스를 만듭니다. 컨트롤(ControlHostElement)이 포함된 테두리 요소의 높이와 너비를 ControlHost 생성자에 전달합니다. 이를 통해 ListBox의 크기가 올바르게 조정됩니다. 그런 다음 ControlHost 개체를 호스트 Border의 Child 속성에 할당하여 페이지에 컨트롤을 호스팅합니다.
샘플에서는 처리기를 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;
}
참고 항목
개념
Windows Presentation Foundation 시작