C++/WinRT でのデリゲートを使用したイベントの処理

このトピックでは、C++/WinRT を使用したイベント処理デリゲートの登録方法と取り消し方法について説明します。 標準的な C++ 関数のようなオブジェクトを使用してイベントを処理できます。

注意

C++/WinRT Visual Studio Extension (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::Windows::UI::Xaml::RoutedEventArgs const& args);

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

上記のコードは、Visual Studio の 空のアプリ (C++/WinRT) プロジェクトから取られたものです。 コード myButton() により、生成されたアクセサー関数が呼び出されます。これにより、myButton という名前が付けられた Button が返されます。 その Button 要素の x:Name を変更すると、生成されるアクセサー関数の名前も変更されます。

注意

この場合、イベント ソース (イベントを発生させるオブジェクト) は、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::Windows::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 のドキュメント内にある構文ブロックを示します (Web ページの右上の [言語] ドロップダウンから [C++/WinRT] を選択します)。 ラムダを受け取るもの、自由関数を受け取るもの、オブジェクトとメンバー関数へのポインターを受け取るもの (上で使ったもの) など、さまざまなコンストラクターがあることに注意してください。

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::Windows::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::Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e) const;

ご覧のとおり、送信者として IInspectable を、引数として KeyRoutedEventArgs クラスのインスタンスを受け取るようにデリゲートを宣言する必要があります。

別の例を見るために、Popup.Closed イベントを見てみましょう。 そのデリゲート型は EventHandler<IInspectable> です。 そのため、デリゲートは送信者として IInspectable を、引数として IInspectable を受け取ります (これは EventHandler の型パラメーターであるため)。

イベント ハンドラーで行う処理が多くない場合は、メンバー関数の代わりにラムダ関数を使用できます。 以下のコード例からはわかりにくいかもしれませんが、RoutedEventHandler デリゲートはラムダ関数から作成されているため、前述の関数呼び出し演算子の構文ともう一度一致させる必要があります。

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::Windows::UI::Xaml::Controls::Button>().Content(box_value(L"Clicked"));
    };
    myButton().Click(click_handler);
    AnotherButton().Click(click_handler);
}

登録済みのデリゲートを取り消す

デリゲートを登録すると、通常、トークンが返されます。 それ以降、そのトークンを使ってデリゲートを取り消すことができます。つまり、デリゲートはイベントから登録解除され、イベントが再び発生しても呼び出されることはありません。

簡単にするため、上のコード例ではそれを行う方法は示されていません。 一方、次のコード例では、トークンを構造体のプライベート データ メンバーに格納し、デストラクターでそのハンドラーを取り消しています。

struct Example : ExampleT<Example>
{
    Example(winrt::Windows::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::Windows::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::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;
};

次の構文ブロックは、ButtonBase::Click イベントに関するドキュメント トピックからの抜粋です。 登録と取り消しに関する 3 つの異なる関数が示されています。 3 番目のオーバーロードを見ると、宣言する必要があるイベント リボーカーの型が正確にわかります。 そして、同じ種類のデリゲートを、registerrevoke with event_revoker の両方のオーバーロードに渡すことができます。

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

注意

上記のコード例では、Button::Click_revokerwinrt::event_revoker<winrt::Windows::UI::Xaml::Controls::Primitives::IButtonBase> の型エイリアスです。 同じようなパターンがすべての C++/WinRT イベントに適用されます。 各 Windows ランタイム イベントには、イベント リボーカーを返す失効関数オーバーロードがあり、そのリボーカーの型はイベント ソースのメンバーです。 そのため、別の例を挙げると、CoreWindow:: SizeChanged イベントには、型 CoreWindow::SizeChanged_revoker の値を返す登録関数オーバーロードがあります。

ページ ナビゲーションのシナリオでハンドラーの取り消しを検討することがあります。 あるページに入って出ることを繰り返す場合、そのページから移動する際にハンドラーを取り消すことができます。 または、同じページ インスタンスを再使用している場合は、トークンの値を確認し、まだ設定されていない場合 (if (!m_token){ ... }) にのみ登録します。 3 番目のオプションとしては、イベント リボーカーをデータ メンバーとしてページに格納します。 このトピックで後ほど説明する 4 番目のオプションでは、ラムダ関数内で this オブジェクトの強参照または弱参照をキャプチャします。

自動取り消しのデリゲートの登録が失敗する場合

デリゲートを登録するときに winrt::auto_revoke を指定しようとして、結果が winrt::hresult_no_interface 例外である場合、通常それはイベント ソースで弱い参照がサポートされていないことを意味します。 たとえば、Windows.UI.Composition 名前空間ではよくあることです。 このような場合は、自動取り消し機能を使用できません。 イベント ハンドラーの手動取り消しにフォールバックする必要があります。

非同期アクションと非同期操作のデリゲート型

上記の例では、RoutedEventHandler デリゲート型を使用していますが、他にも多くのデリゲート型があります。 たとえば、非同期アクションと非同期操作 (進行状況ありとなし) には、対応する型のデリゲートを必要とする完了イベントと進行状況イベントがあります。 たとえば、進行状況ありの非同期操作の進行状況イベント (IAsyncOperationWithProgress を実装するもの) には、AsyncOperationProgressHandler 型のデリゲートが必要です。 次に、ラムダ関数を使用してこの型のデリゲートを作成するコード例を示します。 この例では、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 を使用した同時実行操作と非同期操作」を参照してください。

注意

非同期アクションまたは操作に、複数の完了ハンドラーを実装するのは誤りです。 完了したイベントに対して 1 つのデリゲートを持つか、それを co_await することができます。 両方ある場合、2 つ目は失敗します。

コルーチンではなくデリゲートにこだわる場合は、もっと簡単な構文を選択できます。

async_op_with_progress.Completed(
    [](auto&& /*sender*/, AsyncStatus const /* args */)
{
    // ...
});

値を返すデリゲート型

一部のデリゲート型はそれ自体で値を返す必要があります。 たとえば、ListViewItemToKeyHandler は文字列を返します。 次に示すのは、この種のデリゲートを作成する例です (ラムダ関数が値を返すことに注意してください)。

using namespace winrt::Windows::UI::Xaml::Controls;

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

イベント処理デリゲートで this ポインターに安全にアクセスする

オブジェクトのメンバー関数でイベントを処理する場合、あるいはオブジェクトのメンバー関数内にあるラムダ関数内からイベントを処理する場合、イベント受信側 (イベントを処理するオブジェクト) とイベント ソース (イベントを発生させるオブジェクト) の相対的な有効期間を考慮する必要があります。 詳細とコード例については、「C++/WinRT の強参照と弱参照」を参照してください。

重要な API