Reactive Extensionsの入門の入門から、必殺技を繰り出すまで

ReactiveExtensionsの学習にはかずきさんのブログがよくまとまっていると感じました。ReactiveExtensions(以下Rx)は非同期処理でその真価を発すると推測していますが、イベント向けのFromEventというファクトリメソッドで遊んでみます。

まず、Observable.FromEventメソッドのシグネチャは次のようになっています。

 Observable.FromEvent

public static IObservable<TEventArgs> FromEvent<TDelegate, TEventArgs>(
	Func<Action<TEventArgs>, TDelegate> conversion,
	Action<TDelegate> addHandler,
	Action<TDelegate> removeHandler
)

「ややこしー、特に第一引数は一体何モノ?」と思ったのが私の第一印象でした。WPFでの実装例は下のような感じになります。

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this MainWindow window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    handler => (sender, e) => handler(e),
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }
    }

第2,3引数はイベントへのハンドラの追加・削除なのでいいとして、第1引数ちゃんと理解できていますか?3つのラムダ式ではhandlerを引数にしていますが、第1引数のそれと第2,3引数のそれとでは型が違います。第1引数は =>演算子が続いているので切れ目がパッと理解できなかったのですが、切れ目はここです。

fromeventについて

つまり下のように書き換えられます。こうするとわかりやすいかも。

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this MainWindow window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => {
                        MouseButtonEventHandler handler = (sender, e) =>
                        {
                            mouseAction(e);
                        };
                        return handler;
                    },
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }
    }

上のコードで第1引数内のhandlerと、第2,3引数内のhandlerの型は同じにしています。このhanlderデリゲート変数は省略でき、省略表記すると良く見るソースになるわけです。

さて、”シーケンス”を処理するイベントを構成して真価を発揮するRxですが、実務でイベントをシーケンス処理したい好例は思いつきませんでしたが、Rxでできることを初めて知ったとき、私の頭にひらめいた「アレ」を作ってみたいと思います。何をしたいかはコードをみればわかってきます。以下WPFの例ですが、WindowsFormsでもイベントの型が変わるだけでやることは何も変わりません。まず先ほど作っていたマウス関連イベントを拡張メソッドとして作成します。(※拡張メソッドである必要はありませんし、今回は有難みもほとんどありません;)

 RxEventHelper.cs

    using System;
    using System.Windows;
    using System.Windows.Input;
    using System.Reactive.Linq;

    public static class RxEventHelper
    {
        public static IObservable<MouseButtonEventArgs> MouseDownAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => {
                        MouseButtonEventHandler handler = (sender, e) =>
                        {
                            mouseAction(e);
                        };
                        return handler;
                    },
                    handler => window.MouseDown += handler,
                    handler => window.MouseDown -= handler
                );
        }

        public static IObservable<MouseEventArgs> MouseMoveAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseEventHandler, MouseEventArgs>
                (
                    mouseAction => (s, e) => mouseAction(e),
                    handler => window.MouseMove += handler,
                    handler => window.MouseMove -= handler
                );
        }

        public static IObservable<MouseButtonEventArgs> MouseUpAsObservable(this UIElement window)
        {
            return Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>
                (
                    mouseAction => (s, e) => mouseAction(e),
                    handler => window.MouseUp += handler,
                    handler => window.MouseUp -= handler
                );
        }
    }

MouseDown, MouseUpで同じようにかけますが、先ほどの通り意識して見比べられるようにしておきました、対象クラスはUIElementにしていれば問題ないでしょう。WindowsFormsならFormかControlにしておけばよさそうですね。今回はWPFなのでイベント処理を行うのはMainWindowです、とはいえXAMLは編集不要です。デフォルトのGridが張り付けられたままでいいです。c#のソースは以下のようにしてみます。

MainWindow.xaml.cs

namespace RxTest
{
    using System;
    using System.Windows;
    using System.Reactive.Linq;

    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private Point prev = new Point(0,0);
        private IObservable<Point> dragOperation;

        public MainWindow()
        {
            InitializeComponent();
            RegisterXXXCommand();
        }

        public void RegisterXXXCommand()
        {
            dragOperation = this
                .MouseMoveAsObservable()
                .SkipUntil(this.MouseDownAsObservable().Do(_ => this.CaptureMouse()))
                .TakeUntil(this.MouseUpAsObservable().Do(_ => this.ReleaseMouseCapture()))
                .Select(e => e.GetPosition(null));
                //.Where(p => { prev = p; return (prev.X < p.X && prev.Y > p.Y); });

            Subscrbe();
        }

        private void Subscrbe()
        {
            dragOperation.Subscribe(p => { }, () =>
            {
                Subscrbe();
                hadouken();
            });
        }

        private void hadouken(){
            // 体内のエネルギーを自らの両手の中に..
        }

    }
}

MouseDown~MouseUpはRxの例で見かけたことがあるかと思います。そこで使っているRepeatをここでは使っていません。なぜでしょうか? まずここでやりたいことはMouseDown~MouseMove~MouseUpの一連の操作が行われた場合に1度だけ処理をしたいのです。しかし、MouseMoveAsOvservableではマウス移動のたびにメッセージが飛んできます。途中フィルタリングはできても、完了まで待機というのは難しいものと思われます(今の私にはわかりませんでした)。

そのため一連の処理が完了したOnCompletedで”必殺技”を放つようにしました。必殺技とは言え何発も打てないとダメなので、dragOperationの最後にRepeat()を付けたいところですがRepeat()をするとOnCompletedは呼ばれなくなります。「Repeatを使わずに繰り返してSubscribeしたい」、じゃあどうしようということでOnCompletedで再びSubscribeしている訳です。コード上再起っぽく見えますが、再帰呼び出しにはなっていません。また、イレギュラーなことにOnCompletedで処理をすることだけが目的なので、普通最もよく使うはずのOnNextでは何もしたくありません。これは必須引数になっていますので、p=>{} として何もしないAction<Point>を定義しています。

コマンド解釈のところは本来はもっと頑張る必要がありそうです。とりあえず前後関係を把握して[下]・[右下]・[右]の順に位置が移動するということは、前後関係からいえばX,Yはこう移動するはずで…と想定できると思います。さらにリアルなことを言えばタイミング制約もあるでしょうが、ここまでで。。。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中