Mouse wheel event to work with hovered control
Asked Answered
U

4

11

In my C# 3.5 Windows Forms application, I have a few SplitContainers. There is a list control inside each (dock fill). When the focus is on one of these controls and I move mouse wheel, the list, which is now focused, is scrolled.

My task is to scroll the list, which is currently hovered by mouse, not the one which is selected. Is it possible in Windows Forms? If not, is it possible with PInvoke?

Unsightly answered 14/6, 2012 at 13:39 Comment(1)
It seems they made "scroll whatever the mouse cursor is over" the standard behaviour in Windows 10. Which is kind of annoying in most cases, actually.Otology
S
8

It looks like you can use the IMessageFilter and PInvoke to handle this. An example in VB can be found at Redirect Mouse Wheel Events to Unfocused Windows Forms Controls. You should be able to easily convert this to C#.

Points of Interest

This class uses the following techniques for the given task:

  • Listen to the control's MouseEnter and MouseLeave events to determine when the mouse pointer is over the control.
  • Implement IMessageFilter to catch WM_MOUSEWHEEL messages in the application.
  • PInvoke the Windows API call SendMessage redirecting the WM_MOUSEWHEEL message to the control's handle.
  • The IMessageFilter object is implemented as a singleton of the MouseWheelRedirector class and accessed by the shared members Attach, Detach, and Active.

Using a VB.NET to C# converter, this is what you end up with:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;

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

public class MouseWheelRedirector : IMessageFilter
{
    private static MouseWheelRedirector instance = null;
    private static bool _active = false;
    public static bool Active
    {
       get { return _active; }
       set
       { 
          if (_active != value) 
          {
             _active = value;
             if (_active)
             {
                if (instance == null)
                {
                    instance = new MouseWheelRedirector();
                }
                Application.AddMessageFilter(instance);
             }
             else
             {
                if (instance != null)
                {
                   Application.RemoveMessageFilter(instance);
                }
             }
          }
       }
    }

    public static void Attach(Control control)
    {
       if (!_active)
          Active = true;
       control.MouseEnter += instance.ControlMouseEnter;
       control.MouseLeave += instance.ControlMouseLeaveOrDisposed;
       control.Disposed += instance.ControlMouseLeaveOrDisposed;
    }

    public static void Detach(Control control)
    {
       if (instance == null)
          return;
       control.MouseEnter -= instance.ControlMouseEnter;
       control.MouseLeave -= instance.ControlMouseLeaveOrDisposed;
       control.Disposed -= instance.ControlMouseLeaveOrDisposed;
       if (object.ReferenceEquals(instance.currentControl, control))
          instance.currentControl = null;
    }

    private MouseWheelRedirector()
    {
    }


    private Control currentControl;
    private void ControlMouseEnter(object sender, System.EventArgs e)
    {
       var control = (Control)sender;
       if (!control.Focused)
       {
          currentControl = control;
       }
       else
       {
          currentControl = null;
       }
    }

    private void ControlMouseLeaveOrDisposed(object sender, System.EventArgs e)
    {
       if (object.ReferenceEquals(currentControl, sender))
       {
          currentControl = null;
       }
    }

    private const int WM_MOUSEWHEEL = 0x20a;
    public bool PreFilterMessage(ref System.Windows.Forms.Message m)
    {
       if (currentControl != null && m.Msg == WM_MOUSEWHEEL)
       {
          SendMessage(currentControl.Handle, m.Msg, m.WParam, m.LParam);
          return true;
       }
       else
       {
          return false;
       }
    }

    [DllImport("user32.dll", SetLastError = false)]
    private static extern IntPtr SendMessage(
       IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
 }
Sandpiper answered 14/6, 2012 at 13:55 Comment(4)
This does not appear to work with most common touch pads using methods like right-edge or double-finger scroll. It appears their drivers do some black magic to send the scroll messages straight to the control's scroll bars instead of sending WM_MOUSEWHEEL to the control as they should. Any ideas on how to get around that?Otology
Hmm. Looks like the NumericUpDown doesn't want to listen to this.Otology
You would need to add handlers to the 'PreFilterMessage' that can pass touchpad or Numeric navigation buttons. Currently, it only passes scrollwheel messages.Loadstone
@blackholeearth0_gmail Um. As I commented on the question itself over three years ago, this is all irrelevant on windows 10, since scrolling the hovered control is default behaviour on win10. You don't even need any of this code; it just works anyway.Otology
L
6

I had similar question and found this thread... so posting my belated answer for others who might find this thread. In my case, I just want the mouse wheel events to go to whatever control is under the cursor... just like right-click does (it would be bewildering if right-click went to the focus control rather than the control under the cursor... I argue the same is true for the mouse wheel, except we've gotten used to it).

Anyway, the answer is super easy. Just add a PreFilterMessage to your application and have it redirect mouse wheel events to the control under the mouse:

    public bool PreFilterMessage(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_MOUSEWHEEL:   // 0x020A
            case WM_MOUSEHWHEEL:  // 0x020E
                IntPtr hControlUnderMouse = WindowFromPoint(new Point((int)m.LParam));
                if (hControlUnderMouse == m.HWnd)
                    return false; // already headed for the right control
                else
                {
                    // redirect the message to the control under the mouse
                    SendMessage(hControlUnderMouse, m.Msg, m.WParam, m.LParam);
                    return true;
                } 
             default: 
                return false; 
           } 
}
Lederhosen answered 8/11, 2012 at 16:8 Comment(1)
There's a little bit missing from this. You need to DllImport WindowFromPoint() and SendMessage: [DllImport("user32.dll")] static extern IntPtr WindowFromPoint(Point p);[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam); Also, PreFilterMessage() comes from IMessageFilter, and that implementation needs to be passed to ApplicationAddMessageFilter(). Once that's done, all of the panels in my app became scrollable under the mouse. However, double-clicking no longer highlights text. Odd.Chilton
E
5

This is Brian Kennedy's answer completed with Hank Schultz comment:

First you should make a class implements IMessageFilter:

public class MessageFilter : IMessageFilter
{
    private const int WM_MOUSEWHEEL = 0x020A;
    private const int WM_MOUSEHWHEEL = 0x020E;

    [DllImport("user32.dll")]
    static extern IntPtr WindowFromPoint(Point p);
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);

    public bool PreFilterMessage(ref Message m)
    {
        switch (m.Msg)
        {
            case WM_MOUSEWHEEL:
            case WM_MOUSEHWHEEL:
                IntPtr hControlUnderMouse = WindowFromPoint(new Point((int)m.LParam));
                if (hControlUnderMouse == m.HWnd)
                {
                    //Do nothing because it's already headed for the right control
                    return false;
                }
                else
                {
                    //Send the scroll message to the control under the mouse
                    uint u = Convert.ToUInt32(m.Msg);   
                    SendMessage(hControlUnderMouse, u, m.WParam, m.LParam);
                    return true;
                }
            default:
                return false;
        }
    }
}

Example usage:

public partial class MyForm : Form
{
    MessageFilter mf = null;

    public MyForm
    {
        Load += MyForm_Load;
        FormClosing += MyForm_FormClosing;
    }

    private void MyForm_Load(object sender, EventArgs e)
    {
        mf= new MessageFilter();
        Application.AddMessageFilter(mf);
    }

    private void MyForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        Application.RemoveMessageFilter(mf);
    }
}
Eyebrow answered 14/5, 2016 at 9:22 Comment(1)
This worked perfect, had no issues with it across remote desktop, multimon, etc. Thank you for combining these answers.Inexact
P
3

Use Control.MouseEnter Event to set focus to to the control. E.g. using ActiveControl Property

Prober answered 14/6, 2012 at 13:49 Comment(1)
Sometimes it's handy to scroll a control without focusing it, for example if it takes focus away from a text field.Otology

© 2022 - 2024 — McMap. All rights reserved.