概要
一定時間長押しをして発火するようなボタンを作りたいということがよくあります。
ここではWPFでReactiveExtensionsを用いて次の3種類のボタンを作ります。
- 単純に長押しをしたら発火
- 長押しして発火するまでのProgressも表示して発火
- 長押しを止めたらProgressが戻り、再度押したら再開するResumableなもの
下準備
こんな感じの拡張メソッドを先に用意します。
ButtonExtensions.cs
public static class ButtonExtensions
{
public static IObservable<MouseButtonEventArgs> PreviewMouseDownAsObservable(this ButtonBase button)
=> Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
h => (s, e) => h(e),
h => button.PreviewMouseDown += h,
h => button.PreviewMouseDown -= h);
public static IObservable<MouseButtonEventArgs> PreviewMouseUpAsObservable(this ButtonBase button)
=> Observable.FromEvent<MouseButtonEventHandler, MouseButtonEventArgs>(
h => (s, e) => h(e),
h => button.PreviewMouseUp += h,
h => button.PreviewMouseUp -= h);
}
単純に長押ししたら発火
ボタン上でマウスダウンされたらタイマー開始、マウスアップがあったら中断ですね。
public static IObservable<long> LongPressAsObservable(this ButtonBase button, TimeSpan time)
{
var down = button.PreviewMouseDownAsObservable();
var up = button.PreviewMouseUpAsObservable();
return down
.Select(_ => Observable.Timer(time).TakeUntil(up))
.Switch();
}
使う時はこんな感じです。
LongPressという名前のボタンがあると思って下さい。
この例では2秒で発火しています。
this.LongPress
.LongPressAsObservable(TimeSpan.FromSeconds(2))
.ObserveOn(SynchronizationContext.Current) //.NET5にはObserveOnDispatcherがないので
.Subscribe(_ => this.TestMessage.Text += $"LongPressed!:{DateTime.Now}\n"); //実行したいメソッド
Progressも付ける
上ではシンプルなのを作りましたが、Progressが見えないといつまで押せばいいのか不安になりますね。
というわけでProgressがわかるようにしましょう。
public static IObservable<double> ProgressAsObservable(this ButtonBase button, TimeSpan time)
{
var down = button.PreviewMouseDownAsObservable();
var up = button.PreviewMouseUpAsObservable();
//100になるまでGenerateしてもらいます。この100がトリガーです。
var progress = down
.Select(_ => Observable
.Generate(0d, i => i <= 100, i => ++i, i => i, i => time)
.TakeUntil(up));
return progress.Switch();
}
//ProgressというボタンとProgressBarというプログレスバーがあったとします
this.Progress
.ProgressAsObservable(TimeSpan.FromMilliseconds(1))
.ObserveOn(SynchronizationContext.Current)
.Subscribe(x =>
{
this.ProgressBar.Value = x;
if (x.Equals(100d))
this.TestMessage.Text += $"ProgressCompleted!:{DateTime.Now}\n";
});
ProgressをResumableにする
途中でボタンを押すのを止めたらProgressを戻して欲しい時もあります。
一瞬で0に戻ったらびっくりしますしね。
ただ100%完了してるのに戻られても困るからその辺りも何とかしましょう。
public static IObservable<double> ResumableProgressAsObservable(this ButtonBase button, TimeSpan time)
{
double value = 0;
double limit = 100d;
var down = button.PreviewMouseDownAsObservable();
var up = button.PreviewMouseUpAsObservable();
var increment = down
.Do(_ =>
{
if (value.Equals(limit))
value = 0;
})
.Select(_ => Observable.Generate(value, i => i <= 100, i => ++i, i => i, i => time).TakeUntil(up));
var decrement = up
.Where(_ => !value.Equals(limit))
.Select(_ => Observable.Generate(value, i => i >= 0d, i => --i, i => i, i => time).TakeUntil(down));
return Observable.Merge(increment, decrement).Switch().Do(x => value = x);
}
// Resumableという名前のボタンがあるとします。
this.Resumable
.ResumableProgressAsObservable(TimeSpan.FromMilliseconds(1))
.ObserveOn(SynchronizationContext.Current)
.Subscribe(x =>
{
this.ProgressBar.Value = x;
if (x.Equals(100d))
this.TestMessage.Text += $"ResumableProgressCompleted!:{DateTime.Now}\n";
});
もう少しスマートに書きたいですね。
補足
Tabキー等でフォーカスを失った際にもタイマーの進行を中止させたい場合は、LostFocusを監視しましょう。
ボタン上からマウスが外れた時は単純にMouseLeaveを監視すればいい、というわけにはならないのが辛いですね。
ボタンがマウスキャプチャをリリースしている必要があったりします。
ソースコード