Crear eventos en C++/WinRT

Este tema se basa en el componente de Windows Runtime, y la aplicación que lo usa, que en el tema Componentes de Windows Runtime con C++/WinRT se muestra cómo compilar.

Estas son las nuevas características que agrega este tema.

  • Actualización de la clase de tiempo de ejecución de termómetro para que genere un evento cuando su temperatura cae por debajo del congelamiento.
  • Actualización de la aplicación principal que usa la clase de tiempo de ejecución del termómetro para que controle ese evento.

Nota

Para más información sobre cómo instalar y usar C++/WinRT Visual Studio Extension (VSIX) y el paquete de NuGet (que juntos proporcionan la plantilla de proyecto y compatibilidad de la compilación), consulta Compatibilidad de Visual Studio para C++/WinRT.

Importante

Para conocer los conceptos y términos esenciales que te ayuden a entender cómo consumir y crear clases en tiempo de ejecución con C++/WinRT, consulta Consumir API con C++/WinRT y Crear API con C++/WinRT .

Creación deThermometerWRC y ThermometerCoreApp

Si desea continuar con las actualizaciones que se muestran en este tema, para que pueda compilar y ejecutar el código, el primer paso es seguir el tutorial del tema Componentes de Windows Runtime con C++/WinRT. Después de hacerlo, tendrá el componente de Windows Runtime ThermometerWRC y la aplicación principal ThermometerCoreApp que lo usa.

Actualización de ThermometerWRC para generar un evento

Actualice Thermometer.idl para que se parezca al listado siguiente. Así es cómo se declara un evento cuyo tipo de delegado es EventHandler con un argumento de un número de punto flotante de precisión simple.

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

Guarde el archivo. El proyecto no se compilará completamente en su estado actual, pero realice una compilación ahora de todas formas para generar versiones actualizadas de los archivos de código auxiliar \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h y Thermometer.cpp. Dentro de esos archivos, podrá ver implementaciones de código auxiliar del evento TemperatureIsBelowFreezing. En C++ / WinRT, un evento declarado IDL se implementa como un conjunto de funciones sobrecargadas (similares a la forma en que se implementa una propiedad como un par de funciones get y set sobrecargadas). Una sobrecarga toma un delegado que se va a registrar y devuelve un token (winrt::event_token). La otra toma un token y revoca el registro del delegado asociado.

Ahora abra Thermometer.h y Thermometer.cpp, y actualice la implementación de la clase de tiempo de ejecución Thermometer. En Thermometer.h, agregue las dos funciones sobrecargadas TemperatureIsBelowFreezing, así como un miembro de datos de evento privado que se usará en la implementación de esas funciones.

// 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;
        ...
    };
}
...

Como puede ver más arriba, un evento se representa mediante la plantilla de estructura winrt::event, parametrizada por un tipo de delegado determinado (que se puede parametrizar mediante un tipo args).

En Thermometer.cpp, implemente las dos funciones sobrecargadas 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);
    }
}

Nota

Para conocer de qué se trata un revocador automático de eventos, consulta Revocación de un delegado registrado. La implementación del revocador automático de eventos se incluye de forma gratuita para el evento. Es decir, no es necesario implementar la sobrecarga para el revocador de eventos, ya que la proyección de C++/WinRT la proporciona automáticamente.

Las otras sobrecargas (las sobrecargas de revocación manual y registro) no se incluyen en la proyección. Esto te proporciona la flexibilidad para implementarlas de forma óptima para tu escenario. Una llamada a event::add y a event::remove tal como se muestra en estas implementaciones es un valor predeterminado eficaz y seguro para simultaneidad o subprocesos. Pero si tienes un gran número de eventos, es posible que no desees un campo de evento para cada uno, sino optar en su lugar por algún tipo de implementación dispersa.

Además de esto, también puede ver que la implementación de la función AdjustTemperature se ha actualizado para generar el evento TemperatureIsBelowFreezing si la temperatura cae por debajo del congelamiento.

Actualización de ThermometerCoreApp para controlar el evento

En el proyecto ThermometerCoreApp, en App.cpp, realice los cambios siguientes en el código para registrar un controlador de eventos y, a continuación, haga que la temperatura caiga por debajo del congelamiento.

WINRT_ASSERT es una definición de macro y se expande a _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);
        ...
    }
    ...
};

Tenga en cuenta el cambio en el método OnPointerPressed. Ahora, cada vez que haga clic en la ventana, resta 1 grado de Fahrenheit de la temperatura del termómetro. Y ahora, la aplicación controla el evento que se genera cuando la temperatura cae por debajo del congelamiento. Para demostrar que se genera el evento según lo esperado, coloca un punto de interrupción en la expresión lambda que controla el evento TemperatureIsBelowFreezing, ejecuta la aplicación y haz clic dentro de la ventana.

Delegados con parámetros mediante una ABI

Si el evento debe ser accesible mediante una interfaz binaria de aplicación (ABI), por ejemplo, entre un componente y su aplicación consumidora, el evento debe usar un tipo delegado de Windows Runtime. En el ejemplo anterior se usa el tipo delegado Windows::Foundation::EventHandler<T> de Windows Runtime. TypedEventHandler<TSender, TResult> es otro ejemplo de un tipo delegado de Windows Runtime.

Los parámetros de tipo de esos dos tipos delegados deben cruzar la ABI, por lo que los parámetros deben ser también tipos de Windows Runtime. Eso incluye clases en tiempo de ejecución de Windows y de terceros, y tipos primitivos, como cadenas y números. El compilador le ayuda con el error "T debe ser de tipo WinRT" si olvida esta restricción.

A continuación, se muestra un ejemplo en formato de listados de código. Comience con los proyectos ThermometerWRC y ThermometerCoreApp que creó anteriormente en este tema y edite el código de dichos proyectos para que se parezcan al de los siguientes listados.

Este primer listado es para el proyecto ThermometerWRC. Después de editar ThermometerWRC.idl tal y como se muestra a continuación, compile el proyecto y, a continuación, copie MyEventArgs.h y .cpp en él (desde la carpeta Generated Files) como hizo anteriormente con Thermometer.h y .cpp. Recuerde eliminar el elemento static_assert de ambos archivos.

// 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);
    }
}
...

Este listado es para el proyecto 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.
    });
}
...

Señales simples mediante una ABI

Si no tienes que pasar los parámetros o los argumentos con el evento, puedes definir tu propio tipo delegado simple de Windows Runtime. En el ejemplo siguiente se muestra una versión más sencilla de la clase en tiempo de ejecución Thermometer. Declara un tipo delegado llamado SignalDelegate y lo usa para generar un evento de tipo de señal en lugar de un evento con un parámetro.

// 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);
        ...
    }
    ...
};

Delegados con parámetros, señales simples y devoluciones de llamadas dentro de un proyecto

Si necesitas que tus eventos se mantengan dentro de un proyecto de Visual Studio (no en varios archivos binarios) y que dichos eventos no se limiten a los tipos de Windows Runtime, todavía puedes usar la plantilla de clase winrt::event<Delegate>. Simplemente usa winrt::delegate en lugar de un tipo de delegado de Windows Runtime real, ya que winrt::delegate también admite parámetros que no son de Windows Runtime.

El ejemplo siguiente muestra primero una firma delegada que no toma ningún parámetro (básicamente una señal simple) y, a continuación, una que toma una cadena.

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

Observa que puedes agregar al evento tantos delegados de suscripción como quieras. Sin embargo, los eventos conllevan una cierta sobrecarga. Si todo lo que necesitas es una devolución de llamada simple con un solo delegado de suscripción, puedes usar winrt::delegate<. T> por sí misma.

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

Si vas a migrar desde una base de código de C++/CX en la que los eventos y delegados se usan internamente (no en los archivos binarios), winrt::delegate te ayudará a replicar dicho patrón en C++/WinRT.

Eventos aplazables

Un patrón común en Windows Runtime es el evento aplazable. Un controlador de eventos realiza un aplazamiento llamando al método GetDeferral del argumento del evento. A hacerlo indica al origen del evento que las actividades posteriores al evento se deben posponer hasta que se complete el aplazamiento. Esto permite que un controlador de eventos realice acciones asincrónicas en respuesta a un evento.

La plantilla de estructura winrt::deferrable_event_args es una clase auxiliar para implementar (producir) el patrón de aplazamiento de Windows Runtime. A continuación se muestra un ejemplo.

// 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;
    };
}

Este es el modo en que el destinatario del evento consume el patrón de evento aplazable.

// 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();
});

Como implementador (productor) del origen del evento, deriva la clase de argumentos del evento de winrt::deferrable_event_args. deferrable_event_args<T> implementa T::GetDeferral automáticamente. También expone un nuevo método auxiliar deferrable_event_args::wait_for_deferrals, que se completa cuando se han completado todos los aplazamientos pendientes (si no se ha realizado ningún aplazamiento, se completa inmediatamente).

// 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;
}

Directrices de diseño

Te recomendamos que pases eventos, no delegados, como parámetros de función. La función add de winrt::event es la única excepción, porque tienes que pasar un delegado en ese caso. La razón de esta directriz es que los delegados pueden adoptar diferentes formas en los diferentes lenguajes de Windows Runtime (en términos de si son compatibles con el registro de uno o varios clientes). Los eventos, con su modelo de suscriptor múltiple, constituyen una opción mucho más coherente y predecible.

La firma de un delegado de controlador de eventos debe constar de dos parámetros: sender (IInspectable) y args (algún tipo de argumento de evento, por ejemplo RoutedEventArgs).

Observa que estas instrucciones no se aplican necesariamente si vas a diseñar una API interna. Sin embargo, las API internas terminan por convertirse en públicas con el tiempo.