Controlar eventos mediante delegados en C++/WinRT

Important

¿Construyendo con la SDK de Aplicaciones para Windows? El código de este artículo usa los espacios de nombres de UWP (Windows.UI.Xaml). Si tu proyecto está dirigido a WinUI 3 (SDK de Aplicaciones para Windows), sustituye Microsoft.UI.Xaml (y los espacios de nombres relacionados Microsoft.UI.*) en todo el texto. Consulta Correspondencia entre las API de UWP y el SDK de aplicaciones de Windows para ver la correspondencia completa y la guía de migración de la interfaz de usuario para consultar más detalles.

En este tema se muestra cómo registrar y revocar delegados de control de eventos mediante C++/WinRT. Puede controlar un evento mediante cualquier objeto de tipo función de C++ estándar.

Note

Para obtener información sobre cómo instalar y usar la extensión de Visual Studio de C++/WinRT (VSIX) y el paquete NuGet (que juntos proporcionan compatibilidad con la compilación y la plantilla de proyecto), consulta Visual Studio compatibilidad con C++/WinRT.

Uso de Visual Studio para agregar un controlador de eventos

Una forma cómoda de agregar un controlador de eventos al proyecto es usar la interfaz de usuario (UI) del Diseñador XAML en Visual Studio. Con la página XAML abierta en el Diseñador XAML, selecciona el control cuyo evento quieres controlar. En la página de propiedades de ese control, haga clic en el icono de rayo para enumerar todos los eventos de origen de ese control. A continuación, haga doble clic en el evento que desea controlar; por ejemplo, OnClicked.

El Diseñador XAML añade a sus archivos de origen el prototipo adecuado de la función controladora de eventos (y una implementación básica), para que la sustituya por su propia implementación.

Note

Normalmente, los controladores de eventos no necesitan describirse en el archivo Midl (.idl). Por lo tanto, el Diseñador XAML no agrega prototipos de función de controlador de eventos al archivo Midl. Solo los agrega a tus archivos .h y .cpp.

Registro de un delegado para controlar un evento

Un ejemplo sencillo es controlar el evento click de un botón. Es habitual usar el marcado XAML para registrar una función miembro para manejar el evento, como se muestra a continuación.

// MainPage.xaml
<Button x:Name="myButton" Click="ClickHandler">Click Me</Button>
// MainPage.h
void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */)
{
    myButton().Content(box_value(L"Clicked"));
}

El código anterior se ha tomado del proyecto Aplicación en blanco, empaquetada (WinUI 3 de escritorio) en Visual Studio. El código myButton() llama a una función de acceso generada, que devuelve el Button al que llamamos myButton. Si cambias el x:Name de ese elemento Button, también cambia el nombre de la función de acceso generada.

Note

En este caso, el origen del evento (el objeto que genera el evento) es el botón denominado myButton. Y el destinatario del evento (el objeto que controla el evento) es una instancia de MainPage. Más adelante en este tema encontrará más información sobre cómo gestionar el ciclo de vida de los orígenes y receptores de eventos.

En lugar de hacerlo de forma declarativa en el lenguaje de marcado, puede registrar de forma imperativa una función miembro para gestionar un evento. Es posible que no sea obvio en el ejemplo de código siguiente, pero el argumento de la llamada ButtonBase::Click es una instancia del delegado RoutedEventHandler . En este caso, estamos usando la sobrecarga del constructor RoutedEventHandler que recibe un objeto y un puntero a función miembro.

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click({ this, &MainPage::ClickHandler });
}

Important

Al registrar el delegado, el ejemplo de código anterior pasa un puntero sin procesar this (que apunta al objeto actual). Para obtener información sobre cómo establecer una referencia fuerte o débil al objeto actual, consulte Si usa una función miembro como delegado.

Este es un ejemplo que usa una función miembro estática; tenga en cuenta la sintaxis más sencilla.

// MainPage.h
static void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Microsoft::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click( MainPage::ClickHandler );
}
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */) { ... }

Hay otras maneras de construir un RoutedEventHandler. A continuación se muestra el bloque de sintaxis tomado del tema de documentación de RoutedEventHandler (elija C++/WinRT en la lista desplegable Lenguaje en la esquina superior derecha de la página web). Observe los distintos constructores: uno toma una expresión lambda; otra función libre; y otra (la que hemos usado anteriormente) toma un objeto y una función de puntero a miembro.

struct RoutedEventHandler : winrt::Windows::Foundation::IUnknown
{
    RoutedEventHandler(std::nullptr_t = nullptr) noexcept;
    template <typename L> RoutedEventHandler(L lambda);
    template <typename F> RoutedEventHandler(F* function);
    template <typename O, typename M> RoutedEventHandler(O* object, M method);
    /* ... other constructors ... */
    void operator()(winrt::Windows::Foundation::IInspectable const& sender,
        winrt::Microsoft::UI::Xaml::RoutedEventArgs const& e) const;
};

La sintaxis del operador de llamada de función también resulta útil para ver. Te indica cuáles deben ser los parámetros de tu delegado. Como puede ver, en este caso, la sintaxis del operador de llamada de función coincide con los parámetros de nuestro MainPage::ClickHandler.

Note

Para cualquier evento determinado, para averiguar los detalles de su delegado y los parámetros del delegado, vaya primero al tema de documentación del propio evento. Tomemos el evento UIElement.KeyDown como ejemplo. Visite ese tema y elija C++/WinRT en la lista desplegable Lenguaje . En el bloque de sintaxis al comienzo del tema, verás esto.

// Register
event_token KeyDown(KeyEventHandler const& handler) const;

Esa información nos indica que el evento UIElement.KeyDown (del que estamos hablando) usa un delegado del tipo KeyEventHandler, ya que ese es el tipo que se pasa al registrar un delegado para este tipo de evento. Entonces, siga el vínculo del tema para ir al tipo de delegado KeyEventHandler. Aquí, el bloque de sintaxis contiene un operador de llamada de función. Y, como se mencionó anteriormente, eso te indica qué parámetros debe tener tu delegado.

void operator()(
  winrt::Windows::Foundation::IInspectable const& sender,
  winrt::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

Como puede ver, el delegado debe declararse de forma que reciba un IInspectable como remitente y una instancia de la clase KeyRoutedEventArgs como argumento args.

Para obtener otro ejemplo, echemos un vistazo al evento Popup.Closed. Su tipo de delegado es EventHandler<IInspectable>. Por lo tanto, el delegado tomará un IInspectable como emisor y otro IInspectable (porque ese es el parámetro de tipo de EventHandler) como argumento.

Si no está haciendo mucho trabajo en el controlador de eventos, puede usar una función lambda en lugar de una función miembro. De nuevo, puede que no sea obvio en el ejemplo de código siguiente, pero se está construyendo un delegado RoutedEventHandler a partir de una función lambda que, de nuevo, debe coincidir con la sintaxis del operador de llamada de función que hemos descrito anteriormente.

MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click([this](IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        myButton().Content(box_value(L"Clicked"));
    });
}

Puede optar por ser algo más explícito cuando construye su delegado. Por ejemplo, si desea pasarlo de un lado a otro, o usarlo más de una vez.

MainPage::MainPage()
{
    InitializeComponent();

    auto click_handler = [](IInspectable const& sender, RoutedEventArgs const& /* args */)
    {
        sender.as<winrt::Microsoft::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

Revocar un delegado registrado

Al registrar un delegado, normalmente se devuelve un token. Posteriormente, puede usar ese token para anular el registro del delegado; es decir, el delegado deja de estar registrado en el evento y no se volverá a invocar si el evento se desencadena de nuevo.

Por motivos de simplicidad, ninguno de los ejemplos de código anteriores mostró cómo hacerlo. Pero este siguiente ejemplo de código almacena el token en el miembro de datos privado del struct y revoca su controlador en el destructor.

struct Example : ExampleT<Example>
{
    Example(winrt::Microsoft::UI::Xaml::Controls::Button const& button) : m_button(button)
    {
        m_token = m_button.Click([this](IInspectable const&, RoutedEventArgs const&)
        {
            // ...
        });
    }
    ~Example()
    {
        m_button.Click(m_token);
    }

private:
    winrt::Microsoft::UI::Xaml::Controls::Button m_button;
    winrt::event_token m_token;
};

En lugar de una referencia segura, como en el ejemplo anterior, puedes almacenar una referencia débil al botón (consulta Referencias fuertes y débiles en C++/WinRT).

Note

Cuando un origen de eventos dispara sus eventos de forma síncrona, puedes desvincular tu controlador y tener la certeza de que no recibirás más eventos. Pero, en el caso de eventos asíncronos, incluso después de revocar (y especialmente al revocar dentro del destructor), un evento en tránsito podría llegar a su objeto después de que haya comenzado a destruirse. Encontrar un lugar para cancelar la suscripción antes de que se destruya podría mitigar el problema o, si desea una solución más sólida, consulte Acceder de forma segura al puntero this con un delegado de control de eventos.

Como alternativa, al registrar un delegado, puede especificar winrt::auto_revoke (que es un valor de tipo winrt::auto_revoke_t) para solicitar un revocador de eventos (de tipo winrt::event_revoker). El revocador de eventos mantiene por usted una referencia débil a la fuente del evento (el objeto que desencadena el evento). Puede revocar manualmente llamando a la función miembro event_revoker::revoke ; pero el revocador de eventos llama automáticamente a esa función cuando sale del ámbito. La función revoke comprueba si el origen del evento sigue existiendo y, si es así, revoca su delegado. En este ejemplo, no es necesario almacenar el origen del evento y no es necesario un destructor.

struct Example : ExampleT<Example>
{
    Example(winrt::Microsoft::UI::Xaml::Controls::Button button)
    {
        m_event_revoker = button.Click(
            winrt::auto_revoke,
            [this](IInspectable const& /* sender */,
            RoutedEventArgs const& /* args */)
        {
            // ...
        });
    }

private:
    winrt::Microsoft::UI::Xaml::Controls::Button::Click_revoker m_event_revoker;
};

A continuación se muestra el bloque de sintaxis tomado del tema de documentación del evento ButtonBase::Click . Muestra las tres funciones de registro y revocación diferentes. Puede ver exactamente qué tipo de revocador de eventos necesita declarar a partir de la tercera sobrecarga. Además, puedes pasar los mismos tipos de delegados tanto a las sobrecargas de register como a las de revoke with event_revoker.

// Register
winrt::event_token Click(winrt::Microsoft::UI::Xaml::RoutedEventHandler const& handler) const;

// Revoke with event_token
void Click(winrt::event_token const& token) const;

// Revoke with event_revoker
Button::Click_revoker Click(winrt::auto_revoke_t,
    winrt::Microsoft::UI::Xaml::RoutedEventHandler const& handler) const;

Note

En el ejemplo de código anterior, Button::Click_revoker es un alias de tipo para winrt::event_revoker<winrt::Microsoft::UI::Xaml::Controls::Primitives::IButtonBase>. Un patrón similar se aplica a todos los eventos de C++/WinRT. Cada evento de Windows Runtime tiene una sobrecarga de la función revoke que devuelve un objeto revocador de eventos, y el tipo de ese revocador es un miembro del origen del evento. Por lo tanto, para tomar otro ejemplo, el evento Window::SizeChanged tiene una sobrecarga de función de registro que devuelve un valor de tipo Window::SizeChanged_revoker.

Puede considerar la posibilidad de revocar manipuladores en un escenario de navegación entre páginas. Si entra repetidamente en una página y luego sale de ella, podría desvincular cualquier controlador al salir de la página. Como alternativa, si reutilizas la misma instancia de página, comprueba el valor de tu token y regístralo solo si todavía no se ha establecido (if (!m_token){ ... }). Una tercera opción es almacenar un revocador de eventos en la página como miembro de datos. Y una cuarta opción, como se explica más adelante en este tema, es capturar una referencia fuerte o una débil al objeto this en la función lambda.

Si el delegado de revocación automática no se registra

Si intenta especificar winrt::auto_revoke al registrar un delegado, y el resultado es una excepción winrt::hresult_no_interface, normalmente significa que el origen del evento no admite referencias débiles. Esa es una situación común en el espacio de nombres Microsoft.UI.Composition, por ejemplo. En esta situación, no puede usar la característica de revocación automática. Tendrás que recurrir a revocar manualmente los controladores de eventos.

Delegar tipos para acciones y operaciones asincrónicas

En los ejemplos anteriores se usa el tipo de delegado RoutedEventHandler , pero hay muchos otros tipos de delegados. Por ejemplo, las acciones y operaciones asíncronas (con y sin progreso) tienen eventos de finalización y/o de progreso que esperan delegados del tipo correspondiente. Por ejemplo, el evento de progreso de una operación asincrónica con progreso (que es cualquier cosa que implemente IAsyncOperationWithProgress) requiere un delegado de tipo AsyncOperationProgressHandler. Este es un ejemplo de código de creación de un delegado de ese tipo mediante una función lambda. En el ejemplo también se muestra cómo crear un delegado AsyncOperationWithProgressCompletedHandler .

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    auto async_op_with_progress = syndicationClient.RetrieveFeedAsync(rssFeedUri);

    async_op_with_progress.Progress(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& /* sender */,
            RetrievalProgress const& args)
        {
            uint32_t bytes_retrieved = args.BytesRetrieved;
            // use bytes_retrieved;
        });

    async_op_with_progress.Completed(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& sender,
            AsyncStatus const /* asyncStatus */)
        {
            SyndicationFeed syndicationFeed = sender.GetResults();
            // use syndicationFeed;
        });

    // or (but this function must then be a coroutine, and return IAsyncAction)
    // SyndicationFeed syndicationFeed{ co_await async_op_with_progress };
}

Como sugiere el comentario anterior sobre «coroutine», en lugar de usar un delegado con los eventos de finalización de acciones y operaciones asíncronas, probablemente le resulte más natural usar corrutinas. Para obtener más información y ejemplos de código, consulte Operaciones asincrónicas y simultaneidad con C++/WinRT.

Note

No es correcto implementar más de un controlador de finalización para una acción o operación asincrónica. Puede tener un único delegado para su evento completado o puede co_await hacerlo. Si tiene ambos, el segundo fallará.

Si opta por delegados en lugar de una corrutina, puede usar una sintaxis más sencilla.

async_op_with_progress.Completed(
    [](auto&& /*sender*/, AsyncStatus const /* args */)
{
    // ...
});

Tipos delegados que devuelven un valor

Algunos tipos de delegados deben, por sí mismos, devolver un valor. Un ejemplo es ListViewItemToKeyHandler, que devuelve una cadena. Este es un ejemplo de creación de un delegado de ese tipo (tenga en cuenta que la función lambda devuelve un valor).

using namespace winrt::Microsoft::UI::Xaml::Controls;

winrt::hstring f(ListView listview)
{
    return ListViewPersistenceHelper::GetRelativeScrollPosition(listview, [](IInspectable const& item)
    {
        return L"key for item goes here";
    });
}

Acceso seguro al puntero this mediante un delegado de control de eventos

Si maneja un evento mediante una función miembro de un objeto, o desde una función lambda dentro de la función miembro de un objeto, entonces debe considerar los tiempos de vida relativos del receptor del evento (el objeto que maneja el evento) y del origen del evento (el objeto que genera el evento). Para obtener más información y ejemplos de código, consulta Referencias seguras y débiles en C++/WinRT.

API importantes