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

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


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;}


Approach #1 - is still a valid one (as a pure managed solution) if you work out the specifics.
(capture could be given to a specific control to avoid issues, but I haven't tried)

This should help you get the events ('fixed' events).

Key is to track the mouse move when outside window (and only when mouse is down).

For that you'd need to do the capture (but slightly different than suggested as that won't work - on down/up instead).

private void Window_MouseDown(object sender, MouseEventArgs e){    this.CaptureMouse();}private void Window_MouseUp(object sender, MouseEventArgs e){    this.ReleaseMouseCapture();}private void Window_MouseLeave(object sender, MouseEventArgs e){    test1.Content = "Mouse left";}private void Window_MouseEnter(object sender, MouseEventArgs e){    test1.Content = "Mouse entered";}private void Window_MouseMove(object sender, MouseEventArgs e){    if (Mouse.Captured == this)    {        if (!this.IsMouseInBounds(e))            Window_MouseLeave(sender, e);        else            Window_MouseEnter(sender, e);    }    test2.Content = e.GetPosition(this).ToString();}private bool IsMouseInBounds(MouseEventArgs e){    var client = ((FrameworkElement)this.Content);    Rect bounds = new Rect(0, 0, client.ActualWidth, client.ActualHeight);    return bounds.Contains(e.GetPosition(this));}private Point GetRealPosition(Point mousePoint){    return Application.Current.MainWindow.PointFromScreen(mousePoint);}

Note:
You'd need to finish this according to your situation. I have just 'dummy wired' the mouse move to Enter and Leave and w/o any smart algorithm there (i.e. generated enter/leave will keep on firing). I.e. add some flag to actually save the state of the enter/leave properly.

Also I'm measuring whether mouse is within the 'client bounds' of the Window. You'd need to adjust that if you need that in respect of borders etc.

Also I forgot to add the obvious - wire up the new events MouseDown="Window_MouseDown" MouseUp="Window_MouseUp"


That's "normal" behaviour. Capture the mouse inside MouseEnter handler.

Mouse.Capture(yourUIElement);

and later release it in MouseLeave,

Mouse.Capture(null);

Edited: More explanation.WPF does not track mouse movement precisely. You can deduce that from the fact that if you capture MouseMove event, you can see that the it reports you event every 20milliseconds intervals and not by pixel precision.. more like 8 pixels per event.

Now this is not that horrible, but WPF also does not report mouse movement outside the window, if you happen to move your mouse. This is default behaviour. You can change it throuh Mouse.Capture as said.

Now, you can imagine why this problem happens. If you can move your mouse outside the window faster than mouse move report happens, then WPF still thinks that it's inside the application.