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

Important

使用Windows 应用 SDK进行构建? 本文的代码使用 UWP (Windows.UI.Xaml) 命名空间。 如果项目面向 WinUI 3(Windows 应用 SDK),请在整个过程中替换Microsoft.UI.Xaml(和相关Microsoft.UI.*命名空间)。 有关更多详细信息,请参阅将 UWP API 映射到Windows 应用 SDK,了解完整的映射和 UI 迁移指南

本主题演示如何使用 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::Microsoft::UI::Xaml::RoutedEventArgs const& args);

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

上面的代码取自 Visual Studio 中的 空白应用(已打包)(桌面版 WinUI 3) 项目。 代码 myButton() 调用生成的访问器函数,该函数返回我们命名为 myButtonButton。 如果更改 x:NameButton 元素,则生成的访问器函数的名称也会更改。

注释

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

与其在标记中以声明方式进行,不如以命令式方式注册一个成员函数来处理事件。 下面的代码示例可能并不明显,但 ButtonBase::Click 调用的参数是 RoutedEventHandler 委托的实例。 在本例中,我们使用了采用对象和指向成员函数的指针的 RoutedEventHandler 构造函数重载

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

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

Important

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

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

// MainPage.h
static void ClickHandler(
    winrt::Windows::Foundation::IInspectable const& sender,
    winrt::Microsoft::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::Microsoft::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::Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

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

若要以另一个示例为例,让我们看看 Popup.Closed 事件。 其委托类型为 EventHandler<IInspectable>。 因此,委托会将一个 IInspectable 作为发送方,另一个 IInspectable(因为它是 EventHandler 的类型形参)作为实参

如果在事件处理程序中没有执行太多工作,则可以使用 lambda 函数而不是成员函数。 重复一下,从下面的代码示例来看可能不明显,但一个 RoutedEventHandler 委托正在从 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::Microsoft::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

撤销已注册的代理人

当你注册委托时,通常会向你返回一个令牌。 随后,可以使用该令牌撤销委托;这意味着将从事件取消注册委托,再次引发该事件时不会调用该委托。

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

struct Example : ExampleT<Example>
{
    Example(winrt::Microsoft::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::Microsoft::UI::Xaml::Controls::Button m_button;
    winrt::event_token m_token;
};

与上面的示例中使用强引用不同,你可以存储对该按钮的弱引用(请参阅 C++/WinRT 中的强引用和弱引用)。

注释

当事件源以同步方式引发其事件时,你就可以放心地撤销处理程序:不会收到更多事件了。 但对于异步事件,即使在撤销(尤其是在析构函数中撤销)后,你的对象在开始析构后仍可能收到正在进行的事件。 在析构之前找到取消订阅的地方也许可以缓解此问题;若要查找稳妥的解决方案,请参阅使用事件处理委托安全访问 this 指针

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

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

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

下面是摘自 ButtonBase::Click 事件文档主题的语法块。 它显示了三个不同的注册和撤消函数。 可以清楚地看到从第三个重载进行声明时需要哪种类型的事件撤销程序。 你可以将相同类型的委托传递给“注册”和“使用 event_revoker 撤销”重载

// Register
winrt::event_token Click(winrt::Microsoft::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::Microsoft::UI::Xaml::RoutedEventHandler const& handler) const;

注释

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

在页面-导航方案中,可以考虑撤销处理程序。 如果你反复导航到某个页面,然后再退出,那么可以在导航离开该页面时移除任何处理程序。 或者,如果你正在复用同一个页面实例,请检查令牌值,并且仅在该值尚未设置时才进行注册(if (!m_token){ ... })。 第三个选项是将事件撤销程序作为数据成员存储在页面中。 还有第四个选项,如本主题后文所述,即在 lambda 函数中捕获对 this 对象的强引用或弱引用。

如果“自动撤销”委托无法注册

如果在注册委托时尝试指定 winrt::auto_revoke,并且结果是 winrt::hresult_no_interface 异常,那么这通常意味着,事件源不支持弱引用。 例如,这在 Microsoft.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::Microsoft::UI::Xaml::Controls;

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

使用事件处理委托安全访问 this 指针

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

重要 API