ListView 组 Header 单击 - 如何向 ListView 组 Header 添加上下文菜单?

ListView Group Header Click - How to add a context menu to ListView Group Headers?

我在我的 WinForms 应用程序中使用 ListView,它包含很多值和一些组。组 header 仅显示组的名称,因此我想向组 header 添加一个上下文菜单,其中包含一个项目 "Show description" 以显示该组的详细摘要。

谷歌搜索一段时间后,我只找到了具有此功能的第三方控件。

如何在不使用第 3 方软件的情况下将 ContextMenu 添加到群组 header?

您可以向 ListView 发送 LVM_HITTEST 消息。当你将 -1 传递给 wParam 时,如果 return 值大于 -1 并且结果中已设置 LVHT_EX_GROUP_HEADER,则 return SendMessage 方法的值将被点击组索引。

实施

在下面的实现中,我将 GroupHeaderClick 事件添加到 MyListView class。您可以这样简单地处理事件:

private void myListView1_GroupHeaderClick(object sender, int e)
{
    //Show ContextMenuStrip here. Or just for example:
    MessageBox.Show(myListView1.Groups[e].Header);
}

这里是 MyListView 实现:

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
public class MyListView : ListView
{
    public event EventHandler<int> GroupHeaderClick;
    protected virtual void OnGroupHeaderClick(int e)
    {
        var handler = GroupHeaderClick;
        if (handler != null) handler(this, e);
    }
    private const int LVM_HITTEST = 0x1000 + 18;
    private const int LVHT_EX_GROUP_HEADER = 0x10000000;
    [StructLayout(LayoutKind.Sequential)]
    private struct LVHITTESTINFO
    {
        public int pt_x;
        public int pt_y;
        public int flags;
        public int iItem;
        public int iSubItem;
        public int iGroup;
    }
    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int msg,
        int wParam, ref LVHITTESTINFO ht);
    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);
        var ht = new LVHITTESTINFO() { pt_x = e.X, pt_y = e.Y };
        var value = SendMessage(this.Handle, LVM_HITTEST, -1, ref ht);
        if (value != -1 && (ht.flags & LVHT_EX_GROUP_HEADER) != 0)
            OnGroupHeaderClick(value);
    }
}

Reza Aghaei 的解决方案只有在将组添加到项之前才有效。如果在项目已添加后添加组,则会失败,因为 SendMessage 函数 returns 错误的索引。它实际上是 returns ID 而不是索引。这是 ListView 组件中的一个已知问题。这个想法是将返回值与组 ID 进行比较。

public class WListView : ListView
{
    #region [ PInvoke ]

    private const int LVM_HITTEST = 0x1000 + 18;
    private const int LVM_SUBITEMHITTEST = 0x1000 + 57;
    private const int LVHT_EX_GROUP_HEADER = 0x10000000;

    [StructLayout(LayoutKind.Sequential)]
    private struct LVHITTESTINFO
    {
        public int pt_x;
        public int pt_y;
        public int flags;
        public int iItem;
        public int iSubItem;
        public int iGroup;
    }

    [DllImport("user32.dll", EntryPoint = "SendMessage", CharSet = CharSet.Auto)]
    private static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref LVHITTESTINFO ht);

    #endregion

    /// <summary>
    /// Occurs when a group is clicked.
    /// </summary>
    [Category("Behavior")]
    [Description("Occurs when a group header is clicked.")]
    public event EventHandler<ListViewGroupClickEventArgs> GroupClick;

    /// <summary>
    /// Raises the GroupClick event.
    /// </summary>
    /// <param name="e">Event arguments.</param>
    protected virtual void OnGroupHeaderClick(ListViewGroupClickEventArgs e)
    {
        GroupClick?.Invoke(this, e);
    }

    /// <summary>
    /// Raises the Control.MouseDoubleClick event.
    /// </summary>
    /// <param name="e">Event arguments.</param>
    protected override void OnMouseDown(MouseEventArgs e)
    {
        base.OnMouseDown(e);

        var group = TestGroupHit(e);
        if (group == null)
        {
            return;
        }

        switch (e.Clicks)
        {
            case 1:
                OnGroupHeaderClick(new ListViewGroupClickEventArgs(group));
                break;
        }
    }

    private ListViewGroup TestGroupHit(MouseEventArgs e)
    {
        var ht = new LVHITTESTINFO { pt_x = e.X, pt_y = e.Y };
        var msg = View == System.Windows.Forms.View.Details ? LVM_SUBITEMHITTEST : LVM_HITTEST;
        var value = SendMessage(Handle, msg, -1, ref ht);

        if (value != -1 && (ht.flags & LVHT_EX_GROUP_HEADER) != 0)
        {
            return FindGroupByID(value);
        }

        return null;
    }

    private ListViewGroup FindGroupByID(int id)
    {
        foreach (ListViewGroup group in Groups)
        {
            if (group.ExtractID() == id)
            {
                return group;
            }
        }

        return null;
    }
}

属性 Group.ID 非public。这是提取它的扩展。

public static int ExtractID(this ListViewGroup group)
    {
        try
        {
            return (int) group
                .GetType()
                .GetProperty("ID", BindingFlags.NonPublic | BindingFlags.Instance)
                .GetValue(group, new object[0]);
        }
        catch 
        {
            return -1;
        }
    }

请注意,根据上下文,反射可能很耗时。