反应扩展:匹配复杂的按键序列

本文关键字:复杂 扩展 | 更新日期: 2023-09-27 17:53:05

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

我的WinForms应用程序在后台运行,只在系统托盘中可见。通过给定的键序列,我想激活它的一种形式。顺便说一句,为了连接到全局按键,我使用了一个很好的库http://globalmousekeyhook.codeplex.com/,我能够接收每个按键向下和按键向上事件,而按键向下时产生多个按键向下事件(具有标准键盘重复率)。

我想捕获的一个示例键序列是快速双击Ctrl +插入键(如按住Ctrl键并在给定的时间内按两次插入键)。以下是我当前代码中的内容:

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键是否按下。一种方法是为此创建一些全局变量,并在Merge listener

中更新它
Observable.Merge(ctrlDown, ctrlUp)                
    .Do(b => globabl_bool = b)
    .Subscribe();

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

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

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

然而,我不确定这是否是最有效的方式,因为缓冲区每秒钟都会产生事件,即使没有按下任何键。有没有更好的办法?我还需要把它和Ctrl向下键结合起来

再一次,我需要在按下Ctrl键的情况下按下插入键。我走的方向对吗?

注:另一种可能的方法是只在按下Ctrl键时订阅Insert observable。但我不知道如何做到这一点。也许你对这个也有一些想法?

EDIT:我发现的另一个问题是Buffer不完全适合我的需要。这个问题来自于Buffer每两秒产生一个样本,如果我的第一次按压属于第一个缓冲区,第二次按压属于下一个缓冲区,那么什么都不会发生。如何克服呢?

反应扩展:匹配复杂的按键序列

首先,欢迎来到反应式框架的神奇之处!:)

试试这个,它应该让你开始你想要的-注释行描述发生了什么:

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查询的顺序from子句中的隐式SelectMany调用-因此,类似于

的查询:
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!";

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

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个可观察对象,一个是用于修饰符键(modifierIsPressed),另一个用于当按下修饰符以激活序列(mainKeyDoublePressed)时需要双击的键,最后一个将两者结合在一起。

第一个非常简单:只需将按键和释放转换为bool(使用Select)。需要DistinctUntilChanged,因为如果用户按下并按住某个键,会生成多个事件。我在这个可观察对象中得到的是一个布尔值序列,表示如果modifier key是down

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

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

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

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

不管怎样,让我们继续解释:

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

5。这里有另一个技巧:我将从第4步合并布尔序列到新序列,在新序列中我将监听键up事件。还记得最初的序列1是由按下键事件构建的吗?这里我采用了和第一个可观察对象相同的方法键向下传递为真键向上传递为假

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

好了,差不多就是这样了。我会张贴这个,但不会接受,就像我之前说的。我希望有人能插话开导我。:)

提前感谢!