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:
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.
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetScrollInfo(IntPtr hWnd, int n,
ref ScrollInfoStruct lpScrollInfo);
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;
[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:
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:
protected override void WndProc(ref Message msg)
{
if ((msg.Msg == 308) || (msg.Msg == 32))
{
int onScreenIndex = 0;
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;
}
int oldYPos = yPos;
while (yPos >= this.ItemHeight)
{
yPos -= this.ItemHeight;
onScreenIndex++;
}
ScrollInfoStruct si = new ScrollInfoStruct();
si.fMask = SIF_ALL;
si.cbSize = Marshal.SizeOf(si);
int getScrollInfoResult = 0;
getScrollInfoResult = GetScrollInfo(msg.LParam, SB_VERT, ref si);
if (getScrollInfoResult > 0)
{
onScreenIndex += si.nTrackPos;
if (this.DropDownStyle == ComboBoxStyle.Simple)
{
simpleOffset = si.nTrackPos;
}
}
if (this.DropDownStyle == ComboBoxStyle.Simple)
{
onScreenIndex += simpleOffset;
if (onScreenIndex > ((this.DropDownHeight /
this.ItemHeight) + simpleOffset))
{
onScreenIndex = ((this.DropDownHeight /
this.ItemHeight) + simpleOffset - 1);
}
}
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 != si.nPos)
{
Cursor.Position = new Point(Cursor.Position.X +
xFactor, Cursor.Position.Y);
xFactor = -xFactor;
}
}
scrollPos = si.nPos;
}
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
this.PointToClient(Cursor.Position)
was new to me, and a useful way of getting control based mouse position.
- 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...
- 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.