Question

In WPF I would like to subscribe to the mouse position points only while a key is held down. I then want to set the captured points onto a property, only when the key is released (i.e. when I have a full set of the captured points) and continue listening for the next key down/up combination to create another capture of mouse positions, etc.

My interpretation of the above is that I need to trigger a sequence when a key is down and stop taking when a key is released, but I want the OnNext to receive a set of mouse points.

From a fair amount of reading (I'm new to Rx) I've put together the following pseudo/real sample:

var keyDownSeq = Observable.FromEvent(...);
var keyUpSeq = Observable.FromEvent(...);
var mouseMoveSeq = Observable.FromEvent(...);

var mouseMovesWhileKeyDown = keyDownSeq
    .Where(keyEventArgs => keyEventArgs.IsRepeat == false) //WPF fires the same KeyDown repeatedly
    .Where(keyEventArgs => keyEventArgs.Key == Key.Space)
    .Select(_ => mouseMoveSeq
                    .TakeUntil(keyUpSeq)
                    .ToList())
    .Subscribe(listOfMousePoints => MyProperty = listOfMousePoints);
  1. Will the above do what I think it will and create a list of mouse points that were encountered while the Space bar is held down? Do I need to call ToList() where I do, or should I do this in the Subscribe?

  2. If I remove the second Where clause (allowing any key(s) to be pressed to begin a capture) how can I prevent a second, or third, key being held down and causing duplicates in the resulting sequence?

Thank you.

Edit

Would it be completely incorrect to do the following using a local variable?

  • Set a local variable to the KeyDown sequence value in the Select()
  • Reset the local variable to null when the KeyUpSeq encounters the same key
  • Filter the KeyDownSeq to ignore all values while this variable has value
  • Filter the KeyUpSeq to ignore all KeyUp values that don't match the local variable

Does Rx have a concept of such a local state variable?

Was it helpful?

Solution 2

You have to be careful with race conditions here.

With some setup.

IObservable<Unit> keyDown = Observable.FromEvent(/*keydown*/).Select(_=>true);
IObservable<Unit> keyUp = Observable.FromEvent(/*keyup*/).Select(_=>false);

IObservable<Point> mouseMoves = Observable.FromEvent(...);

Now create an observable that we know will start with a keyDown event

var keys = keyDown.Merge(keyUp).DistinctUntilChanged().SkipWhile(_=>!_);

Use this observable to create a window.

IOBservable<IObservable<Point> mousePaths = 
    mouseMoves
       .Window(keys)
       .Where((_,i)=>i%2==0);

Note that we want to skip the odd windows. Even windows are key down. Odd windows are key up.

Now there are no race conditions and we will never miss a keyDown or keyUp event.

Explanation If you use the other overload of Window, the one with a seperate open and closing trigger, the problem is that the closing trigger generation is lazy. It is not generated and does not subscribe to it's source until the opening edge of the window is triggered. That means there is a small time window for a keyUp event to occur before the window closing trigger is registered.

OTHER TIPS

I see two ways to simplify you setup. The first is to create a single IObservable<bool> that emits exactly when your key switches between down or up position.

//true means key down, false means key up
IObservable<bool> keyChange =
    Observable.Merge(
        Observable.FromEvent(/*keyDown*/).Select(_ => true),
        Observable.FromEvent(/*keyUp*/).Select(_ => false))
    .DistinctUntilChanged();

This is what the histories will look like, where a x is an event firing, a T or F is a boolean, and left to right is increasing time.

keyDown    -----xxxxxxxx--------xxxxxxx------xxxxxxxxxxxxx----
keyUp      xxxxx--------xxxxxxxx-------xxxxxx-------------xxxx
keyChange  F----T-------F-------T------F-----T------------F---

The second is to use Observable.Window to pull out contiguous sequences of mouse moves between key down "window openings" and key up "window closings."

IObservable<Point> mouseMoves = Observable.FromEvent(...);

IObservable<IObservable<Point>> mousePaths = mouseMoves.Window(
    keyChange.Where(b => b),
    _ => keyChange.Where(b => !b));

Documentation for methods used:

The signature for Window can be a little scary at first, but once you understand it it's fairly simple to use.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top