Freigeben über


Autorenereignisse in C++/WinRT

Dieses Thema basiert auf der Windows-Runtime-Komponente und der Anwendung, die das Thema Windows-Runtime-Komponenten mit C++/WinRT zeigt, wie Sie erstellen können.

Hier sind die neuen Funktionen, die dieses Thema hinzufügt.

  • Aktualisieren Sie die Thermometer-Laufzeitklasse, um ein Ereignis auszulösen, wenn die Temperatur unter den Gefrierpunkt fällt.
  • Aktualisieren Sie die Core-App, die die Thermometerlaufzeitklasse verwendet, damit es dieses Ereignis behandelt.

Hinweis

Informationen zum Installieren und Verwenden der C++/WinRT Visual Studio Extension (VSIX) und des NuGet-Pakets (die zusammen Projektvorlage und Buildunterstützung bereitstellen), finden Sie unter Visual Studio-Unterstützung für C++/WinRT.

Von Bedeutung

Wichtige Konzepte und Begriffe, die Ihr Verständnis bei der Nutzung und Erstellung von Laufzeitklassen mit C++/WinRT unterstützen, finden Sie unter Nutzen von APIs mit C++/WinRT und Erstellen von APIs mit C++/WinRT.

Erstellen ThermometerWRC und ThermometerCoreApp

Wenn Sie den in diesem Thema gezeigten Updates folgen möchten, um den Code zu erstellen und auszuführen, besteht der erste Schritt darin, der exemplarischen Vorgehensweise im Thema Windows Runtime-Komponenten mit C++/WinRT zu folgen. Dazu verfügen Sie über die Komponente ThermometerWRC Windows Runtime und die ThermometerCoreApp Core App, die sie nutzt.

Aktualisieren Sie ThermometerWRC-, um ein Ereignis auszulösen.

Aktualisieren Sie Thermometer.idl so, dass sie wie die nachstehende Auflistung aussieht. So deklarieren Sie ein Ereignis, dessen Delegattyp EventHandler mit einem Argument einer Fließkommazahl einfacher Genauigkeit ist.

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

Speichern Sie die Datei. Das Projekt wird in seinem aktuellen Zustand nicht bis zur Fertigstellung erstellt werden, aber führen Sie jetzt dennoch einen Build durch, um die neuesten Versionen der \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h und Thermometer.cpp Stubdateien zu generieren. In diesen Dateien können Sie jetzt Stubimplementierungen des TemperatureIsBelowFreezing-Ereignisses sehen. In C++/WinRT wird ein IDL-deklariertes Ereignis als eine Reihe überladener Funktionen implementiert (ähnlich wie eine Eigenschaft als Paar überladener Get- und Set-Funktionen implementiert wird). Eine Überladung benötigt einen Delegaten, um registriert zu werden, und gibt ein Token zurück (ein winrt::event_token). Die andere nimmt ein Token und löscht die Registrierung des zugehörigen Delegaten.

Öffnen Sie nun Thermometer.h und Thermometer.cpp, und aktualisieren Sie die Implementierung der Thermometer Laufzeitklasse. Fügen Sie in Thermometer.hdie beiden überladenen TemperatureIsBelowFreezing Funktionen sowie ein privates Ereignisdatenelement hinzu, das bei der Implementierung dieser Funktionen verwendet werden soll.

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

Wie Sie oben sehen können, wird ein Ereignis durch die winrt::event Strukturvorlage dargestellt, parametrisiert durch einen bestimmten Delegattyp (der selbst durch einen Argstyp parametrisiert werden kann).

Implementieren Sie in Thermometer.cppdie beiden überladenen TemperatureIsBelowFreezing Funktionen.

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

Hinweis

Details dazu, was ein automatischer Ereigniswiderrufer ist, finden Sie unter Widerrufen eines registrierten Delegierten. Sie erhalten die Implementierung eines automatischen Ereignis-Widerrufs kostenlos für Ihr Ereignis. Mit anderen Worten, Sie müssen die Überladung für den Ereignis-Revoker nicht implementieren, da diese durch die C++/WinRT-Projektion bereitgestellt ist.

Die anderen Überladungen (die Registrierung und die manuelle Widerruf-Überladungen) sind nicht in die Projektion integriert. Dadurch erhalten Sie die Flexibilität, sie optimal für Ihr Szenario zu implementieren. Das Aufrufen event::add und event::remove wie in diesen Implementierungen gezeigt ist ein effizienter und paralleler/threadsicherer Standardwert. Wenn Sie jedoch über eine sehr große Anzahl von Ereignissen verfügen, wollen Sie vielleicht kein Ereignisfeld für jedes, sondern stattdessen eine Art sparse Implementierung verwenden.

Sie können auch oben im Text sehen, dass die Implementierung der Funktion AdjustTemperature aktualisiert wurde, um das Ereignis TemperatureIsBelowFreezing auszulösen, wenn die Temperatur unter den Gefrierpunkt fällt.

Aktualisieren Sie ThermometerCoreApp, um das Ereignis zu verarbeiten.

Nehmen Sie im ThermometerCoreApp--Projekt in App.cppdie folgenden Änderungen am Code vor, um einen Handler für Ereignisse zu registrieren, und dann bewirken Sie, dass die Temperatur unter den Gefrierpunkt fällt.

WINRT_ASSERT ist eine Makrodefinition und expandiert zu _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);
        ...
    }
    ...
};

Beachten Sie die Änderung an der Methode OnPointerPressed. Jedes Mal, wenn Sie auf das Fenster klicken, Sie 1 Grad Fahrenheit von der Temperatur des Thermometers subtrahieren. Und jetzt behandelt die App das Ereignis, das ausgelöst wird, wenn die Temperatur unter den Gefrierpunkt fällt. Um zu veranschaulichen, dass das Ereignis wie erwartet ausgelöst wird, platzieren Sie einen Haltepunkt innerhalb des Lambda-Ausdrucks, der das TemperatureIsBelowFreezing-Ereignis behandelt, führen Sie die App aus, und klicken Sie in das Fenster.

Parametrisierte Delegaten über eine ABI

Wenn auf Ihr Ereignis über eine Anwendungs-Binärschnittstelle (Application Binary Interface, ABI) zugegriffen werden muss, beispielsweise zwischen einer Komponente und der verbrauchenden Anwendung, dann muss Ihr Ereignis einen Windows-Runtime-Delegatentyp verwenden. Im obigen Beispiel wird der Windows::Foundation::EventHandler<T> Windows-Runtime-Delegattyp verwendet. TypedEventHandler<TSender, TResult> ist ein weiteres Beispiel für einen Windows-Runtime-Delegatentyp.

Die Typparameter für diese beiden Delegattypen müssen die ABI überschreiten, daher müssen die Typparameter auch Windows-Runtime-Typen sein. Dazu gehören Windows-Runtime-Klassen, Laufzeitklassen von Drittanbietern und Grundtypen wie Zahlen und Zeichenfolgen. Der Compiler hilft Ihnen bei einem "T muss WinRT-Typ" Fehler sein, wenn Sie diese Einschränkung vergessen.

Nachfolgend finden Sie ein Beispiel in Form von Codeauflistungen. Beginnen Sie mit den ThermometerWRC- und ThermometerCoreApp- Projekten, die Sie zuvor in diesem Thema erstellt haben, und bearbeiten Sie den Code in diesen Projekten so, dass der Code den Code in diesen Auflistungen entspricht.

Diese erste Auflistung ist für das ThermometerWRC Projekt. Nachdem Sie ThermometerWRC.idl wie unten dargestellt bearbeitet haben, erstellen Sie das Projekt, und kopieren Sie dann MyEventArgs.h und .cpp in das Projekt (aus dem Ordner Generated Files), genau wie zuvor mit Thermometer.h und .cpp. Denken Sie daran, die static_assert aus beiden Dateien zu löschen.

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

Dieser Eintrag bezieht sich auf das Projekt 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.
    });
}
...

Einfache Signale über eine ABI

Wenn Sie keine Parameter oder Argumente mit Ihrem Ereignis übergeben müssen, können Sie ihren eigenen einfachen Windows-Runtime-Delegattyp definieren. Das folgende Beispiel zeigt eine einfachere Version der Thermometer Laufzeitklasse. Er deklariert einen Delegattyp namens SignalDelegate und verwendet dann diesen, um ein Signalereignis anstelle eines Ereignisses mit einem Parameter auszulösen.

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

Parametrisierte Stellvertretungen, einfache Signale und Rückrufe innerhalb eines Projekts

Wenn Sie Ereignisse benötigen, die für Ihr Visual Studio-Projekt intern sind (nicht über Binärdateien hinweg), bei denen diese Ereignisse nicht auf Windows-Runtime-Typen beschränkt sind, können Sie weiterhin die winrt::event<Delegat> Klassenvorlage verwenden. Verwenden Sie einfach winrt::delegate anstelle eines tatsächlichen Windows Runtime-Delegatentyps, da winrt::delegate auch Nicht-Windows-Runtime-Parameter unterstützt.

Das folgende Beispiel zeigt zunächst eine Delegatensignatur, die keine Parameter annimmt (im Wesentlichen ein einfaches Signal), und dann eine, die eine Zeichenfolge annimmt.

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

Achten Sie darauf, wie Sie dem Ereignis beliebig viele abonnierende Delegierte hinzufügen können. Es gibt jedoch einen zusätzlichen Aufwand, der einem Ereignis zugeordnet ist. Wenn Sie lediglich einen einfachen Rückruf mit einem einzigen abonnierten Delegat benötigen, können Sie winrt::delegate<... T> allein verwenden.

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

Wenn Sie von einer C++/CX-Codebasis portieren, in der Ereignisse und Delegaten intern im Projekt verwendet werden, hilft Ihnen winrt::delegate, dieses Muster in C++/WinRT zu replizieren.

Aufschiebbare Ereignisse

Ein gängiges Muster in der Windows-Runtime ist das aufschiebbare Ereignis. Ein Ereignishandler verwendet eine Verzögerungs- durch Aufrufen der GetDeferral--Methode des Ereignisarguments. Dadurch wird der Ereignisquelle angegeben, dass Aktivitäten nach dem Ereignis verschoben werden sollen, bis die Verzögerung abgeschlossen ist. Dadurch kann ein Ereignishandler asynchrone Aktionen als Reaktion auf ein Ereignis ausführen.

Die winrt::deferrable_event_args Strukturvorlage ist eine Hilfsklasse zum Implementieren (Produzieren) des Aufschub-Musters der Windows-Runtime. Hier ist ein Beispiel.

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

So verwendet der Ereignisempfänger das aufschiebbare Ereignismuster.

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

Als Implementor (Produzent) der Ereignisquelle leiten Sie Ihre Ereignisargumentklasse von winrt::deferrable_event_argsab. deferrable_event_args<T> implementiert T::GetDeferral- für Sie. Außerdem wird eine neue Hilfsmethode deferrable_event_args::wait_for_deferralsverfügbar gemacht, die abgeschlossen wird, wenn alle ausstehenden Aufschübe abgeschlossen sind (wenn keine Aufschübe vorgenommen wurden, wird sie sofort abgeschlossen).

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

Entwurfsrichtlinien

Es wird empfohlen, Ereignisse und keine Stellvertretungen als Funktionsparameter zu übergeben. Die -Funktion zum Hinzufügen in winrt::event ist die einzige Ausnahme, da Sie in diesem Fall einen Delegat übergeben müssen. Der Grund für diese Richtlinie ist, dass Delegaten in verschiedenen Windows-Runtime-Sprachen unterschiedliche Formen annehmen können, je nachdem, ob sie eine oder mehrere Clientregistrierungen unterstützen. Ereignisse mit ihrem Modell mit mehreren Abonnenten stellen eine viel vorhersehbarere und konsistentere Option dar.

Die Signatur für einen Ereignishandlerdelegat sollte aus zwei Parametern bestehen: Absender (IInspectable) und Args (ein Ereignisargumenttyp, z. B. RoutedEventArgs).

Beachten Sie, dass diese Richtlinien nicht unbedingt gelten, wenn Sie eine interne API entwerfen. Obwohl interne APIs im Laufe der Zeit häufig öffentlich werden.