Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

19 Dec 2019

C++/WinRT学习笔记(七):事件的生成和处理、以及对比C#

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则可以是为某个事件特定的事件参数类型。

https://docs.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/handle-events#revoke-a-registered-delegate

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>

(本篇完)

comments powered by Disqus