在 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.

创建 ThermometerWRC 和 ThermometerCoreApp Create 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. 此示例说明了如何使用单精度浮点数的参数声明委托类型为 EventHandler 的事件。This 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 存根文件的更新版本。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. 如果你忘记了此约束,编译器将帮助你处理“必须为 WinRT 类型”错误。The compiler helps you with a "must be WinRT type" error if you forget that constraint.

下面是代码列表形式的示例。Below is an example in the form of code listings. 从在本主题前面部分创建的 ThermometerWRC 和 ThermometerCoreApp 项目开始,然后编辑这些项目中的代码,使它们看起来与这些列表中的代码相似 。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::event 的 add 函数是例外,因为在这种情况下必须传递委托 。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.

事件处理程序委托的签名应该由以下两个形参组成:发送方 (IInspectable) 和实参(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.