You can create a Custom Control, derived from ComboBox, override its WndProc method to intercept the CB_GETCURSEL message.
Call base.WndProc(ref m)
first. When the message is processed, the Message object's m.Result
property is set to a value (as IntPtr
) that represents the Item currently tracked in the ListBox (the Item highlighted when the Mouse Pointer hovers it).
? Note: prior to .Net Framework 4.8, the CB_GETCURSEL
message result is not bubbled up automatically: LB_GETCUSEL
must be sent to the child ListBox to get the index of the Item currently highlighted.
The ListBox handle is retrieved using GetComboBoxInfo: it could be also accessed using reflection (the private ChildListAutomationObject
property returns the ListBox AutomationElement, which provides the handle), or sending a CB_GETCOMBOBOXINFO
message (but it's the same as calling GetComboBoxInfo()
).
This custom ComboBox raises an Event, ListItemSelectionChanged
, with a custom EventArgs
object, ListItemSelectionChangedEventArgs
, which exposes two public properties: ItemIndex
and ItemText
, set to the Index and Text of the hovered item.
using System.ComponentModel;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class ComboBoxListEx : ComboBox
{
private const int CB_GETCURSEL = 0x0147;
private int listItem = -1;
IntPtr listBoxHandle = IntPtr.Zero;
public event EventHandler<ListItemSelectionChangedEventArgs> ListItemSelectionChanged;
protected virtual void OnListItemSelectionChanged(ListItemSelectionChangedEventArgs e)
=> this.ListItemSelectionChanged?.Invoke(this, e);
public ComboBoxListEx() { }
// .Net Framework prior to 4.8 - get the handle of the ListBox
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
listBoxHandle = GetComboBoxListInternal(this.Handle);
}
protected override void WndProc(ref Message m)
{
int selItem = -1;
base.WndProc(ref m);
switch (m.Msg) {
case CB_GETCURSEL:
selItem = m.Result.ToInt32();
break;
// .Net Framework prior to 4.8
// case CB_GETCURSEL can be left there or removed: it's always -1
case 0x0134:
selItem = SendMessage(listBoxHandle, LB_GETCUSEL, 0, 0);
break;
default:
// Add Case switches to handle other events
break;
}
if (listItem != selItem) {
listItem = selItem;
OnListItemSelectionChanged(new ListItemSelectionChangedEventArgs(
listItem, listItem < 0 ? string.Empty : GetItemText(Items[listItem]))
);
}
}
public class ListItemSelectionChangedEventArgs : EventArgs
{
public ListItemSelectionChangedEventArgs(int idx, string text) {
ItemIndex = idx;
ItemText = text;
}
public int ItemIndex { get; private set; }
public string ItemText { get; private set; }
}
// -------------------------------------------------------------
// .Net Framework prior to 4.8
[DllImport("user32.dll", CharSet = CharSet.Auto)]
internal static extern bool GetComboBoxInfo(IntPtr hWnd, ref COMBOBOXINFO pcbi);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
internal static extern int SendMessage(IntPtr hWnd, uint uMsg, int wParam, int lParam);
private const int LB_GETCUSEL = 0x0188;
[StructLayout(LayoutKind.Sequential)]
internal struct COMBOBOXINFO
{
public int cbSize;
public Rectangle rcItem;
public Rectangle rcButton;
public int buttonState;
public IntPtr hwndCombo;
public IntPtr hwndEdit;
public IntPtr hwndList;
public void Init() => this.cbSize = Marshal.SizeOf<COMBOBOXINFO>();
}
internal static IntPtr GetComboBoxListInternal(IntPtr cboHandle)
{
var cbInfo = new COMBOBOXINFO();
cbInfo.Init();
GetComboBoxInfo(cboHandle, ref cbInfo);
return cbInfo.hwndList;
}
}
Works like this:
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…