Criar eventos em C++/WinRT

Este tópico se baseia no componente do Windows Runtime, e no aplicativo de consumo, que o tópico Componentes do Windows Runtime com C++/WinRT mostra como criar.

Aqui estão os novos recursos que esse tópico adiciona.

  • Atualize a classe de runtime do termômetro para acionar um evento quando a temperatura ficar abaixo do congelamento.
  • Atualize o Aplicativo de Núcleo que consome a classe de runtime de termômetro para que ele manipule esse evento.

Observação

Para saber mais sobre como instalar e usar a VSIX (Extensão do Visual Studio) para C++/WinRT e o pacote NuGet (que juntos fornecem um modelo de projeto e suporte ao build), confira Suporte ao Visual Studio para C++/WinRT.

Importante

Para ver conceitos e termos essenciais que ajudam a entender como utilizar e criar classes de runtime com C++/WinRT, confira Utilizar APIs com C++/WinRT e Criar APIs com C++/WinRT.

Criar ThermometerWRC e ThermometerCoreApp

Se você deseja acompanhar as atualizações mostradas neste tópico para poder compilar e executar o código, então a primeira etapa é seguir o passo a passo no tópico Componentes do Windows Runtime com C++/WinRT. Ao fazer isso, você terá o componente do Windows Runtime ThermometerWRC e o Aplicativo de Núcleo ThermometerCoreApp que o consome.

Atualize ThermometerWRC para acionar um evento

Atualize Thermometer.idl para se parecer com a listagem abaixo. É como declarar um evento cujo tipo delegado é EventHandler com um argumento de um número de ponto flutuante de precisão simples.

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

Salve o arquivo. O projeto não será criado até a conclusão no estado atual dele, mas execute um build agora em qualquer caso para gerar versões atualizadas dos arquivos stub \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h e Thermometer.cpp. Dentro desses arquivos, agora você pode ver implementações de stub do evento TemperatureIsBelowFreezing. No C++/WinRT, um evento declarado de IDL é implementado como um conjunto de funções sobrecarregadas (de modo semelhante à maneira como uma propriedade é implementada como um par de funções get e set sobrecarregadas). Uma sobrecarga resulta no registro de um delegado e retorna um token (um winrt::event_token). A outra recebe um token e revoga o registro do delegado associado.

Agora, abra Thermometer.h e Thermometer.cpp e atualize a implementação da classe de runtime Thermometer. Em Thermometer.h, adicione as duas funções TemperatureIsBelowFreezing sobrecarregadas, assim como um membro de dados de evento privado a ser usado na implementação dessas funções.

// 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 você pode ver acima, um evento é representado pelo modelo de struct winrt::event, parametrizado por um tipo de delegado específico (que pode ser parametrizado por um tipo args).

No Thermometer.cpp, implemente as duas funções de TemperatureIsBelowFreezing sobrecarregadas.

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

Observação

Para obter detalhes sobre o que é um revogador de evento automático, consulte Revogar um delegado registrado. Você recebe uma implementação do revogador de evento automático gratuitamente para seu evento. Em outras palavras, você não precisa implementar a sobrecarga para o revogador de evento – isso é fornecido para você pela projeção C++/WinRT.

As outras sobrecargas (sobrecargas de registro e revogação manual) não são implantadas na projeção. Isso é feito para lhe dar a flexibilidade de implementá-las de forma ideal para seu cenário. Chamar event::add e event::remove, conforme mostrado nessas implementações, é um padrão eficiente, simultâneo e thread-safe. Mas se houver um grande número de eventos, talvez não seja ideal um campo de evento para cada um, em vez disso, opte por algum tipo de implementação esparsa.

Você também pode ver acima que a implementação da função AdjustTemperature foi atualizada para gerar o evento TemperatureIsBelowFreezing se a temperatura ficar abaixo do congelamento.

Atualize ThermometerCoreApp para manipular o evento

No projeto ThermometerCoreApp, em App.cpp, faça as alterações a seguir no código para registrar um manipulador de eventos e, em seguida, faça a temperatura ficar abaixo do congelamento.

WINRT_ASSERT é uma definição de macro e se expande para _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);
        ...
    }
    ...
};

Esteja ciente da alteração no método OnPointerPressed. Agora, cada vez que você clicar na janela, você subtrairá 1 grau Fahrenheit da temperatura do termômetro. E agora, o aplicativo está manipulando o evento gerado quando a temperatura fica abaixo do congelamento. Para demonstrar que o evento está sendo gerado conforme o esperado, coloque um ponto de interrupção dentro da expressão lambda que está manipulando o evento TemperatureIsBelowFreezing, execute o aplicativo e clique dentro da janela.

Delegados parametrizados em uma ABI

Se o evento precisa ser acessível por meio de uma ABI (interface binária de aplicativo), como entre um componente e o aplicativo de consumo dele, o evento precisa usar um tipo delegado do Windows Runtime. O exemplo acima usa o tipo delegado Windows::Foundation::EventHandler<T> do Windows Runtime. TypedEventHandler<TSender, TResult> é outro exemplo de um tipo delegado do Windows Runtime.

Os parâmetros de tipo desses dois tipos delegados têm que cruzar a ABI, portanto, os parâmetros de tipo também devem ser do tipo do Windows Runtime. Isso inclui classes de runtime do Windows e de terceiros, bem como tipos primitivos, como números e cadeias de caracteres. O compilador ajudará você indicando o erro "T precisa ser do tipo WinRT" se você esquecer essa restrição.

Abaixo está um exemplo na forma de listagens de código. Comece com os projetos ThermometerWRC e ThermometerCoreApp que você criou anteriormente neste tópico e edite o código nesses projetos para que ele se pareça com o código nessas listagens.

A primeira listagem é para o projeto ThermometerWRC. Depois de editar ThermometerWRC.idl conforme mostrado abaixo, crie o projeto e copie MyEventArgs.h e .cpp no projeto (da pasta Generated Files) da mesma forma que fazia anteriormente com Thermometer.h e .cpp. Lembre-se de excluir o static_assert de ambos os arquivos.

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

Essa listagem é do projeto 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.
    });
}
...

Sinais simples em uma ABI

Se você não precisa passar nenhum parâmetro ou argumento com seu evento, defina seu próprio tipo delegado simples do Windows Runtime. O exemplo a seguir mostra uma versão mais simples da classe de runtime Thermometer. Ele declara um tipo delegado chamado SignalDelegate e, em seguida, usa isso para gerar um evento de tipo de sinal ao invés de um evento com um 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, sinais simples e retornos de chamada dentro de um projeto

Se você precisar de eventos internos a seu projeto do Visual Studio (não entre binários), em que esses eventos não se limitem aos tipos do Windows Runtime, você ainda poderá usar o modelo da classe winrt::event<Delegate>. Basta usar winrt::delegate, em vez de um tipo de delegado do Windows Runtime real, já que winrt::delegate também dá suporte a parâmetros que não são do Windows Runtime.

O exemplo a seguir mostra primeiro uma assinatura do delegado que não usa parâmetros (basicamente, um sinal simples) e, em seguida, outra que usa uma cadeia de caracteres.

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

Observe como é possível adicionar tantos delegados de assinaturas quanto desejar ao evento. No entanto, há alguma sobrecarga associada a um evento. Se tudo o que você precisa é um retorno de chamada simples com apenas um único delegado da assinatura, então, use somente 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!");

Se estiver fazendo a portabilidade de uma base de código do C++/CX, em que os eventos e delegados são usados internamente em um projeto, winrt::delegate ajuda a replicar esse padrão em C++/WinRT.

Eventos adiáveis

Um padrão comum no Windows Runtime é um evento adiável. Um manipulador de eventos obtém um adiamento chamando o método GetDeferral do argumento do evento. Isso vai indicar à origem do evento que as atividades posteriores ao evento deverão ser adiadas até que o adiamento seja concluído. Isso permite que um manipulador de eventos execute ações assíncronas como resposta a um evento.

O modelo de struct winrt::deferrable_event_args é uma classe auxiliar usada para implementar (produzir) o padrão de adiamento do Windows Runtime. Veja um exemplo.

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

Veja abaixo de que modo o destinatário do evento consome o padrão de evento adiável.

// 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 (produtor) da origem do evento, derive sua classe de argumentos de evento de winrt::deferrable_event_args. deferrable_event_args<T> implementa o T::GetDeferral para você. Ele também vai expor um novo método auxiliar deferrable_event_args::wait_for_deferrals, que será concluído quando todos os adiamentos pendentes forem concluídos (caso nenhum adiamento seja executado, ele será concluído de modo imediato).

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

Diretrizes de design

É recomendável passar eventos, e não delegados, como parâmetros de função. A função add de winrt::event é a única exceção, porque nesse caso é preciso passar um delegado. A razão para essa diretriz é porque os delegados podem assumir diferentes formas em diversas linguagens do Windows Runtime (considerando se dão suporte a um ou vários registros de cliente). Eventos, com seus vários modelos de assinantes, constituem uma opção muito mais previsível e consistente.

A assinatura para um delegado de manipulador de eventos deve consistir de dois parâmetros: sender (IInspectable) e args (algum tipo de argumento de evento, por exemplo RoutedEventArgs).

Observe que essas diretrizes não se aplicam necessariamente se você estiver criando uma API interna. No entanto, as APIs internas geralmente se tornam públicas ao longo do tempo.