【问题标题】:Reactive extensions: matching complex key press sequence反应式扩展:匹配复杂的按键顺序
【发布时间】:2013-01-15 10:12:09
【问题描述】:

我想要实现的是使用 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 按下。我的方向正确吗?

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

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

【问题讨论】:

    标签: c# system.reactive keypress


    【解决方案1】:

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

    试试这个,它应该让你开始你所追求的 - cmets 在线描述发生了什么:

    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!";
    

    【讨论】:

    • 好的,我花了一些时间才回到这个问题并检查您的示例是如何工作的。的确是!这真的是一种与我完全不同的方法,所以读起来很有趣。虽然我做了一些修改,关于 2 次插入压力机之间的超时,但它真的很小。我需要掌握 SelectMany 的东西,它看起来很有趣但很复杂。
    【解决方案2】:

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

    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),最后一个用于组合先两个。

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

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

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

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

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

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

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

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

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

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

    提前致谢!

    【讨论】:

      猜你喜欢
      • 2019-07-29
      • 1970-01-01
      • 2019-10-07
      • 2011-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-22
      相关资源
      最近更新 更多