[.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++ 코드도 사용할 수 있기 때문에 중간 다리 역할이 가능하다.
WPF 프로젝트 만들기
우선 호출할 WPF UI를 만들기 위해 WPF 프로젝트를 만든다. 상위 필터에 C#, Windows, 데스크톱을 선택하고 WPF 앱(.NET Framework)
를 선택하고 프로젝트 이름은 WpfLibrary 로 입력한다.
⚠️ “WPF 클래스 라이브러리” 혹은 “WPF 앱(.NET Core)” 프로젝트로 생성하지 않도록 주의 한다.
클래스 라이브러리 프로젝트로 변경하기
[프로젝트 우 클릭 > 속성 > 애플리케이션 > 출력 형식]을 클래스 라이브러리로 변경하고, App.xaml
파일을 프로젝트에서 지운다.
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 로 입력한다.
⚠️ CLR 클래스 라이브러리(.NET) 프로젝트와 헷갈리지 않게 주의 한다. 이 프로젝트로 만들 경우 Wpf 클래스 라이브러리를 참조 추가할 수 없다.
WpfLibrary 래퍼 클래스 만들기
프로젝트를 만들었다면 기본으로 추가되어 있는 InterfaceBridge.cpp/.h 는 지우고, 위의 WpfLibrary 의 DialogInterface.cs 를 대상으로 하는 래퍼 클래스를 추가한다. [프로젝트 우 클릭 > 추가 > 새 항목 > C++ 클래스]는 눌러 DialogInterfaceWrapper 클래스를 추가한다. 기타 옵션의 관리됨
을 체크 한다.
추가한 클래스에서 WpfLibrary 프로젝트를 참조해야 하기 프로젝트 하위의 [참조 우 클릭 > 참조 추가]를 하여 WpfLibrary 프로젝트를 추가한다.
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
MFC 프로젝트 만들기
이제 위에서 만든 인터페이스 바이너리를 호출한 네이티브 바이너리를 만들 차례이다. 이 포스트에서는 편의상 MFC로 만들지만 Win32 또는 Console 프로그램이어도 상관 없다. [솔루션 우 클릭 > 추가 > 새 프로젝트]를 눌러 MFC프로젝트를 추가 한다.
상위 필터에 C++, Windows, 데스크톱을 선택하고 MFC 응용 프로그램
를 선택하여 프로젝트를 생성한다. 프로젝트 이름은 MFCApplication 으로 입력한다.
편의상 대화상자 기반으로 생성한다.
대화상자의 크기는 대충 변경 하고 버튼 하나를 추가한다.
네이티브 인터페이스 호출
이 버튼 클릭 처리기에 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 프로젝트를 참조해도 아래와 같은 오류가 뜨면서 구현체를 찾지 못한다.
어쩔 수 없이 프로젝트 속성에 직접 수작업으로 설정해야 한다. [프로젝트 속성 > 링커 > 일반 > 추가 라이브러리 디렉터리] 에 InterfaceBridge 프로젝트를 빌드했을 때 InterfaceBridge.lib 생성되는 상대 경로를 입력해 준다. 그리고 [프로젝트 속성 > 링커 > 입력 > 추가 종속성] 입력란에 InterfaceBridge.lib 을 입력한다.
이제 MFCApplication 프로젝트를 시작 프로젝트로 설정하고, 프로그램을 실행해보면 C++/CLI 인터페이스를 거쳐 WpfLibrary 의 WpfDialog 창이 출력 되는 것을 확인할 수 있다.
💡 팁 : 기본 설정으로는 C++/CLI 프로젝트의 디버깅이 되지 않는다. 이럴 경우에는 디버깅을 시작하는 프로젝트에서 [프로젝트 속성] > 디버깅 > 디버거 형식] 을
혼합(.NET Framework)
으로 변경 하면 디버깅이 가능하다. 또한 네이티브 프로젝트에서 디버깅을 시작하여 C# 프로젝트 까지 디버깅을 이어 가고 싶다면 C# 프로젝트에서 [프로젝트 속성 > 디버그 > 디버거 엔진] 에네이티브 디버깅 사용
에 체크를 설정하면 된다.
댓글남기기