Criar eventos em C++/WinRT

Este tópico baseia-se no componente Windows Runtime e na aplicação consumidora, que os componentes do Windows Runtime com C++/WinRT te mostram como construir.

Aqui estão as novas funcionalidades que este tema acrescenta.

  • Atualize a classe de execução do termómetro para aumentar um evento quando a sua temperatura descer abaixo de zero.
  • Atualize a aplicação Core que consome a classe de execução do termómetro para que gere esse evento.

Note

Para informações sobre a instalação e utilização da Extensão Visual Studio C++/WinRT (VSIX) e do pacote NuGet (que juntos fornecem suporte para templates de projeto e compilação), consulte o suporte Visual Studio para C++/WinRT.

Important

Para conceitos e termos essenciais que apoiam a sua compreensão de como consumir e criar classes de runtime com C++/WinRT, veja Consume APIs with C++/WinRT e Author APIs with C++/WinRT.

Criar TermometerWRC e ThermometerCoreApp

Se quiser acompanhar as atualizações apresentadas neste tópico, para que possa construir e executar o código, então o primeiro passo é seguir o guia nos componentes do Windows Runtime com o tópico C++/WinRT. Ao fazê-lo, terá o componente Windows Runtime ThermometerWRC e a aplicação principal ThermometerCoreApp que o utiliza.

Atualizar TermómetroWRC para desencadear um evento

Atualize Thermometer.idl para ficar como a listagem abaixo. É assim que se declara um evento cujo tipo de 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 concluirá a compilação com sucesso no estado atual, mas execute já uma compilação, ainda assim, para gerar versões atualizadas dos ficheiros stub \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h e Thermometer.cpp. Dentro desses ficheiros pode agora ver implementações stub do evento TemperatureIsBelowFreezing . Em C++/WinRT, um evento declarado por IDL é implementado como um conjunto de funções sobrecarregadas (semelhante à forma como uma propriedade é implementada como um par de funções get e set sobrecarregadas). Uma sobrecarga exige que um delegado seja registado e devolve um token ( um winrt::event_token). A outra recebe um token e revoga o registo do delegado correspondente.

Agora abra Thermometer.h e Thermometer.cpp, e atualize a implementação da classe de runtime Thermometer . Em Thermometer.h, adicionem as duas funções sobrecarregadas TemperatureIsBelowFreezing , bem como um membro privado de dados de eventos 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 pode ver acima, um evento é representado pelo modelo winrt:: event struct, parametrizado por um tipo de delegado particular (que por sua vez pode ser parametrizado por um tipo args).

Em Thermometer.cpp, implemente as duas funções sobrecarregadas 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 detalhes sobre o que é um revogador automático de eventos, consulte Revogar um delegado registado. Tens a implementação automática do revogador de eventos gratuitamente para o teu evento. Ou seja, não precisa de implementar a sobrecarga para o revogador de eventos — isso é fornecido pela projeção C++/WinRT.

As outras sobrecargas (as sobrecargas de registo e revogação manual) não estão integradas na projeção. Isto é para te dar flexibilidade para os implementares de forma ótima para o teu cenário. Invocar event::add e event::remove, como mostrado nestas implementações, é uma predefinição eficiente e segura para concorrência e para threads. Mas se tiveres um número muito grande de eventos, então podes não querer um campo de eventos para cada um, mas sim optar por algum tipo de implementação esparsa.

Também pode ver acima que a implementação da função AdjustTemperature foi atualizada para aumentar o evento TemperatureIsBelowFreezing se a temperatura descer abaixo de zero.

Atualizar a ThermometerCoreApp para gerir o evento

No projeto ThermometerCoreApp , em App.cpp, faça as seguintes alterações ao código para registar um gestor de eventos, e depois faça com que a temperatura desça abaixo de zero.

WINRT_ASSERT é uma definição macro, e expande-se 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 atento à alteração do método OnPointerPressed . Agora, cada vez que clicas na janela, subtrais 1 grau Fahrenheit à temperatura do termómetro. E agora, a aplicação está a processar o evento que é despoletado quando a temperatura desce abaixo do ponto de congelação. Para demonstrar que o evento está a ser acionado como esperado, coloque um ponto de interrupção dentro da expressão lambda que trata o evento TemperatureIsBelowFreezing, execute a aplicação e clique dentro da janela.

Delegados parametrizados através de um ABI

Se o seu evento tiver de ser acessível através de uma interface binária (ABI) de aplicação — como entre um componente e a sua aplicação consumidora — então o seu evento deve usar um tipo de delegado do Windows Runtime. O exemplo acima utiliza o tipo de delegado do Windows Runtime Windows::Foundation::EventHandler<T>. TypedEventHandler<TSender, TResult> é outro exemplo de um tipo de delegado no Windows Runtime.

Os parâmetros de tipo desses dois tipos delegate têm de atravessar a ABI, pelo que esses parâmetros de tipo também têm de ser tipos do Windows Runtime. Isso inclui classes de execução do Windows, classes de execução de terceiros e tipos primitivos como números e cadeias de caracteres. O compilador apresenta-lhe o erro "T deve ser do tipo WinRT" se se esquecer dessa restrição.

Abaixo está um exemplo sob a forma de listagens de códigos. Comece pelos projetos ThermometerWRC e ThermometerCoreApp que criou anteriormente neste tópico e edite o código desses projetos para se assemelhar ao código dessas listas.

Esta primeira listagem é para o projeto ThermometerWRC . Depois de editar ThermometerWRC.idl , como mostrado abaixo, constrói o projeto e depois copia MyEventArgs.h para .cpp dentro do projeto (a partir da Generated Files pasta), tal como fizeste anteriormente com Thermometer.h e .cpp. Não te esqueças de apagar o static_assert de ambos os ficheiros.

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

Se não precisares de passar parâmetros ou argumentos com o teu evento, podes definir o teu próprio tipo simples de delegado no Windows Runtime. O exemplo abaixo mostra uma versão mais simples da classe de runtime do termómetro . Declara um tipo de delegado chamado SignalDelegate e depois 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 rotinas de retorno num projeto

Se precisar de eventos internos ao seu projeto Visual Studio (não entre binários), onde esses eventos não estejam limitados a tipos de Windows Runtime, então pode ainda usar o modelo de classe Delegate< winrt::>event. Basta usar winrt::d elegate em vez de um tipo de delegado Windows Runtime real, já que o winrt::d elegate também suporta parâmetros não Windows Runtime.

O exemplo abaixo mostra primeiro uma assinatura de delegado que não aceita quaisquer parâmetros (essencialmente um sinal simples), e depois uma que aceita uma string.

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

Repare como pode adicionar ao evento tantos delegados subscritores quanto quiser. No entanto, há algum custo administrativo associado a um evento. Se tudo o que precisa é de um callback simples com apenas um único delegado subscrito, então pode usar winrt::delegate<... T> por si só.

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 estiveres a portar de uma base de código C++/CX onde eventos e delegates são utilizados internamente num projeto, então winrt::delegate ajudar-te-á a replicar esse padrão em C++/WinRT.

Eventos adiáveis

Um padrão comum no Windows Runtime é o evento adiável. Um gestor de eventos faz um adiamento ao chamar o método GetDeferral do argumento de evento. Fazê-lo indica à fonte do evento que as atividades pós-evento devem ser adiadas até que o adiamento seja concluído. Isto permite que um gestor de eventos realize ações assíncronas em resposta a um evento.

O modelo de estrutura winrt::deferrable_event_args é uma classe auxiliar para implementar (produzir) o padrão de diferimento do Windows Runtime. Eis 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;
    };
}

Eis 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, a sua classe de argumentos de evento deriva de winrt::deferrable_event_args. deferrable_event_args<T> implementa T::GetDeferral para ti. Também expõe um novo método auxiliar deferrable_event_args::wait_for_deferrals, que termina quando todos os adiamentos pendentes forem concluídos (se não houver adiamentos, então conclui 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 passe eventos, e não delegados, como parâmetros de função. A função add do winrt::event é a única exceção, porque nesse caso tens de passar um delegado. A razão desta orientação é que os delegados podem assumir diferentes formas em diferentes linguagens do Windows Runtime (em termos de suportarem um único registo de cliente ou vários registos). Os eventos, com o seu modelo de múltiplos subscritores, constituem uma opção muito mais previsível e consistente.

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

Note que estas diretrizes não se aplicam necessariamente se estiveres a desenhar uma API interna. No entanto, as APIs internas muitas vezes tornam-se públicas com o tempo.