locked
Making a scrollable panel scroll with mousewheel just by hovering mouse RRS feed

  • General discussion

  • Hi,

    I have a particular problem with a possible solution and I am interested in hearing peoples opinions if it is the right way to go about it (or not  )

    Some applications I have come across, such as Firefox, will allow the user to scroll the document window just by hovering the mouse over the display area and using the mouse wheel. You don't have to focus / select the display area first. With Firefox, the address bar can have the focus and the keyboard input but you can still scroll the document.

    Contrasting with IE and Visual Studio, you have to click into the document area before it responds to mouse wheel messages.

    In my winforms app, I have a tab control with several scrollable tabpages as well as scrollable panels in other places. Some of these just display data and don't have controls that can be focused or tabbed into. I would like to emulate the firefox behaviour described above.

    What I have done is created a class, ScrollPanelMessageFilter, that implementes System.Windows.Forms.IMessageFilter and that takes a Panel as it constructor parameter. It detects the mouse wheel when the cursor is over the display area and sends the message to the panel:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Drawing;
    using System.Text;
    using System.Runtime.InteropServices;
    using System.Windows.Forms;

    namespace MouseWheelTest
    {
        internal class ScrollPanelMessageFilter : IMessageFilter
        {
            int WM_MOUSEWHEEL = 0x20A;
            Panel panel;
            bool panelHasFocus = false;

            [DllImport("user32.dll")]
            static extern bool GetCursorPos(ref Point lpPoint);
            [DllImport("User32.dll")]
            static extern Int32 SendMessage(int hWnd, int Msg, int wParam, int lParam);

            public ScrollPanelMessageFilter(Panel panel)
            {
                this.panel = panel;
                //Go through each control on the panel and add an event handler.
                //We need to know if a control on the panel has focus to prevent sending
                //the scroll message a second time
                AddFocusEvent(panel);
            }

            private void AddFocusEvent(Control parentControl)
            {
                foreach (Control control in parentControl.Controls)
                {
                    if (control.Controls.Count == 0)
                    {
                        control.GotFocus += new EventHandler(control_GotFocus);
                        control.LostFocus += new EventHandler(control_LostFocus);
                    }
                    else
                    {
                        AddFocusEvent(control);
                    }
                }
            }

            void control_GotFocus(object sender, EventArgs e)
            {
                panelHasFocus = true;
            }

            void control_LostFocus(object sender, EventArgs e)
            {
                panelHasFocus = false;
            }

            #region IMessageFilter Members

            public bool PreFilterMessage(ref Message m)
            {
                //filter out all other messages except than mousewheel
                //also only proceed with processing if the panel is focusable, no controls on the panel have focus
                //and the vertical scroll bar is visible
                if (m.Msg == WM_MOUSEWHEEL && panel.CanFocus && !panelHasFocus && panel.VerticalScroll.Visible)
                {
                    //is mouse cordinates over the panel display rectangle?
                    Rectangle rect = panel.RectangleToScreen(panel.ClientRectangle);
                    Point cursorPoint = new Point();
                    GetCursorPos(ref cursorPoint);
                    if ((cursorPoint.X > rect.X && cursorPoint.X < rect.X + rect.Width) &&
                        (cursorPoint.Y > rect.Y && cursorPoint.Y < rect.Y + rect.Height))
                    {
                        //send the mouse wheel message to the panel.
                        SendMessage((int)panel.Handle, m.Msg, (Int32)m.WParam, (Int32)m.LParam);
                        return true;
                    }
                }
                return false;
            }
            #endregion
        }
    }

    In my forms code, I create an instance of the filter above for each panel I want to be scrollable without focus. I then add the filter when the form is activated and remove the filter when the form is deactivated so that the filter is not unnecessarily performing any work.

    public partial class Form1 : Form
        {
            private ScrollPanelMessageFilter filter;

            public Form1()
            {
                InitializeComponent();
            }

            private void Form1_Activated(object sender, EventArgs e)
            {
                filter = new ScrollPanelMessageFilter(panel);
                Application.AddMessageFilter(filter);
            }

            private void Form1_Deactivate(object sender, EventArgs e)
            {
                Application.RemoveMessageFilter(filter);
            }
        }


    So what do the winforms guru's out there think about this? What are the possible side-effects? Are the problems that I could be missing? Do you approve of this approach?

    Your opinions are valued!

    Wednesday, February 1, 2006 1:02 AM

All replies

  • dude this is pretty sweet
    i modified it so i can use it on other controls like richtextbox etc

    thanks
    Monday, February 6, 2006 12:14 AM
  • You're welcome!

    The code has been cooking for a while in one of my apps and the testers haven't reported any side effects. I guess the one thing to look oout for is when you have a scrollable control within a scrollable control (i.e. richtext box on a scrollable panel) and making sure the right control get the mouse wheel message.

    Other than that, it's a nice little usability feature.
    Monday, February 6, 2006 8:50 AM
  • You woudnt happen to have a visual Basic .net version laying around would you?

     

    Thanks,

    AThomas

    Wednesday, April 5, 2006 8:24 PM
  • Hey DamianH - yes, really nice.  One question though.  Why add the event handlers during Form1_Activated and remove them during Form1_Deactivate, instead of adding them during Form1_Load and removing them during Form1_Closing?  The reason I ask is doing this during Load/Closing would eliminate the overhead of adding and removing the handlers each time the user switches away from, and back to, the form in question.
    Friday, July 21, 2006 3:07 PM
  • Thank you for this.

    I've tried this on a Panel that has fields added to it Dynamically.

    Do I have to do anything special to get it to work like this?

    ---Edit fixed it:D  Thank you so much!
    Wednesday, July 26, 2006 10:30 PM
  • DamianH.  That code rocks!  It was almost exactly what I was looking for.  I modified the class to be more generic because I wanted it to work with a Panel and a DataGridView Control.  So I made a few small changes.  The result is pasted below. 

    Thanks,

    -Valkyrie-MT

     

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

    namespace CompanyUtils.Forms
    {

        /// <summary>
        /// Adds wheel mouse scrolling to a control that does not have focus
        /// </summary>
        internal class ScrollPanelMessageFilter : IMessageFilter
        {
            // Original Code Source: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=228499&SiteID=1
           
            int WM_MOUSEWHEEL = 0x20A;
            Control m_control;
            bool controlHasFocus = false;

            [DllImport("user32.dll")]
            static extern bool GetCursorPos(ref Point lpPoint);
            [DllImport("User32.dll")]
            static extern Int32 SendMessage(int hWnd, int Msg, int wParam, int lParam);

            public ScrollPanelMessageFilter(Control control)
            {
                this.m_control = control;
                //Go through each control on the panel and add an event handler.
                //We need to know if a control on the panel has focus to prevent sending
                //the scroll message a second time
                AddFocusEvent(control);
            }
           
            private void AddFocusEvent(Control parentControl)
            {
                foreach (Control aControl in parentControl.Controls)
                {
                    if (aControl.Controls.Count == 0)
                    {
                        aControl.GotFocus += new EventHandler(control_GotFocus);
                        aControl.LostFocus += new EventHandler(control_LostFocus);
                    }
                    else
                    {
                        AddFocusEvent(aControl);
                    }
                }
            }

            void control_GotFocus(object sender, EventArgs e)
            {
                controlHasFocus = true;
            }

            void control_LostFocus(object sender, EventArgs e)
            {
                controlHasFocus = false;
            }

            #region IMessageFilter Members

            public bool PreFilterMessage(ref Message m)
            {
                //filter out all other messages except than mousewheel
                //also only proceed with processing if the panel is focusable, no controls on the panel have focus
                //and the vertical scroll bar is visible
                if (m.Msg == WM_MOUSEWHEEL && m_control.CanFocus && !controlHasFocus)
                {
                    if ((m_control is ScrollableControl) || (m_control is DataGridView))
                    {
                        // If this is some other type of control,
                        // true -> sends the mouse wheel messages anyway
                        // false -> don't send mouse wheel messages to any control other than a ScrollableControl
                        bool scrollbarsvisible = true;

                        // If we are dealing with a scrollable control, we'll check to make sure the scrollbar is visible before sending messages
                        if (m_control is ScrollableControl) scrollbarsvisible = (m_control as ScrollableControl).VerticalScroll.Visible;

                        if (scrollbarsvisible)
                        {
                        //is mouse cordinates over the panel display rectangle?
                        Rectangle rect = m_control.RectangleToScreen(m_control.ClientRectangle);
                        Point cursorPoint = new Point();
                        GetCursorPos(ref cursorPoint);
                        if ((cursorPoint.X > rect.X && cursorPoint.X < rect.X + rect.Width) &&
                            (cursorPoint.Y > rect.Y && cursorPoint.Y < rect.Y + rect.Height))
                        {
                            //send the mouse wheel message to the panel.
                            SendMessage((int)m_control.Handle, m.Msg, (Int32)m.WParam, (Int32)m.LParam);
                            return true;
                        }
                        }
                    }
                }
                return false;
            }
            #endregion
        }
    }

    Friday, August 11, 2006 8:21 PM
  • Well my friend.. I just stumbled of this great piece of code and thought of the same thing as you. Here is a pot to vb of the latter code:

    Imports Microsoft.VisualBasic

    Imports System

    Imports System.Text

    Imports System.Runtime.InteropServices

    Imports System.Windows.Forms

    Imports System.Drawing

    Namespace CompanyUtils.Forms

    ''' <summary>

    ''' Adds wheel mouse scrolling to a control that does not have focus

    ''' </summary>

    Friend Class ScrollPanelMessageFilter

    Implements IMessageFilter

    ' Original Code Source: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=228499&SiteID=1

    Private WM_MOUSEWHEEL As Integer = &H20A

    Private m_control As Control

    Private controlHasFocus As Boolean = False

    <DllImport("user32.dll")> _

    Shared Function GetCursorPos(ByRef lpPoint As Point) As Boolean

    End Function

    <DllImport("User32.dll")> _

    Shared Function SendMessage(ByVal hWnd As Integer, ByVal Msg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Int32

    End Function

    Public Sub New(ByVal control As Control)

    m_control = control

    'Go through each control on the panel and add an event handler.

    'We need to know if a control on the panel has focus to prevent sending

    'the scroll message a second time

    AddFocusEvent(control)

    End Sub

    Private Sub AddFocusEvent(ByVal parentControl As Control)

    For Each aControl As Control In parentControl.Controls

    If aControl.Controls.Count = 0 Then

    AddHandler aControl.GotFocus, AddressOf control_GotFocus

    AddHandler aControl.LostFocus, AddressOf control_LostFocus

    Else

    AddFocusEvent(aControl)

    End If

    Next aControl

    End Sub

    Private Sub control_GotFocus(ByVal sender As Object, ByVal e As EventArgs)

    controlHasFocus = True

    End Sub

    Private Sub control_LostFocus(ByVal sender As Object, ByVal e As EventArgs)

    controlHasFocus = False

    End Sub

    #Region "IMessageFilter Members"

    Public Function PreFilterMessage(ByRef m As Message) As Boolean Implements IMessageFilter.PreFilterMessage

    'filter out all other messages except than mousewheel

    'also only proceed with processing if the panel is focusable, no controls on the panel have focus

    'and the vertical scroll bar is visible

    If m.Msg = WM_MOUSEWHEEL AndAlso m_control.CanFocus AndAlso (Not controlHasFocus) Then

    If (TypeOf m_control Is ScrollableControl) OrElse (TypeOf m_control Is DataGridView) Then

    ' If this is some other type of control,

    ' true -> sends the mouse wheel messages anyway

    ' false -> don't send mouse wheel messages to any control other than a ScrollableControl

    Dim scrollbarsvisible As Boolean = True

    ' If we are dealing with a scrollable control, we'll check to make sure the scrollbar is visible before sending messages

    If TypeOf m_control Is ScrollableControl Then

    scrollbarsvisible = (TryCast(m_control, ScrollableControl)).VerticalScroll.Visible

    End If

    If scrollbarsvisible Then

    'is mouse cordinates over the panel display rectangle?

    Dim rect As Rectangle = m_control.RectangleToScreen(m_control.ClientRectangle)

    Dim cursorPoint As Point = New Point()

    GetCursorPos(cursorPoint)

    If (cursorPoint.X > rect.X AndAlso cursorPoint.X < rect.X + rect.Width) AndAlso (cursorPoint.Y > rect.Y AndAlso cursorPoint.Y < rect.Y + rect.Height) Then

    'send the mouse wheel message to the panel.

    SendMessage(m_control.Handle, m.Msg, m.WParam, m.LParam)

    Return True

    End If

    End If

    End If

    End If

    Return False

    End Function

    #End Region

    End Class

    End Namespace

     

    To add the code to a panel in a form do the following:

    Public Class YourFrm

    Private filter As CompanyUtils.Forms.ScrollPanelMessageFilter

    Private Sub MainFrm_Activated(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Activated

    filter = New CompanyUtils.Forms.ScrollPanelMessageFilter(Panel1)

    Application.AddMessageFilter(filter)

    End Sub

    Private Sub MainFrm_Deactivate(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Deactivate

    Application.RemoveMessageFilter(filter)

    End Sub

    ....

    End Class

     

     

     

    Sunday, October 29, 2006 12:32 AM
  • Thanks a lot.  This code was almost exactly what I was looking for.  I made two changes to make it slightly more useable for my particular circumstance.

    1) My panel is embedded in a user control so I wanted to panel itself to be able to add/remove the MessageFilter.  So I added some code to my class to listen for the ParentChanged event up the control tree until the control or one of its parents is added to a Form and then it registers listeners for the Activated/Deactivate events.

            // The ScrollPanelMessageFilter should be added and removed
            // as the parent form is Activated/Deactivated.
            // Code in this region looks for this control's Parent Form.
            // When it finds it it registers listeners for the Form's
            // Activated/Deactivated events.

            /// <summary>
            /// Tries to find a the parent form of this control.
            /// If the passsed in control isn't a Form, it will
            /// add listeners to that control's ParentChanged event
            /// and see if that control is added to a Form.
            /// </summary>
            /// <param name="c"></param>
            void FindParentForm(Control c)
            {
                if (c == null)
                {
                    // Stops the recursion
                    return;
                }
                else if (c is Form)
                {
                    // Passing null to RegisterParentControl will remove
                    // the listeners
                    RegisterParentControl(null);
                    RegisterParentForm(c as Form);
                }
                else
                {
                    RegisterParentControl(c);
                    // If this control has a parent see if it's a form
                    FindParentForm(c.Parent);
                }
            }

            private Form _parentForm;
            void RegisterParentForm(Form f)
            {
                if (_parentForm == f) return;

                if (_parentForm != null)
                {
                    _parentForm.Activated -= new EventHandler(Form_Activated);
                    _parentForm.Deactivate -= new EventHandler(Form_Deactivate);
                }
                _parentForm = f;
                _parentForm.Activated += new EventHandler(Form_Activated);
                _parentForm.Deactivate += new EventHandler(Form_Deactivate);
            }

            private Control _parentControl;
            void RegisterParentControl(Control c)
            {
                if (_parentControl == c) return;

                if (_parentControl != null)
                {
                    _parentControl.ParentChanged -= new EventHandler(ParentControlChanged);
                }
                if (c != null)
                {
                    _parentControl = c;
                    _parentControl.ParentChanged += new EventHandler(ParentControlChanged);
                }
            }

            void ParentControlChanged(object sender, EventArgs e)
            {
                FindParentForm(sender as Control);
            }

            protected override void OnParentChanged(EventArgs e)
            {
                base.OnParentChanged(e);
                FindParentForm(this.Parent);
            }

            private ScrollPanelMessageFilter _filter;
            void Form_Deactivate(object sender, EventArgs e)
            {
                Application.RemoveMessageFilter(_filter);
            }

            void Form_Activated(object sender, EventArgs e)
            {
                _filter = new ScrollPanelMessageFilter(this);
                Application.AddMessageFilter(_filter);
            }

    2) Controls are added and removed from my Panel after construction so I needed to listen to the ControlAdded/Removed events.

            public ScrollPanelMessageFilter(Panel panel)
            {
                this.panel = panel;
                //Go through each control on the panel and add an event handler.
                //We need to know if a control on the panel has focus to prevent sending
                //the scroll message a second time
                AddFocusEvent(panel);
                panel.ControlAdded += new ControlEventHandler(ControlAdded);
                panel.ControlRemoved += new ControlEventHandler(ControlRemoved);
            }

            void ControlRemoved(object sender, ControlEventArgs e)
            {
                RemoveFocusEvent(e.Control);
            }

            void ControlAdded(object sender, ControlEventArgs e)
            {
                AddFocusEvent(e.Control);
            }

            private void RemoveFocusEvent(Control parentControl)
            {
                foreach (Control control in parentControl.Controls)
                {
                    if (control.Controls.Count == 0)
                    {
                        control.GotFocus -= new EventHandler(control_GotFocus);
                        control.LostFocus -= new EventHandler(control_LostFocus);
                    }
                    else
                    {
                        RemoveFocusEvent(control);
                    }
                }
            }

    Wednesday, January 10, 2007 10:37 PM
  • Hi all, it's been a while since I looked at this topic, glad you found it useful. :) I've been using in an application since this post and there has been no side-effects detected. In fact users complain if they come across a scrollable panel where the wheel doesn't work...

    >Why add the event handlers during Form1_Activated and remove them during Form1_Deactivate

    The reason being is that I want to process the messages even when the form is not activated. For instance my app may be on the right hand side of the screen and the user may have another app on the left. The left on has the focus and is activated, mine isn't. I wanted the user to be able to scroll the panel just by moving the mouse over it and not have to activate it first.

    Your needs may be different of course.
    Wednesday, January 10, 2007 10:55 PM
  • I tried doing my own code to create a scrolling panel but it was buggy like *** so you're a lifesaviour. The scrollable panel thing works like a charm.
    Sunday, October 19, 2008 2:41 PM
  • This thread shows another way of doing this, it works for any control on any form.
    Sunday, October 19, 2008 5:28 PM
  • This thread shows another way of doing this, it works for any control on any form.


    the link is not working anymore,

    can you update it?

     

    M

    Wednesday, September 8, 2010 11:42 AM
  • @m.savazzi, I managed to find that link on the wayback machine.

    Based on that, it appears the link should be:

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

    I haven't tested that solution yet though, hopefully it is a catch-all since I'd prefer it to work like that instead of ad-hoc.


    • Edited by André Luus Wednesday, January 30, 2013 10:33 AM Changed link to actual link
    Wednesday, January 30, 2013 10:32 AM
  • Thank you!

    Very nice piece of code. Been searching for a solution to a similar problem for a while now and yours seems to work very well.

    Tuesday, January 5, 2016 9:01 AM
  • Yes, thanks for this useful code. In my test though, mouse wheel scrolling was not enabled when hovering over the scroll bar itself. I felt this would be more intuitive behavior so I added a line to the VB version. There's probably a slightly cleaner way to do it but this worked for me. This is for a vertical scrollbar. The ADDED code line is shown between the original code lines and modifies rect.Width to include the vertical scroll bar.

    Dim rect As Rectangle = m_control.RectangleToScreen(m_control.DisplayRectangle)
    Dim cursorPoint As Point = New Point()
    GetCursorPos(cursorPoint)
    
    rect.Width = m_control.Width 'THIS IS THE ADDED CODE LINE
    
    If (cursorPoint.X > rect.X AndAlso cursorPoint.X < rect.X + rect.Width) AndAlso (cursorPoint.Y > rect.Y AndAlso cursorPoint.Y < rect.Y + rect.Height) Then



    • Edited by Wade7 Sunday, September 16, 2018 12:52 AM
    Sunday, September 16, 2018 12:07 AM