65.9K
CodeProject 正在变化。 阅读更多。
Home

当鼠标悬停在下拉列表时,ComboBox 触发事件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (20投票s)

2006年5月26日

4分钟阅读

viewsIcon

194293

downloadIcon

3066

本文介绍了一个自定义 ComboBox,它会触发一个事件,让用户知道他们正在悬停在 ComboBox 下拉列表中的哪个项目上。

Sample Image - a sample application running

引言

您是否曾经希望您的 ComboBox 能够提供实时反馈?您是否曾希望它能够在用户单击之前就告知用户他们正在悬停在下拉列表的哪个项目上?在本文中,我将介绍如何继承一个自定义组合框控件,该控件将在鼠标光标悬停在下拉列表的项目上时触发事件。

背景

我接到一个请求,要求有一个组合框,允许用户根据其下拉列表中鼠标悬停的项目来更新某些内容。“容易!”我心想。然后,我坐下来开始做,才发现使用标准的 .NET 控件比我预期的要困难一些。以下是我劳动的成果……如果您知道更简单的方法,我很想听听!

直达代码!

自行测试此功能的最佳方法是运行示例应用程序。您应该会发现,此组合框与标准组合框之间的唯一区别是增加了一个新事件 Hover。这是一个非常有用的事件,它可以在用户实际选择任何新内容之前告知您用户正在悬停在下拉列表中的新项目上。有点像组合框的预览。

在代码本身中,您会注意到我们定义了自己的自定义事件参数类和用于处理我们事件的委托

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);

这只是在标准 EventArgs 中添加了一个名为“itemIndex”的新属性,我们的事件使用它来告知我们当前正在悬停在组合框中的哪个项目。该委托接受 sender 对象以及我们自定义 HoverEventArgs 类提供的事件参数。

第一个真正有趣的点是以下代码段,以及为什么我们必须使用 System.Runtime.InteropServices 程序集。

// 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;
}

我们需要 user32.dll 中的 GetScrollInfo 函数来告知我们当滚动条用于移动组合框的下拉列表部分时,那里发生了什么。通过鼠标坐标很容易知道屏幕上突出显示的项目是哪个,但要知道列表向上或向下滚动了多少是另一回事,我们不得不诉诸于此互操作解决方案。

一旦我们完成了所有这些定义,我们就可以进入此控件的主要部分:重写 protected override void WndProc(ref Message msg) 方法。

在检查我们实际上正在更改列表中的位置(即 msg.Msg 等于 308)之后,我们必须捕获鼠标位置,以便我们可以确定鼠标正在悬停在下拉列表上的项目。您可以通过直接使用 Cursor.Position 功能来实现这一点,但这会给出屏幕坐标。这样做会更简单,以便该点相对于组合框控件

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

一旦我们有了鼠标位置,我们就会计算出列表中以零为基础的突出显示的项目,同时考虑 ComboBox.ItemHeightComboBox.Size.Height,这样调整控件中的元素大小就不会破坏我们的代码。

好了,这是全部功能的重写 WndProc 方法

//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);
}

我们创建一个 ScrollInfoStructsi,来保存来自我们互操作调用的信息。运行此程序可以让我们获取滚动条的状态,我们使用它来修改 onScreenIndex 变量,以表示实际正在下拉列表中悬停的项目。一旦我们确定了这是哪个项目,我们就检查鼠标是否实际上在下拉列表的边界内(因为您可以在控件外部滚动鼠标滚轮来移动它),然后如果我们在这里面,我们就发出事件,说明我们正在悬停在一个新项目上。剩下的就是在这个包含此组合框的窗体上接收事件,然后问题就解决了!您将确切地知道用户何时在您自定义的组合框中悬停在新项目上。

关注点

  1. this.PointToClient(Cursor.Position) 对我来说是新的,并且是一种获取控件基于鼠标位置的有用方法。
  2. 很有趣的是,下拉列表窗口的句柄被传递到 lParam 参数中的 WndProc。这在官方文档中几乎找不到!事实上,我从未找到过……
  3. 用鼠标滚轮移动下拉列表内容没有突出显示新项目——这似乎完全基于鼠标实际移动,因此我不得不添加一些代码来在发生这种情况时将鼠标移动一个像素。

历史

  • 2006年5月29日 - 更新以更好地与 DropDownStyle 设置为 Simple 一起使用。
  • 2006年5月26日 - 代码首次提交。
© . All rights reserved.