4

I have some menus that contain many menuitems. Mouse wheel doesn't scroll them. I have to use the keyboard arrows or click the arrows at top and bottom. Is it possible to use the mouse wheel to scroll toolstrip menu items? Thanks

Jerry
  • 4,258
  • 3
  • 31
  • 58

4 Answers4

6

You can enable it application wide with this class:

public class DropDownMenuScrollWheelHandler : System.Windows.Forms.IMessageFilter
{
    private static DropDownMenuScrollWheelHandler Instance;
    public static void Enable(bool enabled)
    {
        if (enabled)
        {
            if (Instance == null)
            {
                Instance = new DropDownMenuScrollWheelHandler();
                Application.AddMessageFilter(Instance);
            }
        }
        else
        {
            if (Instance != null)
            {
                Application.RemoveMessageFilter(Instance);
                Instance = null;
            }
        }
    }
    private IntPtr activeHwnd;
    private ToolStripDropDown activeMenu;

    public bool PreFilterMessage(ref Message m)
    {
        if (m.Msg == 0x200 && activeHwnd != m.HWnd) // WM_MOUSEMOVE
        {
            activeHwnd = m.HWnd;
            this.activeMenu = Control.FromHandle(m.HWnd) as ToolStripDropDown;
        }
        else if (m.Msg == 0x20A && this.activeMenu != null) // WM_MOUSEWHEEL
        {
            int delta = (short)(ushort)(((uint)(ulong)m.WParam) >> 16);
            handleDelta(this.activeMenu, delta);
            return true;
        }
        return false;
    }

    private static readonly Action<ToolStrip, int> ScrollInternal
        = (Action<ToolStrip, int>)Delegate.CreateDelegate(typeof(Action<ToolStrip, int>),
            typeof(ToolStrip).GetMethod("ScrollInternal",
                System.Reflection.BindingFlags.NonPublic
                | System.Reflection.BindingFlags.Instance));

    private void handleDelta(ToolStripDropDown ts, int delta)
    {
        if (ts.Items.Count == 0)
            return;
        var firstItem = ts.Items[0];
        var lastItem = ts.Items[ts.Items.Count - 1];
        if (lastItem.Bounds.Bottom < ts.Height && firstItem.Bounds.Top > 0)
            return;
        delta = delta / -4;
        if (delta < 0 && firstItem.Bounds.Top - delta > 9)
        {
            delta = firstItem.Bounds.Top - 9;
        }
        else if (delta > 0 && delta > lastItem.Bounds.Bottom - ts.Height + 9)
        {
            delta = lastItem.Bounds.Bottom - owner.Height + 9;
        }
        if (delta != 0)
            ScrollInternal(ts, delta);
    }
}
Bryce Wagner
  • 1,151
  • 7
  • 18
  • 1
    Should be `ts.Height` not `owner.Height` ? Also, can listen for the `MouseWheel` event rather than using `AddMessageFilter`. Other than that, good stuff! – Loathing Jun 21 '15 at 03:02
  • Whose MouseWheel event would you hook into? The AddMessageFilter allows you to handle all mouse wheel scrolling for all menus in the application without worrying about hooking up events to each one. – Bryce Wagner Jun 23 '15 at 14:39
  • I have an extension method that increases the size of the scroll buttons, so it was a natural place to attach the mouse wheel event. Initially `AddMessageFilter` seemed unnecessary, but on second thought it is a good solution. – Loathing Jun 23 '15 at 21:29
  • works perfectly, except in addition to what Loathing mentioned above i had to change "msg == 0x20A" to "m.Msg ..." and the line "int delta = (short)..." to "int delta = m.WParam.ToInt32() >> 16;" – joreg Oct 30 '15 at 17:43
  • Noticed that the top/bottom buttons do not update when wheeled to either end. – sjlewis Aug 05 '16 at 08:56
5

A working solution:

  1. Register for MouseWheel event of your form and DropDownClosed event of your root MenuStripItem (here, rootItem) in the Load event of the form

        this.MouseWheel += Form3_MouseWheel;
        rootItem.DropDownOpened += rootItem_DropDownOpened;
        rootItem.DropDownClosed += rootItem_DropDownClosed;
    
  2. Add the code for Keyboard class which simulate key presses

    public static class Keyboard
    {
        [DllImport("user32.dll")]
        static extern uint keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
    
    
        const byte VK_UP = 0x26; // Arrow Up key
        const byte VK_DOWN = 0x28; // Arrow Down key
    
        const int KEYEVENTF_EXTENDEDKEY = 0x0001; //Key down flag, the key is going to be pressed
        const int KEYEVENTF_KEYUP = 0x0002; //Key up flag, the key is going to be released
    
        public static void KeyDown()
        {
            keybd_event(VK_DOWN, 0, KEYEVENTF_EXTENDEDKEY, 0);
            keybd_event(VK_DOWN, 0, KEYEVENTF_KEYUP, 0);
        }
    
        public static void KeyUp()
        {
            keybd_event(VK_UP, 0, KEYEVENTF_EXTENDEDKEY, 0);
            keybd_event(VK_UP, 0, KEYEVENTF_KEYUP, 0);
        }
    }
    
  3. Add the code for DropDownOpened, DropDownClosed, MouseWheel events:

    bool IsMenuStripOpen  = false;
    
    void rootItem_DropDownOpened(object sender, EventArgs e)
    {
        IsMenuStripOpen = true;
    }
    
    
    void rootItem_DropDownClosed(object sender, EventArgs e)
    {
        IsMenuStripOpen = false;
    }
    
    void Form3_MouseWheel(object sender, MouseEventArgs e)
    {
        if (IsMenuStripOpen)
        {
            if (e.Delta > 0)
            {
                Keyboard.KeyUp();
            }
            else
            {
                Keyboard.KeyDown();
            }
        }
    }
    
Mohsen Afshin
  • 13,273
  • 10
  • 65
  • 90
  • Thanks! Is it possible to scroll the view without simulating up and down arrows, as in Visual Studio 2010 ? – Jerry Oct 30 '12 at 22:14
  • You must implement your own MenuStrip, otherwise the only working solution is this – Mohsen Afshin Oct 31 '12 at 06:21
  • This solution works, but is rather clunky as it moves the selection up and down the list rather than scrolling it per se. If the mouse moves over the list while this happens, the selection jumps back under the mouse which gets very messy and disorienting for the user. Note also that you can use SendKeys to make the last event handler much more tidy: `SendKeys.Send((e.Delta > 0) ? "{UP}" : "{DOWN}");`. – Jason Williams Jun 05 '14 at 22:14
  • The MouseWheel event seems to stop firing when either the up or down button is clicked – sjlewis Aug 05 '16 at 07:43
0

This is very simply using a submenu (ToolStripMenuItem) of the context menu :

Assuming using a form1 (or UserControl) and a contextMenuStrip1 :

private void form1_Load( object sender , EventArgs e )
{
    //this.MouseWheel -= When_MouseWheel;
    this.MouseWheel += When_MouseWheel;
}
void When_MouseWheel( object sender , MouseEventArgs e )
{
    if ( this.contextMenuStrip1.IsDropDown ) {
         //this.Focus();
         if ( e.Delta > 0 ) SendKeys.SendWait( "{UP}" );
         else SendKeys.SendWait( "{DOWN}" );         
    }
}
Godvicien
  • 349
  • 3
  • 6
0

I modified Mohsen Afshin's answer to click the up/down arrows instead of sending up/down key presses. My application had a ContextMenuStrip called menu. Here's the code.

In the initialization:

    menu.VisibleChanged += (s, e) =>
    {
        if (menu.Visible)
        {
            MouseWheel += ScrollMenu;
            menu.MouseWheel += ScrollMenu;
        }
        else
        {
            MouseWheel -= ScrollMenu;
            menu.MouseWheel -= ScrollMenu;
        }
    };

The ScrollMenu function:

[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern void mouse_event(uint dwFlags, uint dx, uint dy, uint cButtons, uint dwExtraInfo);

private void ScrollMenu(object sender, MouseEventArgs e)
{
    Point origin = Cursor.Position;
    int clicks;

    if (e.Delta < 0)
    {
        Cursor.Position = menu.PointToScreen(new Point(menu.DisplayRectangle.Left + 5, menu.DisplayRectangle.Bottom + 5));
        clicks = e.Delta / -40;
    }
    else
    {
        Cursor.Position = menu.PointToScreen(new Point(menu.DisplayRectangle.Left + 5, menu.DisplayRectangle.Top - 5));
        clicks = e.Delta / 40;
    }

    for (int i = 0; i < clicks; i++)
        mouse_event(0x0006, 0, 0, 0, 0);//Left mouse button up and down on cursor position
    Cursor.Position = origin;
}

I was having trouble getting the mouse_event function to click a specific location, so I moved the cursor, clicked, and then moved the cursor back. It doesn't seem the cleanest, but it works.

Tobbs
  • 1,120
  • 1
  • 6
  • 15