xin9le.net

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

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

前回 (というか昨夜)「3 秒押し続けたらイベント発火するボタン」を作ってみたのですが、朝起きてみたら早速以下のようなバグが発見されていました...(oh

  • MouseDown 中に「Alt + Tab」で画面切り替える
  • MouseDown 中にボタン外にマウスカーソルを移動して MouseUp する

どちらもイレギュラーな挙動ですが、こういうユーザー操作は簡単に起こり得るものなので対処せねばならぬ...!ということで、少しだけ手直ししてみました。

Click イベントを利用する

Windows Forms / WPF のようなデスクトップアプリ向けの逃げですが、軽いお遊び程度なのでそこはご容赦いただくとして...。標準の Click イベントはボタン外で MouseUp しても発火しませんし、MouseDown 中に「Alt + Tab」をしても問題が起こりません。とても優秀な出来!これをありがたく利用することにして、事前にボタンを押している時間が長いことだけを判定できれば OK というようにしてみます。

前回同様 Rx ベースで書きたいので、以下のように Click イベントをシーケンス化しておきます。

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<EventArgs> ClickAsObservable(this Control control)
        => Observable.FromEvent<EventHandler, EventArgs>
        (
            handler => (sender, e) => handler(e),
            handler => control.Click += handler,
            handler => control.Click -= handler
        );
}

次に MouseDownClick の経過時間を算出します。前回は Zip メソッドを利用していたのですが、今回は CombineLatest に変更します。Zip だと一旦 MouseDown したら次の MouseUp が来るまで待ってしまうのですが、CombineLatest なら最新の値でペアリングするようになります。このとき場合によっては経過時間が負の値になってしまうことがありますが、「一定時間以上経過していること」として判定するので問題になりません。

public static class ControlExtensions
{
    public static IObservable<Unit> ClickIf(this Control control, TimeSpan threshold)
    {
        var down = control.MouseDownAsObservable().Select(_ => DateTimeOffset.Now);
        var click = control.ClickAsObservable().Select(_ => DateTimeOffset.Now);
        return down
            .CombineLatest(click, (downTime, clickTime) => clickTime - downTime)  // 経過時間を算出
            .Where(x => x >= threshold)  // 一定時間を超えていたら発火
            .Select(_ => Unit.Default);
    }
}

ちゃんと動くよ!

内部実装の変更だけなので、前回同様、以下のような感じで利用できます。今度こそ (?) めでたし!(のはず...

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

        this.button
            .ClickIf(TimeSpan.FromSeconds(3))  // 3 秒押し続けたら
            .Subscribe(_ => Debug.WriteLine("CHACCA!!"));  // お前のハートに火をつけろ!
    }
}

おまけ

まったく内容とは関係ないですが、僕の友人が最近 CHACCA というサービスをはじめました。音楽アーティスト支援をこれまでと違った形で行う面白いアイディアなので、気が向いたら見てあげてください :)