Parent Control Mouse Enter/Leave Events With Child Controls

25,495

Solution 1

After more research, I discovered the Application.AddMessageFilter method. Using this, I created a .NET version of a mouse hook:

class MouseMessageFilter : IMessageFilter, IDisposable
{
    public MouseMessageFilter()
    {
    }

    public void Dispose()
    {
        StopFiltering();
    }

    #region IMessageFilter Members

    public bool PreFilterMessage(ref Message m)
    {
         // Call the appropriate event
         return false;
    }

    #endregion

    #region Events

    public class CancelMouseEventArgs : MouseEventArgs
    {...}

    public delegate void CancelMouseEventHandler(object source, CancelMouseEventArgs e);
    public event CancelMouseEventHandler MouseMove;
    public event CancelMouseEventHandler MouseDown;
    public event CancelMouseEventHandler MouseUp;

    public void StartFiltering()
    {
        StopFiltering();
        Application.AddMessageFilter(this);
    }

    public void StopFiltering()
    {
        Application.RemoveMessageFilter(this);
    }
}

Then, I can handle the MouseMove event in my container control, check to see if the mouse is inside my parent control, and start the work. (I also had to track the last moused over parent control so I could stop the previously started parent.)

---- Edit ----

In my form class, I create and hookup the filter:

public class MyForm : Form
{
   MouseMessageFilter msgFilter;

   public MyForm()
   {...
       msgFilter = new MouseMessageFilter();
       msgFilter.MouseDown += new MouseMessageFilter.CancelMouseEventHandler(msgFilter_MouseDown);
       msgFilter.MouseMove += new MouseMessageFilter.CancelMouseEventHandler(msgFilter_MouseMove);
    }

    private void msgFilter_MouseMove(object source, MouseMessageFilter.CancelMouseEventArgs e)
    {
        if (CheckSomething(e.Control)
            e.Cancel = true;
    }   
}

Solution 2

I feel I found a much better solution than the currently top accepted solution.

The problem with other proposed solutions is that they are either fairly complex (directly handling lower level messages).

Or they fail corner cases: relying on the mouse position on MouseLeave can cause you to miss the mouse exiting if the mouse goes straight from inside a child control to outside the container.

While this solution isn't entirely elegant, it is straightforward and works:

Add a transparent control that takes up the entire space of the container that you want to receive MouseEnter and MouseLeave events for.

I found a good transparent control in Amed's answer here: Making a control transparent

Which I then stripped down to this:

public class TranspCtrl : Control
{
    public TranspCtrl()
    {
        SetStyle(ControlStyles.SupportsTransparentBackColor, true);
        SetStyle(ControlStyles.Opaque, true);
        this.BackColor = Color.Transparent;
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle = cp.ExStyle | 0x20;
            return cp;
        }
    }
}

Example usage:

public class ChangeBackgroundOnMouseEnterAndLeave
{
    public Panel Container;
    public Label FirstLabel;
    public Label SecondLabel;

    public ChangeBackgroundOnMouseEnterAndLeave()
    {
        Container = new Panel();
        Container.Size = new Size(200, 60);

        FirstLabel = new Label();
        FirstLabel.Text = "First Label";
        FirstLabel.Top = 5;

        SecondLabel = new Label();
        SecondLabel.Text = "Second Lable";
        SecondLabel.Top = 30;

        FirstLabel.Parent = Container;
        SecondLabel.Parent = Container;

        Container.BackColor = Color.Teal;

        var transparentControl = new TranspCtrl();
        transparentControl.Size = Container.Size;

        transparentControl.MouseEnter += MouseEntered;
        transparentControl.MouseLeave += MouseLeft;

        transparentControl.Parent = Container;
        transparentControl.BringToFront();
    }

    void MouseLeft(object sender, EventArgs e)
    {
        Container.BackColor = Color.Teal;
    }

    void MouseEntered(object sender, EventArgs e)
    {
        Container.BackColor = Color.Pink;
    }
}

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        var test = new ChangeBackgroundOnMouseEnterAndLeave();
        test.Container.Top = 20;
        test.Container.Left = 20;
        test.Container.Parent = this;
    }
}

Enjoy proper MouseLeave and MouseEnter events!

Solution 3

You can find out whether the mouse is within the bounds of your control like this (assuming this code resides in your container control; if not, replace this with a reference to the container control):

private void MyControl_MouseLeave(object sender, EventArgs e)
{
    if (this.ClientRectangle.Contains(this.PointToClient(Cursor.Position)))
    {
        // the mouse is inside the control bounds
    }
    else
    {
        // the mouse is outside the control bounds
    }
}

Solution 4

i don't think you need to hook the message pump to solve this. Some flagging in your UI should do the trick. i'm thinking that you create a member variable, something like Control _someParent, in your controlling class which will take the reference of the parent control when one of your OnMouseEnter handlers is called. Then, in OnMouseLeave, check the value of the _someParent "flag" and if it's the same as the current sender's then do not actually stop your processing, just return. Only when the parent is different do you stop and reset _someParent to null.

Solution 5

I had the exact same need. Paul Williams' answer provided me with the core idea, but I had difficulty understanding the code. I found another take here, and together, the two examples helped me develop my own version.

To initialize, you pass the container control of interest into the ContainerMessageFilter constructor. The class collects the window handles of the container and all child controls within it.

Then, during operation, the class filters the WM_MOUSEMOVE message, checking the messages's HWnd to determine what control the mouse is moving within. In this way, it determines when the mouse has moved within or outside the set of controls within the container that it is watching.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

public class ContainerMessageFilter : IMessageFilter {
    private const int WM_MOUSEMOVE = 0x0200;

    public event EventHandler MouseEnter;
    public event EventHandler MouseLeave;

    private bool insideContainer;
    private readonly IEnumerable<IntPtr> handles;

    public ContainerMessageFilter( Control container ) {
        handles = CollectContainerHandles( container );
    }

    private static IEnumerable<IntPtr> CollectContainerHandles( Control container ) {
        var handles = new List<IntPtr> { container.Handle };

        RecurseControls( container.Controls, handles );

        return handles;
    }

    private static void RecurseControls( IEnumerable controls, List<IntPtr> handles ) {
        foreach ( Control control in controls ) {
            handles.Add( control.Handle );

            RecurseControls( control.Controls, handles );
        }
    }

    public bool PreFilterMessage( ref Message m ) {
        if ( m.Msg == WM_MOUSEMOVE ) {
            if ( handles.Contains( m.HWnd ) ) {
                // Mouse is inside container
                if ( !insideContainer ) {
                    // was out, now in
                    insideContainer = true;
                    OnMouseEnter( EventArgs.Empty );
                }
            }
            else {
                // Mouse is outside container
                if ( insideContainer ) {
                    // was in, now out
                    insideContainer = false;
                    OnMouseLeave( EventArgs.Empty );
                }
            }
        }

        return false;
    }

    protected virtual void OnMouseEnter( EventArgs e ) {
        var handler = MouseEnter;
        handler?.Invoke( this, e );
    }

    protected virtual void OnMouseLeave( EventArgs e ) {
        var handler = MouseLeave;
        handler?.Invoke( this, e );
    }
}

In the following usage example, we want to monitor mouse entry and exit for a Panel and the child controls that it contains:

public partial class Form1 : Form {
    private readonly ContainerMessageFilter containerMessageFilter;

    public Form1() {
        InitializeComponent();

        containerMessageFilter = new ContainerMessageFilter( panel1 );
        containerMessageFilter.MouseEnter += ContainerMessageFilter_MouseEnter;
        containerMessageFilter.MouseLeave += ContainerMessageFilter_MouseLeave;
        Application.AddMessageFilter( containerMessageFilter );
    }

    private static void ContainerMessageFilter_MouseLeave( object sender, EventArgs e ) {
        Console.WriteLine( "Leave" );
    }

    private static void ContainerMessageFilter_MouseEnter( object sender, EventArgs e ) {
        Console.WriteLine( "Enter" );
    }

    private void Form1_FormClosed( object sender, FormClosedEventArgs e ) {
        Application.RemoveMessageFilter( containerMessageFilter );
    }
}
Share:
25,495

Related videos on Youtube

Paul Williams
Author by

Paul Williams

I've been programming for about 24 years now (eek!). My first job was in COBOL, but mainframes and I do not mix. I coded several years in Java back in the JDK 1.0.2 days. Did some C++ and COM. Worked on Java + WFC from Microsoft. Been writing C# and SQL code since 2004. Married with two kids. Kids are destined to either game or sympathize with gamers. Wife is wonderful, understanding, and sympathetic, but she=g=d=+++----CARRIER LOST

Updated on October 30, 2021

Comments

  • Paul Williams
    Paul Williams over 2 years

    I have a C# .NET 2.0 WinForms app. My app has a control that is a container for two child controls: a label, and some kind of edit control. You can think of it like this, where the outer box is the parent control:

    +---------------------------------+ 
    | [Label Control]  [Edit Control] |
    +---------------------------------+

    I am trying to do something when the mouse enters or leaves the parent control, but I don't care if the mouse moves into one of its children. I want a single flag to represent "the mouse is somewhere inside the parent or children" and "the mouse has moved outside of the parent control bounds".

    I've tried handling MouseEnter and MouseLeave on the parent and both child controls, but this means the action begins and ends multiple times as the mouse moves across the control. In other words, I get this:

    Parent.OnMouseEnter      (start doing something)
    Parent.OnMouseLeave      (stop)
    Child.OnMouseEnter       (start doing something)
    Child.OnMouseLeave       (stop)
    Parent.OnMouseEnter      (start doing something)
    Parent.OnMouseLeave      (stop)

    The intermediate OnMouseLeave events cause some undesired effects as whatever I'm doing gets started and then stopped. I want to avoid that.

    I don't want to capture the mouse as the parent gets the mouse over, because the child controls need their mouse events, and I want menu and other shortcut keys to work.

    Is there a way to do this inside the .NET framework? Or do I need to use a Windows mouse hook?

  • Paul Williams
    Paul Williams almost 15 years
    Interesting idea, but it seems not to work in practice. Sometimes the last set of events for a given parent control are Child.MouseEnter and Child.MouseLeave. In my Child.MouseLeave handler, sometimes the Cursor.Position is still in the bounds of the parent. The code thinks the cursor is still inside, so the "mouse is inside the parent" action never gets stopped.
  • Paul Williams
    Paul Williams almost 15 years
    If I leave the current parent, I don't know in advance I'm going into a child control or really leaving the parent. And if I did as you suggest, the parent OnMouseLeave would have to say "hey, it's me, so continue processing", so moving outside of the control and not into another control would not stop the processing.
  • Paul Sasik
    Paul Sasik almost 15 years
    That's a good point though if your control containment structure doesn't have to be too flexible i.e. n-level containment and/or you take into account the parent type then you could still use this method. For example, you get an enter event, then you take the control it came from and walk up the containment tree until you see a GroupBox or Panel (whatever is containing your buttons) and evaluate on the reference to that value. When it changes.
  • Johan Tidén
    Johan Tidén almost 12 years
    This works great! I hooked up all the child events to the same handlers in the parent class then checked the mouse location. I saved a boolean that saves whether the mouse is inside the control (in order to supress handling of child-to-parent mouse moves etc.
  • jnm2
    jnm2 about 10 years
    This doesn't handle what happens when a topmost window (like task manager) is partially on top of the screen rectangle. This code will act as though the mouse hasn't left when the mouse enters the part of the topmost window that is covering the control.
  • Benny
    Benny about 5 years
    this solution prevent those controls under the transparent control from receiving any mouse event
  • Vali Maties
    Vali Maties over 4 years
    I don't think it's a good idea when your child controls need to be clicked or to receive focus.
  • Ray White
    Ray White over 3 years
    I tried this, but my child button no longer got click events, as described in the other comments above. Not a solution for me.