xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

3 秒間押し続けたらイベント発火するボタンを作る

Twitter で @amay077 さんが以下のような内容を呟いていたのを見て、頭の体操と思って Rx を使って解決してみました。

ボタンのイベントが取れれば動作確認できるので、今回は雑に Windows Forms で試してみます。WPF でも Xamarin でも同様なことはできるでしょう。

追記

以下の実装でバグ報告があったので修正しました...w

ざっくり実装

まずマウスイベントを Rx 化します。イベントをハンドラのまま扱おうとするとだいぶ面倒ですが、シーケンスとして扱えるようにすると一気に楽になれます。

public static class ControlExtensions
{
    public static IObservable<MouseEventArgs> MouseDownAsObservable(this Control control)
        => Observable.FromEvent<MouseEventHandler, MouseEventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.MouseDown += handler,
            handler => control.MouseDown -= handler
        );

    public static IObservable<MouseEventArgs> MouseUpAsObservable(this Control control)
        => Observable.FromEvent<MouseEventHandler, MouseEventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.MouseUp += handler,
            handler => control.MouseUp -= handler
        );
}

この MouseDownMouseUp のイベントを Zip メソッドで対になるようにし、それぞれのイベントが発火された時間の差分を取ります。これが一定時間を超えていれば OK ですね!ちょっと汎用化して拡張メソッドとして切り出すと以下のような感じになるでしょう。

// メソッド名がこんなのでいいかはさておき...
public static IObservable<Unit> ClickIf(this Control control, TimeSpan threshold)
{
    var down = control.MouseDownAsObservable().Select(_ => DateTimeOffset.Now);
    var up = control.MouseUpAsObservable().Select(_ => DateTimeOffset.Now);
    return down
        .Zip(up, (downTime, upTime) => upTime - downTime)  // 経過時間を算出
        .Where(x => x >= threshold)  // 一定時間を超えていたら発火
        .Select(_ => Unit.Default);
}

使ってみる

上記のように実装しておけば、こんなに簡単に実装できます。Rx を使えば押し続けないと発火しないボタンも簡単に作れちゃいますね!

public partial class MainForm : Form
{
    public MainForm()
    {
        this.InitializeComponent();

        this.button
            .ClickIf(TimeSpan.FromSeconds(3))  // 3 秒押し続けたら
            .Subscribe(_ => Debug.WriteLine("fire!!"));  // 発火!
    }
}