我有一个媒体应用程序,允许用户播放、暂停、逐帧步进、快进等。我正试图使用Rx来获得步进和快进的以下行为。
- 如果用户点击向右箭头的次数少于2次/300ms,我想框定步骤
- 如果用户按住向右箭头,我想快进,直到松开向右箭头按钮
我认为我的快进部分是正确的,但不确定如何制作才能获得步骤功能。我也对"更好"的快进方式持开放态度。
//start FF when we get 2 key presses within the threshold time
Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
.Where(k => k.EventArgs.Key == Key.Right)
.Timestamp()
.Buffer(2)
.Where(x => (x[1].Timestamp - x[0].Timestamp).Milliseconds < 300)
.Subscribe(x =>
{
Console.WriteLine("FastForward GO");
_viewModel.FastForward();
});
//stop ff on the key up
Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
.Where(k => k.EventArgs.Key == Key.Right)
.Subscribe(x => {
Console.WriteLine("FastForward STOP");
_viewModel.StopFastForward();
});
解决方案
var up = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
.Where(x => x.EventArgs.KeyCode == Keys.Right);
// Take, Concat, and Repeat work together to prevent repeated KeyDown events.
var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
.Where(x => x.EventArgs.KeyCode == Keys.Right)
.Take(1)
.Concat(up.Take(1).IgnoreElements())
.Repeat();
var t = TimeSpan.FromMilliseconds(300);
var tap = down.SelectMany(x =>
Observable.Amb(
Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
up.Take(1)
))
.Publish()
.RefCount();
var longPress = down.SelectMany(x =>
Observable.Return(x).Delay(t).TakeUntil(tap)
);
有多种方法可以做到这一点,但这可以获得你需要的"长按"one_answers"敲击"。您可以使用longPress
开始快进,使用up
停止快进,并使用tap
进行帧步进。
CCD_ 4在CCD_。
当按键被按下的时间超过CCD_ 7时,CCD_。
up
在密钥被释放时产生。
解释
由于每次物理按键都会多次重复KeyDown事件,因此存在问题。
var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown");
在这种情况下,我们需要一种方法来过滤掉重复的KeyDown事件。我们可以通过使用运算符的组合来做到这一点。首先,我们将使用Take(1)
。这将产生第一个事件并忽略其余事件
var first = down.Take(1);
如果我们只需要按下一次按键,那就太好了。但是,唉,我们需要得到所有的实际按键。我们需要等待KeyUp事件发生,然后重新开始整个过程。为此,我们可以使用Concat
和Repeat
的组合。对于concat可观测,我们需要确保我们只接受1个up事件,并且忽略up可观测的元素,否则我们最终会将所有up事件输入到我们的新可观测中。
var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
.Take(1)
.Contact(up.Take(1).IgnoreElements())
.Repeat();
这为我们提供了实际的向下事件,而没有重复事件之间的间隔。
既然我们已经清理了源可观测性,我们就可以开始以有用的方式组合它们了。我们正在寻找的是一个"点击"事件和一个"长按"事件。要获得tap事件,我们需要执行一个实际向下事件,并确保它不会被按住太久。。。一种方法是使用Amb
运算符。
var tap = down.SelectMany(x =>
Observable.Amb(
Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
up.Take(1)
))
Amb
运算符代表"不明确"。它需要许多Observable,倾听每一个,并等待它们产生一些东西。一旦其中一个产生事件,Amb
操作符就会忽略(处置)其他可观察器。
在我们的例子中,对于发生的每个down事件,我们使用SelectMany
和Amb
运算符来检查哪个先产生或完成。。。单个向上事件,或在时间跨度t之后完成的空可观察对象。如果向上事件发生在空可观察结果完成之前,则为轻敲。否则,我们将忽略它。
现在我们可以对"长按"做类似的事情,只是这次我们想推迟KeyDown事件,直到我们知道它不是敲击。我们可以使用Delay
和TakeUntil
运算符的组合来实现这一点。Delay
确保在注册敲击之前不会出现长按,TakeUntil
确保如果KeyPress最终被证明是敲击,我们会忽略它。
var longPress = down.SelectMany(x =>
Observable.Return(x).Delay(t).TakeUntil(tap)
);
广义解
此版本适用于任何密钥。
var up = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp");
var downWithRepeats = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown");
var down =
Observable.Merge(
up.Select(x => new { e = x, type = "KeyUp" }),
downWithRepeats.Select(x => new { e = x, type = "KeyDown" })
)
.GroupByUntil(
x => x.e.EventArgs.KeyCode,
g => g.Where(y => y.type == "KeyUp")
)
.SelectMany(x => x.FirstAsync())
.Select(x => x.e);
var t = TimeSpan.FromMilliseconds(300);
var tap = down.SelectMany(x =>
Observable.Amb(
Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
up.Where(y => y.EventArgs.KeyCode == x.EventArgs.KeyCode).Take(1)
))
.Publish()
.RefCount();
var longPress = down.SelectMany(x =>
Observable.Return(x).Delay(t).TakeUntil(
tap.Where(y => y.EventArgs.KeyCode == x.EventArgs.KeyCode)
)
);
用法
Observable.Merge(
down .Select(x => string.Format("{0} - press", x.EventArgs.KeyCode)),
tap .Select(x => string.Format("{0} - tap", x.EventArgs.KeyCode)),
longPress.Select(x => string.Format("{0} - longPress", x.EventArgs.KeyCode)),
up .Select(x => string.Format("{0} - up", x.EventArgs.KeyCode))
)
.ObserveOn(SynchronizationContext.Current)
.Select(x => string.Format("{0} - {1}", x, DateTime.Now.ToLongTimeString()))
.Subscribe(text => this.myTextBox.Text = text);
这里有一个Chris的替代方案,它提供三个流,一个用于点击,一个用来开始保持,一个用作结束保持。使用TimeInterval
记录事件之间的持续时间。
WinForms版本
我们可以通过使用GroupByUntil
对KeyDown
进行分组来捕获KeyDown消除重复,直到出现KeyUp
:
TimeSpan limit = TimeSpan.FromMilliseconds(300);
var key = Keys.Right;
var keyUp = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
.Where(i => i.EventArgs.KeyCode == key)
.Select(_ => true);
var keyDown = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
.Where(i => i.EventArgs.KeyCode == key)
.GroupByUntil(k => 0, _ => keyUp)
.SelectMany(x => x.FirstAsync());
var keyDownDuration = keyDown.Select(k => keyUp.TimeInterval()).Switch();
var clicks = keyDownDuration.Where(i => i.Interval < limit);
var beginHold = keyDown.Select(k => Observable.Timer(limit).TakeUntil(keyUp))
.Switch();
var endHold = keyDownDuration.Where(i => i.Interval > limit);
/* usage */
clicks.Subscribe(_ => Console.WriteLine("Click"));
beginHold.Subscribe(_ => Console.WriteLine("Hold Begin"));
endHold.Subscribe(_ => Console.WriteLine("Hold End"));
WPF版本
最初,我错误地认为KevEventArgs
的WPF风格是IsRepeat
在WinForms版本中不可用——这意味着这对OP不起作用,但我会保留它,因为它可能对其他人有用。
TimeSpan limit = TimeSpan.FromMilliseconds(300);
var key = Key.Right;
var keyUp = Observable.FromEventPattern<KeyEventArgs>(this, "KeyUp")
.Where(i => i.EventArgs.Key == key);
var keyDown = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
.Where(i => i.EventArgs.IsRepeat == false
&& i.EventArgs.Key == key);
var keyDownDuration = keyDown.Select(k => keyUp.TimeInterval()).Switch();
var clicks = keyDownDuration.Where(i => i.Interval < limit);
var beginHold = keyDown.Select(k => Observable.Timer(limit).TakeUntil(keyUp))
.Switch();
var endHold = keyDownDuration.Where(i => i.Interval > limit);
/* usage */
clicks.Subscribe(_ => Console.WriteLine("Click"));
beginHold.Subscribe(_ => Console.WriteLine("Hold Begin"));
endHold.Subscribe(_ => Console.WriteLine("Hold End"));
测试代码
包括nuget包rx-main,并将WinForms/WPF或代码片段粘贴到表单生成器的末尾。然后运行代码并按下向右箭头键,同时观察VS Output(VS输出)窗口以查看结果。