前回 (というか昨夜)「3 秒押し続けたらイベント発火するボタン」を作ってみたのですが、朝起きてみたら早速以下のようなバグが発見されていました...(oh
いつも参考にさせて頂いています!
— GXworld (@GXprofile) June 7, 2019
こういうケースZipだと、DownしたけどUpしなかった(Atl+Tabで別ウィンドウへ)、Upだけした(ボタン外からDown後、カーソル移動してボタン上でUpした)という場合におかしくなりますよね?いじわる操作ですが起きてしまうので面倒ですね。
- 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 ); }
次に MouseDown
と Click
の経過時間を算出します。前回は 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 というサービスをはじめました。音楽アーティスト支援をこれまでと違った形で行う面白いアイディアなので、気が向いたら見てあげてください :)