共用方式為


在 C++/WinRT 中撰寫事件

此主題建立於 Windows 執行階段元件和消費應用程式之上,而 "以 C++/WinRT 建立的 Windows 執行階段元件" 主題展示了如何建置這些元件。

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

  • 更新溫度計運行時間類別,以在溫度低於凍結時引發事件。
  • 更新取用溫度計運行時間類別的核心應用程式,以便處理該事件。

備註

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

這很重要

如需基本概念和術語,以幫助您理解如何使用 C++/WinRT 取用和撰寫執行階段類別,請參閱 使用 C++/WinRT 取用 API,以及 使用 C++/WinRT 撰寫 API

建立 溫度計 WRC溫度計 核心應用程式

如果您想要跟進本主題所示的更新,以便建置並執行程式碼,第一個步驟是按照 Windows 執行階段元件與 C++/WinRT 主題中的逐步指南。 如此一來,您將擁有 ThermometerWRC Windows 運行時間元件,以及取用它的 ThermometerCoreApp 核心應用程式。

更新 溫度計WRC 以觸發事件

更新 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.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 型別」錯誤。

以下是程式代碼清單形式的範例。 從在本主題中稍早建立的 溫度計WRCThermometerCoreApp 專案開始,並編輯這些專案中的程式代碼,使之與這些範例中的程式碼相符。

第一個清單適用於 溫度計WRC 專案。 編輯 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::d elegate<...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 函式是一個例外狀況,因為在此情況下您必須傳遞委派。 此指導方針的原因是,委派在不同的 Windows 執行階段語言中可能採取不同形式,這取決於它們是否支持單一或多個用戶端的註冊。 事件與其多個訂閱者模型,構成更可預測且一致的選項。

事件處理程式委派的簽名應該包含兩個參數:傳送者IInspectable),以及 參數 (某種事件參數類型,例如 RoutedEventArgs)。

請注意,如果您要設計內部 API,這些指導方針不一定適用。 雖然內部 API 通常會隨著時間而公開。