창 클래스 분석 및 버튼 클릭 이벤트 구현

독자분들은 VS 2022를 17.9.1 이상 버전으로 업그레이드한 후 생성하는 WinUI3 C++ 기본 예시에는 버튼 하나가 포함되어 있으며, 버튼 클릭 시 백그라운드에서 여러 동작을 실행할 수 있습니다.

새 프로젝트를 생성해 보겠습니다.

지난 시간에 프로젝트 내 각 파일과 역할에 대해 설명했습니다. 이번 시간에는 새 빈 창을 생성한 뒤 창 위에 버튼을 추가하고 버튼 클릭 동작을 구현해보겠습니다.

앞서 말했듯 진입점 클래스는 여기의 App입니다.

    void App::OnLaunched([[maybe_unused]] LaunchActivatedEventArgs const& e)
    {
        window = make<MainWindow>();
        window.Activate();
    }Code language: C++ (cpp)

이 부분에서 메인 창을 생성하며, 메인 창은 프로그램 실행 시 보이는 창을 의미합니다. 아래는 메인 창 관련 파일 구조이며, 창 하나에는 총 4개의 파일이 존재합니다. 하나씩 분석해보겠습니다.

창 관련 파일

MainWindow.xaml (UI 묘사 파일)

순수 XML 마크업 언어로 UI 레이아웃 전용 파일이며, 오직 화면 시각적 요소만 담당하고 비즈니스 로직은 작성하지 않습니다.

레이아웃 컨트롤, Button, TextBox 등 각종 창 컨트롤을 선언할 수 있습니다. WPF부터 계속 사용된 UI 정의 파일로 본질은 XML이며, 백그라운드 이벤트 처리를 위해 동일한 이름의 .h, .cpp 파일과 함께 사용해야 합니다.

공식 기본 예시의 XAML은 빈 상태로 어떤 컨트롤도 포함되지 않습니다.

<?xml version="1.0" encoding="utf-8"?>
<Window
    x:Class="WinuiCppDemo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:WinuiCppDemo"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    Title="WinuiCppDemo">

    <Grid>

    </Grid>
</Window>Code language: YAML (yaml)

MainWindow.idl

인터페이스 정의 파일로 C++/WinRT만의 핵심 파일입니다.

IDL은 Interface Definition Language(인터페이스 정의 언어)의 약자로, 창이 외부에 노출할 API, 속성, 이벤트를 정의하며 C++/WinRT 다중 언어 상호연동의 매개체 역할을 수행합니다.

  1. 창 클래스의 공개 멤버(사용자 정의 속성, 커스텀 이벤트, 호출 가능 메서드)를 규정
  2. 컴파일러가 IDL 파일로부터 WinRT 표준 ABI 인터페이스 코드를 자동 생성해 C++, C#, JS 모두 해당 창을 호출할 수 있게 함
  3. UI XAML의 데이터 바인딩, 커스텀 컨트롤은 IDL에 속성을 선언해야 정상 작동

WinUI3 C++/WinRT에 .idl 파일이 필수인 이유

WinRT 자체가 다중 언어 지원 바이너리 ABI 표준이기 때문입니다.

Windows Runtime(WinRT)은 단순한 C++ 라이브러리가 아닌 다중 언어 호환 바이너리 인터페이스 규격입니다. C++, C#, Python, JS가 동일한 WinRT 컴포넌트를 호출하려면 통일된 인터페이스 설명이 필요하며 C++ 헤더 파일만으로는 상호 호환할 수 없습니다.

C++ 헤더는 C++ 컴파일러만 인식하고 C#/JS는 해석할 수 없지만, IDL은 중립적인 인터페이스 언어로 컴파일러가 모든 언어에서 사용 가능한 ABI 연결 코드로 변환해줍니다.

아래는 예시에서 사용하는 IDL 파일 내용입니다.

namespace WinuiCppDemo
{
    [default_interface]
    runtimeclass MainWindow : Microsoft.UI.Xaml.Window
    {
        MainWindow();
        Int32 MyProperty;
    }
}

앞서 IDL은 인터페이스 묘사 언어라고 설명했습니다. 위 IDL 파일 상단에는 네임스페이스 WinuiCppDemo가 선언되고 그 다음 [default_interface] 어트리뷰트가 작성되어 있습니다.

해당 어트리뷰트는 runtimeclass기본 메인 인터페이스를 지정하며, C++/WinRT로 창/커스텀 컨트롤을 제작할 때 필수로 사용해야 하는 규칙입니다.

MIDL 컴파일러가 자동으로 IMainWindow 순수 가상 인터페이스를 생성하며 클래스의 모든 멤버(생성자, 속성, 메서드)가 포함됩니다.

[default_interface]는 이 IMainWindow가 해당 클래스가 외부에 공개하는 메인 ABI 인터페이스임을 명시합니다.

중괄호 안의 MainWindow();는 매개변수 없는 기본 생성자 선언으로 반드시 명시해야 하며, XAML 엔진이 창을 인스턴스화할 때 이 기본 생성자를 호출합니다.

이는 .xaml.h 파일 내 생성자 구현과 대응됩니다.

Int32 MyProperty;

WinRT 의존 속성(바인딩 가능 속성)을 선언하는 구문으로, IDL의 핵심 장점이 여기서 드러납니다.

Int32 : WinRT 표준 기본 타입 (C++의 int32_t에 대응);

MyProperty : 컴파일러가 완전한 속성 시스템을 자동 생성합니다.

  • Getter : int32_t MyProperty();
  • Setter : void MyProperty(int32_t value);
  • 속성 변경 이벤트 : MyPropertyChanged (속성 수정 시 자동 트리거되며 XAML x:Bind가 이를 통해 UI를 갱신)

오직 IDL에 선언한 멤버만 XAML에서 {x:Bind MyProperty}로 바인딩할 수 있습니다. 만약 .h 파일에 일반 멤버 변수만 선언하면 XAML이 인식하지 못하고 바인딩 오류가 발생합니다.

IDL 파일 우클릭 시 별도로 컴파일할 수 있으며 midl.exe 도구가 처리합니다. C#에서 사용하는 메타데이터 파일로 컴파일되며 C++용 인터페이스 헤더 파일도 생성됩니다.

쉽게 정리하면 IDL은 인터페이스를 생성해 다중 언어 간 호출을 지원하고 XAML 데이터 바인딩에 필요한 메타데이터를 제공하는 파일입니다. WinUI 3 C# 버전에는 IDL이 없는 이유는 C#이 자체 반사와 메타데이터를 지원하는 관리형 언어이기 때문이며, C++ WinUI는 C++ 자체에 반사 및 메타데이터 기능이 없어 IDL이 필요합니다.

.h 파일에 단순히 멤버 변수를 선언만 하면 XAML 엔진이 전혀 인지하지 못합니다.

IDL에 Int32 MyProperty;라고 선언해야 컴파일러가 아래 항목을 자동 생성합니다.

  1. 속성 Get/Set 인터페이스
  2. 속성 변경 알림 이벤트 MyPropertyChanged
  3. XAML 정적 바인딩 검증을 통과시키는 메타데이터

프로젝트에서 오직 C++만 사용하고 C#/JS 등 다른 언어와 연동하지 않더라도 XAML을 사용한다면 반드시 IDL 파일을 작성해야 합니다.

WinRT 설계 목표는 통일된 ABI를 제공해 C++ / C# / JS / Python 간 컴포넌트 상호 호출을 가능하게 하는 것입니다.

IDL은 특정 프로그래밍 언어에 종속되지 않는 중립적 묘사 언어입니다. 각 언어 컴파일러가 IDL로부터 생성된 .winmd 메타데이터 파일을 읽어 클래스, 속성, 메서드를 인식할 수 있습니다.

IDL 파일을 우클릭하면 해당 IDL을 개별 컴파일할 수 있습니다.

IDL의 핵심 역할은 XAML이 UI 바인딩에 필요한 메타데이터를 제공하는 것이며, 부가적으로 다중 언어 상호연동을 구현합니다.

IDL 컴파일 후 xxxx.g.h, xxxx.g.cpp 파일이 생성되며 프로젝트 디렉터리 내 Generated Files 폴더에 저장됩니다.

Generated Files는 자동 생성 코드 저장 디렉터리입니다.

  1. midl.exe, cppwinrt.exe, XAML 컴파일러가 프로젝트 빌드 시 자동 생성하는 각종 파일이 저장됨
  2. 폴더 내 파일을 직접 수정하지 마세요. 재컴파일 시 모든 내용이 덮어쓰기 됨
  3. VS 프로젝트 설정에서 기본적으로 해당 디렉터리를 헤더 포함 경로로 등록하므로 #include “MainWindow.g.h”로 파일을 불러올 수 있음

MainWindow.xaml.h

표준 C++ 헤더 파일로 MainWindow 클래스의 선언부를 작성합니다.

IDL에서 생성한 기본 인터페이스를 자동으로 포함합니다.

클래스 필드, 전용 변수, 전방 선언 등을 작성하며 일반 C++ .h 헤더 파일과 동일한 역할을 수행합니다.

사용할 변수와 함수만 정의하고 구현은 작성하지 않으며, 다른 클래스에서 include로 참조할 수 있습니다.

// 헤더 가드: 헤더 파일 중복 포함을 방지해 중복 정의 컴파일 오류를 막음
#pragma once

// MainWindow.idl 컴파일로 자동 생성된 연결용 헤더 파일 불러오기
// .g.h 내부에는 IDL에서 생성한 IMainWindow 인터페이스, MainWindowT<> 템플릿 기본 클래스, WinRT 메타데이터 관련 정의가 포함됨
// IDL에 선언한 MyProperty, 생성자 인터페이스 모두 이곳에 정의됨
#include "MainWindow.g.h"

// 【구현 네임스페이스】창 비즈니스 로직 구현 클래스를 보관하며 자체 작성한 속성/함수를 여기서 구현
namespace winrt::WinuiCppDemo::implementation
{
    // MainWindowT<MainWindow>는 .g.h에서 제공하는 CRTP 템플릿 기본 클래스
    // 1. IDL로 정의한 IMainWindow 인터페이스를 자동 상속하며 IDL에 선언한 모든 멤버 구현을 강제
    // 2. WinUI 창 하위 로직, XAML 컴포넌트 초기화, ABI 인터페이스 전달 로직을 캡슐화
    struct MainWindow : MainWindowT<MainWindow>
    {
        // 창 기본 생성자, IDL에 선언한 MainWindow();와 대응
        MainWindow()
        {
            // 공식 규칙: 생성자 내에서 InitializeComponent() 호출 금지
            // XAML 컨트롤 초기화는 프레임워크가 지연 실행하며 생성 단계에서 호출하면 생명주기 크래시 발생
            // 마이크로소프트 cppwinrt 공식 문서 참고
            // See https://github.com/microsoft/cppwinrt/tree/master/nuget#initializecomponent
        }

        // IDL 내 Int32 MyProperty;에 대응하는 자동 생성 속성 읽기 인터페이스
        // 속성의 int32_t 값을 반환
        int32_t MyProperty();
        // IDL 내 Int32 MyProperty;에 대응하는 자동 생성 속성 쓰기 인터페이스
        // 속성 값을 수정하며 내부에서 자동으로 MyPropertyChanged 변경 이벤트를 트리거해 x:Bind가 UI 갱신하도록 함
        void MyProperty(int32_t value);
    };
}

// 【팩토리 네임스페이스】WinRT 객체 팩토리 클래스로 외부에 MainWindow 창 객체를 노출하고 인스턴스화하는 역할
namespace winrt::WinuiCppDemo::factory_implementation
{
    // MainWindowT 두 번째 매개변수에 위 implementation::MainWindow 구현 클래스를 바인딩
    // 역할: 통일된 ABI 생성 진입점을 제공해 XAML 엔진, 외부 코드(C#/기타 컴포넌트)에서 창 인스턴스를 생성할 수 있게 함
    struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
    {
    };
}Code language: C++ (cpp)

MainWindow.xaml.cpp

C++ 구현 파일로 모든 비즈니스 로직 코드를 작성하는 곳입니다.

  • MainWindow.xaml.h에 선언한 모든 함수의 구현부 작성
  • 컨트롤 이벤트 처리 (버튼 클릭, 창 로딩, 입력 응답 등)
  • 데이터 가공, 네트워크, 파일 처리, 업무 연산 로직 작성
  • UI 컨트롤 조작, 바인딩 속성 수정, 다이얼로그 팝업, 페이지 이동 등 상호작용 로직 구현
// 미컴파일 헤더 파일 불러오기, 프로젝트 공통 헤더 및 WinRT 기본 투영 헤더를 통합 포함해 컴파일 속도 향상
#include "pch.h"
// 현재 창 클래스의 헤더 파일 불러오기, MainWindow 구현 클래스 선언이 담김
#include "MainWindow.xaml.h"

// 조건부 컴파일: IDL에서 자동 생성된 하위 연결 파일 MainWindow.g.cpp 존재 여부 확인
// .g.cpp는 midl+cppwinrt 도구가 .idl 기반으로 생성하며 속성 변경 이벤트, ABI 하위 전달 로직을 캡슐화
// 파일이 존재하면 불러와 속성 변경 알림(MyPropertyChanged), XAML 런타임 바인딩이 정상 작동하도록 함
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif

// 전역 네임스페이스 축약, 이후 코드에서 winrt:: 접두사를 반복 작성하지 않아도 됨
using namespace winrt;
// WinUI Xaml 핵심 컨트롤, 창, 라우팅 이벤트 등 API 네임스페이스
using namespace Microsoft::UI::Xaml;

// 공식 주석: WinUI 프로젝트 구조, 템플릿 관련 문서 주소
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

// implementation 네임스페이스: MainWindow 창 비즈니스 로직 구현을 담으며 헤더 파일의 클래스 선언과 대응
namespace winrt::WinuiCppDemo::implementation
{
    // IDL에 선언한 MyProperty 속성【읽기 메서드】구현
    // Int32 타입 속성 값을 반환하며 XAML x:Bind가 데이터를 읽을 때 이 함수를 호출
    int32_t MainWindow::MyProperty()
    {
        // 기본 템플릿은 미구현 예외를 던지며 실제 값 저장 변수의 반환 로직으로 직접 교체해야 함
        throw hresult_not_implemented();
    }

    // IDL에 선언한 MyProperty 속성【쓰기 메서드】구현
    // value: 수정할 새 속성 값을 전달하며 값 변경 시 하위에서 자동 MyPropertyChanged 이벤트를 트리거해 바인딩된 UI 갱신
    void MainWindow::MyProperty(int32_t /* value */)
    {
        // 기본 템플릿은 미구현 예외를 던지며 변수 할당 로직을 직접 추가해야 함
        throw hresult_not_implemented();
    }
}Code language: PHP (php)

창 클래스 분석 및 버튼 클릭 동작 구현하기

이제 이 메인 창에 버튼을 추가하고 버튼 클릭 시 MyProperty 속성을 자동으로 1 증가시킨 뒤 다이얼로그를 띄워 현재 MyProperty 값을 표시하는 기능을 구현해보겠습니다.

먼저 XAML에 버튼을 추가합니다.

    <Grid>
        <Button Content="Click Me" HorizontalAlignment="Center" VerticalAlignment="Center" Click="OnButtonClick"/>
    </Grid>Code language: C++ (cpp)

버튼 표시 텍스트는 Click Me이고 클릭 이벤트로 OnButtonClick을 바인딩했습니다.

마우스 커서를 OnButtonClick 위에 두고 F12 키를 누르면 VS가 해당 이벤트 실행 함수를 자동 생성합니다.

그러면 MainWindow.xaml.h, MainWindow.xaml.cpp 파일에 아래 함수가 추가됩니다.

void OnButtonClick(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e);Code language: C++ (cpp)

함수 선언이 추가된 모습입니다.

MainWindow.xaml.cpp 파일에 해당 함수의 구현 코드가 생성됩니다.

그 다음 cpp 파일의 코드를 수정하겠습니다.

이번 시간은 여기까지이고 다음 시간에 버튼 클릭 기능을 완전히 구현하고 속성 바인딩 원리를 알아보겠습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다