以 C++/WinRT 撰寫事件

本主題係根據 Windows 執行階段元件和使用中應用程式建置的,而使用 C++/WinRT 的 Windows 執行階段元件主題會向您示範建置方式。

以下是本主題新增的功能。

  • 更新溫度計執行階段類別,以在其溫度低於冰點時引發事件。
  • 更新使用溫度計執行階段類別的核心應用程式,使其能夠處理該事件。

注意

如需安裝和使用 C++/WinRT Visual Studio 延伸模組 (VSIX) 與 NuGet 套件 (一起提供專案範本和建置支援) 的資訊,請參閱 C++/WinRT 的 Visual Studio 支援

重要

如需支援您了解如何使用 C++/WinRT 使用及撰寫執行階段類別的基本概念和詞彙,請參閱使用 C++/WinRT 使用 API使用 C++/WinRT 撰寫 API

建立 ThermometerWRCThermometerCoreApp

如果您想要遵循本主題所示的更新,讓您可以建置和執行程式碼,則第一個步驟是遵循使用 C++/WinRT 的 Windows 執行階段元件主題中的逐步解說。 如此一來,您將會有 ThermometerWRC Windows 執行階段元件,以及使用其的 ThermometerCoreApp 核心應用程式。

更新 ThermometerWRC 以引發事件

更新 Thermometer.idl,使其看起來像下面的清單。 這是使用單精確度浮點數的引數宣告事件的方式,而此事件的委派類型為 EventHandler

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

儲存檔案。 專案不會以目前狀態完成建置,但在任何情況下都會立即執行建置,以產生 \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.hThermometer.cpp Stub 檔案的更新版本。 在這些檔案中,您現在可以看到 TemperatureIsBelowFreezing 事件的虛設常式實作。 在 C++/WinRT 中,IDL 宣告的事件實作為一組的多載函式 (與將屬性實作為一對多載的 get 和 set 函式類似)。 一個多載會接受將註冊的委派,並傳回預付碼 (winrt::event_token)。 其他則接收預付碼並撤銷相關的委派。

現在開啟 Thermometer.hThermometer.cpp,並更新 Thermometer 執行階段類別的實作。 在 Thermometer.h 中,新增兩個多載的 TemperatureIsBelowFreezing 函式,以及要在實作那些函式時使用的私人事件資料成員。

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

如您所見,事件是由 winrt::event 結構範本表示,由特定委派類型參數化 (其本身可由 args 類型參數化)。

Thermometer.cpp 中,實作兩個多載的 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);
    }
}

注意

如需有關事件撤銷的詳細資訊,請參閱撤銷已註冊的委派。 您可以免費為您的事件取得自動事件撤銷實作。 換句話說,您不需要實作事件撤銷的多載—C++/WinRT 投影會為您提供。

其他多載 (註冊和手動撤銷多載) 則「不會」在投影中內建。 這是為了讓您能夠彈性地以最佳方式在案例中進行實作。 呼叫 event::addevent::remove(如同這些實作所示) 是有效率且並行/執行緒安全的預設值。 但若您有大量的事件,則您可能不想要每一個都有事件欄位,而寧願改用一些疏鬆的實作。

您也可以從上面看到,AdjustTemperature 函式的實作已更新為會在溫度低於冰點時引發 TemperatureIsBelowFreezing 事件。

更新 ThermometerCoreApp 以處理事件

ThermometerCoreApp 專案的 App.cpp 中,對程式碼進行下列變更以註冊事件處理常式,然後讓溫度低於冰點。

WINRT_ASSERT 是巨集定義,而且會發展為 _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);
        ...
    }
    ...
};

請留意 OnPointerPressed 方法的變更。 現在,每當您按一下視窗時,就會讓溫度計的溫度減去華氏 1 度。 現在,應用程式正在處理溫度低於冰點時所引發的事件。 若要示範事件如預期般引發,請將中斷點放在正在處理 TemperatureIsBelowFreezing 事件的 lambda 運算式內、執行應用程式,然後在視窗中按一下。

透過 ABI 的參數化委派

如果您的事件必須可透過應用程式二進位介面 (ABI) 存取—例如界於元件與其取用的應用程式之間—則您的事件必須使用 Windows 執行階段委派類型。 上述範例使用 Windows::Foundation::EventHandler<T> Windows 執行階段委派類型。 TypedEventHandler<TSender, TResult> 是 Windows 執行階段委派類型的另一個範例。

這兩個委派類型的類型參數必須透過 ABI,因此類型參數也必須是 Windows 執行階段類型。 包含 Windows 執行階段類別、第三方執行階段類別,以及數字和字串等基本類型。 如果您忘記此限制的話,編譯器會協助您解決「T 必須為 WinRT 類型」錯誤。

以下是程式代碼清單形式的範例。 從您稍早在本主題中建立的 ThermometerWRCThermometerCoreApp 專案開始,編輯這些專案中的程式碼,使其看起來像這些清單中的程式碼。

第一個清單是 ThermometerWRC 專案的清單。 如下所示編輯 ThermometerWRC.idl 之後,請建立專案,然後將 MyEventArgs.h.cpp 複製到專案中 (從 Generated Files 資料夾),就如同您先前使用 Thermometer.h.cpp 所做的一樣。 請記得從這兩個檔案中刪除 static_assert

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

這個清單是 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.
    });
}
...

跨 ABI 的簡單訊號

如果您不需要透過您的事件傳遞任何參數或與引數,則可以定義自己的簡單 Windows 執行階段委派類型。 以下範例顯示 Thermometer 執行階段類別的簡易版本。 它會宣告名為 SignalDelegate 的委派類型,然後使用該委派類型來引發訊號類型事件,而不是具有參數的事件。

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

參數化委派、簡單訊號,以及專案中的回呼

如果您需要 Visual Studio 專案內部的事件 (而非跨二進位檔),而這些事件不限於 Windows 執行階段類型,則您仍可使用 winrt::event<Delegate> 類別範本。 只要使用 winrt::delegate,而不是實際的 Windows 執行階段委派類型,因為 winrt::delegate 也支援非 Windows 執行階段參數。

下列範例先顯示不採用任何參數的委派簽章 (基本上是簡單訊號),而後顯示採用一個字串的委派簽章。

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

請注意,如何才能將如您想要的多個訂閱委派新增至事件。 不過,事件還有一些相關聯的額外負荷。 如果您只需要僅含單一訂閱委派的簡單回呼,則可以使用 winrt::delegate<...T>本身。

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

如果您正從在內部使用事件和委派的 C++/CX 程式碼基底進行移植,則 winrt::delegate會協助您在 C++/WinRT 中複寫該模式。

可延遲的事件

Windows 執行階段中的常見模式是可延遲的事件。 事件處理常式會藉由呼叫事件引數的 GetDeferral 方法來接受延遲。 這樣做會向事件來源指出事件後活動應該延後到延遲完成為止。 這可讓事件處理常式執行非同步動作,以回應事件。

winrt::deferrable_event_args 結構範本是實作 Windows 執行階段延遲模式的協助程式類別。 以下是範例。

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

以下是事件收件者如何使用可延遲的事件模式。

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

身為事件來源的實作者 (產生者),您可以從 winrt::deferrable_event_args 衍生事件自 args 類別。 deferrable_event_args<T> 會為您實作 T::GetDeferral。 它也會公開新的協助程式方法 deferrable_event_args::wait_for_deferrals,當所有未完成延遲完成時完成 (如果未採用任何延遲,則會立即完成)。

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

設計指導方針

我們建議您將事件 (不是委派) 當作函式參數傳遞。 winrt::eventadd 函式是一個例外狀況,因為您必須在該情況下傳遞委派。 此指導方針的原因是因為委派可以在不同的 Windows 執行階段語言中採用不同的形式 (就它們支援一個或多個用戶端註冊而論)。 事件與其多個訂閱者模型,構成一個更容易預測且一致的選項。

事件處理常式委派的簽章應該包含兩個參數:sender (IInspectable) 和 args (某些事件引數類型,例如 RoutedEventArgs)。

請注意,如果您要設計內部 API,不一定要套用這些指導方針。 雖然經過一段時間,內部 API 通常會變成公開的。