以 C++/WinRT 撰寫事件Author events in C++/WinRT

本主題係根據 Windows 執行階段元件和使用中應用程式建置的,而使用 C++/WinRT 的 Windows 執行階段元件主題會向您示範建置方式。This topic builds on the Windows Runtime component, and the consuming application, that the Windows Runtime components with C++/WinRT topic shows you how to build.

以下是本主題新增的功能。Here are the new features that this topic adds.

  • 更新溫度計執行階段類別,以在其溫度低於冰點時引發事件。Update the thermometer runtime class to raise an event when its temperature goes below freezing.
  • 更新使用溫度計執行階段類別的核心應用程式,使其能夠處理該事件。Update the Core App that consumes the thermometer runtime class so that it handles that event.

注意

如需安裝和使用 C++/WinRT Visual Studio 延伸模組 (VSIX) 與 NuGet 套件 (一起提供專案範本和建置支援) 的資訊,請參閱 C++/WinRT 的 Visual Studio 支援For info about installing and using the C++/WinRT Visual Studio Extension (VSIX) and the NuGet package (which together provide project template and build support), see Visual Studio support for C++/WinRT.

重要

如需一些基本概念和詞彙,以協助了解如何以 C++/WinRT 使用及撰寫執行階段類別,請參閱使用 C++/WinRT 來使用 API使用 C++/WinRT 撰寫 APIFor essential concepts and terms that support your understanding of how to consume and author runtime classes with C++/WinRT, see Consume APIs with C++/WinRT and Author APIs with C++/WinRT.

建立 ThermometerWRCThermometerCoreAppCreate ThermometerWRC and ThermometerCoreApp

如果您想要遵循本主題所示的更新,讓您可以建置和執行程式碼,則第一個步驟是遵循使用 C++/WinRT 的 Windows 執行階段元件主題中的逐步解說。If you want to follow along with the updates shown in this topic, so that you can build and run the code, then the first step is to follow the walkthrough in the Windows Runtime components with C++/WinRT topic. 如此一來,您將會有 ThermometerWRC Windows 執行階段元件,以及使用其的 ThermometerCoreApp 核心應用程式。By doing so, you'll have the ThermometerWRC Windows Runtime component, and the ThermometerCoreApp Core App that consumes it.

更新 ThermometerWRC 以引發事件Update ThermometerWRC to raise an event

更新 Thermometer.idl,使其看起來像下面的清單。Update Thermometer.idl to look like the listing below. 這是使用單精確度浮點數的引數宣告事件的方式,而此事件的委派類型為 EventHandlerThis is how to declare an event whose delegate type is EventHandler with an argument of a single-precision floating-point number.

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

儲存檔案。Save the file. 專案不會以目前狀態完成建置,但在任何情況下都會立即執行建置,以產生 \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.hThermometer.cpp Stub 檔案的更新版本。The project won't build to completion in its current state, but perform a build now in any case to generate updated versions of the \ThermometerWRC\ThermometerWRC\Generated Files\sources\Thermometer.h and Thermometer.cpp stub files. 在這些檔案中,您現在可以看到 TemperatureIsBelowFreezing 事件的虛設常式實作。Inside those files you can now see stub implementations of the TemperatureIsBelowFreezing event. 在 C++/WinRT 中,IDL 宣告的事件實作為一組的多載函式 (與將屬性實作為一對多載的 get 和 set 函式類似)。In C++/WinRT, an IDL-declared event is implemented as a set of overloaded functions (similar to the way a property is implemented as a pair of overloaded get and set functions). 一個多載會接受將註冊的委派,並傳回預付碼 (winrt::event_token)。One overload takes a delegate to be registered, and returns a token (a winrt::event_token). 其他則接收預付碼並撤銷相關的委派。The other takes a token, and revokes the registration of the associated delegate.

現在開啟 Thermometer.hThermometer.cpp,並更新 Thermometer 執行階段類別的實作。Now open Thermometer.h and Thermometer.cpp, and update the implementation of the Thermometer runtime class. Thermometer.h 中,新增兩個多載的 TemperatureIsBelowFreezing 函式,以及要在實作那些函式時使用的私人事件資料成員。In Thermometer.h, add the two overloaded TemperatureIsBelowFreezing functions, as well as a private event data member to use in the implementation of those functions.

// 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 類型參數化)。As you can see above, an event is represented by the winrt::event struct template, parameterized by a particular delegate type (which itself can be parameterized by an args type).

Thermometer.cpp 中,實作兩個多載的 TemperatureIsBelowFreezing 函式。In Thermometer.cpp, implement the two overloaded TemperatureIsBelowFreezing functions.

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

注意

如需有關事件撤銷的詳細資訊,請參閱撤銷已註冊的委派For details of what an auto event revoker is, see Revoke a registered delegate. 您可以免費為您的事件取得自動事件撤銷實作。You get auto event revoker implementation for free for your event. 換句話說,您不需要實作事件撤銷的多載—C++/WinRT 投影會為您提供。In other words, you don't need to implement the overload for the event revoker—that's provided for you by the C++/WinRT projection.

其他多載 (註冊和手動撤銷多載) 則「不會」在投影中內建。The other overloads (the registration and manual revocation overloads) are not baked into the projection. 這是為了讓您能夠彈性地以最佳方式在案例中進行實作。That's to give you the flexibility to implement them optimally for your scenario. 呼叫 event::addevent::remove(如同這些實作所示) 是有效率且並行/執行緒安全的預設值。Calling event::add and event::remove as shown in these implementations is an efficient and concurrency/thread-safe default. 但若您有大量的事件,則您可能不想要每一個都有事件欄位,而寧願改用一些疏鬆的實作。But if you have a very large number of events, then you may not want an event field for each, but rather opt for some kind of sparse implementation instead.

您也可以從上面看到,AdjustTemperature 函式的實作已更新為會在溫度低於冰點時引發 TemperatureIsBelowFreezing 事件。You can also see above that the implementation of the AdjustTemperature function has been updated to raise the TemperatureIsBelowFreezing event if the temperature goes below freezing.

更新 ThermometerCoreApp 以處理事件Update ThermometerCoreApp to handle the event

ThermometerCoreApp 專案的 App.cpp 中,對程式碼進行下列變更以註冊事件處理常式,然後讓溫度低於冰點。In the ThermometerCoreApp project, in App.cpp, make the following changes to the code to register an event handler, and then cause the temperature to go below freezing.

WINRT_ASSERT 是巨集定義,而且會發展為 _ASSERTEWINRT_ASSERT is a macro definition, and it expands to _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 方法的變更。Be aware of the change to the OnPointerPressed method. 現在,每當您按一下視窗時,就會讓溫度計的溫度減去華氏 1 度。Now, each time you click the window, you subtract 1 degree Fahrenheit from the thermometer's temperature. 現在,應用程式正在處理溫度低於冰點時所引發的事件。And now, the app is handling the event that's raised when the temperature goes below freezing. 若要示範事件如預期般引發,請將中斷點放在正在處理 TemperatureIsBelowFreezing 事件的 lambda 運算式內、執行應用程式,然後在視窗中按一下。To demonstrate that the event is being raised as expected, put a breakpoint inside the lambda expression that's handling the TemperatureIsBelowFreezing event, run the app, and click inside the window.

透過 ABI 的參數化委派Parameterized delegates across an ABI

如果您的事件必須可透過應用程式二進位介面 (ABI) 存取—例如界於元件與其取用的應用程式之間—則您的事件必須使用 Windows 執行階段委派類型。If your event must be accessible across an application binary interface (ABI)—such as between a component and its consuming application—then your event must use a Windows Runtime delegate type. 上述範例使用 Windows::Foundation::EventHandler<T> Windows 執行階段委派類型。The example above uses the Windows::Foundation::EventHandler<T> Windows Runtime delegate type. TypedEventHandler<TSender, TResult> 是 Windows 執行階段委派類型的另一個範例。TypedEventHandler<TSender, TResult> is another example of a Windows Runtime delegate type.

這兩個委派類型的類型參數必須透過 ABI,因此類型參數也必須是 Windows 執行階段類型。The type parameters for those two delegate types have to cross the ABI, so the type parameters must be Windows Runtime types, too. 包含 Windows 執行階段類別、第三方執行階段類別,以及數字和字串等基本類型。That includes Windows runtime classes, third-party runtime classes, and primitive types such as numbers and strings. 如果您忘記此限制的話,編譯器會協助您解決「T 必須為 WinRT 類型」錯誤。The compiler helps you with a "T must be WinRT type" error if you forget that constraint.

以下是程式代碼清單形式的範例。Below is an example in the form of code listings. 從您稍早在本主題中建立的 ThermometerWRCThermometerCoreApp 專案開始,編輯這些專案中的程式碼,使其看起來像這些清單中的程式碼。Begin with the ThermometerWRC and ThermometerCoreApp projects that you created earlier in this topic, and edit the code in those projects to look like the code in these listings.

第一個清單是 ThermometerWRC 專案的清單。This first listing is for the ThermometerWRC project. 如下所示編輯 ThermometerWRC.idl 之後,請建立專案,然後將 MyEventArgs.h.cpp 複製到專案中 (從 Generated Files 資料夾),就如同您先前使用 Thermometer.h.cpp 所做的一樣。After editing ThermometerWRC.idl as shown below, build the project, and then copy MyEventArgs.h and .cpp into the project (from the Generated Files folder) just like you did earlier with Thermometer.h and .cpp.

// 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 專案的清單。This listing is for the ThermometerCoreApp project.

// 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 的簡單訊號Simple signals across an ABI

如果您不需要透過您的事件傳遞任何參數或與引數,則可以定義自己的簡單 Windows 執行階段委派類型。If you don't need to pass any parameters or arguments with your event, then you can define your own simple Windows Runtime delegate type. 以下範例顯示 Thermometer 執行階段類別的簡易版本。The example below shows a simpler version of the Thermometer runtime class. 它會宣告名為 SignalDelegate 的委派類型,然後使用該委派類型來引發訊號類型事件,而不是具有參數的事件。It declares a delegate type named SignalDelegate and then it uses that to raise a signal-type event instead of an event with a parameter.

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

參數化委派、簡單訊號,以及專案中的回呼Parameterized delegates, simple signals, and callbacks within a project

如果您需要 Visual Studio 專案內部的事件 (而非跨二進位檔),而這些事件不限於 Windows 執行階段類型,則您仍可使用 winrt::event<Delegate> 類別範本。If you need events that are internal to your Visual Studio project (not across binaries), where those events are not limited to Windows Runtime types, then you can still use the winrt::event<Delegate> class template. 只要使用 winrt::delegate,而不是實際的 Windows 執行階段委派類型,因為 winrt::delegate 也支援非 Windows 執行階段參數。Simply use winrt::delegate instead of an actual Windows Runtime delegate type, since winrt::delegate also supports non Windows Runtime parameters.

下列範例先顯示不採用任何參數的委派簽章 (基本上是簡單訊號),而後顯示採用一個字串的委派簽章。The example below first shows a delegate signature that doesn't take any parameters (essentially a simple signal), and then one that takes a string.

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

請注意,如何才能將如您想要的多個訂閱委派新增至事件。Notice how you can add to the event as many subscribing delegates as you wish. 不過,事件還有一些相關聯的額外負荷。However, there is some overhead associated with an event. 如果您只需要僅含單一訂閱委派的簡單回呼,則可以使用 winrt::delegate<...T> 本身。If all you need is a simple callback with only a single subscribing delegate, then you can use winrt::delegate<... T> on its own.

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 中複寫該模式。If you're porting from a C++/CX codebase where events and delegates are used internally within a project, then winrt::delegate will help you to replicate that pattern in C++/WinRT.

設計指導方針Design guidelines

我們建議您將事件 (不是委派) 當作函式參數傳遞。We recommend that you pass events, and not delegates, as function parameters. winrt::eventadd 函式是一個例外狀況,因為您必須在該情況下傳遞委派。The add function of winrt::event is the one exception, because you must pass a delegate in that case. 此指導方針的原因是因為委派可以在不同的 Windows 執行階段語言中採用不同的形式 (就它們支援一個或多個用戶端註冊而論)。The reason for this guideline is because delegates can take different forms across different Windows Runtime languages (in terms of whether they support one client registration, or multiple). 事件與其多個訂閱者模型,構成一個更容易預測且一致的選項。Events, with their multiple subscriber model, constitute a much more predictable and consistent option.

事件處理常式委派的簽章應該包含兩個參數:sender (IInspectable) 和 args (某些事件引數類型,例如 RoutedEventArgs)。The signature for an event handler delegate should consist of two parameters: sender (IInspectable), and args (some event argument type, for example RoutedEventArgs).

請注意,如果您要設計內部 API,不一定要套用這些指導方針。Note that these guidelines don't necessarily apply if you're designing an internal API. 雖然經過一段時間,內部 API 通常會變成公開的。Although, internal APIs often become public over time.