Click here to Skip to main content
15,908,166 members
Articles / Programming Languages / C#
Article

Getting Scroll Events for a Listbox

Rate me:
Please Sign up or sign in to vote.
4.21/5 (16 votes)
27 Jun 20042 min read 203.1K   1.8K   37   27
The standard framework won't tell when the scrollbar moved or what position it is at. This code gives you that control.

Sample Image - ScrollingListbox.png

Introduction

This example shows how to access scrollbar events in a custom control that is derived from the System.Windows.Forms.ListBox control, in C# .NET. Questions answered are:

  • How do I get the Scroll event?
  • How do I get the Scrollbar position?

Getting Started

Create a new project (File, New, Project...) of type 'Windows Application'. Visual Studio will show the project in your Solution Explorer and 'Form1.cs' in design mode. Right-click the project node and select 'Add, Add User Control...'. Name it 'ScrollingListBox.cs'. It is now visible in the Solution Explorer and it looks like a gray square in the designer.

Press F7 to go to the code and replace the base 'UserControl' by 'ListBox'. Save, compile, go to designer. As you can see, the icon in the Solution Explorer changes and the designer doesn't show a listbox. This is your code at this point:

C#
using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Windows.Forms;

namespace WebsiteSamples
{
    /// <summary>
    /// Summary description for ScrollingListBox.
    /// </summary>
    public class ScrollingListBox : System.Windows.Forms.ListBox
    {
        /// <summary> 
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.Container components = null;

        public ScrollingListBox()
        {
            // This call is required by the Windows.Forms Form Designer.
            InitializeComponent();

            // TODO: Add any initialization after the InitForm call

        }

        /// <summary> 
        /// Clean up any resources being used.
        /// </summary>
        protected override void Dispose( bool disposing )
        {
            if( disposing )
            {
                if(components != null)
                {
                    components.Dispose();
                }
            }
            base.Dispose( disposing );
        }

        #region Component Designer generated code
        /// <summary> 
        /// Required method for Designer support - do not modify 
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            components = new System.ComponentModel.Container();
        }
        #endregion
    }
}

Win32 Stuff

We are going to override the WndProc and handle the scrollbar events. Here're the constants with the same name as in the C:\Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include\WinUser.h include file.

C#
private const int WM_HSCROLL = 0x114;
private const int WM_VSCROLL = 0x115;

private const int SB_LINELEFT = 0;
private const int SB_LINERIGHT = 1;
private const int SB_PAGELEFT = 2;
private const int SB_PAGERIGHT = 3;
private const int SB_THUMBPOSITION = 4;
private const int SB_THUMBTRACK = 5;
private const int SB_LEFT = 6;
private const int SB_RIGHT = 7;
private const int SB_ENDSCROLL = 8;

private const int SIF_TRACKPOS = 0x10;
private const int SIF_RANGE = 0x1;
private const int SIF_POS = 0x4;
private const int SIF_PAGE = 0x2;
private const int SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS;

When we recognize the scroll message in the WndProc, we need to call GetScrollInfo in user32.dll to find out the position. (The Microsoft documentation says that this position is sent in the wparam of the scroll message when the type is SB_THUMBPOSITION or SB_THUMBTRACK, but that's simply not true.) So, first GetScrollInfo is imported from the DLL.

C#
[DllImport("user32.dll", SetLastError=true) ]
private static extern int GetScrollInfo(
    IntPtr hWnd, int n, ref ScrollInfoStruct lpScrollInfo );

The ScrollInfoStruct is called SCROLLINFO in Win32 and it's the typical query construct you see in Microsoft APIs: an info structure is loaded with its own size and a question-code (and optionally arguments for the question). This be the case, the question is 'what's your position?', or 'SIF_ALL'.

C#
private struct ScrollInfoStruct
{
    public int cbSize;
    public int fMask;
    public int nMin;
    public int nMax;
    public int nPage;
    public int nPos;
    public int nTrackPos;
}

Finally, detect the message and fire the event

OK, now let's fire the standard ScrollEvent when we receive a scroll message. For this, we need the event, which I called it 'Scrolled', since this is the naming that Microsoft uses for events (like 'Clicked'):

C#
[Category("Action")]
public event ScrollEventHandler Scrolled = null;

In the WndProc, get the scroll message and fire the Scrolled event. In this example, I only respond to the WM_HSCROLL message, since that's the one needed for an owner drawn listbox control. The code for WM_VSCROLL would be exactly the same, however. In this example, I'm only interested in the end-scroll message, which is fired after every scrolling action.

C#
protected override void WndProc(ref System.Windows.Forms.Message msg)
{
    if( msg.Msg == WM_HSCROLL )
    {
        if( Scrolled != null )
        {
            ScrollInfoStruct si = new ScrollInfoStruct();
            si.fMask = SIF_ALL;
            si.cbSize = Marshal.SizeOf(si);
            GetScrollInfo(msg.HWnd, 0, ref si);

            if( msg.WParam.ToInt32() == SB_ENDSCROLL )
            {
                ScrollEventArgs sargs = new ScrollEventArgs(
                    ScrollEventType.EndScroll,
                    si.nPos);
                Scrolled(this, sargs);
            }
        }
    }
    base.WndProc(ref msg);
}

To be able to use the Marshal class, you have to reference the Interop namespace:

C#
using System.Runtime.InteropServices;

That should do it. You can extend it by capturing the vertical scrollbar or by capturing more scroll events. If you want to know the scrollbar position at any other time, use the GetScrollInfo function.

Also, I haven't tried it, but it probably works for any control with a scrollbar in it.

In case you're wondering, I needed this code to have other controls 'scroll along' with a ListBox.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionwill it work properly in Windows Vista ? Pin
Member 452709619-Aug-09 21:48
Member 452709619-Aug-09 21:48 
GeneralScroll position in wParam of SB_THUMBPOSITION and SB_THUMBTRACK Pin
ZachJohnson1-May-09 15:00
ZachJohnson1-May-09 15:00 
GeneralI made some changes to support vscroll and mouse wheel Pin
Harry Chou26-Sep-07 7:11
Harry Chou26-Sep-07 7:11 
QuestionnPos = 0, all the time. Pin
chbfiv21-Aug-07 17:10
chbfiv21-Aug-07 17:10 
AnswerRe: nPos = 0, all the time. Pin
Bob A A26-Oct-08 10:55
Bob A A26-Oct-08 10:55 
GeneralScrollingListBox VB Code Pin
YogJ6923-Aug-06 11:17
YogJ6923-Aug-06 11:17 
GeneralRe: ScrollingListBox VB Code Pin
Bob A A26-Oct-08 10:58
Bob A A26-Oct-08 10:58 
GeneralAdding the control to the form Pin
thomasholme14-Jan-06 1:19
thomasholme14-Jan-06 1:19 
Generalhorizontal scallbar in listbox Pin
Anonymous21-Jun-05 20:32
Anonymous21-Jun-05 20:32 
QuestionCan i get scroll events for an external application? Pin
Member 161728720-Apr-05 20:11
Member 161728720-Apr-05 20:11 
QuestionShould this work for all controls? Pin
mcollins200314-Mar-05 9:10
mcollins200314-Mar-05 9:10 
AnswerRe: Should this work for all controls? Pin
yuanshu_yun11-Jul-06 2:36
yuanshu_yun11-Jul-06 2:36 
QuestionHow do i set position of scroll? Pin
Danko Greiner25-Feb-05 2:50
Danko Greiner25-Feb-05 2:50 
AnswerRe: How do i set position of scroll? Pin
clivenyong5-Feb-06 7:55
clivenyong5-Feb-06 7:55 
GeneralRe: How do i set position of scroll? Pin
PRMan!!!22-Aug-06 13:19
PRMan!!!22-Aug-06 13:19 
Generalsome code for the world Pin
Anonymous31-Jan-05 4:42
Anonymous31-Jan-05 4:42 
GeneralRe: some code for the world (ScrollUtility.cs) Pin
Anonymous31-Jan-05 4:44
Anonymous31-Jan-05 4:44 
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

// TODO: Change this namespace to whatever namespace your project is
namespace ScrollExposed
{

#region Public Enums

public enum ScrollEventOrientation
{
Horizontal = 0,
Vertical
};

#endregion

#region Interface: IScrollExposed

///
/// When you derive a control, and want to expose scroll events (using this utility as a helper), have
/// the control implement this interface to give things a consistent look.
///

public interface IScrollExposed
{

// I wanted to call this event Scroll, which is what the horizontal and vertical scroll bars expose. However,
// DataGrid already exposes that event, but it only passes an EventArg, not even a ScrollEventArg, which
// basically makes it useless. (on a side note for the data grid, you can get at its scrollbars directly through
// the Controls property)
//
// If you really want to go nuts, you could make two events: Before and After. It just depends whether you
// raise the event before or after the call to base.WndProc(ref msg);
event ScrollEventHandlerExpanded ScrollExpanded;

// These allow the user of controls to work with the scrollbars without worrying about min/max/value. (or
// even how the scrollbars are implemented internally) I have a felling that a major use of all this scroll
// exposure is to keep 2 or more controls lined up (treeview lined up vertically with a grid).
//
// Here's the case I'm trying to make work:
// There is a treeview on the left, and a grid on the right. The rows of the grid line up with the nodes of the
// treeview (when they expand a node, rows are inserted; collapse causes rows to be deleted). So when the
// user vertically scrolls the treeview or grid, the other needs to stay in sync. Since my scroll event exposes a
// percent, all I need to do is call the set of the other control.
double ScrollBarPercentOfMaxHorizontal
{
get;
set;
}

double ScrollBarPercentOfMaxVertical
{
get;
set;
}


}


#endregion

#region Delegate: ScrollEventHandlerExpanded

public delegate void ScrollEventHandlerExpanded(object sender, ScrollEventArgsExpanded e);

#endregion

#region Class: ScrollEventArgsExpanded

public class ScrollEventArgsExpanded : ScrollEventArgs
{

private ScrollEventOrientation _orientation;
private double _percentOfMax;

public ScrollEventArgsExpanded(ScrollEventOrientation orientation, ScrollEventType type, int newValue, double percentOfMax) : base(type, newValue)
{
_orientation = orientation;
_percentOfMax = percentOfMax;
}

///
/// Tells you what direction the user was scrolling to fire this event
///

public ScrollEventOrientation Orientation
{
get
{
return _orientation;
}
}


///
/// This goes from 0 to 1, and tells how far along the scroll bar is:
/// (Value - Min) / (Max - Min)
///

/// <remarks>
/// This will most likely never be 1. The width of the trackbar gets in the way. However, for keeping 2
/// controls in sync, that shouldn't matter.
///
public double PercentOfMax
{
get
{
return _percentOfMax;
}
}


}


#endregion

#region Class: ScrollUtility

///
/// Several helper functions to change a WndProc message into scroll events
///

/// <remarks>
/// Within your derived control, override WndProc. If the m.Msg equals WM_HSCROLL or WM_VSCROLL, then
/// you can use the helper functions within this class to turn the rest of msg into scroll event args
///
public class ScrollUtility
{

#region Declaration Section

//-------------------------- These constants came from C:\Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include\WinUser.h
//
// These are constants that tell you what the m.Msg in WndProc is. ex:
// if(m.Msg == ScrollUtility.WM_HSCROLL)
public const int WM_HSCROLL = 0x114;
public const int WM_VSCROLL = 0x115;

// These are constants that help me decipher the rest of the message from WndProc
private const int SB_LINEUP = 0;
private const int SB_LINELEFT = 0;
private const int SB_LINEDOWN = 1;
private const int SB_LINERIGHT = 1;
private const int SB_PAGEUP = 2;
private const int SB_PAGELEFT = 2;
private const int SB_PAGEDOWN = 3;
private const int SB_PAGERIGHT = 3;
private const int SB_THUMBPOSITION = 4;
private const int SB_THUMBTRACK = 5;
private const int SB_TOP = 6;
private const int SB_LEFT = 6;
private const int SB_BOTTOM = 7;
private const int SB_RIGHT = 7;
private const int SB_ENDSCROLL = 8;

// These are constants that help me call GetScrollInfo (which gives me information about scrollbar position)
private const int SB_HORZ = 0;
private const int SB_VERT = 1;
private const int SB_CTL = 2;
private const int SIF_TRACKPOS = 0x10;
private const int SIF_RANGE = 0x1;
private const int SIF_POS = 0x4;
private const int SIF_PAGE = 0x2;
private const int SIF_ALL = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS;

[DllImport("user32.dll", SetLastError=true)]
private static extern int GetScrollInfo( IntPtr hWnd, int n, ref ScrollInfoStruct lpScrollInfo );

[DllImport("user32.dll", SetLastError=true)]
private static extern int SetScrollInfo( IntPtr hWnd, int n, ref ScrollInfoStruct lpScrollInfo, bool fRedraw );


private struct ScrollInfoStruct
{
public int cbSize;
public int fMask;
public int nMin;
public int nMax;
public int nPage;
public int nPos;
public int nTrackPos;
}


#endregion

#region GetNewScrollEventArgsExpanded

public static ScrollEventArgsExpanded GetNewScrollEventArgsExpanded(Message m)
{

// These are the values that make up the return class
ScrollEventOrientation orientation;
ScrollEventType type;
int newValue;
double percentOfMax;

int getscrollinfoOrientationParam;

// Figure out the orientation
switch (m.Msg)
{
case WM_HSCROLL:
orientation = ScrollEventOrientation.Horizontal;
getscrollinfoOrientationParam = SB_HORZ; // This assumes the control has a default horizonal scrollbar. If that's not the case, you might have to get more elaborite
break;

case WM_VSCROLL:
orientation = ScrollEventOrientation.Vertical;
getscrollinfoOrientationParam = SB_VERT; // This assumes the control has a default vertical scrollbar. If that's not the case, you might have to get more elaborite
break;

default:
throw new ArgumentOutOfRangeException("m.Msg", m.Msg, "Only WM_HSCROLL and WM_VSCROLL are supported");
}

// Now I can call GetScrollInfo
ScrollInfoStruct scrollInfo = GetScrollInfoStruct(m.HWnd, getscrollinfoOrientationParam);

// Figure out the type
switch (LOWORD(m.WParam.ToInt32())) // I need to pull the loword of the integer, because ThumbPos and ThumbTrack use the hiword of the integer for something else
{
case SB_THUMBPOSITION:
type = ScrollEventType.ThumbPosition;
break;

case SB_THUMBTRACK:
type = ScrollEventType.ThumbTrack;
break;

case SB_ENDSCROLL:
type = ScrollEventType.EndScroll;
break;

case SB_TOP: // SB_LEFT is the same value
type = ScrollEventType.First;
break;

case SB_BOTTOM: // SB_RIGHT is the same value
type = ScrollEventType.Last;
break;

case SB_PAGEUP: // SB_PAGELEFT is the same value
type = ScrollEventType.LargeDecrement;
break;

case SB_PAGEDOWN: // SB_PAGERIGHT is the same value
type = ScrollEventType.LargeIncrement;
break;

case SB_LINEUP: // SB_LINELEFT is the same value
type = ScrollEventType.SmallDecrement;
break;

case SB_LINEDOWN: // SB_LINERIGHT is the same value
type = ScrollEventType.SmallIncrement;
break;

default:
throw new ArgumentOutOfRangeException("LOWORD(m.WParam.ToInt32())", LOWORD(m.WParam.ToInt32()), "One of SB_ENDSCROLL, SB_THUMBTRACK, etc (between 0 and 8)");
}

// Figure out the value and percent of max
percentOfMax = GetPercentOfMax(scrollInfo.nPos, scrollInfo.nMin, scrollInfo.nMax);
newValue = scrollInfo.nPos;

// Exit Function
return new ScrollEventArgsExpanded(orientation, type, newValue, percentOfMax);
}


#endregion

#region Get/Set ScrollBarPercentOfMax

public static double GetScrollBarPercentOfMaxHorizontal(IntPtr handle)
{

ScrollInfoStruct scrollInfo = GetScrollInfoStruct(handle, SB_HORZ);

return GetPercentOfMax(scrollInfo.nPos, scrollInfo.nMin, scrollInfo.nMax);

}

public static double GetScrollBarPercentOfMaxVertical(IntPtr handle)
{

ScrollInfoStruct scrollInfo = GetScrollInfoStruct(handle, SB_VERT);

return GetPercentOfMax(scrollInfo.nPos, scrollInfo.nMin, scrollInfo.nMax);

}


public static void SetScrollBarPercentOfMaxHorizontal(IntPtr handle, double percent)
{

SetScrollBarPercentOfMax(handle, percent, SB_HORZ);

}

public static void SetScrollBarPercentOfMaxVertical(IntPtr handle, double percent)
{

SetScrollBarPercentOfMax(handle, percent, SB_VERT);

}

private static void SetScrollBarPercentOfMax(IntPtr handle, double percent, int orientation)
{

// Grab a scroll info structure
ScrollInfoStruct scrollInfo = GetScrollInfoStruct(handle, orientation);

// Set the position to something new
scrollInfo.nPos = Convert.ToInt32(Math.Round(Convert.ToDouble(scrollInfo.nMax - scrollInfo.nMin) * percent));

// Make sure it's not out of range (the user needs to pass in a percent from 0 to 1). Even then, there
// may be some mathmatical error
if(scrollInfo.nPos < scrollInfo.nMin)
{
scrollInfo.nPos = scrollInfo.nMin;
}
else if(scrollInfo.nPos > scrollInfo.nMax)
{
scrollInfo.nPos = scrollInfo.nMax;
}

// I don't want to set everything, just position
scrollInfo.fMask = SIF_POS;

// Tell the control to go to this new position
SetScrollInfo(handle, orientation, ref scrollInfo, true); // It may not need to be byref

}


#endregion

#region Private Support Functions

private static int LOWORD(int value32)
{
return value32 & 0xFFFF;
}

private static int LOWORD(IntPtr valuePtr)
{
return LOWORD(valuePtr.ToInt32());
}


private static int HIWORD(int value32)
{

throw new ApplicationException("Downloaded from VB example. Ported to C#, but needs testing");

//Public Function HIWORD(ByRef pintValue As Int32) As Int32
// If (pintValue And &H80000000) = &H80000000 Then
// Return ((pintValue And &H7FFF0000) \ &H10000) Or &H8000&
// Else
// Return (pintValue And &HFFFF0000) \ &H10000
// End If
//End Function

if ((value32 & 0x80000000) == 0x80000000)
{
return ((value32 & 0x7FFF0000) / 0x10000) | 0x8000;
}
else
{
return Convert.ToInt32((value32 & 0xFFFF0000) / 0x10000);
}

}


private static double GetPercentOfMax(int position, int min, int max)
{

double retVal = Convert.ToDouble(position - min) / Convert.ToDouble(max - min);

// Since this function is a helper to the custom controls, this can be called at design time. So I
// need this if statement here. Note: using (retVal == Double.NaN) doesn't work.
if(Double.IsNaN(retVal))
{
return 0;
}
else
{
return retVal;
}

}


///
/// Fills up a ScrollInfoStruct
///

/// <param name="handle" />The handle to the control
/// <param name="orientation" />SB_HORZ or SB_VERT or maybe SB_CTL
/// <returns>Tells you where the scroll position is (plus min and max of the scrollbar)
private static ScrollInfoStruct GetScrollInfoStruct(IntPtr handle, int orientation)
{

// Make the shell of the structure (set two of the elements that form the question)
ScrollInfoStruct retVal = new ScrollInfoStruct();
retVal.fMask = SIF_ALL;
retVal.cbSize = Marshal.SizeOf(retVal);

// Ask for the rest of the structure to be filled out (based on what I just set up)
GetScrollInfo(handle, orientation, ref retVal);

// Exit Function
return retVal;
}


#endregion

}


#endregion

}
GeneralRe: some code for the world (Derived Listbox) Pin
Anonymous31-Jan-05 4:49
Anonymous31-Jan-05 4:49 
GeneralRe: some code for the world (Derived Listbox) Pin
dhawal kapdia21-Feb-06 19:17
dhawal kapdia21-Feb-06 19:17 
GeneralRe: some code for the world (about ScrollUtility.cs) Pin
Anonymous31-Jan-05 5:05
Anonymous31-Jan-05 5:05 
GeneralRe: some code for the world (about ScrollUtility.cs) Pin
brianlang753-Jun-05 9:15
brianlang753-Jun-05 9:15 
GeneralRe: some code for the world (about ScrollUtility.cs) Pin
kasix16-Nov-05 13:47
kasix16-Nov-05 13:47 
GeneralRe: some code for the world (about ScrollUtility.cs) Pin
PRMan!!!22-Aug-06 13:19
PRMan!!!22-Aug-06 13:19 
QuestionRe: some code for the world (about ScrollUtility.cs) Pin
divinaet20-Aug-07 21:33
divinaet20-Aug-07 21:33 
Generaloriginally Position vs. target Position Pin
thopra19-Jan-05 5:47
thopra19-Jan-05 5:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.