LostPhysx
LostPhysx

Reputation: 3641

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

I use a ListView in my WinForms application, which contains a lot of values and also a few groups. The group headers only show the name of the group, so i want to add a context menu to the group header with an item "Show description" to show a long summary of the Group.

After googling a while I only found third party controls which have this functionality.

How can I add the ContextMenu to the Group header without using 3rd party software?

Upvotes: 0

Views: 3118

Answers (2)

Sergey
Sergey

Reputation: 640

The Reza Aghaei's solution works only if the groups are added before the items. In case a group is added after the items have been added already, it fails, because the SendMessage function returns wrong indices. It actually returns the IDs not indices. That is a known problem in the ListView component. The idea is to compare the returned value to the Group 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;
    }
}

The property Group.ID is non-public. Here is the extension that extracts it.

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

Note that Reflection may be time-consuming depending on the context.

Upvotes: 1

Reza Aghaei
Reza Aghaei

Reputation: 125197

You can send a LVM_HITTEST message to ListView. When you pass -1 to wParam, if the return value is greater than -1 and LVHT_EX_GROUP_HEADER has been set in the result, the return value of SendMessage method will be clicked group index.

Implementation

In below implementations, I've added GroupHeaderClick event to MyListView class. You can simply handle the event this way:

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

Here is MyListView implementation:

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

Upvotes: 4

Related Questions