【问题标题】:Reactive Extension key press media controls反应式扩展按键媒体控制
【发布时间】:2013-12-13 00:52:33
【问题描述】:

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

  1. 如果用户单击右箭头的次数少于 2 次/300 毫秒,我想设置帧步骤。
  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();
            });

【问题讨论】:

  • 嗯,这可能很棘手,您是否看到 KeyDown / KeyUp 上出现重复键? (比如,操作系统生成的)
  • 我没有看到任何重复。 FF 似乎可以正常工作,但不确定如何捕捉单击步骤。这是指向您应该能够运行并查看行为的要点的链接。 gist.github.com/pdoh00/7897459。编辑 - 这将重复 keydown 事件,但在我的情况下这是可以接受的。我更喜欢没有的 sln,但我调用的 api 并不关心我是否重复调用 FF 函数。
  • 所以你想在按钮被点击时跨帧,如果按钮被按住你想快进?这是基本的想法吗?

标签: system.reactive


【解决方案1】:

解决方案

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 进行帧步进。

tapt 的时间跨度内按下并释放一个键时产生。

当按键被按住的时间超过t 时,longPress 产生。

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

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

现在我们已经清理了源 observables,我们可以开始以有用的方式组合它们。我们正在寻找的是一个“点击”事件和一个“长按”事件。要获得点击事件,我们需要获取单个 actual down 事件,并确保它不会被按住太久...一种方法是使用 Amb 运算符.

var tap = down.SelectMany(x =>
    Observable.Amb(
        Observable.Empty<EventPattern<KeyEventArgs>>().Delay(t),
        up.Take(1)
    ))

Amb 运算符代表“模糊”。它需要许多 Observable,监听每个,并等待它们产生一些东西。一旦其中一个产生了一个事件,Amb 操作符就会忽略(释放订阅)其他可观察对象。

在我们的例子中,对于每个发生的 down 事件,我们使用 SelectManyAmb 运算符来检查哪个先产生或完成... t 的时间跨度。如果 up 事件发生在空的 observable 完成之前,它是一个点击。否则,我们忽略它。

现在我们可以为“长按”做类似的事情,除了这次我们想要延迟 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);

【讨论】:

  • 虽然它非常接近......我的利益相关者不会接受框架步骤的“双击”。他们对单击单个箭头将帧推进 1 非常挑剔。话虽如此,除了 Rx 之外,也许还有其他选项。
  • 哦,每 300 毫秒不到两次?能举个例子吗?
  • 当用户单击右箭头键一次然后 FrameStep()。当用户按住右箭头键时,然后是 FastForward()。那是从用户的角度。由于从技术上讲,当按住键时会发生多次“点击”,我选择了 300 毫秒内超过 1 次来表示按下。
  • 我解决了问题并添加了一些解释。
  • 是的。这样做。我需要花点时间学习才能理解所有的部分。非常感谢。
【解决方案2】:

这是 Chris 的替代方案,它提供三个流,一个用于点击,一个用于开始保持,一个用于结束保持。使用TimeInterval 记录事件之间的持续时间。

WinForms 版本

我们可以通过使用GroupByUntilKeyDown 分组,直到出现KeyUp 来捕获消除重复的KeyDown:

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 或代码 sn-ps 粘贴到表单构造函数的末尾。然后运行代码并按下右箭头键,同时观察 VS 输出窗口以查看结果。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-10
    相关资源
    最近更新 更多