Click here to Skip to main content
Click here to Skip to main content

Handling Right-Click Events in ListView Column Headers

, 5 Feb 2008
Rate this:
Please Sign up or sign in to vote.
.NET WinForms doesn't provide any way to handle right-click events on column headers. This C# code shows how to determine which header was right-clicked.

Introduction

If you've ever wanted to handle the right-click event on a ListView column header, you probably discovered there is no way to do it with the standard events and objects provided by the .NET Framework. This article explains how to determine if the user right-clicks a header (vs. anywhere else) in a ListView control and which header was clicked. You can then display the appropriate context menu or perform other processing specific to that header.

Background

I ran into this issue while developing the TracerX logger/viewer for .NET. I wanted the user to be able to right-click a column header (e.g. Thread or Logger) and get a context menu with commands applicable to that column. Unfortunately, the ListView class does not have a RightClick event (nor does the ColumnHeader class). Furthermore, the following events are not even raised when the user right-clicks the header bar: Click, MouseClick, MouseDown, and ColumnClick.

Fortunately, I found that if the ListView control has a context menu, it is displayed whenever the user right-clicks anywhere on the ListView, including the headers. Therefore, the solution to this problem starts with an event handler for the context menu's Opening event. If the handler discovers that the mouse pointer is in the header bar area, it cancels the Opening event, determines which header was clicked, and displays the context menu for that header instead of the one for the ListView.

Details

The key is getting the bounding rectangle of the header bar so we can determine if it contains the mouse pointer. Poking around with Spy++ reveals that the header bar has its own window that is the only child window of the ListView window. I used P/Invoke to call EnumChildWindows to get a handle to the header bar window. One of this function's parameters is a callback (delegate) that gets called for every child window found (the header bar is the only one). I passed a managed code method that sets a member variable to the rectangle occupied by the header bar. Here are the corresponding declarations.

// The area occupied by the ListView header. 
private Rectangle _headerRect;
 
// Delegate that is called for each child window of the ListView. 
private delegate bool EnumWinCallBack(IntPtr hwnd, IntPtr lParam);

// Calls EnumWinCallBack for each child window of hWndParent (i.e. the ListView).
[DllImport("user32.Dll")]
private static extern int EnumChildWindows(
    IntPtr hWndParent, 
    EnumWinCallBack callBackFunc, 
    IntPtr lParam);

// Gets the bounding rectangle of the specified window (ListView header bar). 
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); 

[StructLayout(LayoutKind.Sequential)]
private struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom; 
}

Here's the callback method (passed to and called through EnumChildWindows) that sets _headerRect to the area of the header bar:

// This should get called with the only child window of the ListView,
// which should be the header bar.
private bool EnumWindowCallBack(IntPtr hwnd, IntPtr lParam)
{
    // Determine the rectangle of the ListView header bar and save it in _headerRect.
    RECT rct;
    if (!GetWindowRect(hwnd, out rct))
    {
        _headerRect = Rectangle.Empty;
    }
    else
    {
        _headerRect = new Rectangle(
        rct.Left, rct.Top, rct.Right - rct.Left, rct.Bottom - rct.Top);
    }
    return false; // Stop the enum
}

Now we can get the current position of the mouse pointer and determine if it is in _headerRect. If so, we determine which particular header the mouse is on by adding up the width of each header until the sum exceeds the X offset of the mouse position. The only caveat is that we must add the column widths in the order they are currently displayed, which the user can change by dragging the headers around. The following method returns an array of headers in the correct order:

// This returns an array of ColumnHeaders in the order they are
// displayed by the ListView.  
private static ColumnHeader[] GetOrderedHeaders(ListView lv)
{
    ColumnHeader[] arr = new ColumnHeader[lv.Columns.Count];

    foreach (ColumnHeader header in lv.Columns)
    {
        arr[header.DisplayIndex] = header;
    }

    return arr;
}

Now we can write the event handler that calls all the preceding code. In the sample project attached to this article, the Form object contains two context menus: regularListViewMenu and headerMenu. The ListView's ContextMenuStrip property is set to the former. Here is the context menu's Opening event handler:

// Called when the user right-clicks anywhere in the ListView, including the
// header bar.  It displays the appropriate context menu for the ListView or
// header that was right-clicked. 
private void regularListViewMenu_Opening(object sender, CancelEventArgs e)
{
    // This call indirectly calls EnumWindowCallBack which sets _headerRect
    // to the area occupied by the ListView's header bar.
    EnumChildWindows(
        listView1.Handle, new EnumWinCallBack(EnumWindowCallBack), IntPtr.Zero);

    // If the mouse position is in the header bar, cancel the display
    // of the regular context menu and display the column header context 
    // menu instead.
    if (_headerRect.Contains(Control.MousePosition))
    {
        e.Cancel = true;

        // The xoffset is how far the mouse is from the left edge of the header.
        int xoffset = Control.MousePosition.X - _headerRect.Left;

         // Iterate through the column headers in the order they are displayed, 
         // adding up their widths as we go.  When the sum exceeds the xoffset, 
         // we know the mouse is on the current header. 
        int sum = 0;
        foreach (ColumnHeader header in GetOrderedHeaders(listView1))
        {
            sum += header.Width;
            if (sum > xoffset)
            {
                // This code displays the same context menu for 
                // every header, but changes the menu item
                // text based on the header. It sets the context 
                // menu tag to the header object so
                // the handler for whatever command the user 
                // clicks can know the column.
                headerMenu.Tag = header;
                headerMenu.Items[0].Text = "Command for Header " + header.Text;
                headerMenu.Show(Control.MousePosition);
                break;
            }
        }
    }
    else
    {
        // Allow the regular context menu to be displayed.
        // We may want to update the menu here.
    }
}

The following screen shots illustrate both context menus (the upper left corner of each menu is at the point where the mouse was pointing when the right mouse button was clicked).

ListViewMenu.PNG

HeaderMenu.PNG

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

MarkLTX
Software Developer (Senior)
United States United States
Mark Lauritsen has been a software developer for as long as he can remember.

Comments and Discussions

 
Questioncapture header without context menu PinmemberMember 950575621-Nov-12 20:20 
AnswerRe: capture header without context menu PinmemberMarkLTX23-Nov-12 4:44 
You want to have a context menu for the headers without having a context menu for the whole ListView?
 
The method described in this article requires the ListView to have a context menu, but it doesn't have to really do anything or ever be displayed to the user. Just modify the regularListViewMenu_Opening() handler in the article to cancel the event and NOT display the ListView's context menu when the user doesn't click the header.
GeneralMy vote of 5 PinmemberBig D16-Feb-11 10:24 
AnswerI have a much easier solution... PinmemberDarki69912-Jan-11 5:44 
GeneralRe: I have a much easier solution... PinmemberMarkLTX13-Jan-11 14:56 
GeneralRe: I have a much easier solution... [modified] PinmemberDarki69925-Jan-11 13:32 
GeneralRe: I have a much easier solution... [modified] PinmemberMember 14945549-May-13 1:45 
GeneralRe: I have a much easier solution... PinmemberezbUltra2-Aug-11 4:59 
QuestionHow can I do this in VB.NET? PinmemberVinhTien21-Dec-10 4:49 
Generalslightly easier way Pinmembertoddsecond21-Aug-10 0:53 
GeneralMy vote of 4 Pinmembertoddsecond21-Aug-10 0:46 
GeneralAnother way Pinmembergrelle30-Mar-10 8:30 
GeneralNicely Done - Get's my 5! -- One Small Improvement Suggestion Pinmemberwlburgess8-Mar-10 10:25 
GeneralRe: Nicely Done - Get's my 5! -- One Small Improvement Suggestion PinmemberMarkLTX10-Mar-10 16:48 
GeneralThank yooooooooooou :) PinmemberPanzerPanz17-Aug-09 15:13 
Questionits possible to run the code under windows mobile? PinmemberNahasapeemapetilon2213-Feb-09 4:56 
AnswerRe: its possible to run the code under windows mobile? PinmemberMarkLTX13-Feb-09 6:56 
GeneralThanks! PinmemberJohnWillemse11-Feb-09 21:11 
GeneralHorizontal scroll PinmemberPavel Kostromitinov25-Dec-08 4:48 
GeneralRe: Horizontal scroll PinmemberPavel Kostromitinov25-Dec-08 4:53 
GeneralRe: Horizontal scroll PinmemberMarkLTX25-Dec-08 5:12 
QuestionUsing the code with custom ContextMenu Pinmemberjohram16-Jun-08 1:19 
AnswerRe: Using the code with custom ContextMenu Pinmemberjohram16-Jun-08 1:23 
QuestionWhy the 1 votes? PinmemberMarkLTX23-Apr-08 6:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140827.1 | Last Updated 5 Feb 2008
Article Copyright 2008 by MarkLTX
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid