C++/WinRT의 이벤트 작성Author events in C++/WinRT

이 항목에서는 해당 잔액이 차변으로 전환될 때 이벤트가 발생하는 은행 계좌 —를 나타내는 런타임 클래스를 포함하는 Windows 런타임 구성 요소를 작성하는 방법에 대해서 설명합니다.This topic demonstrates how to author a Windows Runtime component containing a runtime class representing a bank account—a bank account that raises an event when its balance goes into debit. 이 항목에서는 은행 계좌 런타임 클래스를 사용하면서 함수를 호출하여 잔액을 조정하거나, 발생하는 모든 이벤트를 처리하는 주요 앱에 대해서도 설명합니다.This topic also demonstrates a Core App that consumes the bank account runtime class, calls a function to adjust the balance, and handles any events that result.

참고

프로젝트 템플릿 및 빌드 지원을 함께 제공하는 C++/WinRT Visual Studio 확장(VSIX) 및 NuGet 패키지를 설치하고 사용하는 방법에 대한 자세한 내용은 Visual Studio의 C++/WinRT 지원을 참조하세요.For info about installing and using the C++/WinRT Visual Studio Extension (VSIX) and the NuGet package (which together provide project template and build support), see Visual Studio support for C++/WinRT.

중요

C++/WinRT를 사용해 런타임 클래스를 사용하거나 작성하는 방법을 더욱 쉽게 이해할 수 있는 필수 개념과 용어에 대해서는 C++/WinRT를 통한 API 사용C++/WinRT를 통한 API 작성을 참조하세요.For essential concepts and terms that support your understanding of how to consume and author runtime classes with C++/WinRT, see Consume APIs with C++/WinRT and Author APIs with C++/WinRT.

Windows 런타임 구성 요소(BankAccountWRC) 만들기Create a Windows Runtime component (BankAccountWRC)

먼저 Microsoft Visual Studio에서 새 프로젝트를 만듭니다.Begin by creating a new project in Microsoft Visual Studio. Windows 런타임 구성 요소(C++/WinRT) 프로젝트를 만든 다음, 이름을 BankAccountWRC("은행 계좌 Windows 런타임 구성 요소"인 경우)로 지정합니다.Create a Windows Runtime Component (C++/WinRT) project, and name it BankAccountWRC (for "bank account Windows Runtime component"). 프로젝트 이름을 BankAccountWRC로 지정하면 이 항목의 나머지 단계를 가장 쉽게 수행할 수 있습니다.Naming the project BankAccountWRC will give you the easiest experience with the rest of the steps in this topic. 아직 프로젝트를 빌드하지 마세요.Don't built the project yet.

새로 만든 프로젝트에는 Class.idl이라는 이름의 파일이 포함되어 있습니다.The newly-created project contains a file named Class.idl. 해당 파일의 이름을 바꿉니다BankAccount.idl(.idl 파일의 이름을 바꾸면 자동으로 종속 .h.cpp 파일의 이름도 바뀜).Rename that file BankAccount.idl (renaming the .idl file automatically renames the dependent .h and .cpp files, too). BankAccount.idl의 콘텐츠를 아래 목록으로 바꿉니다.Replace the contents of BankAccount.idl with the listing below.

// BankAccountWRC.idl
namespace BankAccountWRC
{
    runtimeclass BankAccount
    {
        BankAccount();
        event Windows.Foundation.EventHandler<Single> AccountIsInDebit;
        void AdjustBalance(Single value);
    };
}

파일을 저장합니다.Save the file. 현재는 프로젝트 빌드가 완료되지 않지만, 지금 빌드하는 것은 BankAccount 런타임 클래스를 구현할 원본 코드 파일을 생성하므로 유용한 작업입니다.The project won't build to completion at the moment, but building now is a useful thing to do because it generates the source code files in which you'll implement the BankAccount runtime class. 따라서 계속해서 지금 빌드합니다(이 단계에서 표시될 것으로 예상되는 빌드 오류는 발견되는 Class.hClass.g.h로 처리해야 함).So go ahead and build now (the build errors you can expect to see at this stage have to do with Class.h and Class.g.h not being found).

빌드 프로세스에서 midl.exe 도구가 실행되어 구성 요소의 Windows 런타임 메타데이터 파일(\BankAccountWRC\Debug\BankAccountWRC\BankAccountWRC.winmd)을 생성합니다.During the build process, the midl.exe tool is run to create your component's Windows Runtime metadata file (which is \BankAccountWRC\Debug\BankAccountWRC\BankAccountWRC.winmd). 그런 다음, cppwinrt.exe 도구가 실행되어(-component 옵션과 함께) 구성 요소를 작성하도록 지원하는 원본 코드 파일을 생성합니다.Then, the cppwinrt.exe tool is run (with the -component option) to generate source code files to support you in authoring your component. 원본 코드 파일에는 IDL로 선언한 BankAccount 런타임 클래스의 구현을 시작할 수 있는 스텁이 포함됩니다.These files include stubs to get you started implementing the BankAccount runtime class that you declared in your IDL. 이 스텁이 \BankAccountWRC\BankAccountWRC\Generated Files\sources\BankAccount.hBankAccount.cpp입니다.Those stubs are \BankAccountWRC\BankAccountWRC\Generated Files\sources\BankAccount.h and BankAccount.cpp.

프로젝트 노드를 마우스 오른쪽 단추로 클릭하고 파일 탐색기에서 폴더 열기를 클릭합니다.Right-click the project node and click Open Folder in File Explorer. 파일 탐색기에서 프로젝트 폴더가 열립니다.This opens the project folder in File Explorer. 여기에서 \BankAccountWRC\BankAccountWRC\Generated Files\sources\ 폴더에서 스텁 파일 BankAccount.hBankAccount.cpp를 복사하여 프로젝트 파일을 포함하는 폴더인 \BankAccountWRC\BankAccountWRC\에 붙여넣고 대상에서 파일을 바꿉니다.There, copy the stub files BankAccount.h and BankAccount.cpp from the folder \BankAccountWRC\BankAccountWRC\Generated Files\sources\ and into the folder that contains your project files, which is \BankAccountWRC\BankAccountWRC\, and replace the files in the destination. 이제 BankAccount.hBankAccount.cpp를 열고 런타임 클래스를 구현합니다.Now, let's open BankAccount.h and BankAccount.cpp and implement our runtime class. BankAccount.h에서 프라이빗 멤버 두 개를 BankAccount 구현(팩터리 구현 아님)에 추가합니다.In BankAccount.h, add two private members to the implementation (not the factory implementation) of BankAccount.

// BankAccount.h
...
namespace winrt::BankAccountWRC::implementation
{
    struct BankAccount : BankAccountT<BankAccount>
    {
        ...

    private:
        winrt::event<Windows::Foundation::EventHandler<float>> m_accountIsInDebitEvent;
        float m_balance{ 0.f };
    };
}
...

위에서 살펴본 대로, 이벤트는 특정 대리자 형식으로 매개 변수가 있는 winrt::event 구조체 템플릿을 기반으로 구현됩니다.As you can see above, the event is implemented in terms of the winrt::event struct template, parameterized by a particular delegate type.

BankAccount.cpp에서는 아래 코드 예제와 같이 함수를 구현합니다.In BankAccount.cpp, implement the functions as shown in the code example below. C++/WinRT에서 IDL로 선언된 이벤트는 오버로드된 함수 세트로 구현됩니다(속성이 오버로드된 get 및 set 함수 세트로 구현되는 것과 유사한 방식).In C++/WinRT, an IDL-declared event is implemented as a set of overloaded functions (similar to the way a property is implemented as a pair of overloaded get and set functions). 한 오버로드는 등록할 대리자를 가지며 토큰을 반환합니다.One overload takes a delegate to be registered, and returns a token. 다른 오버로드는 토큰을 가지며 관련 대리자의 등록을 취소합니다.The other takes a token, and revokes the registration of the associated delegate.

// BankAccount.cpp
...
namespace winrt::BankAccountWRC::implementation
{
    winrt::event_token BankAccount::AccountIsInDebit(Windows::Foundation::EventHandler<float> const& handler)
    {
        return m_accountIsInDebitEvent.add(handler);
    }

    void BankAccount::AccountIsInDebit(winrt::event_token const& token) noexcept
    {
        m_accountIsInDebitEvent.remove(token);
    }

    void BankAccount::AdjustBalance(float value)
    {
        m_balance += value;
        if (m_balance < 0.f) m_accountIsInDebitEvent(*this, m_balance);
    }
}

참고

자동 이벤트 취소자에 대한 자세한 정보는 등록된 대리자 취소를 참조하세요.For details of what an auto event revoker is, see Revoke a registered delegate. 이벤트에 대한 자동 이벤트 취소자 구현을 무료로 가져올 수 있습니다.You get auto event revoker implementation for free for your event. 즉, 이벤트 취소자(C++/WinRT 프로젝션을 통해 제공)에 대한 오버로드를 구현할 필요가 없습니다.In other words, you don't need to implement the overload for the event revoker—that's provided for you by the C++/WinRT projection.

다른 오버로드(등록 및 수동 해지 오버로드)는 프로젝션에 반영되지 않습니다.The other overloads (the registration and manual revocation overloads) are not baked into the projection. 이는 시나리오에 최적으로 구현할 수 있는 유연성을 제공하기 위한 것입니다.That's to give you the flexibility to implement them optimally for your scenario. 이러한 구현에 나온 것처럼 event::addevent::remove 호출은 효율적이며 동시성/스레드로부터 안전한 기본값입니다.Calling event::add and event::remove as shown in these implementations is an efficient and concurrency/thread-safe default. 하지만 매우 많은 수의 이벤트를 처리하는 경우 각각에 대한 이벤트 필드 대신 일부 밀도가 낮은 구현을 원할 수 있습니다.But if you have a very large number of events, then you may not want an event field for each, but rather opt for some kind of sparse implementation instead.

잔액이 마이너스가 될 경우 AdjustBalance 함수의 구현에서 AccountIsInDebit 이벤트가 발생하는 시나리오는 위에서 확인할 수 있습니다.You can also see above that the implementation of the AdjustBalance function raises the AccountIsInDebit event if the balance goes negative.

경고 메시지로 인해 빌드가 중단되는 경우에는 경고를 해결하거나 프로젝트 속성 C/C++ > 일반 > 경고를 오류로 처리아니요(/WX-) 로 설정한 후 프로젝트를 다시 빌드합니다.If any warnings prevent you from building, then either resolve them or set the project property C/C++ > General > Treat Warnings As Errors to No (/WX-), and build the project again.

주요 앱(BankAccountCoreApp)을 만들어 Windows 런타임 구성 요소 테스트Create a Core App (BankAccountCoreApp) to test the Windows Runtime component

이제 BankAccountWRC 솔루션 또는 새로운 솔루션에서 새 프로젝트를 만듭니다.Now create a new project (either in your BankAccountWRC solution, or in a new one). 주요 앱(C++/WinRT) 프로젝트를 만들어서 이름을 BankAccountCoreApp이라고 지정합니다.Create a Core App (C++/WinRT) project, and name it BankAccountCoreApp.

참고

앞에서 설명한 것처럼 Windows 런타임 구성 요소에 대한 Windows 런타임 메타데이터 파일(BankAccountWRC라는 프로젝트)이 \BankAccountWRC\Debug\BankAccountWRC\ 폴더에 생성됩니다.As mentioned earlier, the Windows Runtime metadata file for your Windows Runtime component (whose project you named BankAccountWRC) is created in the folder \BankAccountWRC\Debug\BankAccountWRC\. 해당 경로의 첫 번째 세그먼트는 솔루션 파일이 포함된 폴더의 이름입니다. 다음 세그먼트는 Debug라는 하위 디렉터리입니다. 마지막 세그먼트는 Windows 런타임 구성 요소에 대해 이름이 지정된 하위 디렉터리입니다.The first segment of that path is the name of the folder that contains your solution file; the next segment is the subdirectory of that named Debug; and the last segment is the subdirectory of that named for your Windows Runtime component. 프로젝트 BankAccountWRC의 이름을 지정하지 않은 경우 메타데이터 파일은 \<YourProjectName>\Debug\<YourProjectName>\ 폴더에 있습니다.If you didn't name your project BankAccountWRC, then your metadata file will be in the folder \<YourProjectName>\Debug\<YourProjectName>\.

이제 핵심 앱 프로젝트(BankAccountCoreApp)에서 참조를 추가하고 \BankAccountWRC\Debug\BankAccountWRC\BankAccountWRC.winmd로 이동하거나, 두 프로젝트가 동일한 솔루션에 있는 경우 프로젝트 간 참조를 추가합니다.Now, in your Core App project (BankAccountCoreApp), add a reference, and browse to \BankAccountWRC\Debug\BankAccountWRC\BankAccountWRC.winmd (or add a project-to-project reference, if the two projects are in the same solution). 추가, 확인을 차례로 클릭합니다.Click Add, and then OK. 이제 BankAccountCoreApp을 빌드합니다.Now build BankAccountCoreApp. 드물지만 페이로드 파일인 readme.txt가 존재하지 않는다는 오류가 표시되는 경우에는 해당 파일을 Windows 런타임 구성 요소 프로젝트에서 제외하여 다시 빌드한 다음, BankAccountCoreApp을 다시 빌드합니다.In the unlikely event that you see an error that the payload file readme.txt doesn't exist, exclude that file from the Windows Runtime component project, rebuild it, then rebuild BankAccountCoreApp.

빌드 프로세스에서 cppwinrt.exe 도구가 실행되면서 참조된 .winmd 파일을 원본 코드 파일로 처리합니다. 이 원본 코드 파일에는 구성 요소를 사용할 때 지원할 목적으로 프로젝션된 형식이 포함되어 있습니다.During the build process, the cppwinrt.exe tool is run to process the referenced .winmd file into source code files containing projected types to support you in consuming your component. 구성 요소의 런타임 클래스에 맞게 프로젝션된 형식의 헤더(BankAccountWRC.h)가 \BankAccountCoreApp\BankAccountCoreApp\Generated Files\winrt\ 폴더로 생성됩니다.The header for the projected types for your component's runtime classes—named BankAccountWRC.h—is generated into the folder \BankAccountCoreApp\BankAccountCoreApp\Generated Files\winrt\.

이 헤더를 App.cpp에 추가합니다.Include that header in App.cpp.

#include <winrt/BankAccountWRC.h>

또한 App.cpp에서 다음 코드를 추가하여 BankAccount(프로젝션된 형식의 기본 생성자 사용)를 인스턴스화하고 이벤트 처리기를 등록한 다음, 계좌에서 차변이 발생하도록 합니다.Also in App.cpp, add the following code to instantiate a BankAccount (using the projected type's default constructor), register an event handler, and then cause the account to go into debit.

WINRT_ASSERT는 매크로 정의이며 _ASSERTE로 확장됩니다.WINRT_ASSERT is a macro definition, and it expands to _ASSERTE.

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    BankAccountWRC::BankAccount m_bankAccount;
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_bankAccount.AccountIsInDebit([](const auto &, float balance)
        {
            WINRT_ASSERT(balance < 0.f); // Put a breakpoint here.
        });
    }
    ...

    void Uninitialize()
    {
        m_bankAccount.AccountIsInDebit(m_eventToken);
    }
    ...

    void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
    {
        m_bankAccount.AdjustBalance(-1.f);
        ...
    }
    ...
};

이제 창을 클릭할 때마다 은행 계좌의 잔고에서 1이 감액됩니다.Each time you click the window, you subtract 1 from the bank account's balance. 이벤트가 예상한 대로 발생하는지 확인하려면 AccountIsInDebit 이벤트를 처리하는 람다 식에 중단점을 입력한 후 앱을 실행하여 창 내부를 클릭합니다.To demonstrate that the event is being raised as expected, put a breakpoint inside the lambda expression that's handling the AccountIsInDebit event, run the app, and click inside the window.

ABI에서 매개 변수가 있는 대리자 및 단순 신호Parameterized delegates, and simple signals, across an ABI

구성 요소와 해당 소비 애플리케이션 사이 같은 ABI(Application Binary Interface)를 통해 이벤트에 액세스할 수 있어야 하는 경우 이벤트에는 Windows 런타임 대리자 형식이 사용되어야 합니다.If your event must be accessible across an application binary interface (ABI)—such as between a component and its consuming application—then your event must use a Windows Runtime delegate type. 위 예제에서는 Windows::Foundation::EventHandler<T> Windows 런타임 대리자 형식을 사용합니다.The example above uses the Windows::Foundation::EventHandler<T> Windows Runtime delegate type. TypedEventHandler<TSender, TResult> 는 Windows 런타임 대리자 형식의 또 다른 예입니다.TypedEventHandler<TSender, TResult> is another example of a Windows Runtime delegate type.

두 대리자 형식의 형식 매개 변수는 ABI를 가로질러야 하므로 형식 매개 변수도 Windows 런타임 형식이어야 합니다.The type parameters for those two delegate types have to cross the ABI, so the type parameters must be Windows Runtime types, too. 여기에는 Microsoft 및 타사 런타임 클래스와 숫자, 문자열 등의 기본 형식이 포함됩니다.That includes first- and third-party runtime classes, as well as primitive types such as numbers and strings. 해당 제약 조건을 잊을 경우 컴파일러가 “WinRT 형식이어야 합니다.” 오류를 처리하는 데 도움이 될 수 있습니다. The compiler helps you with a "must be WinRT type" error if you forget that constraint.

이벤트를 통해 매개 변수나 인수를 전달할 필요가 없는 경우에는 고유한 단순 Windows 런타임 대리자 형식을 정의할 수 있습니다.If you don't need to pass any parameters or arguments with your event, then you can define your own simple Windows Runtime delegate type. 아래 예제에서는 BankAccount 런타임 클래스의 더 단순한 버전을 보여 줍니다.The example below shows a simpler version of the BankAccount runtime class. SignalDelegate라는 대리자 형식을 선언한 다음, 이 대리자 형식을 사용하여 매개 변수가 있는 이벤트 대신 신호 형식 이벤트를 발생시킵니다.It declares a delegate type named SignalDelegate and then it uses that to raise a signal-type event instead of an event with a parameter.

// BankAccountWRC.idl
namespace BankAccountWRC
{
    delegate void SignalDelegate();

    runtimeclass BankAccount
    {
        BankAccount();
        event BankAccountWRC.SignalDelegate SignalAccountIsInDebit;
        void AdjustBalance(Single value);
    };
}
// BankAccount.h
...
namespace winrt::BankAccountWRC::implementation
{
    struct BankAccount : BankAccountT<BankAccount>
    {
        ...

        winrt::event_token SignalAccountIsInDebit(BankAccountWRC::SignalDelegate const& handler);
        void SignalAccountIsInDebit(winrt::event_token const& token);
        void AdjustBalance(float value);

    private:
        winrt::event<BankAccountWRC::SignalDelegate> m_signal;
        float m_balance{ 0.f };
    };
}
// BankAccount.cpp
...
namespace winrt::BankAccountWRC::implementation
{
    winrt::event_token BankAccount::SignalAccountIsInDebit(BankAccountWRC::SignalDelegate const& handler)
    {
        return m_signal.add(handler);
    }

    void BankAccount::SignalAccountIsInDebit(winrt::event_token const& token)
    {
        m_signal.remove(token);
    }

    void BankAccount::AdjustBalance(float value)
    {
        m_balance += value;
        if (m_balance < 0.f)
        {
            m_signal();
        }
    }
}
// App.cpp
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    BankAccountWRC::BankAccount m_bankAccount;
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_bankAccount.SignalAccountIsInDebit([] { /* ... */ });
    }
    ...

    void Uninitialize()
    {
        m_bankAccount.SignalAccountIsInDebit(m_eventToken);
    }
    ...

    void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
    {
        m_bankAccount.AdjustBalance(-1.f);
        ...
    }
    ...
};

프로젝트 내의 매개 변수가 있는 대리자, 단순 신호 및 콜백Parameterized delegates, simple signals, and callbacks within a project

C++/WinRT 프로젝트 내에서 내부적으로만 이벤트를 사용하는 경우에는 winrt::event 구조체 템플릿을 사용하지만, 참조로 계산되는 효율적인 대리자인 C++/WinRT의 Windows가 아닌 런타임 winrt::delegate<... T> 구조체 템플릿을 사용하여 매개 변수로 설정합니다.If your event is used only internally within your C++/WinRT project (not across binaries), then you still use the winrt::event struct template, but you parameterize it with C++/WinRT's non-Windows-Runtime winrt::delegate<... T> struct template, which is an efficient, reference-counted delegate. 이는 제한 없이 매개 변수를 지원하며 Windows 런타임 형식으로 제한되지 않습니다.It supports any number of parameters, and they are not limited to Windows Runtime types.

아래 예제에서는 매개 변수(기본적으로 단순 신호)를 사용하지 않는 대리자 서명을 보여 준 후 문자열을 사용하는 대리자 서명을 보여 줍니다.The example below first shows a delegate signature that doesn't take any parameters (essentially a simple signal), and then one that takes a string.

winrt::event<winrt::delegate<>> signal;
signal.add([] { std::wcout << L"Hello, "; });
signal.add([] { std::wcout << L"World!" << std::endl; });
signal();

winrt::event<winrt::delegate<std::wstring>> log;
log.add([](std::wstring const& message) { std::wcout << message.c_str() << std::endl; });
log.add([](std::wstring const& message) { Persist(message); });
log(L"Hello, World!");

구독 대리자를 원하는 만큼 이벤트에 추가하는 방법을 알 수 있습니다.Notice how you can add to the event as many subscribing delegates as you wish. 그러나 이벤트와 연결된 몇 가지 오버헤드가 있습니다.However, there is some overhead associated with an event. 단일 구독 대리자만 사용하는 간단한 콜백만 필요한 경우에는 자체적으로 winrt::delegate<... T> 를 사용할 수 있습니다.If all you need is a simple callback with only a single subscribing delegate, then you can use winrt::delegate<... T> on its own.

winrt::delegate<> signalCallback;
signalCallback = [] { std::wcout << L"Hello, World!" << std::endl; };
signalCallback();

winrt::delegate<std::wstring> logCallback;
logCallback = [](std::wstring const& message) { std::wcout << message.c_str() << std::endl; }f;
logCallback(L"Hello, World!");

이벤트 및 대리자가 프로젝트 내에서 내부적으로 사용되는 C++/CX 코드베이스에서 이식하는 경우에는 winrt::delegate를 통해 C++/WinRT에서 해당 패턴을 복제할 수 있습니다.If you're porting from a C++/CX codebase where events and delegates are used internally within a project, then winrt::delegate will help you to replicate that pattern in C++/WinRT.

디자인 지침Design guidelines

대리자가 아닌 이벤트를 함수 매개 변수로 전달하는 것이 좋습니다.We recommend that you pass events, and not delegates, as function parameters. winrt::eventadd 함수는 하나의 예외입니다. 이 경우에는 대리자를 전달해야 하기 때문입니다.The add function of winrt::event is the one exception, because you must pass a delegate in that case. 대리자는 하나 또는 여러 개의 클라이언트 등록을 지원하는지 여부에 따라 다양한 Windows 런타임 언어에 걸쳐 다른 형식을 사용할 수 있기 때문에 이 지침이 제공됩니다.The reason for this guideline is because delegates can take different forms across different Windows Runtime languages (in terms of whether they support one client registration, or multiple). 여러 구독자 모델이 있는 이벤트는 훨씬 더 예측 가능하고 일관성 있는 옵션을 구성합니다.Events, with their multiple subscriber model, constitute a much more predictable and consistent option.

이벤트 처리기 대리자의 서명은 두 개의 매개 변수인 ‘발신자’(IInspectable) 및 args(RoutedEventArgs와 같은 일부 이벤트 인수 형식)로 구성됩니다. The signature for an event handler delegate should consist of two parameters: sender (IInspectable), and args (some event argument type, for example RoutedEventArgs).

내부 API를 디자인하는 경우에는 이 지침이 반드시 적용되는 것은 아닙니다.Note that these guidelines don't necessarily apply if you're designing an internal API. 그래도 내부 API는 일반적으로 시간이 지남에 따라 공개됩니다.Although, internal APIs often become public over time.