在 C++/WinRT 中撰寫事件

這個主題建立在 Windows 執行階段 元件和消費應用程式之上,而 Windows 執行階段 元件搭配 C++/WinRT 主題則教你如何編譯。

以下是這個主題新增的功能。

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

Note

關於安裝及使用 C++/WinRT Visual Studio 擴充套件(VSIX)及 NuGet 套件(共同提供專案範本與建置支援)的資訊,請參閱 Visual Studio 對 C++/WinRT 的支援。

這很重要

關於支持你如何使用 C++/WinRT 撰寫執行時類別的基本概念與術語,請參見 Consume API with C++/WinRT 及 Author API with C++/WinRT。

建立 ThermometerWRC 與 ThermometerCoreApp

如果你想跟著本主題中示範的更新步驟進行,以便建置及執行程式碼,那麼第一步就是依照 Windows 執行階段 元件與 C++/WinRT 主題中的逐步解說進行。 這樣你就會擁有 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.h 和 Thermometer.cpp 存根檔。 在這些檔案中,你現在可以看到 TemperatureIsBelowFreezing 事件的存根實作。 在 C++/WinRT 中,IDL 宣告事件實作為一組超載函式(類似於屬性實作為一對超載的 get 與 set 函式)。 一次過載會接收一個代理被註冊,並回傳一個令牌( 一個 winrt::event_token)。 另一方則收取代幣,撤銷該代表的登記。

現在打開 Thermometer.h 和 Thermometer.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 struct 模板表示,該模板由特定的代理類型參數化(而該代理類型本身也可由 args 類型參數化)。

在 Thermometer.cpp中,實作兩個超載的 TemperatureIsUnderFreezing 函數。

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

如需了解自動事件撤銷器的詳細資訊,請參閱 撤銷已註冊的委派。 你的活動會免費獲得自動事件撤銷器的實作。 換句話說,你不需要實作事件撤銷器的超載——那是 C++/WinRT 投影提供的。

其他超載(註冊和手動撤銷超載) 則未 內建在投影中。 這是為了讓你有彈性,能根據你的情境最佳地實施它們。 呼叫 event:add 和 event::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 類型」的錯誤。

以下是一個以代碼列表形式呈現的範例。 從你之前在本主題中建立的 ThermometerWRC 和 ThermometerCoreApp 專案開始,並編輯這些專案中的程式碼,使其看起來與這些列表中的程式碼相似。

第一個列表項目是 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 執行階段 代理型態。 以下範例展示了溫度 計 運行時類別的簡化版本。 它宣告了一個名為 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::d elegate 代替實際的 Windows 執行階段 代理類型,因為 winrt::d elegate 也支援非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 衍生你的事件引數類別。 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::event 的 add 函數是唯一的例外,因為在這種情況下你必須通過代理。 制定此指引的原因在於,委派在不同的 Windows 執行階段 語言中可能會有不同的形式(就其是否支援單一用戶端註冊或多個用戶端註冊而言)。 事件採用多重訂閱者模型,因此構成一種更可預測且更一致的選項。

事件處理代理的簽章應包含兩個參數: 發送者 (IInspectable)與 args (某事件參數類型,例如 RoutedEventArgs)。

請注意,這些指引不一定適用於設計內部 API。 不過,內部 API 通常會隨時間公開。