WinUI 3에서는 Windows Forms나 WPF와는 다르게 ShowDialog 메서드가 없기 때문에 좀 복잡한 방법으로 모달 창을 띄워야 합니다. 아무래도 WinUI 3가 UWP에게서 큰 영향을 받았는데 UWP에는 다중 창이라는 개념이 없었다 보니까 이렇게 되지 않았나 싶습니다.
프로젝트 준비
가장 먼저 MainWindow에 모달 창을 띄우는 버튼을 하나 둡시다.
<Button HorizontalAlignment="Center" VerticalAlignment="Center" Click="TheButton_OnClick">모달 창 띄우기</Button>
그리고 TheButton_OnClick 메서드는 다음과 같이 구현해줍시다.
private void TheButton_OnClick(object sender, RoutedEventArgs e) {
ModalWindow mw = new(this);
// Activate() 대신 AppWindow.Show() 사용
mw.AppWindow.Show();
}
여기서 특히 주의해야할 점은 창을 띄울 때 Window.Activate 메서드를 사용하면 안된다는 것입니다. AppWindow 클래스의 Show 메서드를 사용해야 하는데 이유는 잘 모르겠습니다.
프로젝트에 ModalWindow라는 이름의 창을 하나 추가하고 다음과 같이 작성합니다.
public sealed partial class ModalWindow {
private readonly Window parentWindow;
public ModalWindow(Window parent) {
InitializeComponent();
parentWindow = parent;
}
}
이제 앱을 실행해보면 창 하나가 뜨고, 여기서 버튼을 누르면 창이 하나 더 뜰 것입니다.
아직까지는 처리를 하지 않았기 때문에 실제로 모달 창으로 작동하는 건 아닙니다.
PInvoke 함수 정의
이제 PInvoke 함수와 상수를 정의해야 합니다.
private const int GWLP_HWNDPARENT = -8;
[DllImport("user32.dll",
#if !WIN64
EntryPoint = "SetWindowLongW",
#endif
ExactSpelling = true, SetLastError = true)]
private static extern nint SetWindowLongPtrW(IntPtr hWnd, int index, nint newLong);
이 코드가 32비트에서 정상적으로 작동하려면 csproj 파일에 다음 내용을 추가해야 합니다. 왜냐하면 64비트에서는 SetWindowLongPtrW 함수를 사용해야 하지만 32비트에서는 SetWindowLongPtrW 함수가 없고 SetWindowLongW 함수를 사용해야 하기 때문에 그렇습니다. winuser.h 파일 살펴보신 분들은 아시겠지만 SetWindowLongPtrW는 32비트로 컴파일힐 때는 SetWindowLongW에 대한 매크로일 뿐입니다. 물론 64비트로만 빌드할거라면 아래 내용과 위의 #if 전처리기로 감싼 부분은 없어도 됩니다.
<PropertyGroup Condition="'$(Platform)' == 'x64' or '$(Platform)' == 'ARM64'">
<DefineConstants>$(DefineConstants);WIN64</DefineConstants>
</PropertyGroup>
모달 창으로 만들기
이제 진짜 모달 장으로 만들어봅시다.
public sealed partial class ModalWindow {
private const int GWLP_HWNDPARENT = -8;
private readonly Window parentWindow;
public ModalWindow(Window parent) {
InitializeComponent();
parentWindow = parent;
// 모달 창으로 만들어주는 Presenter
var presenter = OverlappedPresenter.CreateForDialog();
presenter.IsModal = true;
// 부모 창 설정
SetWindowLongPtrW(WindowNative.GetWindowHandle(this), GWLP_HWNDPARENT, WindowNative.GetWindowHandle(parent));
AppWindow.SetPresenter(presenter);
Closed += ModalWindow_Closed;
}
[DllImport("user32.dll",
#if !WIN64
EntryPoint = "SetWindowLongW",
#endif
ExactSpelling = true, SetLastError = true)]
private static extern nint SetWindowLongPtrW(IntPtr hWnd, int index, nint newLong);
// 이거 안해주면 모달 창 닫을 때 부모 창이 뒤로 들어가버립니다.
private void ModalWindow_Closed(object sender, WindowEventArgs e) => parentWindow.Activate();
}
이제 프로그램을 실행해 보면 모달 창으로 뜨는 것을 확인할 수 있습니다.
이전 내용. 이 방법으로 하시면 안됩니다. 맨 밑에서 볼 수 있듯이 몇가지 문제가 있습니다.
프로젝트 준비
가장 먼저 MainWindow에 모달 창을 띄우는 버튼을 하나 둡시다.
<Button HorizontalAlignment="Center" VerticalAlignment="Center" Click="TheButton_OnClick">모달 창 띄우기</Button>
그리고 TheButton_OnClick 메서드는 다음과 같이 구현해줍시다.
private void TheButton_OnClick(object sender, RoutedEventArgs e) {
ModalWindow mw = new(this);
mw.Activate();
}
프로젝트에 ModalWindow라는 이름의 창을 하나 추가하고 다음과 같이 작성합니다.
using Microsoft.UI.Xaml;
using WinRT.Interop;
namespace App1;
/// <summary>
/// An empty window that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class ModalWindow {
private readonly IntPtr parentHwnd;
public ModalWindow(Window parent) {
InitializeComponent();
parentHwnd = WindowNative.GetWindowHandle(parent);
}
}
이제 앱을 실행해보면 창 하나가 뜨고, 여기서 버튼을 누르면 창이 하나 더 뜰 것입니다.
아직까지는 처리를 하지 않았기 때문에 실제로 모달 창으로 작동하는 건 아닙니다.
PInvoke 함수 및 WndProc 정의
이제 PInvoke 함수와 상수를 정의해야 합니다.
private const int GWLP_HWNDPARENT = -8;
private const int GWLP_WNDPROC = -4;
private const uint WM_DESTROY = 0x0002;
[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
private static extern nint SetWindowLongPtrW(IntPtr hWnd, int index, nint newLong);
[DllImport("user32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnableWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool enable);
[DllImport("user32.dll", ExactSpelling = true)]
private static extern nint CallWindowProcW(IntPtr prevWndProc, IntPtr hWnd, uint msg, nint wParam, nint lParam);
WndProc 대리자 타입을 정의합시다.
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate nint WndProc(IntPtr hWnd, uint uMsg, nint wParam, nint lParam);
모달 창으로 만들기
이제 진짜 모달 장으로 만들어봅시다.
public sealed partial class ModalWindow {
private const int GWLP_HWNDPARENT = -8;
private const int GWLP_WNDPROC = -4;
private const uint WM_DESTROY = 0x0002;
private readonly IntPtr parentHwnd;
private readonly IntPtr originalWndProc;
public ModalWindow(Window parent) {
InitializeComponent();
parentHwnd = WindowNative.GetWindowHandle(parent);
var presenter = OverlappedPresenter.CreateForDialog();
presenter.IsModal = true;
SetWindowLongPtrW(WindowNative.GetWindowHandle(this), GWLP_HWNDPARENT, parentHwnd);
AppWindow.SetPresenter(presenter);
Activated += ModalWindow_Activated;
originalWndProc = SetWindowLongPtrW(WindowNative.GetWindowHandle(this), GWLP_WNDPROC,
Marshal.GetFunctionPointerForDelegate(new WndProc(MyWndProc)));
}
[DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
private static extern nint SetWindowLongPtrW(IntPtr hWnd, int index, nint newLong);
[DllImport("user32.dll", ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnableWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.Bool)] bool enable);
[DllImport("user32.dll", ExactSpelling = true)]
private static extern nint CallWindowProcW(IntPtr prevWndProc, IntPtr hWnd, uint msg, nint wParam, nint lParam);
private nint MyWndProc(IntPtr hWnd, uint uMsg, nint wParam, nint lParam) {
if (uMsg == WM_DESTROY) {
EnableWindow(parentHwnd, true);
}
return CallWindowProcW(originalWndProc, hWnd, uMsg, wParam, lParam);
}
private void ModalWindow_Activated(object sender, WindowActivatedEventArgs args) => EnableWindow(parentHwnd, false);
}
이제 프로그램을 실행해 보면 모달 창으로 뜨는 것을 확인할 수 있습니다.
이 방법은 완벽한 방법이 아닙니다. 영상에 보이듯이 모달 창을 닫으면 원래 창이 뒤로 들어가 버립니다. 무엇보다도 가끔씩 비주얼 스튜디오 디버거에서도 안 잡히고 프로그램이 꺼지는 경우가 있습니다. 이벤트 뷰어를 보면 0xc000027b라고 잡히기는 하는데 검색해도 정확히 뭐가 문제인지는 안보이더라고요. 그래서 WinDbg로 디버깅을 시도했는데 WinDbg를 물렸을 때는 절대로 재현이 안되더군요(…). 그래서 잘 모르겠습니다.
