Author events in C++/WinRT
这篇文档讲述如何创建事件,举的是下面这个例子:
// BankAccountWRC.idl
namespace BankAccountWRC
{
runtimeclass BankAccount
{
BankAccount();
event Windows.Foundation.EventHandler<Single> AccountIsInDebit;
void AdjustBalance(Single value);
};
}
如果你创建的是WinRT组件项目,那么cppwinrt.exe在运行的时候会自动加上-component
选项。
做成的头文件如下:
// BankAccount.h
...
namespace winrt::BankAccountWRC::implementation
{
struct BankAccount : BankAccountT<BankAccount>
{
...
private:
winrt::event<Windows::Foundation::EventHandler<float>> m_accountIsInDebitEvent;
float m_balance{ 0.f };
};
}
...
一个IDL中定义的事件,会对应到做成类中若干个方法:
// BankAccount.cpp
...
namespace winrt::BankAccountWRC::implementation
{
winrt::event_token BankAccount::AccountIsInDebit(Windows::Foundation::EventHandler<float> const& handler)
{
return m_accountIsInDebitEvent.add(handler);
}
void BankAccount::AccountIsInDebit(winrt::event_token const& token) noexcept
{
m_accountIsInDebitEvent.remove(token);
}
void BankAccount::AdjustBalance(float value)
{
m_balance += value;
if (m_balance < 0.f) m_accountIsInDebitEvent(*this, m_balance);
}
}
winrt::event的add和remove方法可以用来添加事件处理器,这个操作是线程安全的。
为BankAccountWRC生成的winmd文件在
\BankAccountWRC\Debug\BankAccountWRC\
路径下。为了录用这个WinRT组件中的Winmd,可以在应用项目中添加引用到\BankAccountWRC\Debug\BankAccountWRC\BankAccountWRC.winmd
,或者直接添加一个项目到项目的引用。
我们来看一下BankAccountWRC是如何在应用项目中被录用的:
struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
BankAccountWRC::BankAccount m_bankAccount;
winrt::event_token m_eventToken;
...
void Initialize(CoreApplicationView const &)
{
m_eventToken = m_bankAccount.AccountIsInDebit([](const auto &, float balance)
{
WINRT_ASSERT(balance < 0.f); // Put a breakpoint here.
});
}
...
void Uninitialize()
{
m_bankAccount.AccountIsInDebit(m_eventToken);
}
...
void OnPointerPressed(IInspectable const &, PointerEventArgs const & args)
{
m_bankAccount.AdjustBalance(-1.f);
...
}
...
};
WINRT_ASSERT是一个宏,会扩展成_ASSERTE。
像上面的那个例子中列举的那样,如果你的事件是要能够跨越ABI被访问的(在组件和其消费者之间),那么事件的类型就必须是一个WinRT Delegate。上面例子中使用的是Windows::Foundation::EventHandler<T>
,另外一个例子是TypedEventHandler<TSender, TResult>
。像T、TSender、TResult这些也必须是WinRT类型(runtimeclass或者原始类型)。
如果你的事件不需要传递任何参数,那么你可以在idl中自定义简单的idl:
// BankAccountWRC.idl
namespace BankAccountWRC
{
delegate void SignalDelegate();
runtimeclass BankAccount
{
BankAccount();
event BankAccountWRC.SignalDelegate SignalAccountIsInDebit;
void AdjustBalance(Single value);
};
}
如果你只是想在项目内部使用事件,而不用跨ABI。那么你照样可以使用winrt::event
来定义事件,同时你可以使用winrt::delegate
来定义事件处理器。winrt::delegate
的参数不需要是WinRT类型:
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!");
winrt::event
可以注册多个delegate,如果你确定你的事件只有一个delegate,那么可以直接使用winrt::delegate
:
winrt::delegate<std::wstring> logCallback;
logCallback = [](std::wstring const& message) { std::wcout << message.c_str() << std::endl; }f;
logCallback(L"Hello, World!");
最后一点小指示:能传event就不要直接传delegate,event在不同的语言映射中比较统一。事件处理器一般有两个参数:sender(IInspectable)和args(比如RoutedEventArgs)。
winrt::event struct template (C++/WinRT)
winrt::event保存着一个delegate队列,当事件发生时,按顺序逐个调用这些delegate。它是一个模板,event::delegate_type用来指示delegate类型。add()方法用来往队列添加delegate;remove方法从队列删除delegate;operator()可以用来调用队列上保存的所有delegate; operator bool可以用来判断队列上是否有delegate。
winrt::event_token add(Delegate const& delegate);
void remove(winrt::event_token const token);
winrt::delegate struct template (C++/WinRT)
winrt::delegate是一个模板化的IUnknown,可以支持不同的类型。
template <typename... T>
struct delegate : Windows::Foundation::IUnknown
EventHandler Delegate
Windows::Foundation::EventHandler<T>
是一个泛类型参数的WinRT类型,用来表示接受一个参数的Delegate。
TypedEventHandler<TSender,TResult> Delegate
Windows::Foundation::TypedEventHandler<TSender,TResult>
是带两个泛类型参数的的WinRT类型,是WinRT中比较通用的delegate类型。TSender一般是IInspectable类型,可以为任意WinRT物件;TResult则可以是为某个事件特定的事件参数类型。
Handle events by using delegates in C++/WinRT
首先看一下如何处理XAML按键事件:
<Button x:Name="Button" Click="ClickHandler">Click Me</Button>
// MainPage.cpp
void MainPage::ClickHandler(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
{
Button().Content(box_value(L"Clicked"));
}
上面的MainPage::ClickHandler的类型跟Windows.Foundation.TypedEventHandle<TSender, TResult>
很像。通常来说,TSender会是IInspectable类型,而TResult会是具体的事件相关的类型。比如,对于 KeyEventHandler,它的声明如下:
struct KeyEventHandler : winrt::Windows::Foundation::IUnknown
{
KeyEventHandler(std::nullptr_t = nullptr) noexcept;
template <typename L> KeyEventHandler(L lambda);
template <typename F> KeyEventHandler(F* function);
template <typename O, typename M> KeyEventHandler(O* object, M method);
void operator()(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;
};
可以想象,它的TResult是winrt::Windows::UI::Xaml::Input::KeyRoutedEventArgs 类型。
对于不需要参数的事件,他们的Delegate可能是EventHandler类型。相应的例子是 Popup.Closed Event。
对于简单的时间处理器,你可以直接使用lambda函数:
MainPage::MainPage()
{
InitializeComponent();
Button().Click([this](IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
{
Button().Content(box_value(L"Clicked"));
});
}
lambda函数的签名必须跟事件处理器的签名一致才行。
在事件上注册了一个事件处理器之后,会返回一个winrt::event_token。把同样的token交换给事件,就可以取消注册某个事件处理器:
winrt::event_token token = m_button.Click([this](IInspectable const&, RoutedEventArgs const&)
{
// ...
});
m_button.Click(token);
C++/WinRT还提供auto_revoke机制。注册事件处理器的时候可以返回一个revoker,当这个revoker析构时,自动取消事件处理器的注册:
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;
};
使用auto_revoke 需要WinRT类型支持弱引用,如果不支持弱引用(比如 Windows.UI.Composition命名空间中的类型),那么就会出现winrt::hresult_no_interface异常。
WinRT中的delegate操作非常多,比如对于异步操作IAsyncOperationWithProgress,其对应的delegate是AsyncOperationProgressHandler,文中举了一个例子说明如何用lambda实现那个delegate。有些delegate有返回值,比如ListViewItemToKeyHandler。
Move to C++/WinRT from C#\
用于XAML事件处理器的方法,在C#里面是可以private的,但是在C++/WinRT里面必须是public,或者可以声明为其父类的友元:
namespace winrt::MyProject::implementation
{
struct MyPage : MyPageT<MyPage>
{
private:
friend MyPageT;
void OpenButton_Click(
winrt::Windows:Foundation::IInspectable const& sender,
winrt::Windows::UI::Xaml::RoutedEventArgs const& args);
}
};
如果要支持XAML的{Binding}
,请查看Binding object declared using {Binding}
从C++ WinRT 2.0.190530.8开始, winrt::single_threaded_observable_vector
创建的vector同时支持 IObservableVector<T>
和IObservableVector<IInspectable>
.
winrt::to_hstring
可以把一些类型转为hstring,如果需要转化enum,可以重载to_hstring:
namespace winrt
{
hstring to_hstring(StatusEnum status)
{
switch (status)
{
case StatusEnum::Success: return L"Success";
case StatusEnum::AccessDenied: return L"AccessDenied";
case StatusEnum::DisabledByPolicy: return L"DisabledByPolicy";
default: return to_hstring(static_cast<int>(status));
}
}
}
这样这个enum就可以用在XAML中:
<TextBlock>
Most recent status is <Run Text="{x:Bind LatestOperation.Status}"/>.
</TextBlock>
在unbox的时候,如果指针为空,那么C#会抛出异常,但是C++/WinRT会直接crash。对应的办法是在C++/WinRT中使用winrt::unbox_value_or来在指针为空的时候返回一个默认值。
像hstring这种不是从IInspectable派生出来的类型,在boxing的时候会被转化为IReference<T>
,表示其可以nullify。注意,如果hstring本身为Null,则表示其值存在且为空,这和IReference<hstring>
为空是有区别的,后者表示其值不存在。 C#中string默认为引用类型,而C++/WinRT中hstring默认为值类型,这两者在处理空指针方面有很大不同。文中有详述。
在C++/WinRT,需要使用unsealed关键字来把一个runtimeclass标注为可以被继承。C#中则不需要。另外C++/WinRT需要处理很多头文件相关的问题,C#也不需要,C#还有partial class,方便把一个类分成不同的部分,放在不同的地方实现。
另外,C++/WinRT的对象要在XAML中使用(比如用在{x:bind}
中),需要在IDL中声明。另外,boolean绑定在C#中映射为true或者false,但是在C++/WinRT中则为Windows.Foundation.IReference<Boolean>
。
(本篇完)