3

私が達成しようとしているのは、複雑なキーのプレスとリリースのシーケンスを Rx で処理することです。Rx の経験はほとんどありませんが、現在の仕事には明らかに十分ではないので、助けを求めてここにいます。

私の WinForms アプリはバックグラウンドで実行されており、システム トレイにしか表示されません。特定のキー シーケンスによって、そのフォームの 1 つをアクティブにしたいと考えています。ところで、グローバル キー プレスに接続するために、私は素敵なライブラリhttp://globalmousekeyhook.codeplex.com/を使用しています。すべてのキー ダウン イベントとキー アップ イベントを受け取ることができます。生成されます (標準のキーボード リピート レートで)。

キャプチャしたいキー シーケンスの例の 1 つは、Ctrl + Insert キーをすばやく 2 回押すことです (Ctrl キーを押しながら、Insert キーを一定時間内に 2 回押すなど)。これが私のコードに現在あるものです:

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キーが押されているかどうかを追跡する必要があるということです。1 つの方法は、そのためのグローバル変数を作成し、それを Merge リスナーで更新することです。

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

しかし、それは Rx アプローチ全体を台無しにすると思います。Rxパラダイムにとどまりながらそれを達成する方法についてのアイデアはありますか?

次に、Ctrl が押されている間に、特定の時間内に 2 回の 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 は 1 秒ごとにイベントを生成するため、これが最も効率的な方法かどうかはわかりません。より良い方法はありますか?また、どうにかしてそれを Ctrl down と組み合わせる必要があります。

繰り返しになりますが、Ctrl を押している間に Insert を 2 回押したことを追跡する必要があります。私は正しい方向に進んでいますか?

PS 別の可能なアプローチは、Ctrl が押されている間のみ、Insert Observable をサブスクライブすることです。しかし、それを達成する方法がわかりません。これについてもいくつかのアイデアがありますか?

編集:私が見つけたもう 1 つの問題は、Buffer が私のニーズに正確に合わないことです。問題は、Buffer が 2 秒ごとにサンプルを生成し、最初のプレスが最初のバッファーに属し、2 番目が次のバッファーに属している場合、何も起こらないという事実から生じます。それを克服する方法は?

4

2 に答える 2

3

まず、リアクティブ フレームワークの脳を曲げる魔法へようこそ! :)

これを試してみてください。あなたが求めていることを始めることができるはずです - 何が起こっているのかを説明するコメントが並んでいます:

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つのオブザーバブルを設定しています。1つは修飾キー(modifierIsPressed)用、もう1つは修飾キーを押してシーケンス()をアクティブにするときにダブルクリックする必要があるキー用mainKeyDoublePressed、最後に2つを組み合わせたものです。

最初の方法は非常に簡単です。キーの押下とリリースをブール値に変換するだけです(を使用してSelect)。DistinctUntilChangedユーザーがキーを押し続けると、複数のイベントが生成されるため、が必要になります。このオブザーバブルで取得しているのは、修飾キーが押されているかどうかを示す一連のブール値です。

次に、メインキーが処理される最もトリッキーなものです。ステップバイステップで行きましょう:

  1. キーダウン(重要)イベントをタイムスパンTimeIntervalに置き換えるために使用しています
  2. 次に、関数を使用して実際のタイムスパンを取得していSelectます(次のステップの準備のため)
  3. 次に、最もトリッキーなもの、Scan。これは、前のシーケンス(この場合はタイムスパン)から2つの連続する要素をそれぞれ取得し、それらを2つのパラメーターとして関数に渡します。その関数の出力(パラメーターと同じタイプ、タイムスパンである必要があります)はさらに渡されます。私の場合の関数は非常に単純なことを行います。2番目のパラメーターを返すだけです。

なんで?ここで私の実際のタスクを思い出すときが来ました。時間内に互いに十分に近いボタンを2回押すのをキャッチすることです(私の例では0.5秒のように)。私の入力は、前のイベントが発生してからどれだけの時間が経過したかを示す一連のタイムスパンです。そのため、2つのイベントを待つ必要があります。最初のイベントは通常十分な長さです。ユーザーが最後にキーを押したときから数分以上かかる可能性があるためです。ただし、ユーザーがキーを2回すばやく押すと、最後に2回押したときの違いがわかるため、2回目のタイムスパンは短くなります。

複雑に聞こえますよね?次に、簡単な方法で考えます。常に2つの最新のイベントScanを組み合わせます。この場合、それが私のニーズに合うのはそのためです。ダブルクリックを聞く必要があります。3回連続して押すのを待つ必要があるとしたら、ここで途方に暮れるでしょう。そのため、私はこのアプローチを制限付きと呼び、潜在的に任意のキーの組み合わせを処理するために、誰かがより優れたより一般的なソリューションを提供するかどうかを待ちます。

とにかく、説明を続けましょう:

4 Select(ti => ti < doubleClickDelay).:ここでは、シーケンスをタイムスタンプからブール値に変換し、十分に速い連続イベントの場合はtrueを渡し、十分に速くないイベントの場合はfalseを渡します。

5.もう1つのトリックがあります。ステップ4のブールシーケンスを新しいシーケンスにマージし、キーアップイベントをリッスンします。元のシーケンスはキーダウンイベントから作成されたことを覚えていますか?したがって、ここでは、基本的に、観察可能なナンバーワンと同じアプローチを採用しています。つまり、キーダウンの場合はtrueを渡し、キーアップの場合はfalseを渡します。

次に、関数を使用するのが非常に簡単になりますCombineLatest。関数は、各シーケンスから最後のイベントを取得し、それらをリストとしてWhere関数に渡します。関数は、すべてが真であるかどうかをチェックします。これが私の目標を達成する方法です。これで、修飾キーが押されている間にメインキーが2回押されたことがわかります。メインのキーアップイベントにマージすると、状態がクリアされるので、次に修飾キーを押してもシーケンスはトリガーされません。

それで、ここに行きます、それはほとんどそれです。これを投稿しますが、前に言ったように受け入れません。誰かがチャイムを鳴らして教えてくれることを願っています。:)

前もって感謝します!

于 2013-01-15T17:18:27.420 に答える