Creare eventi in C++/WinRT

Questo argomento si basa sul componente Windows Runtime e sull'applicazione che lo utilizza, che l'argomento Componenti Windows Runtime con C++/WinRT illustra come compilare.

Di seguito sono riportate le nuove funzionalità aggiunte da questo argomento.

  • Aggiornare la classe di runtime del termometro per generare un evento quando la temperatura scende sotto il livello di congelamento.
  • Aggiornare l'app principale che utilizza la classe di runtime del termometro in modo che gestisca tale evento.

Nota

Per informazioni sull'installazione e sull'uso dell'Estensione C++/WinRT per Visual Studio (VSIX) e del pacchetto NuGet, che insieme forniscono il modello di progetto e il supporto della compilazione, vedi Supporto di Visual Studio per C++/WinRT.

Importante

Per i concetti essenziali e i termini che possono aiutarti a comprendere come usare e creare classi di runtime con C++/WinRT, vedi Usare API con C++/WinRT e Creare API con C++/WinRT.

Creare ThermometerWRC e ThermometerCoreApp

Se si vuole procedere con gli aggiornamenti illustrati in questo argomento, in modo da poter compilare ed eseguire il codice, il primo passaggio consiste nel seguire la procedura dettagliata descritta nell'argomento Componenti Windows Runtime con C++/WinRT. In questo modo, saranno disponibili il componente ThermometerWRC di Windows Runtime e l'app principale ThermometerCoreApp che lo utilizza.

Aggiornare ThermometerWRC per generare un evento

Aggiornare Thermometer.idl in modo che abbia un aspetto simile a quello riportato di seguito. Questo è il modo per dichiarare un evento il cui tipo delegato è EventHandler con un argomento di un numero a virgola mobile e precisione singola.

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

Salva il file. Il progetto non verrà compilato fino al completamento nello stato corrente, ma eseguirà una compilazione in qualsiasi caso per generare versioni aggiornate dei file stub \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h e Thermometer.cpp. All'interno di questi file è ora possibile visualizzare le implementazioni stub dell'evento TemperatureIsBelowFreezing. In C++/WinRT un evento dichiarato in IDL viene implementato come un set di funzioni in overload, in modo analogo al modo in cui una proprietà viene implementata come una coppia di funzioni get e set in overload. Uno degli overload accetta un delegato ai fini della registrazione e restituisce un token (winrt::event_token). L'altro accetta un token e revoca la registrazione del delegato associato.

A questo punto aprire Thermometer.h e Thermometer.cpp e aggiornare l'implementazione della classe di runtime Thermometer. In Thermometer.h aggiungere le due funzioni di TemperatureIsBelowFreezing in overload, nonché un membro dati di evento privato da usare nell'implementazione di tali funzioni.

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

Come si può notare, un evento è rappresentato dal modello di struct WinRT:: Event, parametrizzato da un particolare tipo delegato, che può essere parametrizzato da un tipo di argomenti.

In Thermometer.cpp implementare le due funzioni TemperatureIsBelowFreezing in overload.

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

Per informazioni dettagliate su un revocatore di evento automatico, vedi Revocare un delegato registrato. Puoi ottenere gratuitamente l'implementazione di un revocatore di evento automatico per il tuo evento. In altre parole, non devi implementare l'overload per il revocatore di evento, poiché viene fornito automaticamente dalla proiezione C++/WinRT.

Gli altri overload (di registrazione e revoca manuale) non vengono inclusi nella proiezione. Hai così la flessibilità di implementarli nel modo più adatto al tuo scenario. L'uso delle chiamate a event::add e event::remove, come illustrato in queste implementazioni, è un'operazione predefinita di concorrenza/thread-safe efficiente. Quando è disponibile un numero molto elevato di eventi, tuttavia, potresti non volere un campo evento per ognuno, ma optare per un'implementazione di tipo sparse.

È anche possibile vedere sopra che l'implementazione della funzione AdjustTemperature è stata aggiornata per generare l'evento TemperatureIsBelowFreezing se la temperatura scende sotto il livello di congelamento.

Aggiornare ThermometerCoreApp per gestire l'evento

Nel progetto ThermometerCoreApp, in App.cpp, apportare al codice le modifiche seguenti per registrare un gestore dell'evento e quindi fare in modo che la temperatura scenda sotto il livello di congelamento.

WINRT_ASSERT è una definizione di macro e si espande in 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);
        ...
    }
    ...
};

Tenere presente la modifica apportata al metodo OnPointerPressed. A questo punto, ogni volta che si fa clic sulla finestra, viene sottratto 1 grado Fahrenheit dalla temperatura del termometro. Ora l'app gestisce l'evento generato quando la temperatura scende sotto il livello di congelamento. Per dimostrare che l'evento viene generato nel modo previsto, inserire un punto di interruzione all'interno dell'espressione lambda che gestisce l'evento TemperatureIsBelowFreezing, eseguire l'app e fare clic all'interno della finestra.

Delegati con parametri in un'ABI

Se l'evento deve essere accessibile in tutta un'interfaccia applicativa binaria (ABI), ad esempio tra un componente e la relativa applicazione dispendiosa, l'evento deve usare un tipo di delegato di Windows Runtime. L'esempio precedente usa il tipo di delegato di Windows Runtime Windows::Foundation::EventHandler<T>. TypedEventHandler<TSender, TResult> è un altro esempio di tipo di delegato di Windows Runtime.

I parametri di tipo per i due tipi di delegato devono attraversare l'ABI, pertanto anche i parametri di tipo devono essere tipi di Windows Runtime. Sono incluse le classi di runtime di Windows, le classi di runtime di terze parti, nonché i tipi primitivi come stringhe e numeri. Il compilatore mostra l'errore "T deve essere di tipo WinRT", se si è dimenticato di applicare tale vincolo.

Di seguito è riportato un esempio sotto forma di elenchi di codice. Iniziare con i progetti ThermometerWRC e ThermometerCoreApp creati in precedenza in questo argomento e modificare il codice in tali progetti in modo che sia analogo al codice di questi elenchi.

Il primo elenco è per il progetto ThermometerWRC. Dopo la modifica di ThermometerWRC.idl come illustrato di seguito, compila il progetto e quindi copia MyEventArgs.h e .cpp nel progetto (dalla cartella Generated Files) come in precedenza per Thermometer.h e .cpp. Ricordarsi di eliminare il static_assert da entrambi i file.

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

Questo elenco è per il progetto 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.
    });
}
...

Segnali semplici in un'ABI

Se non devi passare parametri o argomenti con l'evento, puoi definire un tuo tipo di delegato di Windows Runtime semplice. L'esempio seguente mostra una versione semplificata della classe di runtime Thermometer. Dichiara un tipo di delegato denominato SignalDelegate e quindi lo usa per generare un evento di tipo di segnale anziché un evento con un parametro.

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

Delegati con parametri, segnali semplici e callback all'interno di un progetto

Se sono necessari eventi interni al progetto di Visual Studio (non tra tutti i file binari) che non siano limitati ai tipi Windows Runtime, puoi comunque usare il modello di classe winrt::event<Delegate>. Usa semplicemente winrt::delegate anziché un tipo di delegato Windows Runtime effettivo, dal momento che winrt::delegate supporta anche parametri non Windows Runtime.

L'esempio seguente mostra innanzitutto una firma delegato che non accetta parametri, essenzialmente un segnale semplice, e poi una che accetta una stringa.

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

Nota che puoi aggiungere all'evento tutti i delegati di sottoscrizione che vuoi. Tuttavia, l'evento comporta un certo sovraccarico. Se ti serve soltanto un callback semplice con un solo delegato sottoscrizione, puoi usare winrt::delegate<... T> da solo.

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

Per la conversione da una codebase C++/CX in cui gli eventi e i delegati vengono usati internamente in un progetto, winrt::delegate ti consentirà di replicare tale modello in C++/WinRT.

Eventi rinviabili

Un modello comune in Windows Runtime è l'evento rinviabile. Un gestore eventi accetta un differimento richiamando il metodo GetDeferral dell'argomento dell’evento. In questo modo viene indicato all'origine evento che le attività post-evento devono essere posticipate fino al completamento del differimento. In questo modo il gestore eventi può eseguire azioni asincrone in risposta a un evento.

Il modello di struct winrt::deferrable_event_args è una classe helper per implementare (produrre) il modello di differimento di Windows Runtime. Ecco un esempio.

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

Ecco come il destinatario dell'evento usa il modello di evento rinviabile.

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

L'implementatore (producer) dell'origine evento deriva la classe args dell'evento da winrt::d eferrable_event_args. deferrable_event_args<T> implementa automaticamente T::GetDeferral . Espone inoltre un nuovo metodo helper deferrable_event_args::wait_for_deferrals, che viene completato quando tutti i differimenti in sospeso sono stati completati (se non sono stati eseguiti differimenti, viene completato immediatamente).

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

Linee guida di progettazione

È consigliato passare eventi, e non delegati, come parametri di funzione. La funzione add di winrt::event è l'unica eccezione, infatti in questo caso è necessario passare un delegato. Il motivo di questa linea guida è che i delegati possono assumere forme diverse in linguaggi Windows Runtime diversi per quanto riguarda il supporto di una sola registrazione del client o più. Gli eventi, con il loro modello a più sottoscrittori, costituiscono un'opzione molto più prevedibile e coerente.

La firma per un delegato di gestore eventi deve essere costituita da due parametri: mittente, ovvero IInspectable, e args, ovvero un tipo di argomento dell'evento, ad esempio RoutedEventArgs.

Nota che queste linee guida possono non essere applicabili quando si progetta un'API interna. Tuttavia, le API interne diventano spesso pubbliche nel corso del tempo.