다음을 통해 공유


연습: Win32에서 WPF 시계 호스트

Win32 애플리케이션 안에 WPF를 넣으려면 WPF 콘텐츠가 포함된 HWND를 제공하는 HwndSource를 사용하세요. 먼저 HwndSource를 만들고 CreateWindow와 비슷한 매개 변수를 제공합니다. 그런 다음 그 안에 넣으려는 WPF 콘텐츠에 대해 HwndSource에 알려 줍니다. 마지막으로 HwndSource에서 HWND를 가져옵니다. 이 연습은 운영 체제 날짜 및 시간 속성 대화 상자를 다시 구현하는 Win32 애플리케이션 내에 혼합 WPF를 만드는 방법을 보여 줍니다.

필수 구성 요소

WPF 및 Win32 상호 운용성을 참조하세요.

이 자습서를 사용하는 방법

이 자습서는 상호 운용 애플리케이션을 생성하는 중요한 단계에 대해 중점적으로 설명합니다. 자습서는 Win32 시계 상호 운용 샘플로 지원되지만 이 샘플은 최종 제품을 반영합니다. 이 자습서는 기존 Win32 프로젝트에서 직접 시작하고 호스트된 WPF를 애플리케이션에 추가하는 것처럼 단계를 문서화합니다. 최종 제품을 Win32 시계 상호 운용 샘플과 비교할 수 있습니다.

Win32 내에서 Windows Presentation Framework의 연습(HwndSource)

다음 그래픽에서는 이 자습서의 의도된 최종 제품을 보여 줍니다.

Screenshot that shows the Date and Time Properties dialog box.날짜 및 시간 속성 대화 상자를 보여주는 스크린샷.

Visual Studio에서 C++ Win32 프로젝트를 만들고 대화 상자 편집기를 사용하여 다음을 만들면 이 대화 상자를 다시 만들 수 있습니다.

Recreated Date and Time Properties dialog box다시 만든 날짜 및 시간 속성 대화 상자

(HwndSource를 사용하기 위해 Visual Studio를 사용할 필요가 없고, Win32 프로그램을 작성하기 위해 C++를 사용할 필요가 없지만 이 방법은 매우 일반적이며 단계별 자습서 설명에 아주 적합합니다).

WPF 시계를 대화 상자에 넣으려면 5개의 특정 하위 단계를 수행해야 합니다.

  1. Visual Studio에서 프로젝트 설정을 변경하여 Win32 프로젝트가 관리 코드(/clr)를 호출할 수 있도록 합니다.

  2. 별도의 DLL에서 WPFPage를 만듭니다.

  3. 이 WPFPageHwndSource 안에 넣습니다.

  4. Handle 속성을 사용하여 이 Page의 HWND를 가져옵니다.

  5. Win32를 사용하여 더 큰 Win32 애플리케이션 내에서 HWND를 배치할 위치를 결정합니다.

/clr

첫 번째 단계에서는 이 관리되지 않는 Win32 프로젝트를 관리 코드를 호출할 수 있는 프로젝트로 변환합니다. /clr 컴파일러 옵션을 사용하여 사용하려는 DLL에 연결하고 WPF에서 사용할 수 있도록 Main 메서드를 조정합니다.

C++ 프로젝트 내에서 관리 코드를 사용하려면 win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 선택합니다. 일반 속성 페이지(기본값)에서 공용 언어 런타임 지원을 /clr로 변경합니다.

다음으로 WPF에 필요한 DLL(PresentationCore.dll, PresentationFramework.dll, System.dll, WindowsBase.dll, UIAutomationProvider.dll 및 UIAutomationTypes.dll)에 대한 참조를 추가합니다. 다음 지침에서는 운영 체제가 C: 드라이브에 설치되어 있다고 가정합니다.

  1. win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 해당 대화 상자 내에서 참조...를 선택합니다.

  2. win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 참조...를 선택합니다.

  3. 새 참조 추가를 클릭하고 찾아보기 탭을 클릭한 다음 C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationCore.dll을 입력하고 확인을 클릭합니다.

  4. PresentationFramework.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\PresentationFramework.dll)에 대해 반복합니다.

  5. WindowsBase.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\WindowsBase.dll)에 대해 반복합니다.

  6. UIAutomationTypes.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationTypes.dll)에 대해 반복합니다.

  7. UIAutomationProvider.dll(C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\UIAutomationProvider.dll)에 대해 반복합니다.

  8. 새 참조 추가를 클릭하고 System.dll을 선택한 다음 확인을 클릭합니다.

  9. 확인을 클릭하여 참조를 추가하기 위한 win32clock 속성 페이지를 종료합니다.

마지막으로 WPF에서 사용할 수 있도록 STAThreadAttribute_tWinMain 메서드에 추가합니다.

[System::STAThreadAttribute]
int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)

이 특성은 CLR(공용 언어 런타임)에게 COM(구성 요소 개체 모델)을 초기화할 때 WPF(및 Windows Forms)에 필요한 단일 STA(스레드 아파트 모델)를 사용해야 함을 알려 줍니다.

Windows Presentation Framework 페이지 만들기

다음으로 WPFPage를 정의하는 DLL을 만듭니다. 종종 WPFPage를 독립 실행형 애플리케이션을 만들고 이런 방식으로 WPF 부분을 작성하고 디버그하는 것이 가장 간단합니다. 완료되면 프로젝트를 마우스 오른쪽 단추로 클릭하고 속성을 클릭한 다음 애플리케이션으로 이동하여 출력 형식을 Windows 클래스 라이브러리로 변경하여 해당 프로젝트를 DLL로 변환할 수 있습니다.

그런 다음 WPF dll 프로젝트를 Win32 프로젝트(두 개의 프로젝트를 포함하는 하나의 솔루션)와 결합할 수 있습니다. 솔루션을 마우스 오른쪽 단추로 클릭하고 추가\기존 프로젝트를 선택합니다.

Win32 프로젝트에서 해당 WPF dll을 사용하려면 참조를 추가해야 합니다.

  1. win32clock 프로젝트를 마우스 오른쪽 단추로 클릭하고 참조...를 선택합니다.

  2. 새 참조 추가를 클릭합니다.

  3. 프로젝트 탭을 클릭하고, WPFClock을 선택하고, 확인을 클릭합니다.

  4. 확인을 클릭하여 참조를 추가하기 위한 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를 배치할 위치를 정확하게 알 수 없습니다. Visual Studio 대화 상자 편집기를 사용하여 시계가 들어갈 위치("여기에 시계 삽입")에 Win32 STATIC 컨트롤을 배치하고, 해당 컨트롤을 사용하여 WPF 시계의 위치를 지정할 수 있습니다.

WM_INITDIALOG를 처리하는 위치에서 GetDlgItem을 사용하여 자리 표시자 STATIC에 대한 HWND를 검색합니다.

HWND placeholder = GetDlgItem(hDlg, IDC_CLOCK);

그런 다음 해당 자리 표시자 STATIC의 크기 및 위치를 계산하여 해당 위치에 WPF 시계를 배치할 수 있습니다.

RECT 사각형의 경우입니다.

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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://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);
        }
    }
}

최종 결과는 다음과 같습니다.

Final result Date and Time Properties dialog box최종 결과 날짜 및 시간 속성 대화 상자

이 스크린샷을 생성한 코드와 최종 결과를 비교하려면 Win32 시계 상호 운용 샘플을 참조하세요.

참고 항목