C# 反应式扩展:匹配复杂的按键顺序
我试图实现的是用Rx处理一些复杂的按键和释放序列。我在Rx方面有一些经验,但这显然不足以满足我目前的任务,所以我来这里寻求帮助 我的WinForms应用程序正在后台运行,仅在系统托盘中可见。通过给定的键序列,我想激活它的一个窗体。顺便说一句,为了连接到全局按键,我使用了一个很好的库,我能够接收每一个按键下降和上升事件,当按键下降时,会产生多个按键下降事件(以标准键盘重复率) 我想要捕捉的一个示例键序列是快速按Ctrl+Insert键(比如在给定的时间段内按住Ctrl键并按Insert两次)。以下是我目前代码中的内容:C# 反应式扩展:匹配复杂的按键顺序,c#,system.reactive,keypress,C#,System.reactive,Keypress,我试图实现的是用Rx处理一些复杂的按键和释放序列。我在Rx方面有一些经验,但这显然不足以满足我目前的任务,所以我来这里寻求帮助 我的WinForms应用程序正在后台运行,仅在系统托盘中可见。通过给定的键序列,我想激活它的一个窗体。顺便说一句,为了连接到全局按键,我使用了一个很好的库,我能够接收每一个按键下降和上升事件,当按键下降时,会产生多个按键下降事件(以标准键盘重复率) 我想要捕捉的一个示例键序列是快速按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);
但我认为它破坏了整个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-down组合起来
所以,我再次需要在按住Ctrl键的同时跟踪双击。我走的方向对吗
另外,另一种可能的方法是只在按下Ctrl键时订阅Insert observable。但不知道如何实现这一目标。也许你也有一些想法
编辑:我发现的另一个问题是缓冲区不能完全满足我的需要。问题在于缓冲区每两秒钟产生一次样本,如果我的第一次按压属于第一个缓冲区,第二次按压属于下一个缓冲区,那么什么都不会发生。如何克服这个问题?好吧,我想出了一些解决办法。它是有效的,但有一些限制,我将进一步解释。我暂时不会接受这个答案,也许其他人会提供一个更好更通用的方法来解决这个问题。无论如何,以下是当前的解决方案:
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();
}
让我解释一下这里发生了什么,也许对一些人会有用。我基本上设置了3个观察值,一个用于修改器键(modifierIsPressed
),另一个用于在按下修改器激活序列时需要双击的键(mainKeyDoublePressed
),最后一个是将两个键结合在一起的
第一个非常简单:只需将按键和释放转换为bool(使用Select
)<需要使用code>DistinctUntilChanged,因为如果用户按住某个键,将生成多个事件。我在这个可观察对象中得到的是一系列布尔值,表示如果修改键被按下
然后是最棘手的一个,处理主键。让我们一步一步来:
TimeInterval
来用时间跨度替换键关闭(这很重要)事件Select
函数得到实际的时间跨度(为下一步做准备)扫描
。它所做的是从前面的序列(本例中为timespans)中获取每两个连续元素,并将它们作为两个参数传递到函数中。进一步传递该函数的输出(必须与参数的类型相同,时间跨度)。在我的例子中,函数的作用非常简单:只返回第二个参数李>
为什么??现在是时候记住我在这里的实际任务了:捕捉到在时间上彼此足够接近的按钮的两次按下(就像我的示例中的半秒钟)。我的输入是一系列的时间跨度,表示自上次事件发生以来经过了多少时间。这就是为什么我需要等待两个事件:第一个事件通常足够长,因为它会告诉用户上次按下按键的时间,可能是几分钟或更长。但是,如果用户快速按两次键,那么第二次时间间隔将很小,因为它将显示最后两次快速按下之间的差异
听起来很复杂,对吧?然后用一种简单的方式来思考:Scan
总是将两个最新事件组合在一起。这就是为什么在这种情况下它适合我的需要:我需要听双击。如果我需要连续等待3次印刷,我在这里会不知所措。这就是为什么我称这种方法为有限的,并且仍然等待是否有人会提供更好、更通用的解决方案来处理任何可能的键组合
无论如何,让我们继续解释:
4.Select(ti=>ti
:这里我只是将序列从时间戳转换为布尔值,对于足够快的连续事件传递true,对于不够快的事件传递false
5.还有一个技巧:我将布尔序列从第4步合并到新的一步,在新的一步中,我将听向上键事件。还记得最初的序列1是从按键关闭事件构建的吗?所以在这里,我基本上采用了与可观测数字1相同的方法:向下传递true,向上传递false
然后,使用combinelateest
函数变得非常简单,它从每个序列中提取最后一个事件,并将它们作为列表进一步传递到Where
函数,该函数检查所有事件是否都为真。这就是我如何实现我的目标:现在我知道当主键被按下两次而修改键被按下时。在main key up事件中进行合并可确保清除状态,因此下次按下modifier键不会触发序列
好了,差不多就这样了。我会发布这个,但不会接受,就像我之前说的。我希望有人能插话来启发我。:)
提前谢谢 首先,欢迎来到Re的大脑弯曲魔法
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"); });
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();
}
}
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!";