Criar eventos no C++/WinRT

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

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

  • Atualize a classe de runtime do termômetro para gerar um evento quando sua temperatura ficar abaixo de zero.
  • Atualize o Aplicativo Principal que consome a classe de runtime do termômetro para que ele trate desse evento.

Note

Para obter informações sobre como instalar e usar o VSIX (Extensão de Visual Studio do C++/WinRT) e o pacote NuGet (que juntos fornecem suporte ao modelo de projeto e ao build), consulte Visual Studio suporte para C++/WinRT.

Important

Para obter conceitos e termos essenciais que dão suporte à compreensão de como consumir e criar classes de runtime com C++/WinRT, consulte Consumir APIs com APIs C++/WinRT e Criar com C++/WinRT.

Criar ThermometerWRC e ThermometerCoreApp

Se você quiser acompanhar as atualizações mostradas neste tópico, para que possa compilar e executar o código, a primeira etapa é seguir o passo a passo nos componentes do Windows Runtime com o tópico C++/WinRT. Ao fazer isso, você terá o componente ThermometerWRC Windows Runtime e o aplicativo ThermometerCoreApp Core que o consome.

Atualizar TermômetroWRC para gerar um evento

Atualize Thermometer.idl para ficar como a listagem abaixo. É assim que se declara um evento cujo tipo delegado é EventHandler com um argumento de um número de ponto flutuante de precisão única.

// 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á compilado até o fim em seu estado atual, mas faça uma compilação agora assim mesmo 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 por IDL é implementado como um conjunto de funções sobrecarregadas (semelhante à maneira como uma propriedade é implementada como um par de funções get e set sobrecarregadas). Uma sobrecarga leva um delegado a ser registrado e retorna um token (um winrt::event_token). O outro usa um token e revoga o registro do delegado associado.

Agora, abra Thermometer.h e Thermometer.cpp e atualize a implementação da classe de tempo de execução Thermometer. Em Thermometer.h, adicione as duas funções sobrecarregadas TemperatureIsBelowFreezing, bem como um membro de dados de evento privado para usar 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 delegado específico (que pode ser parametrizado por um tipo de args).

Em Thermometer.cpp, implemente as duas sobrecargas da função 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);
    }
}

Note

Para obter detalhes do que é um revogador de evento automático, consulte Revogar um delegado registrado. Você obtém a implementação do revogador de eventos automático gratuitamente para o evento. Em outras palavras, você não precisa implementar a sobrecarga para o revogador de eventos, que é fornecida para você pela projeção C++/WinRT.

As outras sobrecargas (as sobrecargas de registro e revogação manual) não estão incorporadas à projeção. Isso é para dar a você a flexibilidade para implementá-los de forma ideal para o seu cenário. Chamar evento::add e event::remove conforme mostrado nessas implementações é um padrão eficiente e simultâneo/thread-safe. Mas se você tiver um número muito grande de eventos, talvez não queira um campo de evento para cada um, mas 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 aumentar o evento TemperatureIsBelowFreezing se a temperatura ficar abaixo de zero.

Atualizar ThermometerCoreApp para manipular o evento

No projeto ThermometerCoreApp , in App.cpp, faça as seguintes alterações no código para registrar um manipulador de eventos e faça com que a temperatura fique abaixo de zero.

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ê clica na janela, subtrai 1 grau Fahrenheit da temperatura do termômetro. E agora, o aplicativo está processando o evento que é disparado quando a temperatura fica abaixo do ponto de congelamento. Para demonstrar que o evento está sendo gerado conforme o esperado, coloque um ponto de interrupção dentro da expressão lambda que manipula o evento TemperatureIsBelowFreezing, execute o aplicativo e clique dentro da janela.

Delegados parametrizados em uma ABI

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

Os parâmetros de tipo desses dois tipos de delegado precisam atravessar a ABI, portanto os parâmetros de tipo também devem ser tipos do Windows Runtime. Isso inclui classes de runtime do Windows, classes de runtime de terceiros e tipos primitivos, como números e cadeias de caracteres. O compilador ajuda você com um erro "T deve ser tipo WinRT" se você esquecer essa restrição.

Veja abaixo 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 se parecer com o código nessas listagens.

Esta primeira listagem é para o projeto ThermometerWRC . Depois de editar ThermometerWRC.idl, conforme mostrado abaixo, compile o projeto e, em seguida, copie MyEventArgs.h e .cpp para o projeto (da pasta Generated Files), assim como você fez 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);
    }
}
...

Esta listagem é para o 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 precisar passar parâmetros ou argumentos para o seu evento, poderá definir seu próprio tipo delegado simples do Windows Runtime. O exemplo a seguir mostra uma versão mais simples da classe de runtime termômetro . Ele declara um tipo delegado chamado SignalDelegate e, em seguida, usa-o para gerar um evento do tipo sinal em vez 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 callbacks em um projeto

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

O exemplo a seguir mostra primeiro uma assinatura delegada que não usa parâmetros (essencialmente um sinal simples) e, em seguida, uma 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 que você pode adicionar ao evento quantos delegados inscritos quiser. No entanto, há alguma sobrecarga associada a um evento. Se tudo o que você precisa é de um retorno de chamada simples com apenas um único delegado inscrito, então você pode usar winrt::delegate<... T> isoladamente.

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 você estiver migrando de uma base de código C++/CX em que eventos e delegados são usados internamente em um projeto, então winrt::delegate ajudará você a replicar esse padrão em C++/WinRT.

Eventos adiáveis

Um padrão comum no Windows Runtime é o evento adiável. Um manipulador de eventos solicita um diferimento chamando o método GetDeferral do argumento de evento. Isso indica à origem do evento que as atividades pós-evento devem ser adiadas até que o adiamento seja concluído. Isso permite que um manipulador de eventos execute ações assíncronas em resposta a um evento.

O modelo de estrutura winrt::deferrable_event_args é uma classe auxiliar para implementar (isto é, 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 como 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, você faz sua classe de argumentos de evento derivar de winrt::deferrable_event_args. < deferrable_event_argsT> implementa T::GetDeferral para você. Ele também expõe um novo método auxiliar deferrable_event_args::wait_for_deferrals, que é concluído quando todos os adiamentos pendentes foram concluídos (se nenhum adiamento foi feito, ele é concluído imediatamente).

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

Recomendamos que você passe eventos, e não delegados, como parâmetros de função. A função de adição de winrt::event é a única exceção, pois você deve passar um delegado nesse caso. O motivo dessa diretriz é que os delegados podem assumir formas diferentes entre as várias linguagens do Windows Runtime, em termos de suportarem um único registro de cliente ou vários registros. Os eventos, com seu modelo de assinante múltiplo, constituem uma opção muito mais previsível e consistente.

A assinatura de um delegado de manipulador de eventos deve consistir em 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.