Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

ComboBox firing events when hovering on the dropdown list

4.66/5 (22 votes)
29 May 20064 min read 1   3.1K  
This article describes a custom ComboBox that will fire an event that lets the user know which item is being hovered over in the dropdown list of a ComboBox.

Sample Image - a sample application running

Introduction

Have you ever wanted live feedback from your ComboBox? Have you ever wanted it to be able to let you know which item the user is hovering over in the dropdown list before it is clicked? In this article, I will show how to inherit a custom combo box control which will fire events when the mouse cursor is hovering over the items in the dropdown list.

Background

I had a request for a combo box that allowed you to update something depending on which item was being hovered over in its dropdown list. "Easy!", I thought. Then, I sat down to do it, and discovered that with the standard .NET control, it was a little more difficult than I had anticipated. Here are the results of my labor... if you know a simpler way, I'd love to hear it!

Straight to the Code!

The easiest way to test this out for yourself is to run the sample application. You should see that the only difference between this combo box and a standard one is the addition of a new event, Hover. This is a very useful event which lets you know when the user is hovering over a new item in the dropdown list before the user actually chooses anything new. Kind of like a preview for the combo box.

In the code itself, you'll notice that we define our own custom event arguments class and a delegate to handle our event:

C#
public class HoverEventArgs : EventArgs
{
    private int _itemIndex = 0;
    public int itemIndex
    {
        get
        {
            return _itemIndex;
        }
        set
        {
            _itemIndex = value;
        }
    }
}

...

public delegate void HoverEventHandler(object sender, HoverEventArgs e);

This just adds a new property to the standard EventArgs, called "itemIndex", which our event uses to inform us which item in the combo box is currently being hovered over. The delegate accepts the sender object and the event arguments provided by our custom HoverEventArgs class.

The first real point of interest is the following section of code, and the reason we have to use the System.Runtime.InteropServices assembly.

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

// Win32 constants
private const int SB_VERT = 1;
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;

private const int SCROLLBAR_WIDTH = 17;
private const int LISTBOX_YOFFSET = 21;

// Return structure for the GetScrollInfo method
[StructLayout(LayoutKind.Sequential)]
private struct ScrollInfoStruct
{
    public int cbSize;
    public int fMask;
    public int nMin;
    public int nMax;
    public int nPage;
    public int nPos;
    public int nTrackPos;
}

We need the GetScrollInfo function in user32.dll to let us know what's happening to our dropdown list part of the combo box when the scrollbar is used to move it. It is fairly trivial to know which item is being highlighted on screen by the mouse co-ordinates, but to know how far we've scrolled up or down the list is another matter entirely, and we have to resort to this interop solution.

Once we're done defining all of that, we can get along to the main part of this control: overriding the protected override void WndProc(ref Message msg) method.

After checking to see that we're actually changing the position in the list (that is, msg.Msg equals 308), we have to go ahead and capture the mouse position so we can determine the item being hovered over by the mouse over the dropdown list. You can accomplish this by using the Cursor.Position functionality directly, but that gives you screen co-ordinates. Much easier to do the following so that the point is relative to the combo box control:

C#
Point LocalMousePosition = this.PointToClient(Cursor.Position);
xPos = LocalMousePosition.X;
yPos = LocalMousePosition.Y - this.Size.Height - 1;

Once we have the mouse position, we calculate which item is highlighted from a zero based standpoint in the list, taking into account ComboBox.ItemHeight and the ComboBox.Size.Height, so that resizing elements in the control shouldn't break our code.

Okay, here is the overridden WndProc method in all its glory:

C#
//Capture messages coming to our combobox
protected override void WndProc(ref Message msg)
{
    //This message code indicates the value in the list is changing
    //32 is for DropDownStyle == Simple
    if ((msg.Msg == 308) || (msg.Msg == 32))
    {
        int onScreenIndex = 0;

        // Get the mouse position relative to this control
        Point LocalMousePosition = this.PointToClient(Cursor.Position);
        xPos = LocalMousePosition.X;

        if (this.DropDownStyle == ComboBoxStyle.Simple)
        {
            yPos = LocalMousePosition.Y - (this.ItemHeight + 10);
        }
        else
        {   
            yPos = LocalMousePosition.Y - this.Size.Height - 1;
        }

        // save our y position which we need to ensure the cursor is
        // inside the drop down list for updating purposes
        int oldYPos = yPos;

        // get the 0-based index of where the cursor is on screen
        // as if it were inside the listbox
        while (yPos >= this.ItemHeight)
        {
            yPos -= this.ItemHeight;
            onScreenIndex++;
        }

        //if (yPos < 0) { onScreenIndex = -1; }
        ScrollInfoStruct si = new ScrollInfoStruct();
        si.fMask = SIF_ALL;
        si.cbSize = Marshal.SizeOf(si);
        // msg.LParam holds the hWnd to the drop down list that appears
        int getScrollInfoResult = 0;
        getScrollInfoResult = GetScrollInfo(msg.LParam, SB_VERT, ref si);
        
        // k returns 0 on error, so if there is no error add the current
        // track position of the scrollbar to our index
        if (getScrollInfoResult > 0)
        {
            onScreenIndex += si.nTrackPos;
            
            if (this.DropDownStyle == ComboBoxStyle.Simple)
            {
                simpleOffset = si.nTrackPos;
            }
        }

        // Add our offset modifier if we're a simple combobox since we don't
        // continuously receive scrollbar information in this mode.
        // Then make sure the item we're previewing is actually on screen.
        if (this.DropDownStyle == ComboBoxStyle.Simple)
        {
            onScreenIndex += simpleOffset;
            if (onScreenIndex > ((this.DropDownHeight / 
                                  this.ItemHeight) + simpleOffset))
            {
                onScreenIndex = ((this.DropDownHeight / 
                                  this.ItemHeight) + simpleOffset - 1);
            }
        }

        // Check we're actually inside the drop down window that appears and 
        // not just over its scrollbar before we actually try to update anything
        // then if we are raise the Hover event for this comboBox
        if (!(xPos > this.Width - SCROLLBAR_WIDTH || xPos < 1 || 
              oldYPos < 0 || ((oldYPos > this.ItemHeight * 
              this.MaxDropDownItems) && this.DropDownStyle 
              != ComboBoxStyle.Simple)))
        {
            HoverEventArgs e = new HoverEventArgs();
            e.itemIndex = (onScreenIndex > this.Items.Count - 1) ? 
                           this.Items.Count - 1 : onScreenIndex;
            OnHover(e);
            // if scrollPos doesn't equal the nPos from our ScrollInfoStruct then
            // the mousewheel was most likely used to scroll the drop down list
            // while the mouse was inside it - this means we have to manually
            // tell the drop down to repaint itself to update where it is hovering
            // still posible to "outscroll" this method but it works better than
            // without it present
            if (scrollPos != si.nPos)
            {
                Cursor.Position = new Point(Cursor.Position.X + 
                                  xFactor, Cursor.Position.Y);
                xFactor = -xFactor;
            }
        }
        scrollPos = si.nPos;
    }
    // Pass on our message
    base.WndProc(ref msg);
}

We create a ScrollInfoStruct, si, to hold the information from our interop call. Running this allows us to grab the state of the scrollbar which we use to alter the onScreenIndex variable to represent the actual item being hovered over in the dropdown list. Once we have determined which item this is, we check to see if the mouse is actually inside the bounds of the drop down (since you can scroll the mouse wheel when you're not inside the control, to move it), and then if we are, we send out the event saying we're hovering over a new item. All that remains is to sink the event on the form containing this combo box, and then problem solved! You'll know exactly when a user hovers over a new item in your custom combo box.

Points of Interest

  1. this.PointToClient(Cursor.Position) was new to me, and a useful way of getting control based mouse position.
  2. Very interesting that the handle of the dropdown list window is passed to the WndProc in the lParam argument. Incredibly difficult to find this officially documented anywhere! In fact, I never did...
  3. Moving the content of the dropdown list with the mouse wheel didn't highlight a new item - this seems to be based purely on the mouse actually moving, so I had to add a little code to move the mouse by a pixel when this happens.

History

  • 5/29/2006 - Updated to work better with a DropDownStyle of Simple.
  • 5/26/2006 - First submission of code.

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