用户控件:如何添加鼠标滚轮监听器?

3
我正在创建一个用户控件,当鼠标悬停在控件上并滚动鼠标滚轮时,该控件应该有所反应。
目前,我正在按照以下方式实现:
    public MyUserControl()
    {
        this.MouseWheel += new MouseEventHandler(MouseWheelHandler);
    }

    private void MouseWheelHandler(object sender, System.Windows.Forms.MouseEventArgs e)
    {
        if (e.Delta > 0)
            incIndex();

        if (e.Delta < 0)
            decIndex();
    }

    protected override void OnMouseEnter(EventArgs e)
    {
        this.Focus();

        base.OnMouseEnter(e);
    }

实际上这个方法可以正常工作,但问题在于 "this.Focus();" 部分会破坏我的表单/应用程序的行为。

有更好的方法来解决这个问题吗?


为什么你需要集中你的控制? - dknaack
4
http://social.msdn.microsoft.com/forums/en-US/winforms/thread/eb922ed2-1036-41ca-bd15-49daed7b637c/ - Hans Passant
@nr1:你说的“它破坏了我的表单/应用程序的行为”,是什么意思? - dknaack
我有两个窗体:一个包含用户控件的主窗体,以及一个比主窗体稍小的第二个窗体,有时会显示在主窗体前面。当用户将鼠标悬停在主窗体上的用户控件上时,第二个窗体会消失(尽管它在前景中),因为使用了this.Focus()语句。 - nr1
哇,这个简单的问题的解决方案看起来非常复杂。 - Zuoanqh
显示剩余2条评论
4个回答

7

我曾经遇到同样的问题,最终实现了@Paul_Westcott和@nr1两位的解决方案的混合。这是一种类似@Paul_Westcott解决方案的本地解决方案(仅适用于订阅该方案的winforms控件)。它对多个监视器和MDI安全(在应用程序内被其他窗口重叠)。

public static class MouseWheelHandlerForWinformsControl
{
    private class MouseWheelMessageFilter : IMessageFilter
    {
        [DllImport("user32.dll")]
        private static extern IntPtr WindowFromPoint(Point pt);

        private readonly Control mCtrl;
        private readonly Action<MouseEventArgs> mOnMouseWheel;

        public MouseWheelMessageFilter(Control ctrl, Action<MouseEventArgs> onMouseWheel)
        {
            mCtrl = ctrl;
            mOnMouseWheel = onMouseWheel;
        }

        public bool PreFilterMessage(ref Message m)
        {
            // handle only mouse wheel messages
            if (m.Msg != 0x20a)
                return false;

            Point mouseAbsolutePosition = new Point(m.LParam.ToInt32());
            Point mouseRelativePosition = mCtrl.PointToClient(mouseAbsolutePosition);

            IntPtr hControlUnderMouse = WindowFromPoint(mouseAbsolutePosition);
            Control controlUnderMouse = Control.FromHandle(hControlUnderMouse);

            if (controlUnderMouse != mCtrl)
                return false;

            MouseButtons buttons = GetMouseButtons(m.WParam.ToInt32());
            int delta = m.WParam.ToInt32() >> 16;

            var e = new MouseEventArgs(buttons, 0, mouseRelativePosition.X, mouseRelativePosition.Y, delta);

            mOnMouseWheel(e);

            return true;
        }

        private static MouseButtons GetMouseButtons(int wParam)
        {
            MouseButtons buttons = MouseButtons.None;

            if(HasFlag(wParam, 0x0001)) buttons |= MouseButtons.Left;
            if(HasFlag(wParam, 0x0010)) buttons |= MouseButtons.Middle;
            if(HasFlag(wParam, 0x0002)) buttons |= MouseButtons.Right;
            if(HasFlag(wParam, 0x0020)) buttons |= MouseButtons.XButton1;
            if(HasFlag(wParam, 0x0040)) buttons |= MouseButtons.XButton2;

            return buttons;
        }

        private static bool HasFlag(int input, int flag)
        {
            return (input & flag) == flag;
        }
    }

    public static void MemorySafeAdd(Control ctrl, Action<MouseEventArgs> onMouseWheel)
    {
        if (ctrl == null || onMouseWheel == null)
            throw new ArgumentNullException();

        var filter = new MouseWheelMessageFilter(ctrl, onMouseWheel);
        Application.AddMessageFilter(filter);
        ctrl.Disposed += (s, e) => Application.RemoveMessageFilter(filter);
    }
}

一旦您将这个辅助类添加到您的解决方案中,您只需要在一行中将控件myControl订阅鼠标滚轮事件,如下所示:

public void Init() {
    MouseWheelHandlerForWinformsControl.MemorySafeAdd(myControl, OnMouseWheelEvent);
}

void OnMouseWheelEvent(MouseEventArgs args) {
    // do what you need here
}

难以置信我是第一个点赞的人!这个解决方案独树一帜,只需要添加一行代码即可,避免了其他解决方案中存在的问题。但如果您也提供使用它的代码行,那就更好了,例如:“MouseWheelHandlerForWinformsControl.MemorySafeAdd(your_control, YourControl_MouseWheel);”。 - Zuoanqh
@Zuoanqh 谢谢你的反馈。实际上这是我们软件中正在使用的代码,它可以工作并且已经在多屏和MDI环境下进行了测试。根据您的备注进行了相应的答复升级。 - jeromerg
1
哦,你一定要喜欢互联网 :-) 我们公司的某个人已经在我们的代码库中修复了 MDI 问题,但是另一个人开始抱怨 MDI 问题,然后我回到这里,你已经为我完成了工作 :-) 做得好,@jeromerg! - Paul Westcott

3

http://social.msdn.microsoft.com/forums/en-US/winforms/thread/eb922ed2-1036-41ca-bd15-49daed7b637c/复制粘贴:

该应用程序中的“主线程”是指创建应用程序的第一个线程。在WinForms应用程序中,这就是UI线程。

当您执行耗时操作(如下载文件或处理大量数据)时,应将其放在单独的线程中执行,以避免阻塞UI线程。否则,用户界面可能会停止响应,直到操作完成。

在WinForms中,可以使用BackgroundWorker组件轻松地将操作放置在后台线程中。此外,还可以使用Task Parallel Library(TPL)或Thread类手动创建和管理线程。

using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsApplication1 {
  public partial class Form1 : Form, IMessageFilter {
    public Form1() {
      InitializeComponent();
      Application.AddMessageFilter(this);
    }

    public bool PreFilterMessage(ref Message m) {
      if (m.Msg == 0x20a) {
        // WM_MOUSEWHEEL, find the control at screen position m.LParam
        Point pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);
        IntPtr hWnd = WindowFromPoint(pos);
        if (hWnd != IntPtr.Zero && hWnd != m.HWnd && Control.FromHandle(hWnd) != null) {
          SendMessage(hWnd, m.Msg, m.WParam, m.LParam);
          return true;
        }
      }
      return false;
    }

    // P/Invoke declarations
    [DllImport("user32.dll")]
    private static extern IntPtr WindowFromPoint(Point pt);
    [DllImport("user32.dll")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp);
  }
}

这个很棒,谢谢Hans Passant


警告:new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16)存在缺陷。负坐标会导致错误!每个参数必须首先转换为short。最简单的方法是使用专用的Point(int)构造函数,如下所示:new Point(m.LParam.ToInt32() - jeromerg

2

这是对已经发布的解决方案略微的变化,以避免使用P/Invoke。

public static class MouseWheelHandler
{
    public static void Add(Control ctrl, Action<MouseEventArgs> onMouseWheel)
    {
        if (ctrl == null || onMouseWheel == null)
            throw new ArgumentNullException();

        var filter = new MouseWheelMessageFilter(ctrl, onMouseWheel);
        Application.AddMessageFilter(filter);
        ctrl.Disposed += (s, e) => Application.RemoveMessageFilter(filter);
    }

    class MouseWheelMessageFilter
        : IMessageFilter
    {
        private readonly Control _ctrl;
        private readonly Action<MouseEventArgs> _onMouseWheel;

        public MouseWheelMessageFilter(Control ctrl, Action<MouseEventArgs> onMouseWheel)
        {
            _ctrl = ctrl;
            _onMouseWheel = onMouseWheel;
        }

        public bool PreFilterMessage(ref Message m)
        {
            var parent = _ctrl.Parent;
            if (parent != null && m.Msg == 0x20a) // WM_MOUSEWHEEL, find the control at screen position m.LParam
            {
                var pos = new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16);

                var clientPos = _ctrl.PointToClient(pos);

                if (_ctrl.ClientRectangle.Contains(clientPos)
                 && ReferenceEquals(_ctrl, parent.GetChildAtPoint(parent.PointToClient(pos))))
                {
                    var wParam = m.WParam.ToInt32();
                    Func<int, MouseButtons, MouseButtons> getButton =
                        (flag, button) => ((wParam & flag) == flag) ? button : MouseButtons.None;

                    var buttons = getButton(wParam & 0x0001, MouseButtons.Left)
                                | getButton(wParam & 0x0010, MouseButtons.Middle)
                                | getButton(wParam & 0x0002, MouseButtons.Right)
                                | getButton(wParam & 0x0020, MouseButtons.XButton1)
                                | getButton(wParam & 0x0040, MouseButtons.XButton2)
                                ; // Not matching for these /*MK_SHIFT=0x0004;MK_CONTROL=0x0008*/

                    var delta = wParam >> 16;
                    var e = new MouseEventArgs(buttons, 0, clientPos.X, clientPos.Y, delta);
                    _onMouseWheel(e);

                    return true;
                }
            }
            return false;
        }
    }
}

然后,这可以从像个人控件一样的控件中使用

class MyControl
    : Control
{
    public MyControl()
    {
        ...
        MouseWheelHandler.Add(this, MyOnMouseWheel);
    }

    void MyOnMouseWheel(MouseEventArgs e)
    {
        ...
    }
}

警告:new Point(m.LParam.ToInt32() & 0xffff, m.LParam.ToInt32() >> 16)有缺陷。在负坐标下会出现错误!每个参数必须首先转换为short类型。最简单的方法是使用专用的Point(int)构造函数,如下所示:new Point(m.LParam.ToInt32()) - jeromerg
警告:此解决方案不支持MDI,因为即使鼠标在覆盖控件的窗口上方,它也会滚动(MDI窗口)! - jeromerg

0

当使用FlowLayoutPanel时,Paul Westcott的答案非常有效。 我的MyOnMouseWheel事件实现如下:

void MyOnMouseWheel(MouseEventArgs e)
{
    int ChangeIncrement = (this.panel1.VerticalScroll.SmallChange * 4); //Change the 4 to any positive number to scroll more or less one each scroll event.
    if (e.Delta < 0)
    {
        int NewValue = this.panel1.VerticalScroll.Value + ChangeIncrement;
        if (NewValue > this.panel1.VerticalScroll.Maximum)
        {
            this.panel1.VerticalScroll.Value = this.panel1.VerticalScroll.Maximum;
        }
        else
        {
            this.panel1.VerticalScroll.Value = NewValue;
        }
    }
    else if (e.Delta > 0)
    {
        int NewValue = this.panel1.VerticalScroll.Value - ChangeIncrement;
        if (NewValue < this.panel1.VerticalScroll.Minimum)
        {
            this.panel1.VerticalScroll.Value = this.panel1.VerticalScroll.Minimum;
        }
        else
        {
            this.panel1.VerticalScroll.Value = NewValue;
        }
    }
    this.panel1.PerformLayout();
}

这段代码可以进行一些优化。例如:NewValue = this.panel1.VerticalScroll.Value + ((e.Delta>0)?-1:1)* ChangeIncrement; - Zuoanqh
this.panel1.VerticalScroll.Value = max(this.panel1.VerticalScroll.Minimum, min(this.panel1.VerticalScroll.Maximum, NewValue)) 这段代码的意思是:将 this.panel1.VerticalScroll.Value 的值设为 max(this.panel1.VerticalScroll.Minimum, min(this.panel1.VerticalScroll.Maximum, NewValue))。 - Zuoanqh
加上一个 if (e.Delta==0) return; 这一行代码,就可以将整个 IF 树简化为 咳咳 三行代码。 - Zuoanqh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接