强制重绘 ComboBox 的 DropDownList

问题描述 投票:0回答:1

我正在使用绑定到

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 个单位中,则复选框被视为已切换。

  • 如果命中测试是复选框切换,则下拉列表保持打开状态并且 CheckBox 控件切换状态。
  • 如果命中测试位于复选框的右侧,则下拉列表将关闭并提交,而不会再对复选框进行任何状态更改。

当鼠标移到下拉列表项上时,

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
在新状态下重新绘制?

c# winforms custom-controls mouse-hook
1个回答
0
投票

当鼠标在列表上移动时,鼠标事件挂钩已经在跟踪项目索引。我们可以观察到,每当内部选择的索引发生变化时,基础

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

© www.soinside.com 2019 - 2024. All rights reserved.