3

我想要实现的是使用 Rx 处理一些复杂的按键和释放序列。我对 Rx 有一些经验,但这显然不足以满足我目前的工作,所以我在这里寻求帮助。

我的 WinForms 应用程序在后台运行,仅在系统托盘中可见。通过给定的键序列,我想激活其中一种形式。顺便说一句,要连接到全局按键,我正在使用一个不错的库http://globalmousekeyhook.codeplex.com/我能够接收每个按键按下和按键按下事件,并且当按键按下时,多个 KeyDown 事件是产生(具有标准键盘重复率)。

我要捕获的示例键序列之一是快速双 Ctrl + Insert 按键(例如按住 Ctrl 键并在给定时间段内按两次 Insert)。这是我目前在我的代码中的内容:

var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyDown");
var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyUp");

var ctrlDown = keyDownSeq.Where(ev => ev.EventArgs.KeyCode == Keys.LControlKey).Select(_ => true);
var ctrlUp = keyUpSeq.Where(ev => ev.EventArgs.KeyCode == Keys.LControlKey).Select(_ => false);

但后来我被困住了。我的想法是我需要以某种方式跟踪 Ctrl 键是否按下。一种方法是为此创建一些全局变量,并在一些合并侦听器中更新它

Observable.Merge(ctrlDown, ctrlUp)                
    .Do(b => globabl_bool = b)
    .Subscribe();

但我认为它破坏了整个 Rx 方法。关于如何在保持 Rx 范式的同时实现这一目标的任何想法?

然后,当 Ctrl 按下时,我需要在给定时间内捕获两次 Insert 按下。我正在考虑使用缓冲区:

var insertUp = keyUpSeq.Where(ev => ev.EventArgs.KeyCode == Keys.Insert);
insertUp.Buffer(TimeSpan.FromSeconds(1), 2)
    .Do((buffer) => { if (buffer.Count == 2) Debug.WriteLine("happened"); })
    .Subscribe();

但是我不确定这是否是最有效的方式,因为 Buffer 会每隔一秒产生一次事件,即使没有按下任何键。有没有更好的办法?而且我还需要以某种方式将其与 Ctrl 结合起来。

所以再一次,我需要在 Ctrl 按下时跟踪两次 Insert 按下。我是否朝着正确的方向前进?

PS 另一种可能的方法是仅在 Ctrl 按下时订阅 Insert observable。不知道如何实现这一点。也许对此也有一些想法?

编辑:我发现的另一个问题是 Buffer 并不完全适合我的需要。问题在于 Buffer 每两秒产生一次样本,如果我的第一次按下属于第一个缓冲区,第二次属于下一个缓冲区,那么什么也不会发生。如何克服它?

4

2 回答 2

3

首先,欢迎来到 Reactive Framework 的神奇魔力!:)

试试这个,它应该让你开始你所追求的 - 在线评论以描述正在发生的事情:

using(var hook = new KeyboardHookListener(new GlobalHooker()))
{
    hook.Enabled = true;
    var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(hook, "KeyDown");
    var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(hook, "KeyUp");    

    var ctrlPlus =
        // Start with a key press...
        from keyDown in keyDownSeq

        // and that key is the lctrl key...
        where keyDown.EventArgs.KeyCode == Keys.LControlKey

        from otherKeyDown in keyDownSeq
            // sample until we get a keyup of lctrl...
            .TakeUntil(keyUpSeq
                .Where(e => e.EventArgs.KeyCode == Keys.LControlKey))

            // but ignore the fact we're pressing lctrl down
            .Where(e => e.EventArgs.KeyCode != Keys.LControlKey)
        select otherKeyDown;

    using(var sub = ctrlPlus
           .Subscribe(e => Console.WriteLine("CTRL+" + e.EventArgs.KeyCode)))
    {
        Console.ReadLine();
    }
}

现在这并不完全符合您的要求,但稍作调整,就可以轻松适应。关键位是组合 linq 查询SelectMany的顺序from子句中的隐式调用 - 因此,查询如下:

var alphamabits = 
    from keyA in keyDown.Where(e => e.EventArgs.KeyCode == Keys.A)
    from keyB in keyDown.Where(e => e.EventArgs.KeyCode == Keys.B)
    from keyC in keyDown.Where(e => e.EventArgs.KeyCode == Keys.C)
    from keyD in keyDown.Where(e => e.EventArgs.KeyCode == Keys.D)
    from keyE in keyDown.Where(e => e.EventArgs.KeyCode == Keys.E)
    from keyF in keyDown.Where(e => e.EventArgs.KeyCode == Keys.F)
    select new {keyA,keyB,keyC,keyD,keyE,keyF};

(非常)大致翻译为:

if A, then B, then C, then..., then F -> return one {a,b,c,d,e,f}

有道理?

(好吧,既然你已经读到这里了……)

var ctrlinsins =
    from keyDown in keyDownSeq
    where keyDown.EventArgs.KeyCode == Keys.LControlKey
    from firstIns in keyDownSeq
      // optional; abort sequence if you leggo of left ctrl
      .TakeUntil(keyUpSeq.Where(e => e.EventArgs.KeyCode == Keys.LControlKey))
      .Where(e => e.EventArgs.KeyCode == Keys.Insert)
    from secondIns in keyDownSeq
      // optional; abort sequence if you leggo of left ctrl
      .TakeUntil(keyUpSeq.Where(e => e.EventArgs.KeyCode == Keys.LControlKey))
      .Where(e => e.EventArgs.KeyCode == Keys.Insert)
    select "Dude, it happened!";
于 2013-01-16T04:03:31.973 回答
1

好吧,我想出了一些解决方案。它有效,但有一些限制,我将进一步解释。一段时间内我不会接受答案,也许其他人会提供更好,更通用的方法来解决这个问题。无论如何,这是当前的解决方案:

private IDisposable SetupKeySequenceListener(Keys modifierKey, Keys doubleClickKey, TimeSpan doubleClickDelay, Action<Unit> actionHandler)
{
    var keyDownSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyDown");
    var keyUpSeq = Observable.FromEventPattern<KeyEventArgs>(m_KeyboardHookManager, "KeyUp");

    var modifierIsPressed = Observable
        .Merge(keyDownSeq.Where(ev => (ev.EventArgs.KeyCode | modifierKey) == modifierKey).Select(_ => true),
               keyUpSeq.Where(ev => (ev.EventArgs.KeyCode | modifierKey) == modifierKey).Select(_ => false))
        .DistinctUntilChanged()
        .Do(b => Debug.WriteLine("Ctrl is pressed: " + b.ToString()));

    var mainKeyDoublePressed = Observable
        .TimeInterval(keyDownSeq.Where(ev => (ev.EventArgs.KeyCode | doubleClickKey) == doubleClickKey))
        .Select((val) => val.Interval)
        .Scan((ti1, ti2) => ti2)
        .Do(ti => Debug.WriteLine(ti.ToString()))
        .Select(ti => ti < doubleClickDelay)
        .Merge(keyUpSeq.Where(ev => (ev.EventArgs.KeyCode | doubleClickKey) == doubleClickKey).Select(_ => false))
        .Do(b => Debug.WriteLine("Insert double pressed: " + b.ToString()));

    return Observable.CombineLatest(modifierIsPressed, mainKeyDoublePressed)
        .ObserveOn(WindowsFormsSynchronizationContext.Current)
        .Where((list) => list.All(elem => elem))
        .Select(_ => Unit.Default)
        .Do(actionHandler)
        .Subscribe();
}

用法:

var subscriptionHandler = SetupKeySequenceListener(
    Keys.LControlKey | Keys.RControlKey, 
    Keys.Insert | Keys.C, 
    TimeSpan.FromSeconds(0.5),
    _ => { WindowState = FormWindowState.Normal; Show(); Debug.WriteLine("IT HAPPENED"); });

让我解释一下这里发生了什么,也许它对某些人有用。我基本上设置了 3 个 Observable,一个用于修饰键 ( modifierIsPressed),另一个用于在按下修饰键以激活序列 () 时需要双击的键,mainKeyDoublePressed最后一个用于首先组合两者。

第一个非常简单:只需将按键和释放转换为 bool(使用Select)。DistinctUntilChanged需要,因为如果用户按住某个键,则会生成多个事件。我在这个 observable 中得到的是一系列布尔值,表示修饰键是否关闭。

然后是最棘手的一个,处理主键的地方。让我们一步一步来:

  1. 我用来用时间跨度TimeInterval替换按键(这很重要)事件
  2. 然后我用函数得到实际Select的时间跨度(为下一步做准备)
  3. 然后是最棘手的事情,Scan. 它所做的是从前一个序列(在我们的例子中为时间跨度)中获取每两个连续元素,并将它们作为两个参数传递给一个函数。该函数的输出(必须与参数的类型相同,时间跨度)被进一步传递。在我的例子中,这个函数做了非常简单的事情:只返回第二个参数。

为什么?是时候在这里记住我的实际任务了:抓住某个按钮的双击,这些按钮在时间上彼此足够接近(例如在我的示例中为半秒)。我的输入是一系列时间跨度,表示自上一个事件发生以来经过了多长时间。这就是为什么我需要等待两个事件:第一个事件通常足够长,因为它会告诉用户上次按下键的时间,可能是几分钟或更长时间。但是如果用户快速按键两次,那么第二次时间跨度会很小,因为它会区分最后两次快速按键之间的差异。

听起来很复杂,对吧?然后用一个简单的方式想一想:Scan总是结合两个最新的事件。这就是为什么它在这种情况下适合我的需求:我需要听双击。如果我需要连续等待 3 次按下,我会在这里不知所措。这就是为什么我称这种方法为有限的,并且仍然等待是否有人会提供更好和更通用的解决方案,以处理潜在的任何组合键。

无论如何,让我们继续解释:

4. Select(ti => ti < doubleClickDelay):这里我只是将序列从时间戳转换为布尔值,为足够快的连续事件传递 true,为不够快的事件传递 false。

5.这是另一个技巧:我正在将第 4 步的布尔序列合并到新的序列中,在那里我监听按键事件。还记得最初的序列一是根据按键事件构建的,对吧?所以在这里我基本上采用了与第一个可观察到的方法相同的方法:将 true 传递给 key down 并将 false 传递给 key up 。

然后它变得超级容易使用的CombineLatest函数,它从每个序列中获取最后的事件,并将它们作为列表进一步传递给Where函数,该函数检查它们是否都为真。这就是我实现目标的方式:现在我知道在按住修饰键的同时按下主键两次。合并主键向上​​事件可确保我清除状态,因此下一次按下修饰键不会触发序列。

所以我们开始吧,差不多就是这样。正如我之前所说,我会发布这个,但不会接受。我希望有人会插话并启发我。:)

提前致谢!

于 2013-01-15T17:18:27.420 回答