Родительский контроль Мышь Вводить / оставлять события с дочерними элементами

У меня есть приложение WinForms на C# .NET 2.0. Мое приложение имеет элемент управления, который является контейнером для двух дочерних элементов управления: метки и некоторого элемента управления редактирования. Вы можете думать об этом так, где внешний блок является родительским элементом управления:

+ --------------------------------- + 
| [Контроль надписей]  [Редактировать контроль] |
+---------------------------------+

Я пытаюсь что-то сделать, когда мышь входит в родительский элемент управления или покидает его, но мне все равно, переместится ли мышь в одного из ее дочерних элементов. Я хочу, чтобы один флаг представлял "мышь находится где-то внутри родителя или потомков" и "мышь вышла за пределы родительского элемента управления".

Я попытался обработать MouseEnter и MouseLeave на родительском и обоих дочерних элементах управления, но это означает, что действие начинается и заканчивается несколько раз, когда мышь перемещается по элементу управления. Другими словами, я получаю это:

Parent.OnMouseEnter (начать что-то делать)
Parent.OnMouseLeave      (остановка)
Child.OnMouseEnter       (начать что-то делать)
Child.OnMouseLeave       (остановка)
Parent.OnMouseEnter      (начать что-то делать)
Parent.OnMouseLeave      (остановка)

Промежуточные события OnMouseLeave вызывают некоторые нежелательные эффекты, поскольку все, что я делаю, запускается, а затем останавливается. Я хочу избежать этого.

Я не хочу захватывать мышь, так как родитель перебирает мышь, потому что дочерние элементы управления нуждаются в событиях мыши, и я хочу, чтобы меню и другие сочетания клавиш работали.

Есть ли способ сделать это внутри.NET Framework? Или мне нужно использовать крючок мыши для Windows?

6 ответов

Решение

После дополнительных исследований я обнаружил метод Application.AddMessageFilter. Используя это, я создал версию.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;
    }   
}

Я чувствую, что нашел гораздо лучшее решение, чем самое популярное в настоящее время решение.

Проблема с другими предлагаемыми решениями заключается в том, что они либо достаточно сложны (напрямую обрабатывают сообщения более низкого уровня).

Или они терпят неудачу в угловых случаях: полагаясь на положение мыши в MouseLeave, вы можете пропустить выход мыши, если мышь выходит из дочернего элемента управления прямо внутрь контейнера.

Хотя это решение не совсем элегантно, оно простое и работает:

Добавьте прозрачный элемент управления, который занимает все пространство контейнера, для которого вы хотите получать события MouseEnter и MouseLeave.

Я нашел хороший прозрачный элемент управления в ответе Амеда: сделать элемент управления прозрачным

Который я тогда урезал до этого:

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!

Вы можете узнать, находится ли мышь в пределах вашего элемента управления следующим образом (при условии, что этот код находится в вашем элементе управления контейнера; если нет, замените 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
    }
}

Я не думаю, что вам нужно подключить насос сообщений, чтобы решить эту проблему. Некоторые пометки в вашем интерфейсе должны помочь. Я думаю, что вы создаете переменную-член, что-то вроде Control _someParent, в вашем управляющем классе, которая будет брать ссылку на родительский элемент управления при вызове одного из ваших обработчиков OnMouseEnter. Затем в OnMouseLeave проверьте значение флага _someParent и, если он совпадает с текущим отправителем, фактически не прекращайте обработку, просто вернитесь. Только когда родитель отличается, вы останавливаете и сбрасываете _someParent на ноль.

У меня была точно такая же потребность. Ответ Пола Уильямса дал мне основную идею, но мне было трудно понять код. Я нашел еще один вариант здесь, и вместе два примера помогли мне разработать собственную версию.

Для инициализации вы передаете интересующий контейнерный контроль в 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 );
    }
}

Я нашел очень простое решение для этого.

Я создал простое логическое поле в своей форме:

      private bool _cursorInPanel = false;

Контейнер и все дочерние элементы управления связаны с одними и теми же обработчиками событий:

      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
}

Кажется, это работает нормально. Моя цель состояла в том, чтобы просто показать кнопку, когда мышь входит в панель, и это работает довольно хорошо (по крайней мере, в .NET 5)

Другие вопросы по тегам