這很重要
使用 Windows 應用程式 SDK 建置? 本文程式碼使用 UWP (Windows.UI.Xaml) 命名空間。 如果您的專案目標為 WinUI 3(Windows 應用程式 SDK),請全文將 Microsoft.UI.Xaml(以及相關的 Microsoft.UI.* 命名空間)一律替換。 完整地圖與 UI 遷移指南請參閱「將 UWP API 映射到 Windows 應用程式 SDK」以獲得更多細節。
本主題說明如何使用 C++/WinRT 註冊與撤銷事件處理代理。 你可以用任何標準的 C++ 函式物件來處理事件。
Note
關於安裝及使用 C++/WinRT Visual Studio 擴充套件(VSIX)及 NuGet 套件(共同提供專案範本與建置支援)的資訊,請參閱 Visual Studio 對 C++/WinRT 的支援。
使用 Visual Studio 來新增事件處理器
在專案中加入事件處理器的一個方便方式是使用 Visual Studio 中的 XAML 設計師使用者介面(UI)。 在 XAML 設計器中開啟你的 XAML 頁面,選擇你想處理的事件控制項。 在該控制項的屬性頁面,點擊閃電圖示,列出所有由該控制項來源的事件。 接著,雙擊你想處理的事件;例如, OnClicked。
XAML Designer 會將適當的事件處理函式原型(以及存根實作)加入你的原始檔案,讓你隨時用自己的實作替換。
Note
通常,您的事件處理常式通常不需要在 MIDL 檔案中加以描述(.idl)。 所以,XAML Designer 不會把事件處理程式的原型加入你的 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 中的 Blank App, Packaged(桌面版為 WinUI 3)專案。 程式碼 myButton() 會呼叫產生的存取子函式,該函式會傳回我們命名為 myButton 的 Button。 如果你變更那個 Button 元素的 x:Name,則產生的存取子函式名稱也會跟著改變。
Note
在這種情況下,事件來源(引發事件的物件)就是名為 myButton 的 Button。 而事件接收者(處理事件的物件)是 MainPage 的一個實例。 本主題後面會有更多關於如何管理事件來源與事件接收者的生命週期的資訊。
你不必在標記中以宣告式方式處理,而是可以用命令式方式註冊成員函式來處理事件。 從下面的程式碼範例可能不明顯,但 ButtonBase::Click 呼叫的參數是 RoutedEventHandler 代理的實例。 在此案例中,我們使用 RoutedEventHandler 建構子過載,該建構器會接收一個物件和一個指標到成員函式。
// MainPage.cpp
MainPage::MainPage()
{
InitializeComponent();
myButton().Click({ this, &MainPage::ClickHandler });
}
這很重要
當註冊代理時,上述程式碼範例會傳遞一個原始 的 this 指標(指向當前物件)。 若要了解如何建立目前物件的強參考或弱參考,請參閱 如果您使用成員函式作為委派。
這裡有一個使用靜態成員函數的範例;請注意語法較為簡單。
// 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 的參數相符。
Note
對於任何指定的事件,若要了解其委派的詳細資料以及該委派的參數,請先參閱該事件本身的說明文件主題。 以 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 的型別參數)作為 args。
如果你在事件處理程序中工作量不大,可以使用 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);
}
撤銷註冊代表
當你註冊代理人時,通常會退還一個代幣給你。 你可以用該代幣來撤銷你的委託;這表示該代表未登記於該活動,若活動再次提出,將不會被召喚。
為了簡化起見,上述程式碼範例中沒有示範如何做到這點。 但接下來的程式碼範例是將憑證存放在結構體的私有資料成員中,並在 destructor 中撤銷其處理程序。
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 中的強參考與弱參考)。
Note
當事件來源以同步方式觸發事件時,你可以移除你的事件處理常式,並確信之後不會再收到任何事件。 但對於非同步事件,即使已解除註冊(尤其是在解構函式內解除註冊時),仍在傳遞中的事件,仍可能在物件開始解構後才抵達該物件。 在銷毀前找到一個取消訂閱的地方可能會減輕這個問題,或者想要更穩健的解決方案,請參考 「用事件處理代理器安全地存取 此 指標」。
或者,當你註冊委派時,可以指定 winrt::auto_revoke(其值的型別為 winrt::auto_revoke_t),以要求取得事件撤銷器(型別為 winrt::event_revoker)。 事件撤銷程式會替你保留事件來源(亦即引發事件的物件)的弱式參考。 你可以透過呼叫 event_revoker::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 事件文件主題的語法區塊。 它顯示了三種不同的註冊與撤銷功能。 你可以清楚看到第三次超載時需要宣告哪種類型的事件撤銷器。 而且你也可以將相同類型的委派同樣傳遞給 register 和 revoke with 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;
Note
在上述程式碼範例中, Button::Click_revoker 是 的 winrt::event_revoker<winrt::Microsoft::UI::Xaml::Controls::Primitives::IButtonBase>型別名。 類似的模式適用於所有 C++/WinRT 事件。 每個 Windows 執行階段 事件都有一個 revoke 函式過載,會回傳一個事件 revoker,而該 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 的並行與非同步操作。
Note
對於非同步動作或操作,實作多個 completion handler 是不正確的做法。 你可以選擇一個代表來代表完成的活動,也可以選擇 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 中的強參考與弱參考。