다음을 통해 공유


C++/WinRT에서 이벤트 작성

이 주제는 C++/WinRT 주제가 설명하는 Windows 런타임 구성 요소 및 소비 애플리케이션을 구축하는 방법을 바탕으로 한 Windows 런타임 구성 요소를 다룹니다.

이 항목에서 추가하는 새로운 기능은 다음과 같습니다.

  • 온도가 영하로 떨어지면 이벤트를 발생하도록 온도계 런타임 클래스를 업데이트합니다.
  • 해당 이벤트를 처리할 수 있도록 온도계 런타임 클래스를 사용하는 Core 앱을 업데이트합니다.

비고

C++/WinRT VSIX(Visual Studio 확장) 및 NuGet 패키지(프로젝트 템플릿 및 빌드 지원을 함께 제공)를 설치하고 사용하는 방법에 대한 자세한 내용은 C++/WinRT대한 Visual Studio 지원을 참조하세요.

중요합니다

C++/WinRT를 사용하여 런타임 클래스를 소비 및 작성하는 방법에 대한 이해를 돕는 필수 개념과 용어에 대해서는 C++/WinRT를 사용하여 API 소비하기C++/WinRT를 사용하여 API 작성하기를 참조하세요.

ThermometerWRCThermometerCoreApp 만들기.

최신 정보를 반영해 코드를 작성하고 실행하고 싶다면, 첫 번째 단계로 Windows 런타임 구성 요소와 C++/WinRT 토픽에 있는 연습 단계를 따르세요. 이 작업을 수행하면 ThermometerWRC Windows 런타임 구성 요소와 이를 사용하는 ThermometerCoreApp 코어 앱이 있습니다.

ThermometerWRC을 업데이트하여 이벤트를 트리거합니다.

Thermometer.idl을 아래 목록처럼 업데이트합니다. 단정밀도 부동 소수점 숫자를 인수로 사용하는 EventHandler 대리자 형식의 이벤트를 선언하는 방법입니다.

// Thermometer.idl
namespace ThermometerWRC
{
    runtimeclass Thermometer
    {
        Thermometer();
        void AdjustTemperature(Single deltaFahrenheit);
        event Windows.Foundation.EventHandler<Single> TemperatureIsBelowFreezing;
    };
}

파일을 저장합니다. 프로젝트는 현재 상태에서 완료로 빌드되지 않지만, 지금이라도 빌드를 수행하여 업데이트된 버전의 \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.hThermometer.cpp 스텁 파일을 생성하세요. 이러한 파일 내에서 이제 TemperatureIsBelowFreezing 이벤트의 스텁 구현을 볼 수 있습니다. C++/WinRT에서 IDL 선언 이벤트는 오버로드된 함수 집합으로 구현됩니다(속성이 오버로드된 get 및 set 함수 쌍으로 구현되는 방식과 유사). 하나의 오버로드는 등록할 대리자를 가져오고 토큰(winrt::event_token)을 반환합니다. 다른 하나는 토큰을 사용하고 연결된 대리자의 등록을 취소합니다.

이제 Thermometer.hThermometer.cpp을 열고, 온도계 런타임 클래스의 구현을 업데이트합니다. Thermometer.h에서 두 오버로드된 TemperatureIsBelowFreezing 함수와 함께 해당 함수의 구현에 사용할 프라이빗 이벤트 데이터 멤버도 추가합니다.

// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
    struct Thermometer : ThermometerT<Thermometer>
    {
        ...
        winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler);
        void TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept;

    private:
        winrt::event<Windows::Foundation::EventHandler<float>> m_temperatureIsBelowFreezingEvent;
        ...
    };
}
...

위에서 볼 수 있듯이 이벤트는 winrt::event 구조체 템플릿으로 표현되며, 특정 대리자 형식(자체는 인수 형식으로 매개 변수화될 수 있습니다)으로 매개 변수화됩니다.

Thermometer.cpp두 오버로드된 TemperatureIsBelowFreezing 함수를 구현합니다.

// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
    winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<float> const& handler)
    {
        return m_temperatureIsBelowFreezingEvent.add(handler);
    }

    void Thermometer::TemperatureIsBelowFreezing(winrt::event_token const& token) noexcept
    {
        m_temperatureIsBelowFreezingEvent.remove(token);
    }

    void Thermometer::AdjustTemperature(float deltaFahrenheit)
    {
        m_temperatureFahrenheit += deltaFahrenheit;
        if (m_temperatureFahrenheit < 32.f) m_temperatureIsBelowFreezingEvent(*this, m_temperatureFahrenheit);
    }
}

비고

자동 이벤트 취소자에 대한 자세한 내용은 등록된 대리자취소를 참조하세요. 무료로 귀하의 이벤트에 대한 자동 이벤트 취소 기능을 구현할 수 있습니다. 즉, C++/WinRT 프로젝션이 제공하는 이벤트 리보커의 오버로드를 구현할 필요가 없습니다.

다른 오버로드(등록 및 수동 해지 오버로드)는 프로젝션에 내장되어 있지 않습니다. 이는 시나리오에 최적으로 구현할 수 있는 유연성을 제공하기 위한 것입니다. 이벤트::add이벤트::remove를 구현에 표시된 대로 호출하는 것은 효율적이고 동시성과 스레드에 안전한 기본 설정입니다. 그러나 이벤트가 매우 많은 경우 각각에 대한 이벤트 필드를 원하지 않고 대신 일종의 스파스 구현을 선택할 수 있습니다.

온도가 영하로 떨어지면 TemperatureIsBelowFreezing 이벤트를 발생하도록 AdjustTemperature 함수의 구현이 업데이트되었음을 위에서 확인할 수도 있습니다.

이벤트를 처리하도록 ThermometerCoreApp 업데이트

ThermometerCoreApp 프로젝트에서 App.cpp다음 코드를 변경하여 이벤트 처리기를 등록한 다음 온도가 영하로 떨어지도록 합니다.

WINRT_ASSERT은 매크로 정의이며 _ASSERTE로 확장됩니다.

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto &, float temperatureFahrenheit)
        {
            WINRT_ASSERT(temperatureFahrenheit < 32.f); // Put a breakpoint here.
        });
    }
    ...

    void Uninitialize()
    {
        m_thermometer.TemperatureIsBelowFreezing(m_eventToken);
    }
    ...
    
    void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
    {
        m_thermometer.AdjustTemperature(-1.f);
        ...
    }
    ...
};

OnPointerPressed 메서드의 변경 사항에 유의하세요. 이제 창을 클릭할 때마다 온도계 온도에서 화씨 1도를 뺄 있습니다. 이제 앱은 온도가 영하로 떨어지면 발생하는 이벤트를 처리합니다. 이벤트가 예상대로 발생하는 것을 보여주려면 TemperatureIsBelowFreezing 이벤트를 처리하는 람다 식 내에 중단점을 배치하고, 앱을 실행한 후 창 안쪽을 클릭하세요.

ABI에서 매개 변수가 설정된 대리자

이벤트에 애플리케이션 이진 인터페이스(ABI), 예를 들어 구성 요소와 이를 사용하는 애플리케이션 간에 액세스할 수 있어야 하는 경우, 해당 이벤트는 Windows 런타임 대리자 유형을 사용해야 합니다. 위의 예제에서는 Windows::Foundation::EventHandler<T> Windows 런타임 대리자 형식을 사용합니다. TypedEventHandler<TSender, TResult>는 Windows 런타임 대리자 유형의 또 다른 예입니다.

이러한 두 대리자 형식에 대한 형식 매개 변수는 ABI를 교차해야 하므로 형식 매개 변수도 Windows 런타임 형식이어야 합니다. 여기에는 Windows 런타임 클래스, 타사 런타임 클래스 및 숫자 및 문자열과 같은 기본 형식이 포함됩니다. 이 컴파일러는 해당 제약 조건을 잊어버린 경우 "T는 WinRT 형식이어야 합니다" 오류로 도움을 줍니다.

다음은 코드 목록 형식의 예입니다. 이 항목에서 이전에 만든 ThermometerWRCThermometerCoreApp 프로젝트의 코드를 이 목록에 있는 코드처럼 편집하세요.

이 첫 번째 목록은 ThermometerWRC 프로젝트에 대한 것입니다. 아래와 같이 ThermometerWRC.idl을 편집한 후 프로젝트를 빌드하고, 이전에 MyEventArgs.h.cpp를 했던 것처럼 Generated Files 폴더에서 Thermometer.h.cpp를 프로젝트로 복사하십시오. 기억하세요, 두 파일에서 static_assert을 삭제하십시오.

// ThermometerWRC.idl
namespace ThermometerWRC
{
    [default_interface]
    runtimeclass MyEventArgs
    {
        Single TemperatureFahrenheit{ get; };
    }

    [default_interface]
    runtimeclass Thermometer
    {
        ...
        event Windows.Foundation.EventHandler<ThermometerWRC.MyEventArgs> TemperatureIsBelowFreezing;
        ...
    };
}

// MyEventArgs.h
#pragma once
#include "MyEventArgs.g.h"

namespace winrt::ThermometerWRC::implementation
{
    struct MyEventArgs : MyEventArgsT<MyEventArgs>
    {
        MyEventArgs() = default;
        MyEventArgs(float temperatureFahrenheit);
        float TemperatureFahrenheit();

    private:
        float m_temperatureFahrenheit{ 0.f };
    };
}

// MyEventArgs.cpp
#include "pch.h"
#include "MyEventArgs.h"
#include "MyEventArgs.g.cpp"

namespace winrt::ThermometerWRC::implementation
{
    MyEventArgs::MyEventArgs(float temperatureFahrenheit) : m_temperatureFahrenheit(temperatureFahrenheit)
    {
    }

    float MyEventArgs::TemperatureFahrenheit()
    {
        return m_temperatureFahrenheit;
    }
}

// Thermometer.h
...
struct Thermometer : ThermometerT<Thermometer>
{
...
    winrt::event_token TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler);
...
private:
    winrt::event<Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs>> m_temperatureIsBelowFreezingEvent;
...
}
...

// Thermometer.cpp
#include "MyEventArgs.h"
...
winrt::event_token Thermometer::TemperatureIsBelowFreezing(Windows::Foundation::EventHandler<ThermometerWRC::MyEventArgs> const& handler) { ... }
...
void Thermometer::AdjustTemperature(float deltaFahrenheit)
{
    m_temperatureFahrenheit += deltaFahrenheit;

    if (m_temperatureFahrenheit < 32.f)
    {
        auto args = winrt::make_self<winrt::ThermometerWRC::implementation::MyEventArgs>(m_temperatureFahrenheit);
        m_temperatureIsBelowFreezingEvent(*this, *args);
    }
}
...

이 목록은 ThermometerCoreApp 프로젝트에 대한 것입니다.

// App.cpp
...
void Initialize(CoreApplicationView const&)
{
    m_eventToken = m_thermometer.TemperatureIsBelowFreezing([](const auto&, ThermometerWRC::MyEventArgs args)
    {
        float degrees = args.TemperatureFahrenheit();
        WINRT_ASSERT(degrees < 32.f); // Put a breakpoint here.
    });
}
...

ABI를 통한 간단한 신호

이벤트와 함께 매개 변수 또는 인수를 전달할 필요가 없는 경우 고유한 간단한 Windows 런타임 대리자 형식을 정의할 수 있습니다. 아래 예제에서는 더 간단한 버전의 Thermometer 런타임 클래스를 보여줍니다. SignalDelegate라는 대리자 형식을 선언하고 나서, 매개 변수가 있는 이벤트 대신 신호 형식의 이벤트를 발생시키는 데 이를 사용합니다.

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

    runtimeclass Thermometer
    {
        Thermometer();
        event ThermometerWRC.SignalDelegate SignalTemperatureIsBelowFreezing;
        void AdjustTemperature(Single value);
    };
}
// Thermometer.h
...
namespace winrt::ThermometerWRC::implementation
{
    struct Thermometer : ThermometerT<Thermometer>
    {
        ...

        winrt::event_token SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler);
        void SignalTemperatureIsBelowFreezing(winrt::event_token const& token);
        void AdjustTemperature(float deltaFahrenheit);

    private:
        winrt::event<ThermometerWRC::SignalDelegate> m_signal;
        float m_temperatureFahrenheit{ 0.f };
    };
}
// Thermometer.cpp
...
namespace winrt::ThermometerWRC::implementation
{
    winrt::event_token Thermometer::SignalTemperatureIsBelowFreezing(ThermometerWRC::SignalDelegate const& handler)
    {
        return m_signal.add(handler);
    }

    void Thermometer::SignalTemperatureIsBelowFreezing(winrt::event_token const& token)
    {
        m_signal.remove(token);
    }

    void Thermometer::AdjustTemperature(float deltaFahrenheit)
    {
        m_temperatureFahrenheit += deltaFahrenheit;
        if (m_temperatureFahrenheit < 32.f)
        {
            m_signal();
        }
    }
}
// App.cpp
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
    ThermometerWRC::Thermometer m_thermometer;
    winrt::event_token m_eventToken;
    ...
    
    void Initialize(CoreApplicationView const &)
    {
        m_eventToken = m_thermometer.SignalTemperatureIsBelowFreezing([] { /* ... */ });
    }
    ...

    void Uninitialize()
    {
        m_thermometer.SignalTemperatureIsBelowFreezing(m_eventToken);
    }
    ...

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

프로젝트 내의 매개변수를 가진 대리자, 단순 신호 및 콜백

Visual Studio 프로젝트 내부 이벤트가 필요하고, 해당 이벤트가 Windows 런타임 형식에 제한되지 않는 경우, 여전히 winrt::event<Delegate> 클래스 템플릿을 사용할 수 있습니다. winrt::delegate가 Windows 런타임이 아닌 매개 변수도 지원하므로, 실제 Windows 런타임 대리자 형식 대신 winrt::delegate를 사용하기만 하면 됩니다.

아래 예제에서는 먼저 매개 변수(기본적으로 간단한 신호)를 취하지 않는 대리자 서명과 문자열을 사용하는 대리자 서명을 보여줍니다.

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!");

이벤트에 원하는 만큼의 구독 대리자를 추가할 수 있다는 점을 확인하십시오. 그러나 이벤트와 관련된 오버헤드가 있습니다. 단순히 단일 구독 대리자가 있는 간단한 콜백이 필요한 경우, winrt::delegate<... T>를 독립적으로 사용할 수 있습니다.

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::d 선택 취소를 C++/WinRT에서 해당 패턴을 복제하는 데 도움이 됩니다.

지연 가능한 이벤트

Windows 런타임의 일반적인 패턴은 지연 가능한 이벤트입니다. 이벤트 처리기 는 이벤트 인수의 GetDeferral 메서드를 호출하여 지연을(를) 취합니다. 이렇게 하면 이벤트 원본에 지연이 완료될 때까지 이벤트 후 활동을 연기해야 임을 나타냅니다. 이렇게 하면 이벤트 처리기가 이벤트에 대한 응답으로 비동기 작업을 수행할 수 있습니다.

winrt::deferrable_event_args 구조체 템플릿은 Windows 런타임 지연 패턴을 구현하는 데 도움이 되는 클래스입니다. 다음은 예제입니다.

// Widget.idl
namespace Sample
{
    runtimeclass WidgetStartingEventArgs
    {
        Windows.Foundation.Deferral GetDeferral();
        Boolean Cancel;
    };

    runtimeclass Widget
    {
        event Windows.Foundation.TypedEventHandler<
            Widget, WidgetStartingEventArgs> Starting;
    };
}

// Widget.h
namespace winrt::Sample::implementation
{
    struct Widget : WidgetT<Widget>
    {
        Widget() = default;

        event_token Starting(Windows::Foundation::TypedEventHandler<
            Sample::Widget, Sample::WidgetStartingEventArgs> const& handler)
        {
            return m_starting.add(handler);
        }
        void Starting(event_token const& token) noexcept
        {
            m_starting.remove(token);
        }

    private:
        event<Windows::Foundation::TypedEventHandler<
            Sample::Widget, Sample::WidgetStartingEventArgs>> m_starting;
    };

    struct WidgetStartingEventArgs : WidgetStartingEventArgsT<WidgetStartingEventArgs>,
                                     deferrable_event_args<WidgetStartingEventArgs>
    //                               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    {
        bool Cancel() const noexcept { return m_cancel; }
        void Cancel(bool value) noexcept { m_cancel = value; }
        bool m_cancel = false;
    };
}

이벤트 수신자가 지연 가능한 이벤트 패턴을 사용하는 방법은 다음과 같습니다.

// EventRecipient.h
widget.Starting([](auto sender, auto args) -> fire_and_forget
{
    auto deferral = args.GetDeferral();
    if (!co_await CanWidgetStartAsync(sender))
    {
        // Do not allow the widget to start.
        args.Cancel(true);
    }
    deferral.Complete();
});

이벤트 소스의 구현자(생산자)로서 winrt::deferrable_event_args클래스로부터 이벤트 인수 클래스를 파생합니다. deferrable_event_argsT T::GetDeferral 구현합니다. 모든 대기 중인 지연이 완료될 때 완료되는 새 도우미 메서드 deferrable_event_args::wait_for_deferrals를 노출합니다(지연이 없으면 즉시 완료됩니다).

// Widget.h
IAsyncOperation<bool> TryStartWidget(Widget const& widget)
{
    auto args = make_self<WidgetStartingEventArgs>();
    // Raise the event to let people know that the widget is starting
    // and give them a chance to prevent it.
    m_starting(widget, *args);
    // Wait for deferrals to complete.
    co_await args->wait_for_deferrals();
    // Use the results.
    bool started = false;
    if (!args->Cancel())
    {
        widget.InsertBattery();
        widget.FlipPowerSwitch();
        started = true;
    }
    co_return started;
}

디자인 지침

함수 매개변수로 이벤트를 전달하고 대리자는 전달하지 않는 것이 좋습니다. 예외로 winrt::event추가 함수가 있는데, 이 경우 대리자를 반드시 전달해야 합니다. 이 지침이 있는 이유는 Windows 런타임 언어마다 대리자가 (단일 클라이언트 등록을 지원하는지, 아니면 여러 클라이언트 등록을 지원하는지에 따라) 서로 다른 형식으로 나타날 수 있기 때문입니다. 여러 구독자 모델을 사용하는 이벤트는 훨씬 더 예측 가능하고 일관된 옵션을 구성합니다.

이벤트 처리기 대리자의 서명은 두 매개 변수(보낸 사람(IInspectable), 및 인수(예를 들어 RoutedEventArgs같은 이벤트 인수 유형))로 구성되어야 합니다.

내부 API를 디자인하는 경우 이러한 지침이 반드시 적용되지는 않습니다. 하지만 내부 API는 시간이 지남에 따라 공개되는 경우가 많습니다.