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() 调用生成的访问器函数,该函数返回我们命名为 myButton 的 Button。 如果更改 x:Name 该 Button 元素,则生成的访问器函数的名称也会更改。
注释
在这种情况下,事件源(引发事件的对象)是名为 myButton 的 Button。 事件接收方(处理事件的对象)是 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 类的一个实例作为 args 参数。
若要以另一个示例为例,让我们看看 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::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 中的强引用和弱引用)。
注释
当事件源同步引发事件时,你可以注销你的事件处理程序,并且可以确信不会再收到任何事件。 但是,对于异步事件,即使在撤销之后(尤其是在析构函数中撤销时),已在传递途中的事件也可能在对象开始析构后到达该对象。 在销毁之前查找取消订阅的位置可能会缓解此问题,或者对于可靠的解决方案,请参阅 使用事件处理委托安全地访问 此 指针。
或者,在注册委托时,可以指定 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 事件文档主题的语法块。 它显示了三个不同的注册和撤消函数。 从第三个重载中,你可以明确看出需要声明哪种类型的事件撤销器(revoker)。 你也可以将同类委托传递给 register 和 带有 event_revoker 的 revoke 这两种重载。
// 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 运行时 事件都有一个返回 event revoker 的 revoke 函数重载,而该 event revoker 的类型是事件源的成员类型。 因此,若要采用另一个示例, 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 的并发和异步操作。
注释
为异步操作或任务实现多个 完成处理程序 是不正确的。 可以为其 completed 事件指定单个委托,也可以 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";
});
}
使用事件处理委托安全地访问 此 指针
如果使用对象的成员函数处理事件,或者从对象成员函数内的 lambda 函数内处理事件,则需要考虑事件接收者的相对生存期(处理事件的对象)和事件源(引发该事件的对象)。 有关详细信息和代码示例,请参阅 C++/WinRT 中的强引用和弱引用。