Reactive Extension按键按下媒体控制



我有一个媒体应用程序,允许用户播放、暂停、逐帧步进、快进等。我正试图使用Rx来获得步进和快进的以下行为。

  1. 如果用户点击向右箭头的次数少于2次/300ms,我想框定步骤
  2. 如果用户按住向右箭头,我想快进,直到松开向右箭头按钮

我认为我的快进部分是正确的,但不确定如何制作才能获得步骤功能。我也对"更好"的快进方式持开放态度。

//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事件发生,然后重新开始整个过程。为此,我们可以使用ConcatRepeat的组合。对于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事件,我们使用SelectManyAmb运算符来检查哪个先产生或完成。。。单个向上事件,或在时间跨度t之后完成的空可观察对象。如果向上事件发生在空可观察结果完成之前,则为轻敲。否则,我们将忽略它。

现在我们可以对"长按"做类似的事情,只是这次我们想推迟KeyDown事件,直到我们知道它不是敲击。我们可以使用DelayTakeUntil运算符的组合来实现这一点。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版本

我们可以通过使用GroupByUntilKeyDown进行分组来捕获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输出)窗口以查看结果。

相关内容

  • 没有找到相关文章

最新更新