WPF: mouse leave event doesn't trigger with mouse down

Fedor T. picture Fedor T. · Apr 12, 2013 · Viewed 11.9k times · Source

I'm having an issue with mouse enter / leave events. When mouse button is pressed and hold with cursor inside the control and then cursor is moved out of the control fast enough this events don't trigger.

Could you please advice me why it happens? Is there any way to obtain these events properly?

Please check the sample project to see it in action: https://www.dropbox.com/s/w5ra2vzegjtauso/SampleApp.zip

Update. I have found the same problem here without an answer. Started bounty there.

Answer

Mike Fuchs picture Mike Fuchs · Apr 15, 2013

EDIT: After Sisyphe correctly noted that the behavior did not work for elements with mouse interaction, I have rewritten the code.

The behavior can be attached to a window or any other FrameworkElement. By default, all contained elements will be monitored for MouseLeave while the left mouse button is down, and the handlers executed. The behavior can also be applied just to its associated element by setting MonitorSubControls="False".

What the behavior does, basically (refer to the comments in the code for more detail):

  • Is only "active" if left mouse button is pressed
  • Watches for mouse position changes from in- to outside of an element. In this case, executes the event handlers.

Known limitations (could all be resolved with some more effort, I reckon, but don't seem too important to me):

  • Does not execute handlers for transitions to a contained element ("inner" boundaries)
  • Does not guarantee correct order of execution of the handlers
  • Does not resolve that for slow transitions to the outside of the window, e.LeftButton is reported as released (bug?).
  • I decided not to use the Win32 hook and instead using a timer, who will not fire more than about every 0.15 seconds (despite a smaller interval set, clock drift?). For fast mouse movements, the evaluated points could be too far apart and miss an element that is just flitted across.

enter image description here

This script produces the output below: With the behavior attached to the window, moving inside the orangeBorder (leaves blueBorder by inner boundary with mouse button released: 0), pressing left mousebutton inside the orange border and moving (fast) outside the window executes the leave handlers (1 - 4). Releasing the mouse button outside the window, moving back in over the goldTextBox (5), pressing left mousebutton in the textbox, leaving (fast or slow) outside the window again executes the correct handlers (6 - 9).

enter image description here

Xaml (example):

<Window x:Class="WpfApplication1.MouseLeaveControlWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:beh="clr-namespace:WpfApplication1.Behavior"
            Title="MouseLeaveControlWindow" Height="300" Width="300" x:Name="window" MouseLeave="OnMouseLeave">
    <i:Interaction.Behaviors>
        <beh:MonitorMouseLeaveBehavior />
    </i:Interaction.Behaviors>
    <Grid x:Name="grid" MouseLeave="OnMouseLeave" Background="Transparent">
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Border x:Name="blueBorder" MouseLeave="OnMouseLeave" Background="SteelBlue" Margin="50" Grid.RowSpan="2" />
        <Border x:Name="orangeBorder" MouseLeave="OnMouseLeave"  Background="DarkOrange" Margin="70, 70, 70, 20" />
        <TextBox x:Name="goldTextBox" MouseLeave="OnMouseLeave" Background="Gold" Margin="70, 20, 70, 70" Grid.Row="1" Text="I'm a TextBox" />
    </Grid>
</Window>

Code behind (just for debug purpose):

public partial class MouseLeaveControlWindow : Window
{
    public MouseLeaveControlWindow()
    {
        InitializeComponent();
    }

    private int i = 0;
    private void OnMouseLeave(object sender, MouseEventArgs e)
    {
        FrameworkElement fe = (FrameworkElement)sender;
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            System.Diagnostics.Debug.WriteLine(string.Format("{0}: Left {1}.", i, fe.Name)); i++;
        }
        else
        {
            System.Diagnostics.Debug.WriteLine(string.Format("{0}: Left {1} (Released).", i, fe.Name)); i++;
        }
    }
}

MonitorMouseLeaveBehavior:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Interop;
using System.ComponentModel;
using System.Windows.Media;
using WpfApplication1.Helpers;

namespace WpfApplication1.Behavior
{
    public class MonitorMouseLeaveBehavior : Behavior<FrameworkElement>
    {
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };

        [DllImport("user32.dll")]
        public static extern short GetAsyncKeyState(UInt16 virtualKeyCode);

        private enum VK
        {
            LBUTTON = 0x01
        }

        private bool _tracking;
        private const int _interval = 1;
        private Timer _checkPosTimer = new Timer(_interval);
        private Dictionary<FrameworkElement, RoutedEventHandlerInfo[]> _leaveHandlersForElement = new Dictionary<FrameworkElement, RoutedEventHandlerInfo[]>();
        private Window _window;
        private Dictionary<FrameworkElement, Rect> _boundsByElement = new Dictionary<FrameworkElement, Rect>();
        private Dictionary<FrameworkElement, bool> _wasInside = new Dictionary<FrameworkElement, bool>();
        private List<FrameworkElement> _elements = new List<FrameworkElement>();


        /// <summary>
        /// If true, all subcontrols are monitored for the mouseleave event when left mousebutton is down.
        /// True by default.
        /// </summary>
        public bool MonitorSubControls { get { return (bool)GetValue(MonitorSubControlsProperty); } set { SetValue(MonitorSubControlsProperty, value); } }
        public static readonly DependencyProperty MonitorSubControlsProperty = DependencyProperty.Register("MonitorSubControls", typeof(bool), typeof(MonitorMouseLeaveBehavior), new PropertyMetadata(true, OnMonitorSubControlsChanged));

        private static void OnMonitorSubControlsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            MonitorMouseLeaveBehavior beh = (MonitorMouseLeaveBehavior)d;
            beh.AddOrRemoveLogicalChildren((bool)e.NewValue);
        }

        /// <summary>
        /// Initial actions
        /// </summary>
        protected override void OnAttached()
        {
            _window = this.AssociatedObject is Window ? (Window)this.AssociatedObject : Window.GetWindow(this.AssociatedObject); // get window
            _window.SourceInitialized += (s, e) =>
            {
                this.AddOrRemoveLogicalChildren(this.MonitorSubControls); // get all monitored elements
                this.AttachHandlers(true); // attach mousedown and sizechanged handlers
                this.GetAllBounds(); // determine bounds of all elements
                _checkPosTimer.Elapsed += (s1, e1) => Dispatcher.BeginInvoke((Action)(() => { CheckPosition(); }));
            };
            base.OnAttached();
        }

        protected override void OnDetaching()
        {
            this.AttachHandlers(false);
            base.OnDetaching();
        }

        /// <summary>
        /// Starts or stops monitoring of the AssociatedObject's logical children.
        /// </summary>
        /// <param name="add"></param>
        private void AddOrRemoveLogicalChildren(bool add)
        {
            if (_window != null && _window.IsInitialized)
            {
                AddOrRemoveSizeChangedHandlers(false);
                _elements.Clear();
                if (add)
                    _elements.AddRange(VisualHelper.FindLogicalChildren<FrameworkElement>(this.AssociatedObject));
                _elements.Add(this.AssociatedObject);
                AddOrRemoveSizeChangedHandlers(true);
            }
        }

        /// <summary>
        /// Attaches/detaches size changed handlers to the monitored elements
        /// </summary>
        /// <param name="add"></param>
        private void AddOrRemoveSizeChangedHandlers(bool add)
        {
            foreach (var element in _elements)
            {
                element.SizeChanged -= element_SizeChanged;
                if (add) element.SizeChanged += element_SizeChanged;
            }
        }

        /// <summary>
        /// Adjusts the stored bounds to the changed size
        /// </summary>
        void element_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            FrameworkElement fe = sender as FrameworkElement;
            if (fe != null)
                GetBounds(fe);
        }

        /// <summary>
        /// Attaches/Detaches MouseLeftButtonDown and SizeChanged handlers 
        /// </summary>
        /// <param name="attach">true: attach, false: detach</param>
        private void AttachHandlers(bool attach)
        {
            AddOrRemoveSizeChangedHandlers(attach);

            if (attach)
                _window.PreviewMouseLeftButtonDown += window_PreviewMouseLeftButtonDown;
            else // detach
                _window.PreviewMouseLeftButtonDown -= window_PreviewMouseLeftButtonDown;
        }

        /// <summary>
        /// Gets the bounds for all monitored elements
        /// </summary>
        private void GetAllBounds()
        {
            _boundsByElement.Clear();
            foreach (var element in _elements)
                GetBounds(element);
        }

        /// <summary>
        /// Gets the bounds of the control, which are used to check if the mouse position
        /// is located within. Note that this only covers rectangular control shapes.
        /// </summary>
        private void GetBounds(FrameworkElement element)
        {
            Point p1 = new Point(0, 0);
            Point p2 = new Point(element.ActualWidth, element.ActualHeight);
            p1 = element.TransformToVisual(_window).Transform(p1);
            p2 = element.TransformToVisual(_window).Transform(p2);

            if (element == _window) // window bounds need to account for the border
            {
                var titleHeight = SystemParameters.WindowCaptionHeight + 2 * SystemParameters.ResizeFrameHorizontalBorderHeight; //  not sure about that one
                var verticalBorderWidth = SystemParameters.ResizeFrameVerticalBorderWidth;
                p1.Offset(-verticalBorderWidth, -titleHeight);
                p2.Offset(-verticalBorderWidth, -titleHeight);
            }

            Rect bounds = new Rect(p1, p2);

            if (_boundsByElement.ContainsKey(element))
                _boundsByElement[element] = bounds;
            else
                _boundsByElement.Add(element, bounds);
        }

        /// <summary>
        /// For all monitored elements, detach the MouseLeave event handlers and store them locally,
        /// to be executed manually.
        /// </summary>
        private void RerouteLeaveHandlers()
        {
            foreach (var element in _elements)
            {
                if (!_leaveHandlersForElement.ContainsKey(element))
                {
                    var handlers = ReflectionHelper.GetRoutedEventHandlers(element, UIElement.MouseLeaveEvent);
                    if (handlers != null)
                    {
                        _leaveHandlersForElement.Add(element, handlers);
                        foreach (var handler in handlers)
                            element.MouseLeave -= (MouseEventHandler)handler.Handler; // detach handlers
                    }
                }
            }
        }

        /// <summary>
        /// Reattach all leave handlers that were detached in window_PreviewMouseLeftButtonDown.
        /// </summary>
        private void ReattachLeaveHandlers()
        {
            foreach (var kvp in _leaveHandlersForElement)
            {
                FrameworkElement fe = kvp.Key;
                foreach (var handler in kvp.Value)
                {
                    if (handler.Handler is MouseEventHandler)
                        fe.MouseLeave += (MouseEventHandler)handler.Handler;
                }
            }

            _leaveHandlersForElement.Clear();
        }

        /// <summary>
        /// Checks if the mouse position is inside the bounds of the elements
        /// If there is a transition from inside to outside, the leave event handlers are executed
        /// </summary>
        private void DetermineIsInside()
        {
            Point p = _window.PointFromScreen(GetMousePosition());
            foreach (var element in _elements)
            {
                if (_boundsByElement.ContainsKey(element))
                {
                    bool isInside = _boundsByElement[element].Contains(p);
                    bool wasInside = _wasInside.ContainsKey(element) && _wasInside[element];

                    if (wasInside && !isInside)
                        ExecuteLeaveHandlers(element);

                    if (_wasInside.ContainsKey(element))
                        _wasInside[element] = isInside;
                    else
                        _wasInside.Add(element, isInside);
                }
            }
        }

        /// <summary>
        /// Gets the mouse position relative to the screen
        /// </summary>
        public static Point GetMousePosition()
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }

        /// <summary>
        /// Gets the mouse button state. MouseEventArgs.LeftButton is notoriously unreliable.
        /// </summary>
        private bool IsMouseLeftButtonPressed()
        {
            short leftMouseKeyState = GetAsyncKeyState((ushort)VK.LBUTTON);
            bool ispressed = leftMouseKeyState < 0;

            return ispressed;
        }

        /// <summary>
        /// Executes the leave handlers that were attached to the controls.
        /// They have been detached previously by this behavior (see window_PreviewMouseLeftButtonDown), to prevent double execution.
        /// After mouseup, they are reattached (see CheckPosition)
        /// </summary>
        private void ExecuteLeaveHandlers(FrameworkElement fe)
        {
            MouseDevice mouseDev = InputManager.Current.PrimaryMouseDevice;
            MouseEventArgs mouseEvent = new MouseEventArgs(mouseDev, 0) { RoutedEvent = Control.MouseLeaveEvent };

            if (_leaveHandlersForElement.ContainsKey(fe))
            {
                foreach (var handler in _leaveHandlersForElement[fe])
                {
                    if (handler.Handler is MouseEventHandler)
                        ((MouseEventHandler)handler.Handler).Invoke(fe, mouseEvent);
                }
            }
        }

        /// <summary>
        /// Sets the mouse capture (events outside the window are still directed to it),
        /// and tells the behavior to watch out for a missed leave event
        /// </summary>
        private void window_PreviewMouseLeftButtonDown(object sender, MouseEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("--- left mousebutton down ---"); // todo remove

            this.RerouteLeaveHandlers();
            _tracking = true;
            _checkPosTimer.Start();
        }

        /// <summary>
        /// Uses the _tracking field as well as left mouse button state to determine if either 
        /// leave event handlers should be executed, or monitoring should be stopped.
        /// </summary>
        private void CheckPosition()
        {
            if (_tracking)
            {
                if (IsMouseLeftButtonPressed())
                {
                    this.DetermineIsInside();
                }
                else
                {
                    _wasInside.Clear();
                    _tracking = false;
                    _checkPosTimer.Stop();
                    System.Diagnostics.Debug.WriteLine("--- left mousebutton up ---"); // todo remove

                    // invoking ReattachLeaveHandlers() immediately would rethrow MouseLeave for top grid/window 
                    // if both a) mouse is outside window and b) mouse moves. Wait with reattach until mouse is inside window again and moves.
                    _window.MouseMove += ReattachHandler; 
                }
            }
        }

        /// <summary>
        /// Handles the first _window.MouseMove event after left mouse button was released,
        /// and reattaches the MouseLeaveHandlers. Detaches itself to be executed only once.
        /// </summary>
        private void ReattachHandler(object sender, MouseEventArgs e)
        {
            ReattachLeaveHandlers();
            _window.MouseMove -= ReattachHandler; // only once
        }
    }
}

VisualHelper.FindLogicalChildren, ReflectionHelper.GetRoutedEventHandlers:

public static List<T> FindLogicalChildren<T>(DependencyObject obj) where T : DependencyObject
{
    List<T> children = new List<T>();
    foreach (var child in LogicalTreeHelper.GetChildren(obj))
    {
        if (child != null)
        {
            if (child is T)
                children.Add((T)child);

            if (child is DependencyObject)
                children.AddRange(FindLogicalChildren<T>((DependencyObject)child)); // recursive
        }
    }
    return children;
}
/// <summary>
/// Gets the list of routed event handlers subscribed to the specified routed event.
/// </summary>
/// <param name="element">The UI element on which the event is defined.</param>
/// <param name="routedEvent">The routed event for which to retrieve the event handlers.</param>
/// <returns>The list of subscribed routed event handlers.</returns>
public static RoutedEventHandlerInfo[] GetRoutedEventHandlers(UIElement element, RoutedEvent routedEvent)
{
    var routedEventHandlers = default(RoutedEventHandlerInfo[]);
    // Get the EventHandlersStore instance which holds event handlers for the specified element.
    // The EventHandlersStore class is declared as internal.
    var eventHandlersStoreProperty = typeof(UIElement).GetProperty("EventHandlersStore", BindingFlags.Instance | BindingFlags.NonPublic);
    object eventHandlersStore = eventHandlersStoreProperty.GetValue(element, null);

    if (eventHandlersStore != null)
    {
        // Invoke the GetRoutedEventHandlers method on the EventHandlersStore instance 
        // for getting an array of the subscribed event handlers.
        var getRoutedEventHandlers = eventHandlersStore.GetType().GetMethod("GetRoutedEventHandlers", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        routedEventHandlers = (RoutedEventHandlerInfo[])getRoutedEventHandlers.Invoke(eventHandlersStore, new object[] { routedEvent });
    }
    return routedEventHandlers;
}