Создание событий в C++/WinRT

В этом разделе используются сведения о компоненте среды выполнения Windows и потребляющем приложении, создание которых описано в статье Создание компонентов среды выполнения Windows с помощью C++/WinRT.

Вот новые функции, которые добавляются в этом разделе:

  • Обновление класса среды выполнения термометра, чтобы он порождал событие, когда его температура будет опускаться ниже нуля.
  • Обновление базового приложения, которое использует класс среды выполнения термометра для обработки этого события.

Примечание.

Сведения об установке и использовании расширения C++/WinRT для Visual Studio (VSIX) и пакета NuGet (которые вместе обеспечивают поддержку шаблона проекта и сборки) см. в разделе о поддержке C++/WinRT в Visual Studio.

Важно!

Основные понятия и термины, которые помогают понять, как использовать и создавать классы среды выполнения с помощью C++/WinRT, описаны в разделах Использование интерфейсов API с помощью C++/WinRT и Создание интерфейсов API с помощью C++/WinRT.

Создайте компонент ThermometerWRC и приложение ThermometerCoreApp

Если вы хотите следовать обновлениям, указанным в этом разделе, для создания и выполнения кода, то в первую очередь необходимо выполнить пошаговое руководство по созданию компонентов среды выполнения Windows с помощью C++/WinRT. Таким образом вы получите компонент среды выполнения Windows ThermometerWRC и базовое приложение 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.h и Thermometer.cpp. Внутри этих файлов теперь вы можете увидеть реализации заглушек события TemperatureIsBelowFreezing. В C++/WinRT объявленное в IDL событие реализуется как набор перегруженных функций (аналогично реализации свойства в виде пары перегруженных функций get и set). Одна перегрузка принимает делегат для регистрации и возвращает маркер (winrt::event_token). Другая получает маркер и отменяет регистрацию связанного делегата.

Теперь откройте Thermometer.h и Thermometer.cpp, а затем обновите реализацию класса среды выполнения Thermometer. В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, параметризованным по конкретному типу делегата (который сам может быть параметризован по типу args).

В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.

Другие перегрузки (перегрузки регистрации и отзыва вручную) не помещаются в проекцию. Благодаря этому вы можете оптимально реализовать их в соответствии со своим сценарием. Вызов функций event::add и event::remove таким образом, как в этих реализациях, эффективен и безопасен с точки зрения параллельной обработки и многопоточной обработки по умолчанию. Но если у вас очень большое количество событий, вам может не требоваться поле для каждого из них, и вы выберете вместо этого какой-либо другой вид выборочной реализации.

Кроме того, из приведенных ниже сведений видно, что реализация функции AdjustTemperature обновлена и будет порождать событие TemperatureIsBelowFreezing, когда температура будет опускаться ниже нуля.

Обновление приложения 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. Теперь каждый раз, когда щелкаете окно, вы будете вычитать один градус по Фаренгейту температуры термометра. И теперь приложение обрабатывает событие, которое возникает, когда температура опускается ниже нуля. Чтобы продемонстрировать, что событие вызывается должным образом, поместите точку останова внутри лямбда-выражения, которое обрабатывает событие TemperatureIsBelowFreezing, запустите приложение и щелкните внутри окна.

Параметризованные делегаты в ABI

Если ваше событие должно быть доступно через двоичный интерфейс приложения (ABI), например, между компонентом и его потребляющим приложением, то ваше событие должно использовать тип делегата среды выполнения Windows. В примере выше используется делегат среды выполнения Windows Windows::Foundation::EventHandler<T>. TypedEventHandler<TSender, TResult> является еще одним примером типа делегата среды выполнения Windows.

Параметры типа для этих двух типов делегатов должны пересекать ABI, поэтому параметры типа также должны быть типами среды выполнения Windows. Это включает в себя классы среды выполнения Windows, классы сторонней среды выполнения и примитивные типы, такие как числа и строки. Если вы забудете об этом ограничении, компилятор отобразит ошибку T must be WinRT type (T требуется тип WinRT).

Ниже приведен пример в виде листингов кода. Начните с проектов ThermometerWRC и ThermometerCoreApp, которые вы создали ранее, проходя этот раздел, и измените код в этих проектах так, чтобы он был похож на код в этих листингах.

Первый листинг предназначен для проекта 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, поскольку winrt::delegate также поддерживает параметры для среды выполнения, отличной от среды выполнения Windows.

В приведенном ниже примере сначала показана подпись делегата, которая не принимает никакие параметры (по сути, простой сигнал), а затем подпись, которая принимает строку.

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::d elegate<... 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::delegateпоможет скопировать этот шаблон в 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_args<T> реализует для вас 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), и args (некоторый тип аргумента события, например RoutedEventArgs).

Обратите внимание, что при проектировании внутреннего API, эти рекомендации не обязательно применять. Хотя внутренние API со временем часто становятся общедоступными.