父控制鼠标使用子控件输入/离开事件

时间:2009-07-21 19:33:42

标签: c# .net winforms events mouse

我有一个C#.NET 2.0 WinForms应用程序。我的应用程序有一个控件,它是两个子控件的容器:标签和某种编辑控件。您可以这样想,外框是父控件:

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

我试图在鼠标进入或离开父控件时做某事,但我不在乎鼠标是否移动到其中一个子控件中。我想要一个标志来表示“鼠标在父或子的内部”和“鼠标移动到父控件边界之外”。

我尝试在父级和两个子控件上处理MouseEnter和MouseLeave,但这意味着当鼠标在控件上移动时,操作会多次开始和结束。换句话说,我明白了:

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

中间的OnMouseLeave事件会导致一些不受欢迎的影响,因为我正在做的事情开始然后停止。我想避免这种情况。

我不想捕获鼠标,因为父控件需要鼠标移动,因为子控件需要鼠标事件,我希望菜单和其他快捷键能够工作。

有没有办法在.NET框架内执行此操作?或者我需要使用Windows鼠标挂钩吗?

5 个答案:

答案 0 :(得分:8)

经过更多的研究,我发现了Application.AddMessageFilter method。使用它,我创建了一个鼠标钩子的.NET版本:

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

然后,我可以在容器控件中处理MouseMove事件,检查鼠标是否在我的父控件中,然后开始工作。 (我还必须跟踪最后一个moused over parent控件,这样我就可以停止之前启动的父控件了。)

----编辑----

在我的表单类中,我创建并连接过滤器:

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

答案 1 :(得分:7)

我觉得我找到了比当前最受欢迎的解决方案更好的解决方案。

其他提出的解决方案的问题是它们要么相当复杂(直接处理较低级别的消息)。

或者他们在角落情况下失败:如果鼠标直接从子控件内部移动到容器外部,依赖MouseLeave上的鼠标位置会导致您错过鼠标退出。

虽然这个解决方案并不完全优雅,但它很简单并且有效:

添加一个透明控件,占用要接收MouseEnter和MouseLeave事件的容器的整个空间。

我在Amed的答案中找到了一个很好的透明控制:Making a control transparent

然后我将其删除:

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

使用示例:

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

享受正确的MouseLeave和MouseEnter事件!

答案 2 :(得分:3)

你可以像这样找出鼠标是否在你的控件的范围内(假设这个代码驻留在你的容器控件中;如果没有,用引用容器控件替换this):

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

答案 3 :(得分:2)

我认为你不需要挂钩消息泵来解决这个问题。您的UI中的某些标记应该可以解决问题。我在想你在你的控制类中创建一个成员变量,比如Control _someParent,当你的一个OnMouseEnter处理程序被调用时,它将引用父控件。然后,在OnMouseLeave中,检查_someParent“flag”的值,如果它与当前发送者的值相同,则不要实际停止处理,只需返回。只有当父级不同时,才会停止并将_someParent重置为null。

答案 4 :(得分:1)

我有完全相同的需求。 Paul Williams的回答为我提供了核心思想,但我很难理解代码。我找到了另一个here,这两个例子帮助我开发了自己的版本。

要进行初始化,请将感兴趣的容器控件传递给ContainerMessageFilter构造函数。该类收集容器的窗口句柄及其中的所有子控件。

然后,在操作期间,该类过滤WM_MOUSEMOVE消息,检查消息的HWnd以确定鼠标在哪个控件内移动。通过这种方式,它可以确定鼠标何时移动到正在观察的容器内的控件集内部或外部。

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

在以下用法示例中,我们要监视Panel及其包含的子控件的鼠标进入和退出:

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