Compartir a través de


Creación de eventos en C++/WinRT

Este tema se basa en el componente de Windows Runtime y en la aplicación que consume, que los componentes de Windows Runtime con C++/WinRT tema le muestran cómo compilar.

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

  • Actualice la clase de ejecución del termómetro para generar un evento cuando su temperatura descienda por debajo del punto de congelación.
  • Actualice la aplicación de núcleo que consume la clase de tiempo de ejecución del termómetro para que controle ese evento.

Nota:

Para obtener información acerca de cómo instalar y usar la Extensión de Visual Studio C++/WinRT (VSIX) y el paquete NuGet (que juntos proporcionan plantillas de proyecto y soporte de compilación), consulte la compatibilidad de Visual Studio con C++/WinRT.

Importante

Para conocer los conceptos y términos esenciales que apoyan la comprensión de cómo consumir y desarrollar clases de tiempo de ejecución con C++/WinRT, consulte Consumir APIs con C++/WinRT y Desarrollar APIs con C++/WinRT.

Crear ThermometerWRC y ThermometerCoreApp

Si deseas mantenerte al día con las actualizaciones que se muestran en este tema, para poder compilar y ejecutar el código, el primer paso es seguir la guía paso a paso en el tema sobre componentes de Windows Runtime con C++/WinRT. Al hacerlo, tendrás el componente ThermometerWRC Windows Runtime y la aplicación ThermometerCoreApp Core que la consume.

Actualice TermómetroWRC para generar un evento

Actualice Thermometer.idl para que tenga un aspecto similar al de la lista siguiente. Así es como se declara un evento cuyo tipo de delegado es EventHandler con un argumento de un número de punto flotante de precisión sencilla.

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

Guarde el archivo. El proyecto no se completará en su estado actual, pero realice una compilación ahora de todos modos 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 ahora puede ver implementaciones simuladas del evento TemperatureIsBelowFreezing. En C++/WinRT, un evento declarado por IDL se implementa como un conjunto de funciones sobrecargadas (de forma similar 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 (un winrt::event_token). El otro toma un token y revoca el registro del delegado asociado.

Ahora abra Thermometer.h y Thermometer.cppy actualice la implementación de la clase en tiempo de ejecución de Termómetro. En Thermometer.h, agregue las dos funciones sobrecargadas de TemperatureIsBelowFreezing, así como un miembro de datos privado de evento 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 anteriormente, un evento se representa mediante la plantilla de estructura winrt::event, parametrizado por un tipo de delegado determinado (que se puede parametrizar mediante un tipo de argumentos).

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 obtener más información sobre lo que es un revocador de eventos automático, consulte Revocar un delegado registrado. Puede obtener la implementación del revocador de eventos automático de forma gratuita para su evento. En otras palabras, no es necesario implementar la sobrecarga para el revocador del evento; eso lo proporciona la proyección de C++/WinRT.

Las otras sobrecargas (las sobrecargas de registro y revocación manual) se no hornear en la proyección. Esto es para proporcionarle la flexibilidad de implementarlos de forma óptima para su escenario. Llamar a event::add y event::remove como se muestra en estas implementaciones es un valor predeterminado eficaz y seguro para la concurrencia y los subprocesos. Pero si tiene un gran número de eventos, es posible que no desee un campo de evento para cada uno, sino que opte por algún tipo de implementación dispersa en su lugar.

También puede ver arriba que la implementación de la función AdjustTemperature se ha actualizado para generar el evento TemperatureIsBelowFreezing si la temperatura baja por debajo del punto de congelación.

Actualice ThermometerCoreApp para gestionar el evento

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

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, restar 1 grados Fahrenheit de la temperatura del termómetro. Y ahora, la aplicación controla el evento que se genera cuando la temperatura va por debajo de la congelación. Para demostrar que el evento se está generando según lo previsto, ponga un punto de interrupción dentro de la expresión lambda que controla el evento TemperatureIsBelowFreezing, ejecute la aplicación y haga clic en la ventana.

Delegados parametrizados a través de una ABI

Si su evento debe ser accesible a través de una interfaz binaria de aplicación (ABI), como puede ser entre un componente y la aplicación que lo consume, entonces su evento debe utilizar un tipo de delegado de Windows Runtime. En el ejemplo anterior se usa el tipo de delegado Windows::Foundation::EventHandler<T> Windows Runtime. TypedEventHandler<TSender, TResult> es otro ejemplo de un tipo de delegado de Windows Runtime.

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

A continuación se muestra un ejemplo en forma de listados de código. Comience con los proyectos ThermometerWRC y ThermometerCoreApp que creó anteriormente en este tema, y edite el código de esos proyectos para que sea parecido al código de estas listas.

Esta primera lista es para el proyecto ThermoWRC. Después de editar ThermometerWRC.idl como se muestra a continuación, compile el proyecto y, a continuación, copie MyEventArgs.h y .cpp en el proyecto (desde la Generated Files carpeta) como hizo anteriormente con Thermometer.h y .cpp. Recuerde eliminar el 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);
    }
}
...

Esta lista es para el proyecto TermómetroCoreApp.

// 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 en una ABI

Si no necesitas pasar ningún parámetro o argumento con tu evento, puedes definir tu propio tipo de delegado simple de Windows Runtime. El siguiente ejemplo muestra una versión más sencilla de la clase de ejecución Termómetro. Declara un tipo de delegado denominado SignalDelegate y luego lo usa para generar un evento tipo 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 parametrizados, señales simples y devoluciones de llamada dentro de un proyecto

Si necesitas eventos que sean internos para tu proyecto de Visual Studio (y no se utilicen a través de archivos binarios), donde esos eventos no estén limitados a los tipos de Windows Runtime, aún puedes usar la plantilla de clase winrt::event Delegate <>. Simplemente usa winrt::d elegate en lugar de un tipo de delegado real de Windows Runtime, ya que winrt::d elegate también admite parámetros que no son de Windows Runtime.

En el ejemplo siguiente se muestra primero una firma de delegado 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!");

Note cómo puede agregar al evento tantos delegados suscriptores como desee. Sin embargo, hay cierta sobrecarga asociada a un evento. Si todo lo que necesita es una simple devolución de llamada con un solo delegado de suscripción, puede usar winrt::delegate<... T> por separado.

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 un código base de C++/CX donde los eventos y delegados se usan internamente dentro de un proyecto, winrt::d elegate te ayudará a replicar ese patrón en C++/WinRT.

Eventos aplazables

Un patrón común en Windows Runtime es el evento aplazable. Un controlador de eventos toma un aplazamiento llamando al método GetDeferral del argumento del evento. Al hacerlo, se 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 el patrón de aplazamiento de Windows Runtime. Este es 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;
    };
}

Así es como el receptor del evento consume el patrón de eventos que se puede aplazar.

// 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 de eventos de winrt::deferrable_event_args. deferrable_event_args<T> implementa T::GetDeferral. También expone un nuevo método auxiliar deferrable_event_args::wait_for_deferrals, que termina cuando se han cumplido todos los aplazamientos pendientes (si no se ha tomado 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

Se recomienda pasar eventos y no delegados, como parámetros de función. El agregar función de winrt::event es la única excepción, ya que debe pasar un delegado en ese caso. La razón de esta guía es que los delegados pueden adoptar diferentes formas en distintos lenguajes de Windows Runtime (en términos de si admiten un registro de cliente o varios). Los eventos, con su modelo de varios suscriptores, constituyen una opción mucho más predecible y coherente.

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

Tenga en cuenta que estas directrices no se aplican necesariamente si va a diseñar una API interna. Aunque las API internas suelen convertirse en públicas a lo largo del tiempo.