在 C++/WinRT 中撰寫事件

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

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

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

Note

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

這很重要

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

建立 ThermometerWRCThermometerCoreApp

如果你想跟著本主題中示範的更新步驟進行,以便建置及執行程式碼,那麼第一步就是依照 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.hThermometer.cpp 存根檔。 在這些檔案中,你現在可以看到 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 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: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.cppGenerated 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::eventadd 函數是唯一的例外,因為在這種情況下你必須通過代理。 制定此指引的原因在於,委派在不同的 Windows 執行階段 語言中可能會有不同的形式(就其是否支援單一用戶端註冊或多個用戶端註冊而言)。 事件採用多重訂閱者模型,因此構成一種更可預測且更一致的選項。

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

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