Родительский контроль Мышь Вводить / оставлять события с дочерними элементами
У меня есть приложение 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)