在 C++/WinRT 中使用委托处理事件

本主题演示如何使用 C++/WinRT注册和撤销事件处理委托。 可以使用任何标准C++类似函数的对象来处理事件。

注释

有关安装和使用 C++/WinRT Visual Studio 扩展 (VSIX) 和 NuGet 包(它们共同提供项目模板和生成支持)的信息,请参阅 Visual Studio 对 C++/WinRT的支持。

使用 Visual Studio 添加事件处理程序

向项目添加事件处理程序的一种便捷方法是在 Visual Studio 中使用 XAML 设计器用户界面(UI)。 在 XAML 设计器中打开 XAML 页面后,选择要处理其事件的控件。 在该控件的属性页中,单击闪电图标可列出该控件源的所有事件。 然后,双击您想要处理的事件,例如事件 OnClicked

XAML 设计器将相应的事件处理程序函数原型(和存根实现)添加到源文件中,可供你替换为自己的实现。

注释

通常,事件处理程序不需要在 Midl 文件中描述(.idl)。 因此,XAML 设计器不会将事件处理程序函数原型添加到 Midl 文件。 它只添加你的 .h.cpp 文件。

注册委托以处理事件

一个简单的示例是处理按钮的单击事件。 通常,使用 XAML 标记注册成员函数来处理事件,如下所示。

// MainPage.xaml
<Button x:Name="myButton" Click="ClickHandler">Click Me</Button>
// MainPage.h
void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Windows::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */)
{
    myButton().Content(box_value(L"Clicked"));
}

上面的代码取自 Visual Studio 中的 空白应用(C++/WinRT) 项目。 代码 myButton() 调用生成的访问器函数,该函数返回 名为 myButtonButton。 如果您更改了 x:Name 元素的 ,那么生成的访问器函数名称也会随之变化。

注释

在这种情况下,事件源(引发事件的对象)是名为 myButton的按钮 。 事件接收方(处理事件的对象)是 MainPage实例。 本主题后面提供了有关管理事件源和事件收件人生存期的详细信息。

不必在标记中以声明方式执行此操作,而是必须注册成员函数来处理事件。 下面的代码示例可能并不明显,但 ButtonBase::Click 调用的参数是 RoutedEventHandler 委托的实例。 在本例中,我们使用 RoutedEventHandler 构造函数重载,其中包括一个对象和一个成员函数指针。

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click({ this, &MainPage::ClickHandler });
}

重要

注册委托时,上面的代码示例将传递一个原始的 指针(指向当前对象)。 若要了解如何建立对当前对象的强或弱引用,请参阅 如果使用成员函数作为委托

下面是使用静态成员函数的示例;请注意更简单的语法。

// MainPage.h
static void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Windows::UI::Xaml::RoutedEventArgs const& args);

// MainPage.cpp
MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click( MainPage::ClickHandler );
}
void MainPage::ClickHandler(
    IInspectable const& /* sender */,
    RoutedEventArgs const& /* args */) { ... }

还有其他方法可以构造 RoutedEventHandler。 下面是从 RoutedEventHandler 的文档主题中获取的语法块(从网页右上角的 语言 下拉列表中选择 C++/WinRT)。 请注意各种构造函数:一个构造函数采用 lambda 表达式,另一个采用自由函数,还有一个(我们上面使用的那个)采用一个对象和一个指向成员函数的指针。

struct RoutedEventHandler : winrt::Windows::Foundation::IUnknown
{
    RoutedEventHandler(std::nullptr_t = nullptr) noexcept;
    template <typename L> RoutedEventHandler(L lambda);
    template <typename F> RoutedEventHandler(F* function);
    template <typename O, typename M> RoutedEventHandler(O* object, M method);
    /* ... other constructors ... */
    void operator()(winrt::Windows::Foundation::IInspectable const& sender,
        winrt::Windows::UI::Xaml::RoutedEventArgs const& e) const;
};

函数调用运算符的语法值得查看。 它会告诉你委托所需的参数是什么。 正如你所看到的,在这种情况下,函数调用运算符语法与 MainPage::ClickHandler的参数匹配。

注释

对于任何给定的事件,要弄清楚其委托的详细信息以及该委托的参数,请首先查看关于该事件的文档主题。 让我们以 UIElement.KeyDown 事件为例,。 访问该主题,并从 语言 下拉列表中选择 C++/WinRT。 在主题开头的语法块中,你将看到这一点。

// Register
event_token KeyDown(KeyEventHandler const& handler) const;

该信息告诉我们,UIElement.KeyDown 事件(这是我们正在讨论的话题)具有委托类型 KeyEventHandler,因为这是在向此事件类型注册委托时要传递的类型。 现在,点击该主题的链接,进入 KeyEventHandler 委托 类型。 此处,语法块包含函数调用运算符。 正如上面提到的,这告诉你委托需要什么参数。

void operator()(
  winrt::Windows::Foundation::IInspectable const& sender,
  winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

如你所看到的,委托需要声明为将 IInspectable 作为发送方,并将 KeyRoutedEventArgs 类的实例 作为参数。

若要以另一个示例为例,让我们看看 Popup.Closed 事件。 其委托类型是 EventHandler<IInspectable>。 因此,你的委托将会接收一个 IInspectable 作为发送者,并且接收另一个 IInspectable 作为参数(因为这是 EventHandler的类型参数)。

如果在事件处理程序中没有执行太多工作,则可以使用 lambda 函数而不是成员函数。 同样,在下面的代码示例中可能并不明显,但一个RoutedEventHandler 委托正从一个lambda函数构造,这个lambda函数的语法需要与我们之前讨论的函数调用运算符的语法相匹配。

MainPage::MainPage()
{
    InitializeComponent();

    myButton().Click([this](IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        myButton().Content(box_value(L"Clicked"));
    });
}

构造你的委托时,可以选择更明确一点。 例如,如果你想传递它,或者多次使用它。

MainPage::MainPage()
{
    InitializeComponent();

    auto click_handler = [](IInspectable const& sender, RoutedEventArgs const& /* args */)
    {
        sender.as<winrt::Windows::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

撤销已注册的委托代表

注册委托时,通常会收到一个令牌。 随后可以使用该令牌撤销委托,这意味着该委托将从事件中注销,从而在事件再次触发时不会被调用。

为简单起见,上述代码示例都未演示如何执行此操作。 但下一个代码示例将令牌存储在结构体的私有数据成员中,并在析构函数中取消其处理程序。

struct Example : ExampleT<Example>
{
    Example(winrt::Windows::UI::Xaml::Controls::Button const& button) : m_button(button)
    {
        m_token = m_button.Click([this](IInspectable const&, RoutedEventArgs const&)
        {
            // ...
        });
    }
    ~Example()
    {
        m_button.Click(m_token);
    }

private:
    winrt::Windows::UI::Xaml::Controls::Button m_button;
    winrt::event_token m_token;
};

与其使用强引用(如上例中所示),还可以存储对按钮的弱引用(请参阅 C++/WinRT 中的强引用和弱引用)。

注释

当事件源同步引发其事件时,可以撤销事件处理程序,您可以确信不会再收到任何事件。 但是,对于异步事件,即使在撤消(尤其是在析构函数中撤消时),正在进行的事件仍然可能在对象开始析构后到达你的对象。 在销毁前查找取消订阅的位置可能会缓解此问题,或者对于可靠的解决方案,请参阅 使用事件处理委托安全地访问此 指针

或者,注册委托时,可以指定 winrt::auto_revoke(这是一个类型为 的 winrt::auto_revoke_t的值)来请求一个事件吊销程序(类型为 的 winrt::event_revoker)。 事件撤销器为您保留对事件源(引发事件的对象)的弱引用。 可以通过调用 event_revoker::revoke 成员函数来手动撤销;但是,当函数超出范围时,事件吊销程序会自动调用该函数本身。 撤回 函数会检查事件源是否仍然存在,如果是这样,则撤回委托。 在此示例中,无需存储事件源,也不需要析构函数。

struct Example : ExampleT<Example>
{
    Example(winrt::Windows::UI::Xaml::Controls::Button button)
    {
        m_event_revoker = button.Click(
            winrt::auto_revoke,
            [this](IInspectable const& /* sender */,
            RoutedEventArgs const& /* args */)
        {
            // ...
        });
    }

private:
    winrt::Windows::UI::Xaml::Controls::Button::Click_revoker m_event_revoker;
};

下面是从文档主题中提取的 ButtonBase::Click 事件的语法块。 它显示了三个不同的注册和撤消函数。 可以确切看到从第三个重载中需要声明的事件撤销器类型。 你可以将相同类型的委托传递给 寄存器,或者使用 event_revoker 的重载来撤消到

// Register
winrt::event_token Click(winrt::Windows::UI::Xaml::RoutedEventHandler const& handler) const;

// Revoke with event_token
void Click(winrt::event_token const& token) const;

// Revoke with event_revoker
Button::Click_revoker Click(winrt::auto_revoke_t,
    winrt::Windows::UI::Xaml::RoutedEventHandler const& handler) const;

注释

在上面的代码示例中,Button::Click_revokerwinrt::event_revoker<winrt::Windows::UI::Xaml::Controls::Primitives::IButtonBase>的类型别名。 类似的模式适用于所有C++/WinRT 事件。 每个 Windows 运行时事件都有一个用于返回事件吊销器的吊销函数重载,并且该吊销器的类型是事件源的成员。 因此,若要采用另一个示例,CoreWindow::SizeChanged 事件具有一个注册函数重载,它返回 CoreWindow::SizeChanged_revoker类型的值。

可以考虑在页面导航场景中撤回处理程序。 如果反复导航到页面,然后返回,则可以在离开页面时撤销任何处理程序。 或者,如果重新使用同一页实例,请先检查令牌的值,仅在尚未设置时才进行注册(if (!m_token){ ... })。 第三个选项是将事件撤销程序作为数据成员存储在页面中。 稍后在本主题中描述的第四个选项是,在您的 lambda 函数中捕获对此 对象的 强引用或弱引用。

如果自动撤销代理人无法注册

如果在注册委托时尝试指定 winrt::auto_revoke,并且结果是 winrt::hresult_no_interface 异常,则这通常意味着事件源不支持弱引用。 例如,在 Windows.UI.Composition 命名空间中,这种情况很常见。 在这种情况下,无法使用自动撤销功能。 您必须退而手动注销您的事件处理程序。

异步操作和操作的委托类型

上面的示例使用 RoutedEventHandler 委托类型,但当然还有其他许多委托类型。 例如,异步操作和动作(有进度和无进度)会触发已完成和/或进度事件,这些事件需要相应类型的委托。 例如,带进度的异步操作(实现 IAsyncOperationWithProgress)需要一个类型为 AsyncOperationProgressHandler的委托。 下面是一个使用 lambda 函数定义该类型委托的代码示例。 该示例还演示如何编写 AsyncOperationWithProgressCompletedHandler 委托。

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    auto async_op_with_progress = syndicationClient.RetrieveFeedAsync(rssFeedUri);

    async_op_with_progress.Progress(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& /* sender */,
            RetrievalProgress const& args)
        {
            uint32_t bytes_retrieved = args.BytesRetrieved;
            // use bytes_retrieved;
        });

    async_op_with_progress.Completed(
        [](
            IAsyncOperationWithProgress<SyndicationFeed,
            RetrievalProgress> const& sender,
            AsyncStatus const /* asyncStatus */)
        {
            SyndicationFeed syndicationFeed = sender.GetResults();
            // use syndicationFeed;
        });

    // or (but this function must then be a coroutine, and return IAsyncAction)
    // SyndicationFeed syndicationFeed{ co_await async_op_with_progress };
}

正如上述“协同例程”注释所建议的,而不是将委托与异步操作和操作的已完成事件一起使用,你可能会发现使用协同例程更自然。 有关详细信息和代码示例,请参阅 使用 C++/WinRT 进行并发和异步操作

注释

对于异步操作或任务,实现多个 完成处理器 是不正确的。 可以为其已完成事件创建单个委托,或者您也可以 co_await。 如果你有这两个,则第二个将失败。

如果坚持委托而不是协同例程,则可以选择更简单的语法。

async_op_with_progress.Completed(
    [](auto&& /*sender*/, AsyncStatus const /* args */)
{
    // ...
});

返回某个值的委托类型

某些委托类型本身必须返回一个值。 例如,ListViewItemToKeyHandler返回字符串。 下面是编写该类型委托的示例(请注意,lambda 函数返回一个值)。

using namespace winrt::Windows::UI::Xaml::Controls;

winrt::hstring f(ListView listview)
{
    return ListViewPersistenceHelper::GetRelativeScrollPosition(listview, [](IInspectable const& item)
    {
        return L"key for item goes here";
    });
}

使用事件处理委托安全地访问 的此 指针

如果使用对象的成员函数处理事件,或者从对象成员函数内的 lambda 函数内处理事件,则需要考虑事件接收者的相对生存期(处理事件的对象)和事件源(引发该事件的对象)。 有关详细信息和代码示例,请参阅 C++/WinRT中的 强引用和弱引用。

重要 API