代码之家  ›  专栏  ›  技术社区  ›  Paul Williams

父控件鼠标进入/离开具有子控件的事件

  •  19
  • Paul Williams  · 技术社区  · 17 年前

    我有一个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 回复  |  直到 8 年前
        1
  •  8
  •   Paul Williams    17 年前

    经过更多的研究,我发现 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事件,检查鼠标是否在父控件中,然后开始工作。(我还必须跟踪上一个鼠标悬停在父控件上的情况,以便停止先前启动的父控件。)

    ------编辑

    在我的窗体类中,我创建并连接筛选器:

    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;
        }   
    }
    
        2
  •  7
  •   Community Mohan Dere    9 年前

    我觉得我找到了一个比目前公认的顶级解决方案更好的解决方案。

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

    或者,它们会在角情况下失败:如果鼠标直接从子控件内部转到容器外部,则依赖鼠标在mouseleave上的位置可能会导致错过鼠标退出。

    虽然此解决方案并不完全优雅,但它非常简单且有效:

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

    我在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活动!

        3
  •  3
  •   Fredrik Mörk    17 年前

    您可以像这样找出鼠标是否在控件的边界内(假定此代码驻留在容器控件中;如果不在,则替换 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
        }
    }
    
        4
  •  2
  •   Paul Sasik    17 年前

    我认为你不需要挂信息泵来解决这个问题。用户界面中的一些标记应该可以做到这一点。我认为您在控制类中创建了一个成员变量,类似于control-someparent,当调用一个onMouseEnter处理程序时,它将引用父控件。然后,在onmouseleave中,检查someparent“标志”的值,如果它与当前发件人的值相同,则不要实际停止处理,只需返回即可。只有当父级不同时,您才会停止并将ousomeParent重置为空。

        5
  •  1
  •   Darryl    8 年前

    我也有同样的需求。保罗·威廉姆斯的回答为我提供了核心思想,但我很难理解代码。我又找到了一个 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 );
        }
    }
    
    推荐文章