Поделиться через


События автора в C++/WinRT

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

Ниже приведены новые функции, которые добавляет этот раздел.

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

Замечание

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

Это важно

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

Создайте ThermometerWRC и ThermometerCoreApp

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

Теперь откройте Thermometer.h и Thermometer.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 , параметризованным определенным типом делегата (который может быть параметризован типом 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.

Другие перегрузки (регистрации и ручного отзыва) не встроены в проекцию. Это позволяет реализовать их оптимально для вашего сценария. Вызов события :add и события ::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. Теперь каждый раз, когда вы щелкаете окно, вычитаете 1 градуса Фахренхейт из температуры термометра. И теперь приложение обрабатывает событие, которое возникает, когда температура идет ниже замораживания. Чтобы продемонстрировать, что событие вызывается должным образом, поместите точку останова внутри лямбда-выражения, обрабатывающего событие TemperatureIsBelowFreezing, запустите приложение и щелкните в окне.

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

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

Параметры типа для этих двух типов делегатов должны пересекать ABI, поэтому параметры типа также должны быть типами среды выполнения Windows. Это включает классы среды выполнения Windows, классы сторонних сред выполнения и примитивные типы, такие как числа и строки. Компилятор выдает ошибку "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<Делегат>. Просто используйте winrt::delegate вместо реального типа делегата среды выполнения Windows, так как winrt::delegate также поддерживает не Windows Runtime параметры.

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

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::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 (с точки зрения поддержки одной регистрации клиента или нескольких). События, с их несколькими моделями подписчиков, представляют собой гораздо более предсказуемый и согласованный вариант.

Подпись делегата обработчика событий должна состоять из двух параметров: sender (IInspectable), и args (некоторый тип аргумента события, например RoutedEventArgs).

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