سؤال

In WPF, I want to be able to use the mouse events with reactive extensions to create an observable for a UIElement that works like a Click event. There's plenty of examples of using this to create drag/drop behavior, but I can't find anything for just a simple click.

I'm anticipating it'll involve observables on MouseLeftButtonDown, MouseLeftButtonUp, MouseLeave, and MouseEnter. But I'm unsure what combination of Merge, SelectMany, TakeUntil, or TakeWhile I need to use. In trying to wrap it all up in an extension, here is what I have so far:

public static IDisposable GetClick(this UIElement item, Action clickAction)
        {
            var obs1 = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeftButtonDown += h,
                    h => item.MouseLeftButtonDown -= h);

            var obs2 = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeftButtonUp += h,
                    h => item.MouseLeftButtonUp -= h);

            var obs3 = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeave += h,
                    h => item.MouseLeave -= h);

            var obs4 = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseEnter += h,
                    h => item.MouseEnter -= h);

             var finalObs = ???

             return finalObs.Subscribe(x => clickAction.Invoke());

}

هل كانت مفيدة؟

المحلول

The following seems to work, but I suspect that it's possible to do it in a neater way.

var click = mouseEnter
    .SelectMany(_ => mouseDown.TakeUntil(mouseLeave))
    .SelectMany(_ => mouseUp.TakeUntil(mouseLeave).Take(1));

I've renamed finalObs to click, obs1 to mouseDown, obs2 to mouseUp...

EDIT: Added Take(1) to fix the flaw pointed out by Enigmativity

EDIT(2):

Here is another solution that I like more.

You'll need to add a .Select(_ => "U") to the definition of mouseUp, .Select(_ => "D") to mouseDown...

var click = Observable.Merge(mouseDown, mouseUp, mouseLeave, mouseEnter)
    .Scan((s, c) => c == "L" ? "" : s + c)  // Create a string of the events, reset on mouseLeave
    .Where(s => s.Length >= 2 && s.Substring(s.Length - 2) == "DU");

After thinking about it, it's impossible to get exactly correct behavior in the case where the user mouse downs over the item, then moves outside of the item, then moves back and mouse ups. This is because you don't get mouse ups when not over the item, so you can't be sure they didn't mouse up, then mouse down while outside.

نصائح أخرى

The correct way is to use this.CaptureMouse and this.ReleaseMouseCapture which solves some of the problems in the accepted answer to do with detecting the mouse leaving and returning. A full (untested) solution using ReactiveUI to bind the events is.

// Create a factory for capturing the mouse and and releasing it as an 
// IDisposable compatible with Observable.Using
Func<IDisposable> captureDisposable = () => {
        this.CaptureMouse(); 
        return Disposable.Create(()=>this.ReleaseMouseCapture());
};

// Capture the mouse and then release it on mouse up
var up = Observable.Using
    ( captureDisposable
    , capture => this.Events().PreviewMouseUp.Take(1)
    );

// Create the click event
var click = this.Events().PreviewMouseDown.Select(e=>up).Switch();
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top