Condividi tramite


Creare eventi in C++/WinRT

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

Ecco le nuove funzionalità aggiunte in questo argomento.

  • Aggiornare la classe di runtime del termometro per generare un evento quando la temperatura scende sotto zero.
  • Aggiornare l'app principale che utilizza la classe di runtime del termometro in modo da gestire l'evento.

Annotazioni

Per informazioni sull'installazione e l'uso del C++/WinRT Visual Studio Extension (VSIX) e del pacchetto NuGet (che insieme forniscono il modello di progetto e il supporto per la compilazione), vedere supporto di Visual Studio per C++/WinRT.

Importante

Per i concetti e i termini essenziali che supportano la tua comprensione su come utilizzare e creare classi di runtime con C++/WinRT, vedi Usare le API con C++/WinRT e Creare le API con C++/WinRT.

Creare ThermometerWRC e ThermometerCoreApp

Se vuoi seguire gli aggiornamenti illustrati in questo argomento, in modo da poter compilare ed eseguire il codice, il primo passaggio consiste nel seguire la procedura dettagliata nell'argomento componenti Windows Runtime con C++/WinRT. In questo modo, avrai il componente ThermometerWRC Windows Runtime e l'applicazione centrale ThermometerCoreApp Core App che lo utilizza.

Aggiornare TermometroWRC per generare un evento

Aggiornare Thermometer.idl in modo che sia simile all'elenco seguente. Ecco come dichiarare un evento il cui tipo delegato è EventHandler con un argomento di un numero a virgola mobile a 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à completato nello stato corrente, ma esegui ora una compilazione 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 provvisorie dell'evento TemperatureIsBelowFreezing. In C++/WinRT, un evento dichiarato da IDL viene implementato come un insieme di funzioni sovraccaricate (analogamente al modo in cui una proprietà viene implementata come coppia di funzioni get e set sovraccaricate). Un overload accetta un delegato da registrare e restituisce un token (un winrt::event_token). L'altro accetta un token e revoca la registrazione del delegato associato.

Ora apri Thermometer.h e Thermometer.cppe aggiorna l'implementazione della classe di runtime del termometro . In Thermometer.h, aggiungere le due funzioni sovraccaricate TemperatureIsBelowFreezing, oltre a un membro dati evento privato da utilizzare 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 sopra, un evento è rappresentato dal modello di struct winrt::event, con parametri in base a un particolare tipo delegato (che può essere parametrizzato da un tipo args).

In Thermometer.cpp, implementare le due funzioni sovraccaricate 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);
    }
}

Annotazioni

Per dettagli su cosa sia un revocatore automatico di eventi, vedere Revocare un delegato registrato. Ottieni gratuitamente l'implementazione di un gestore automatico di revoca eventi per il tuo evento. In altre parole, non è necessario implementare l'overload per il revocatore di eventi, in quanto viene fornito automaticamente dalla proiezione C++/WinRT.

Gli altri sovraccarichi (il sovraccarico di registrazione e revoca manuale) sono non inseriti nella proiezione. Questo consente di offrire la flessibilità necessaria per implementarle in modo ottimale per lo scenario in uso. La chiamata di event::add e event::remove, come illustrato in queste implementazioni, è un'impostazione predefinita efficiente e sicura per l’uso concorrente/thread. Tuttavia, se si dispone di un numero molto elevato di eventi, è possibile che non si desideri un campo evento per ognuno, ma piuttosto optare per un certo tipo di implementazione sparsa.

È anche possibile notare che l'implementazione della funzione AdjustTemperature è stata aggiornata per generare l'evento TemperatureIsBelowFreezing se la temperatura scende sotto zero.

Aggiornare ThermometerCoreApp per gestire l'evento

Nel progetto ThermometerCoreApp, in App.cpp, apportare le seguenti modifiche al codice per registrare un gestore di eventi e quindi fare in modo che la temperatura scenda sotto lo zero.

WINRT_ASSERT è una definizione di macro e si espande fino a _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. Ora, ogni volta che si fa clic sulla finestra, si sottrarre 1 grado Fahrenheit dalla temperatura del termometro. E ora l'app gestisce l'evento che viene generato quando la temperatura scende al di sotto del congelamento. Per dimostrare che l'evento viene generato come previsto, inserire un punto di interruzione all'interno dell'espressione lambda che gestisce l'evento TemperatureIsBelowFreezing, eseguire l'applicazione e fare clic nella finestra.

Delegati con parametri in un'interfaccia ABI

Se l'evento deve essere accessibile in un'interfaccia ABI (Application Binary Interface), ad esempio tra un componente e l'applicazione che ne usa, l'evento deve usare un tipo delegato di Windows Runtime. L'esempio precedente usa il tipo di delegato Windows Runtime Windows::Foundation::EventHandler<T>. TypedEventHandler<TSender, TResult> è un altro esempio di tipo delegato di Windows Runtime.

I parametri di tipo per questi due delegati devono attraversare l'ABI, quindi devono essere anch'essi tipi di Windows Runtime. Includono le classi di Windows Runtime, le classi di runtime di terze parti e i tipi primitivi, come numeri e stringhe. Il compilatore mostra un errore "T deve essere di tipo WinRT" se si dimentica di rispettare il vincolo.

Di seguito è riportato un esempio sotto forma di listato 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 abbia un aspetto simile al codice in questi elenchi.

Questo primo elenco riguarda il progetto ThermometerWRC. Dopo aver modificato ThermometerWRC.idl come illustrato di seguito, compilare il progetto e quindi copiare MyEventArgs.h e .cpp nel progetto (dalla cartella Generated Files) esattamente come è stato fatto in precedenza con 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 di 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'interfaccia ABI

Se non è necessario passare parametri o argomenti con l'evento, è possibile definire un semplice tipo di delegato di Windows Runtime. L'esempio seguente mostra una versione più semplice della classe di runtime Thermometer. Dichiara un tipo delegato denominato SignalDelegate e quindi lo usa per generare un evento di tipo 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 file binari), in cui tali eventi non sono limitati ai tipi di Windows Runtime, è comunque possibile usare il modello di classedelegatowinrt::event. È sufficiente usare winrt::delegate anziché un tipo di delegato Windows Runtime effettivo, poiché winrt::delegate supporta anche parametri non di Windows Runtime.

L'esempio seguente mostra prima di tutto una firma del delegato che non accetta parametri (essenzialmente un segnale semplice) e quindi 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 come puoi aggiungere all'evento quanti delegati iscritti desideri. Tuttavia, esiste un sovraccarico associato a un evento. Se tutto ciò di cui hai bisogno è un semplice callback con un solo delegato di sottoscrizione, allora 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!");

Se si esegue il porting da una base di codice C++/CX in cui gli eventi e i delegati vengono usati internamente a un progetto, winrt::delegate vi aiuterà a replicare tale modello in C++/WinRT.

Eventi rinviabili

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

Il modello di struct winrt::deferrable_event_args è una classe helper per implementare il modello di rinvio in 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();
});

In quanto implementatore (produttore) dell'origine evento, deriva la tua classe di argomenti dell'evento da winrt::deferrable_event_args. deferrable_event_args<T> implementa automaticamente per te T::GetDeferral. Espone anche 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 rinvii, 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 per la progettazione

È consigliabile passare eventi e non delegati come parametri di funzione. La funzione aggiungi di winrt::event è l'unica eccezione, perché è necessario passare un delegato in questo caso. Il motivo di questa linea guida è dovuto al fatto che i delegati possono assumere forme diverse in diversi linguaggi di Windows Runtime (in termini di supporto di una registrazione client o di più). Gli eventi, con il modello di sottoscrittore multiplo, costituiscono un'opzione molto più prevedibile e coerente.

La firma per un delegato del gestore eventi dovrebbe essere costituita da due parametri: sender (IInspectable) e args (un tipo di argomento dell'evento, ad esempio RoutedEventArgs).

Si noti che queste linee guida non si applicano necessariamente se si progetta un'API interna. Anche se le API interne diventano spesso pubbliche nel tempo.