简体   繁体   中英

Parent Control Mouse Enter/Leave Events With Child Controls

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?

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

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!

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

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.

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

I found a very simple solution to this.

I created a simple boolean field in my form:

private bool _cursorInPanel = false;

The container and all child controls are wired to the same event handlers:

private void Container_MouseEnter(object sender, EventArgs e)
{
    _cursorInPanel = true;

    //Do whatever you want when the cursor is inside the container
}

private async void Container_MouseLeave(object sender, EventArgs e)
{
    _cursorInPanel = false;
    await Task.Delay(500);
    if (_cursorInPanel) return;

    //Do whatever you want when the cursor leaves the container
}

This seems to work fine. My objective was to simply show a button when the mouse enters a panel, and this works quite well (in .NET 5 at least)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM