Readme Card

[.NET]C++에서 WPF호출 방법

개요

윈도우 개발을 진행하다 보면 관리되지 않는 네이티브 코드(C++)에서 관리되는 코드(C#)을 호출해야 할 일이 종종 있다. 일반적으로 생산성이 좋은 C#으로 UI를 만들고 C++로 작성한 공용 모듈을 호출하는 일이 대부분이지만 반대의 경우도 가끔 있다. 예를 들어 윈도우 시스템과 밀접하게 돌아가는 LowLevel 레벨 컴포넌트(ex. Windows Credential Provider)에서 UI를 필요로 할때 이다. 이런 컴포넌트들은 대부분 C/C++로 작성되어 있기 때문에 UI를 Win32, 또는 MFC로 개발하는 것이 일반적이지만 보다 생산성 높은 .NET UI을 호출할 수 있다면 그 선택이 최선의 선택 이라고 확신할 수는 없을 것이다. 이 포스트에서는 위에서 언급한 시나리오를 가정하여 C++ 로 작성된 바이너리에서 C#으로 작성된 UI(WPF)를 어떻게 호출하지 샘플 코드를 통해 Step by Step 으로 설명한다.


호출 구조

샘플 프로젝트를 만들기 전에 일단 구조를 확인해 보자. 구조는 꽤 간단한 편인데, 핵심은 관리되지 않는 C++ 코드에서 관리되는 C# 코드를 접근하기 위해 중간 다리 역할을 해주는 모듈이 필요하다. 이 모듈은 .NET과 C++ 중간의 개발 환경을 제공하는 CLR 라이브러리로 C++/CLI을 사용하여 구현할 수 있다. C++/CLI는 C++과 비슷한 문법으로 .NET 프레임워크의 개체에 접근할 수 있고 네이티브 C++ 코드도 사용할 수 있기 때문에 중간 다리 역할이 가능하다.

image


WPF 프로젝트 만들기

우선 호출할 WPF UI를 만들기 위해 WPF 프로젝트를 만든다. 상위 필터에 C#, Windows, 데스크톱을 선택하고 WPF 앱(.NET Framework)를 선택하고 프로젝트 이름은 WpfLibrary 로 입력한다.

image

⚠️ “WPF 클래스 라이브러리” 혹은 “WPF 앱(.NET Core)” 프로젝트로 생성하지 않도록 주의 한다.


클래스 라이브러리 프로젝트로 변경하기

[프로젝트 우 클릭 > 속성 > 애플리케이션 > 출력 형식]을 클래스 라이브러리로 변경하고, App.xaml 파일을 프로젝트에서 지운다.

image

WPF Window 만들기

프로젝트 생성 시 기본으로 생기는 MainWindow.xaml 을 지우고 최종 목표로 호출할 WPF Window를 생성한다. [프로젝트 우 클릭>추가>새 항목] 을 눌러 창(WPF)로 선택하고 파일명은 WpfDialog.xaml 로 만든다. (MainWindow.xaml에서 변경해도 된다.)

WpfDialog.xaml 에는 대충 TextBlock 으로 WPF UI임 알 수 있도록 적어 준다.

📄WpfDialog.xaml

1
2
3
4
5
6
7
8
9
10
11
12
<Window x:Class="WpfLibrary.WpfDialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfLibrary"
        mc:Ignorable="d"
        Title="Window" Height="274" Width="318">
    <Grid>
        <TextBlock HorizontalAlignment="Center" Margin="0,103,0,0" TextWrapping="Wrap" Text="WPF Window" VerticalAlignment="Top" Height="45" Width="150" FontSize="24"/>
    </Grid>
</Window>

📄WpfDialog.xaml.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
using System.Windows;
using System.Windows.Interop;

namespace WpfLibrary
{
    /// <summary>
    /// Window.xaml에 대한 상호 작용 논리
    /// </summary>
    public partial class WpfDialog : Window
    {
        public WpfDialog(IntPtr parentWindowhandle)
        {
            InitializeComponent();
            var interop = new WindowInteropHelper(this);
            interop.Owner = parentWindowhandle;
        }
    }
}

인터페이스 클래스 만들기

이제 위에서 만든 Window 를 외부에서 호출할 수 있도록 도와주는 인터페이스 클래스를 생성한다. [프로젝트 우 클릭 > 추가 > 새 항목]에서 클래스를 선택하고 이름은 DialogInterface 로 한다.

📄DialogInterface.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;
using System.Threading;
using System.Windows;

namespace WpfLibrary
{
    public class DialogInterface
    {
        public int ShowModalDialog(IntPtr parentWindowHandle)
        {
            bool dialogResult = false;
            Thread thread = new Thread(() =>
            {
                WpfDialog w = new WpfDialog(parentWindowHandle);
                w.WindowStartupLocation = WindowStartupLocation.CenterScreen;
                dialogResult = (bool)w.ShowDialog();
            });

            thread.SetApartmentState(ApartmentState.STA);            
            thread.Start();
            thread.Join();
            return dialogResult ? 0 : -1;
        }
    }
}

WPF 는 UI 스레드 정책을 STA(Single-Threaded Apartment)로 규정하고 있기 때문에 WPF Window UI 스레드가 아닌 다른 스레드에서 UI에 엑세스 하려면 STA 예외가 발생한다. 때문에 위 코드에서는 별도의 STA 스레드를 생성하여 UI 를 생성한다.

C++/CLI 인터페이스 바이너리 만들기

이제 위의 WpfLibrary 와 네이티브 바이너리 사이에서 다리 역할을 해주는 인터페이스 모듈을 만들 차례다. [솔루션 우 클릭 > 추가 > 새 프로젝트]를 눌러 C++/CLI 프로젝트를 추가 한다. 상위 필터에 C++, Windows, 데스크톱을 선택하고 CLR 클래스 라이브러리(.NET Framework)를 선택하여 프로젝트를 생성한다. 프로젝트 이름은 InterfaceBridge 로 입력한다.

image

⚠️ CLR 클래스 라이브러리(.NET) 프로젝트와 헷갈리지 않게 주의 한다. 이 프로젝트로 만들 경우 Wpf 클래스 라이브러리를 참조 추가할 수 없다.


WpfLibrary 래퍼 클래스 만들기

프로젝트를 만들었다면 기본으로 추가되어 있는 InterfaceBridge.cpp/.h 는 지우고, 위의 WpfLibrary 의 DialogInterface.cs 를 대상으로 하는 래퍼 클래스를 추가한다. [프로젝트 우 클릭 > 추가 > 새 항목 > C++ 클래스]는 눌러 DialogInterfaceWrapper 클래스를 추가한다. 기타 옵션의 관리됨을 체크 한다.

image

추가한 클래스에서 WpfLibrary 프로젝트를 참조해야 하기 프로젝트 하위의 [참조 우 클릭 > 참조 추가]를 하여 WpfLibrary 프로젝트를 추가한다.

image

DialogInterfaceWrapper 클래스에 아래 코드를 입력한다.

📄DialogInterfaceWrapper.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma once

using namespace System;
using namespace WpfLibrary;

namespace InterfaceBridge {

	public ref class DialogInterfaceWrapper
	{
	public:
		int ShowModalDialog(IntPtr handle);
	};
}

📄DialogInterfaceWrapper.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "pch.h"
#include "DialogInterfaceWrapper.h"

namespace InterfaceBridge {

	int DialogInterfaceWrapper::ShowModalDialog(IntPtr handle)
	{
		DialogInterface^ c = gcnew DialogInterface();
		int result = c->ShowModalDialog(handle);
		return result;
	}
}

이제 네이티브 바이너리에서 호출할 용도로 방금 만든 DialogInterfaceWrapper 클래스를 호출하는 API를 만들고 dllexport 해야 한다.

[프로젝트 우 클릭 > 추가 > 새 항목 > C++ 클래스] 를 눌러 NativeInterface 클래스를 추가한다. 이번엔 관리됨 체크를 해제하여 클래스를 생성한다. 생성된 .h파일과 .cpp 에 아래 코드를 입력한다.

네이티브 인터페이스 만들기

📄NativeInterface.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

#ifdef INTERFACEBRIDGE_EXPORTS 
#define INTERFACEBRIDGE_API __declspec( dllexport )
#else
#define INTERFACEBRIDGE_API __declspec( dllimport )
#endif

#ifndef _WINDOWS_
#include <Windows.h>
#endif

class INTERFACEBRIDGE_API NativeInterface
{
public:
	NativeInterface();
	~NativeInterface();

	static int ShowModalDialog(HWND hwnd);
};

📄NativeInterface.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include "pch.h"
#include "NativeInterface.h"
#include "DialogInterfaceWrapper.h"

NativeInterface::NativeInterface()
{
}

NativeInterface::~NativeInterface()
{
}

int NativeInterface::ShowModalDialog(HWND hwnd)
{
	IntPtr managedHWND(hwnd);
	InterfaceBridge::DialogInterfaceWrapper^ diw = gcnew InterfaceBridge::DialogInterfaceWrapper();
	return diw->ShowModalDialog(managedHWND);
}

여타 다른 DLL 을 만드는 것 처럼 아래 코드로 EXPORT 기호를 정의하고 [프로젝트 속성 > 링커 > C/C++ > 전처리기 > 전처리기 정의]에 해당 EXPORT 기호를 추가한다. 각 구성과 플랫폼 마다 EXPORT 기호를 따로 설정할 것이 아니기에 모든 구성과 모든 플랫폼으로 설정한다. 이제 NativeInterface 클래스는 API 클래스로 노출되었다. 이 클래스를 호출하고 싶은 네이티브 코드에서 사용하면 되는 것이다.

1
2
3
4
5
#ifdef INTERFACEBRIDGE_EXPORTS 
#define INTERFACEBRIDGE_API __declspec( dllexport )
#else
#define INTERFACEBRIDGE_API __declspec( dllimport )
#endif

image

MFC 프로젝트 만들기

이제 위에서 만든 인터페이스 바이너리를 호출한 네이티브 바이너리를 만들 차례이다. 이 포스트에서는 편의상 MFC로 만들지만 Win32 또는 Console 프로그램이어도 상관 없다. [솔루션 우 클릭 > 추가 > 새 프로젝트]를 눌러 MFC프로젝트를 추가 한다.

상위 필터에 C++, Windows, 데스크톱을 선택하고 MFC 응용 프로그램를 선택하여 프로젝트를 생성한다. 프로젝트 이름은 MFCApplication 으로 입력한다.

image

편의상 대화상자 기반으로 생성한다.

image

대화상자의 크기는 대충 변경 하고 버튼 하나를 추가한다.

image

네이티브 인터페이스 호출

이 버튼 클릭 처리기에 C++/CLI 인터페이스 모듈의 클래스를 사용하여 메서드를 호출하도록 한다.

1
2
3
4
5
6
7
#include "../InterfaceBridge/NativeInterface.h"

void CMFCApplication1Dlg::OnBnClickedButton1()
{
	// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
	int result = NativeInterface::ShowModalDialog(m_hWnd);
}

C++/CLI 인터페이스 바이너리 참조 추가 하기

왜 인지 모르겠지만 여타 다른 DLL 을 사용할 때 처럼 프로젝트 하위의 참조메뉴를 사용하여 InterfaceBridge 프로젝트를 참조해도 아래와 같은 오류가 뜨면서 구현체를 찾지 못한다.

image

어쩔 수 없이 프로젝트 속성에 직접 수작업으로 설정해야 한다. [프로젝트 속성 > 링커 > 일반 > 추가 라이브러리 디렉터리] 에 InterfaceBridge 프로젝트를 빌드했을 때 InterfaceBridge.lib 생성되는 상대 경로를 입력해 준다. 그리고 [프로젝트 속성 > 링커 > 입력 > 추가 종속성] 입력란에 InterfaceBridge.lib 을 입력한다.

image

image

이제 MFCApplication 프로젝트를 시작 프로젝트로 설정하고, 프로그램을 실행해보면 C++/CLI 인터페이스를 거쳐 WpfLibrary 의 WpfDialog 창이 출력 되는 것을 확인할 수 있다.

image

💡 팁 : 기본 설정으로는 C++/CLI 프로젝트의 디버깅이 되지 않는다. 이럴 경우에는 디버깅을 시작하는 프로젝트에서 [프로젝트 속성] > 디버깅 > 디버거 형식] 을 혼합(.NET Framework) 으로 변경 하면 디버깅이 가능하다. 또한 네이티브 프로젝트에서 디버깅을 시작하여 C# 프로젝트 까지 디버깅을 이어 가고 싶다면 C# 프로젝트에서 [프로젝트 속성 > 디버그 > 디버거 엔진] 에 네이티브 디버깅 사용 에 체크를 설정하면 된다.

DotNet 카테고리 내 다른 글 보러가기

댓글남기기