자습서: Win32 응용 프로그램 호스팅 WPF 콘텐츠 만들기
업데이트: 2007년 11월
WPF를 Win32 응용 프로그램 내부에 배치하려면 WPF 콘텐츠를 포함하는 HWND를 제공하는 HwndSource를 사용합니다. 먼저 HwndSource를 만들어 CreateWindow와 유사한 매개 변수를 제공합니다. 그런 다음 HwndSource로 내부에 포함할 WPF 콘텐츠를 전달합니다. 마지막으로 HwndSource에서 HWND를 가져옵니다. 이 연습에서는 Win32 응용 프로그램 내부에 운영 체제 날짜 및 시간 속성 대화 상자를 다시 구현하는 혼합 WPF를 생성하는 방법에 대해 설명합니다.
사전 요구 사항
WPF 및 Win32 상호 운용성 개요를 참조하십시오.
이 자습서를 사용하는 방법
이 자습서에서는 상호 운용 응용 프로그램을 만드는 중요 단계에 대해 집중적으로 설명합니다. 또한 Win32 시계 상호 운용 샘플도 제공하지만 이 샘플에는 최종 제품이 반영되어 있습니다. 이 자습서에서는 기존에 직접 만든 Win32 프로젝트부터 시작하여 호스팅되는 WPF를 응용 프로그램에 추가하는 단계를 설명합니다. 최종 제품을 Win32 시계 상호 운용 샘플과 비교할 수 있습니다.
Windows Presentation Foundation 내의 Win32에 대한 연습(HwndSource)
다음 그래픽에서는 이 자습서의 최종 결과물을 보여 줍니다.
Microsoft Visual Studio에서 C++ Win32 프로젝트를 만들고 대화 상자 편집기로 다음을 만들어 이 대화 상자를 다시 생성할 수 있습니다.
HwndSource를 사용하기 위해 Microsoft Visual Studio를 사용할 필요가 없으며 Win32 프로그램을 작성하기 위해 C++를 사용할 필요가 없지만 이러한 도구를 사용하는 것이 작업을 수행하는 일반적인 방식이며 자습서의 단계별 설명에 적합합니다.
대화 상자에 WPF 시계를 배치하려면 다음과 같은 다섯 가지 하위 단계를 완료해야 합니다.
Microsoft Visual Studio에서 프로젝트 설정을 변경하여 관리 코드(/clr)를 호출하는 Win32 프로젝트를 활성화합니다.
별도의 DLL에서 WPFPage를 만듭니다.
WPFPage를 HwndSource 내부에 배치합니다.
Win32를 사용하여 더 큰 Win32 응용 프로그램 내에서 HWND를 배치할 위치를 결정합니다.
/clr
첫 번째 단계는 이 관리되지 않는 Win32 프로젝트를 관리 코드를 호출할 수 있는 프로젝트로 변환하는 것입니다. /clr 컴파일러 옵션을 사용합니다. 그러면 사용할 필수 DLL에 링크되고 WPF에 사용할 Main 메서드가 수정됩니다.
C++ 프로젝트 내부에서 관리 코드를 사용할 수 있으려면 win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 일반 속성 페이지(기본값)에서 CLR(공용 언어 런타임) 지원을 /clr로 변경합니다.
그런 다음 WPF에 필요한 DLL인 PresentationCore.dll, PresentationFramework.dll, System.dll, WindowsBase.dll, UIAutomationProvider.dll 및 UIAutomationTypes.dll에 대한 참조를 추가합니다. 다음 지침에서는 운영 체제가 C: 드라이브에 설치되어 있다고 가정합니다.
win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 참조...를 선택한 다음 대화 상자 내부에서 다음을 수행합니다.
win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 참조...를 선택합니다.
새 참조 추가를 클릭하고 찾아보기 탭을 클릭한 다음 C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll을 입력하고 확인을 클릭합니다.
PresentationFramework.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll)에 대해서도 이 과정을 반복합니다.
WindowsBase.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll)에 대해서도 이 과정을 반복합니다.
UIAutomationTypes.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll)에 대해서도 이 과정을 반복합니다.
UIAutomationProvider.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll)에 대해서도 이 과정을 반복합니다.
새 참조 추가를 클릭한 다음 System.dll을 선택하고 확인을 클릭합니다.
확인을 클릭하여 참조를 추가하는 데 사용되는 win32clock 속성 페이지를 종료합니다.
마지막으로, WPF와 함께 사용할 _tWinMain 메서드에 STAThreadAttribute를 추가합니다.
[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
이 특성을 사용하면 CLR(공용 언어 런타임)에서 COM(구성 요소 개체 모델)을 초기화할 때 WPF 및 Windows Forms에 필요한 STA(단일 스레드 아파트 모델)를 사용하게 만들 수 있습니다.
Windows Presentation Framework 페이지 만들기
다음으로 WPF Page를 정의하는 DLL을 만듭니다. WPF Page를 독립 실행형 응용 프로그램으로 만들고 WPF 부분을 작성한 후 디버그하는 것이 가장 쉬운 방법인 경우가 많습니다. 작업을 마친 후에는 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 클릭한 다음 응용 프로그램으로 이동하여 출력 형식을 Windows Class Library로 변경함으로써 프로젝트를 DLL로 변환할 수 있습니다.
그런 다음 WPF dll 프로젝트를 Win32 프로젝트(두 개의 프로젝트가 들어 있는 솔루션 하나)로 결합할 수 있습니다. 이를 위해서는 솔루션을 마우스 오른쪽 단추로 클릭하고 기존 프로젝트 추가를 선택합니다.
Win32 프로젝트에서 해당 WPF dll을 사용하려면 참조를 추가해야 합니다.
win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 참조...를 선택합니다.
새 참조 추가를 클릭합니다.
프로젝트 탭을 클릭합니다. WPFClock을 선택하고 확인을 클릭합니다.
확인을 클릭하여 참조를 추가하는 데 사용되는 win32clock 속성 페이지를 종료합니다.
HwndSource
다음으로 HwndSource를 사용하여 WPFPage를 HWND처럼 만듭니다. 다음 코드 블록을 C++ 파일에 추가합니다.
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;
HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);
UIElement^ page = gcnew WPFClock::Clock();
source->RootVisual = page;
return (HWND) source->Handle.ToPointer();
}
}
}
이는 조금 더 자세한 설명이 필요한 긴 코드입니다. 첫 번째 부분에는 모든 호출을 정규화할 필요가 없도록 다양한 절이 있습니다.
namespace ManagedCode
{
using namespace System;
using namespace System::Windows;
using namespace System::Windows::Interop;
using namespace System::Windows::Media;
다음으로 WPF 콘텐츠를 만들고 이 콘텐츠를 둘러싸는 HwndSource를 만든 다음 HWND를 반환하는 함수를 정의합니다.
HWND GetHwnd(HWND parent, int x, int y, int width, int height) {
먼저 CreateWindow와 비슷한 매개 변수를 갖는 HwndSource를 만듭니다.
HwndSource^ source = gcnew HwndSource(
0, // class style
WS_VISIBLE | WS_CHILD, // style
0, // exstyle
x, y, width, height,
"hi", // NAME
IntPtr(parent) // parent window
);
다음으로 WPF 생성자를 호출하여 해당 콘텐츠 클래스를 만듭니다.
UIElement^ page = gcnew WPFClock::Clock();
그런 다음 페이지를 HwndSource에 연결합니다.
source->RootVisual = page;
마지막 줄에서 HwndSource에 대한 HWND가 반환됩니다.
return (HWND) source->Handle.ToPointer();
Hwnd 위치 지정
이제 WPF 시계를 포함하는 HWND가 만들어졌으므로 Win32 대화 상자 내부에 이 HWND를 배치해야 합니다. HWND를 배치할 위치를 알고 있다면 앞서 정의한 GetHwnd 함수에 크기와 위치만 전달하면 됩니다. 하지만 리소스 파일을 사용하여 대화 상자를 정의한 경우에는 HWND 배치 위치를 정확히 모를 수도 있습니다. 이럴 때는 Microsoft Visual Studio 대화 상자 편집기를 사용하여 시계를 배치할 위치(“Insert clock here”)에 Win32 STATIC 컨트롤을 놓고 해당 컨트롤을 사용하여 WPF 시계의 위치를 지정합니다.
WM_INITDIALOG를 처리하는 위치에서 GetDlgItem을 사용하여 자리 표시자 STATIC에 대한 HWND를 검색합니다.
HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);
그런 다음 해당 자리 표시자 STATIC의 크기와 위치를 계산하여 해당 위치에 WPF 시계를 배치할 수 있습니다.
RECT rectangle;
GetWindowRect(placeholder, &rectangle);
int width = rectangle.right - rectangle.left;
int height = rectangle.bottom - rectangle.top;
POINT point;
point.x = rectangle.left;
point.y = rectangle.top;
result = MapWindowPoints(NULL, hDlg, &point, 1);
계속해서 자리 표시자 STATIC을 숨깁니다.
ShowWindow(placeholder, SW_HIDE);
해당 위치에서 WPF 시계 HWND를 만듭니다.
HWND clock = ManagedCode::GetHwnd(hDlg, point.x, point.y, width, height);
자습서를 재미 있게 만들고 실제적인 WPF 시계를 작성하기 위해서는 이 시점에서 WPF 시계 컨트롤을 만들어야 합니다. 대부분의 작업은 태그에서 수행할 수 있으며 몇 가지 이벤트 처리기만 코드 숨김으로 처리합니다. 이 자습서는 컨트롤 디자인이 아닌 상호 운용에 대한 것이므로 여기에서는 WPF 시계에 대한 전체 코드가 개별적인 빌드 지침이나 각 부분의 의미에 대한 설명 없이 코드 블록으로 제공됩니다. 이 코드를 자유롭게 사용하여 컨트롤의 모양과 동작 및 기능을 변경합니다.
다음은 태그입니다.
<Page x:Class="WPFClock.Clock"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
>
<Grid>
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#fcfcfe" Offset="0" />
<GradientStop Color="#f6f4f0" Offset="1.0" />
</LinearGradientBrush>
</Grid.Background>
<Grid Name="PodClock" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.Resources>
<Storyboard x:Key="sb">
<DoubleAnimation From="0" To="360" Duration="12:00:00" RepeatBehavior="Forever"
Storyboard.TargetName="HourHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
<DoubleAnimation From="0" To="360" Duration="01:00:00" RepeatBehavior="Forever"
Storyboard.TargetName="MinuteHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
<DoubleAnimation From="0" To="360" Duration="0:1:00" RepeatBehavior="Forever"
Storyboard.TargetName="SecondHand"
Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)"
/>
</Storyboard>
</Grid.Resources>
<Ellipse Width="108" Height="108" StrokeThickness="3">
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Color="LightBlue" Offset="0" />
<GradientStop Color="DarkBlue" Offset="1" />
</LinearGradientBrush>
</Ellipse.Stroke>
</Ellipse>
<Ellipse VerticalAlignment="Center" HorizontalAlignment="Center" Width="104" Height="104" Fill="LightBlue" StrokeThickness="3">
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Color="DarkBlue" Offset="0" />
<GradientStop Color="LightBlue" Offset="1" />
</LinearGradientBrush>
</Ellipse.Stroke>
</Ellipse>
<Border BorderThickness="1" BorderBrush="Black" Background="White" Margin="20" HorizontalAlignment="Right" VerticalAlignment="Center">
<TextBlock Name="MonthDay" Text="{Binding}"/>
</Border>
<Canvas Width="102" Height="102">
<Ellipse Width="8" Height="8" Fill="Black" Canvas.Top="46" Canvas.Left="46" />
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="0" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="30" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="60" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="90" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="120" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="150" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="180" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="210" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="240" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="48" Fill="Black" Width="4" Height="8">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="270" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="300" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle Canvas.Top="5" Canvas.Left="49" Fill="Black" Width="2" Height="6">
<Rectangle.RenderTransform>
<RotateTransform CenterX="2" CenterY="46" Angle="330" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="HourHand" Canvas.Top="21" Canvas.Left="48"
Fill="Black" Width="4" Height="30">
<Rectangle.RenderTransform>
<RotateTransform x:Name="HourHand2" CenterX="2" CenterY="30" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="MinuteHand" Canvas.Top="6" Canvas.Left="49"
Fill="Black" Width="2" Height="45">
<Rectangle.RenderTransform>
<RotateTransform CenterX="1" CenterY="45" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle x:Name="SecondHand" Canvas.Top="4" Canvas.Left="49"
Fill="Red" Width="1" Height="47">
<Rectangle.RenderTransform>
<RotateTransform CenterX="0.5" CenterY="47" />
</Rectangle.RenderTransform>
</Rectangle>
</Canvas>
</Grid>
</Grid>
</Page>
다음은 함께 제공된 코드 숨김입니다.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace WPFClock
{
/// <summary>
/// Interaction logic for Clock.xaml
/// </summary>
public partial class Clock : Page
{
private DispatcherTimer _dayTimer;
public Clock()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Clock_Loaded);
}
void Clock_Loaded(object sender, RoutedEventArgs e) {
// set the datacontext to be today's date
DateTime now = DateTime.Now;
DataContext = now.Day.ToString();
// then set up a timer to fire at the start of tomorrow, so that we can update
// the datacontext
_dayTimer = new DispatcherTimer();
_dayTimer.Interval = new TimeSpan(1, 0, 0, 0) - now.TimeOfDay;
_dayTimer.Tick += new EventHandler(OnDayChange);
_dayTimer.Start();
// finally, seek the timeline, which assumes a beginning at midnight, to the appropriate
// offset
Storyboard sb = (Storyboard)PodClock.FindResource("sb");
sb.Begin(PodClock, HandoffBehavior.SnapshotAndReplace, true);
sb.Seek(PodClock, now.TimeOfDay, TimeSeekOrigin.BeginTime);
}
private void OnDayChange(object sender, EventArgs e)
{
// date has changed, update the datacontext to reflect today's date
DateTime now = DateTime.Now;
DataContext = now.Day.ToString();
_dayTimer.Interval = new TimeSpan(1, 0, 0, 0);
}
}
}
최종 결과는 다음과 같습니다.
이 스크린 샷을 생성하는 데 사용된 코드와 최종 결과를 비교하려면 Win32 시계 상호 운용 샘플을 참조하십시오.