Reactive Extension键按媒体控件

时间:2013-12-10 23:24:43

标签: system.reactive

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

  1. 如果用户点击右箭头少于2次/ 300毫秒,我想进行帧步。
  2. 如果用户按住右箭头,我想快进,直到释放右箭头按钮。
  3. 我认为我的快进部分是正确的,但我不确定如何制作这个以获得步骤功能。我也愿意采用“更好”的方式来做快进。

    //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();
                });
    

2 个答案:

答案 0 :(得分:2)

解决方案

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)
    );

有多种方法可以做到这一点,但这有助于获得所需的“longPress”以及“tap”。您可以使用longPress开始快速发送,up停止快进,tap进行快速转发。

tap的时间范围内按下并释放某个键时,

t会产生。

当密钥被按下的时间超过longPress时,

t会产生。

密钥释放后,

up会产生。

存在问题,因为Keydown事件会在每次按下一个键时重复多次。

var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown");

在这种情况下,我们需要一种方法来过滤掉重复的KeyDown事件。我们可以通过使用运算符组合来实现。首先,我们将使用Take(1)。这将产生第一个事件而忽略其余事件。

var first = down.Take(1);

如果我们只需要按一下实际按键,那就太棒了。但是,唉,我们需要得到所有的实际按键。我们需要等待KeyUp事件发生并开始整个过程​​。为此,我们可以使用ConcatRepeat的组合。对于concat observable,我们需要确保我们只接受1个up事件,并且我们忽略了up observable的元素,否则我们最终会将所有up事件提供给我们新的observable。

var down = Observable.FromEventPattern<KeyEventArgs>(this, "KeyDown")
                     .Take(1)
                     .Contact(up.Take(1).IgnoreElements())
                     .Repeat();

这为我们提供了实际向下事件,没有中间重复事件。

现在我们已经清理了我们的源可观察对象,我们可以用有用的方式开始编写它们。我们正在寻找的是“点击”活动和“长按”活动。要获得点击事件,我们需要执行单个实际注册事件,并确保它没有按下太长时间......一种方法是使用{{1} } operator。

Amb

var tap = down.SelectMany(x => Observable.Amb( Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t), up.Take(1) )) 运算符代表“模棱两可”。它需要一些Observable,听取每一个,并等待它们产生一些东西。一旦其中一个产生事件,Amb运算符将忽略(处理其他可观察的订阅)。

在我们的案例中,对于发生的每个事件,我们使用AmbSelectMany运算符来检查哪个收益率或首先完成......单个向上事件或空的可观察事件在t的一个时间段之后完成。如果up事件发生在空observable完成之前,则触发它。否则,我们会忽略它。

现在我们可以为“长按”做类似的事情,除了这次我们想要延迟KeyDown事件,直到我们知道它不是一个点击。我们可以使用AmbDelay运算符的组合来执行此操作。 TakeUntil确保在注册点击之前不会长按,Delay确保我们忽略了KeyPress,如果它最终是一个点按。

TakeUntil

广义解决方案

此版本适用于任何密钥。

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)
        )
    );

答案 1 :(得分:1)

这是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或代码段粘贴到Form构造函数的末尾。然后运行代码并按右箭头键,同时观察VS Output窗口以查看结果。