Click here to Skip to main content
15,867,308 members
Articles / Programming Languages / C#
Article

Handling Right-Click Events in ListView Column Headers

Rate me:
Please Sign up or sign in to vote.
4.88/5 (28 votes)
5 Feb 2008CPOL3 min read 126.5K   2.2K   51   26
.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.

C#
// 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:

C#
// 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:

C#
// 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:

C#
// 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)


Written By
Software Developer (Senior)
United States United States
Mark Lauritsen has been a software developer since 1983, starting at IBM and using a variety of languages including PL/1, Pascal, REXX, Ada, C/C++ and C#. Mark currently works at a midstream energy company developing Windows services and desktop applications in C#.

Comments and Discussions

 
QuestionSome years too late, but there's an even easier way... Pin
Member 84937858-Aug-20 4:16
Member 84937858-Aug-20 4:16 
PraiseVery efficent way Pin
Linchao Pan29-Nov-18 14:47
Linchao Pan29-Nov-18 14:47 
Questioncapture header without context menu Pin
Member 950575621-Nov-12 20:20
Member 950575621-Nov-12 20:20 
AnswerRe: capture header without context menu Pin
MarkLTX23-Nov-12 4:44
MarkLTX23-Nov-12 4:44 
GeneralMy vote of 5 Pin
eyedia16-Feb-11 10:24
eyedia16-Feb-11 10:24 
AnswerI have a much easier solution... Pin
Darki69912-Jan-11 5:44
Darki69912-Jan-11 5:44 
GeneralRe: I have a much easier solution... Pin
MarkLTX13-Jan-11 14:56
MarkLTX13-Jan-11 14:56 
GeneralRe: I have a much easier solution... [modified] Pin
Darki69925-Jan-11 13:32
Darki69925-Jan-11 13:32 
GeneralRe: I have a much easier solution... [modified] Pin
Member 14945549-May-13 1:45
Member 14945549-May-13 1:45 
GeneralRe: I have a much easier solution... Pin
Marko032-Aug-11 4:59
Marko032-Aug-11 4:59 
Thanks Smile | :)
QuestionHow can I do this in VB.NET? Pin
VinhTien21-Dec-10 4:49
VinhTien21-Dec-10 4:49 
Generalslightly easier way Pin
toddsecond21-Aug-10 0:53
toddsecond21-Aug-10 0:53 
GeneralMy vote of 4 Pin
toddsecond21-Aug-10 0:46
toddsecond21-Aug-10 0:46 
GeneralAnother way Pin
grelle30-Mar-10 8:30
grelle30-Mar-10 8:30 
GeneralNicely Done - Get's my 5! -- One Small Improvement Suggestion Pin
wlburgess8-Mar-10 10:25
wlburgess8-Mar-10 10:25 
GeneralRe: Nicely Done - Get's my 5! -- One Small Improvement Suggestion Pin
MarkLTX10-Mar-10 16:48
MarkLTX10-Mar-10 16:48 
GeneralThank yooooooooooou :) Pin
PanzerPanz17-Aug-09 15:13
PanzerPanz17-Aug-09 15:13 
Questionits possible to run the code under windows mobile? Pin
Nahasapeemapetilon2213-Feb-09 4:56
Nahasapeemapetilon2213-Feb-09 4:56 
AnswerRe: its possible to run the code under windows mobile? Pin
MarkLTX13-Feb-09 6:56
MarkLTX13-Feb-09 6:56 
GeneralThanks! Pin
JohnWillemse11-Feb-09 21:11
JohnWillemse11-Feb-09 21:11 
GeneralHorizontal scroll Pin
Pavel Kostromitinov25-Dec-08 4:48
Pavel Kostromitinov25-Dec-08 4:48 
GeneralRe: Horizontal scroll Pin
Pavel Kostromitinov25-Dec-08 4:53
Pavel Kostromitinov25-Dec-08 4:53 
GeneralRe: Horizontal scroll Pin
MarkLTX25-Dec-08 5:12
MarkLTX25-Dec-08 5:12 
QuestionUsing the code with custom ContextMenu Pin
johram16-Jun-08 1:19
johram16-Jun-08 1:19 
AnswerRe: Using the code with custom ContextMenu Pin
johram16-Jun-08 1:23
johram16-Jun-08 1:23 

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

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