我正在使用绑定到
ComboBox
控件列表的 CheckBox
来制作一个选中的组合框列表。具体实现是我自己的,不过思路来自一个SO问题,这个问题的link好像暂时没了。当需要在下拉列表中绘制项目时,我的方法是将复选框呈现为位图,并将位图绘制到 DrawItem
覆盖提供的范围内。
同时获取相对于下拉列表hWnd的屏幕坐标,将矩形塞入复选框的
Tag
属性中,以便在WM_LBUTTONDOWN
钩子拦截点击时进行命中测试。
protected override void OnDrawItem(DrawItemEventArgs e)
{
base.OnDrawItem(e);
CheckBox checkbox;
checkbox = CheckBoxItems[e.Index];
checkbox.Size = e.Bounds.Size;
// PInvoke to get the screen coordinates relative to the drop list.
POINT p = new POINT(e.Bounds.Left, e.Bounds.Top);
ClientToScreen(_hwndList, ref p);
checkbox.Tag = new Rectangle(
new Point(p.X, p.Y),
e.Bounds.Size);
using (var bitmap = new Bitmap(e.Bounds.Width, e.Bounds.Height))
{
checkbox.DrawToBitmap(bitmap, new Rectangle(Point.Empty, e.Bounds.Size));
e.Graphics.DrawImage(bitmap, e.Bounds);
}
}
自定义
ComboBox
类实现IMessageFilter.
class ComboBoxEx : ComboBox, IMessageFilter
{
public ComboBoxEx()
{
DrawMode = DrawMode.OwnerDrawFixed;
DoubleBuffered= true;
Application.AddMessageFilter(this);
Disposed += (sender, e) =>Application.RemoveMessageFilter(this);
DataSource = CheckBoxItems;
DisplayMember = "Text";
// Add items for testing purposes
CheckBoxItems.Add(new CheckBox { Text = "Essential" });
CheckBoxItems.Add(new CheckBox { Text = "Primary"});
CheckBoxItems.Add(new CheckBox { Text = "Evoke" });
CheckBoxItems.Add(new CheckBox { Text = "Retain" });
CheckBoxItems.Add(new CheckBox { Text = "Model" });
CheckBoxItems.Add(new CheckBox { Text = "Personality" });
}
protected override void OnCreateControl()
{
base.OnCreateControl();
// Get the handle of the listbox
var mi = typeof(ComboBox).GetMethod("GetListHandle", BindingFlags.NonPublic | BindingFlags.Instance);
var aspirant = (IntPtr)mi?.Invoke(this, new object[] { });
_hwndList = aspirant;
}
.
.
.
它将屏幕坐标中的
MousePosition
与Tag
属性中捕获的屏幕矩形进行比较。还有一个原始命中测试,如果命中发生在偏移量的前 30 个单位中,则复选框被视为已切换。
当鼠标移到下拉列表项上时,
SelectedIndex
在内部发生变化。 WM_MOUSEMOVE
使该值保持最新状态。
public bool PreFilterMessage(ref Message m)
{
if (m.HWnd.Equals(_hwndList))
{
switch (m.Msg)
{
case WM_MOUSEMOVE:
SelectedIndexInDropDown = SelectedIndex;
break;
case WM_LBUTTONDOWN:
var capture = MousePosition;
var checkbox =
CheckBoxItems
.FirstOrDefault(_ => ((Rectangle)_.Tag).Contains(capture));
if (checkbox != null)
{
var rect = (Rectangle)checkbox.Tag;
var delta = capture.X - rect.Left;
// Hit test.
// - Keep open if checkbox toggled.
// - Otherwise close without toggling.
if (delta < 30)
{
checkbox.Checked = !checkbox.Checked;
updateText();
return true;
}
}
break;
}
}
return false;
}
虽然所有这些都运行良好,但出现与原始帖子中相同的问题。作为列表项后备存储的“真实”复选框被切换,但屏幕上的视觉效果是在其先前状态下捕获的位图。问题是这个位图不会仅仅通过使那个矩形无效而改变。所以问题仍然存在:我们如何让
CheckBox
在新状态下重新绘制?
当鼠标在列表上移动时,鼠标事件挂钩已经在跟踪项目索引。我们可以观察到,每当内部选择的索引发生变化时,基础
ComboBox
控件就会发出DrawItem
命令。它“几乎”按原样工作。如果您单击复选框,滚动 different 项目,然后返回您刚刚选中的项目,突然,复选框以正确的状态重新绘制。
这表明所有要做的就是为已检查的项目重新发出
CB_SETCURSEL
消息。事实上,这似乎确实解决了问题并立即在正确的状态下绘制复选框。
public bool PreFilterMessage(ref Message m)
{
if (m.HWnd.Equals(_hwndList))
{
switch (m.Msg)
{
case WM_MOUSEMOVE:
SelectedIndexInDropDown = SelectedIndex;
break;
case WM_LBUTTONDOWN:
var capture = MousePosition;
var checkbox =
CheckBoxItems
.FirstOrDefault(_ => ((Rectangle)_.Tag).Contains(capture));
if (checkbox != null)
{
var rect = (Rectangle)checkbox.Tag;
var delta = capture.X - rect.Left;
// Hit test.
// - Keep open if checkbox toggled.
// - Otherwise close without toggling.
if (delta < 30)
{
checkbox.Checked = !checkbox.Checked;
// HERE
SendMessage(Handle, CB_SETCURSEL, CheckBoxItems.IndexOf(checkbox), 0);
updateText();
return true;
}
}
break;
}
}
return false;
}