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.
private Rectangle _headerRect;
private delegate bool EnumWinCallBack(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.Dll")]
private static extern int EnumChildWindows(
IntPtr hWndParent,
EnumWinCallBack callBackFunc,
IntPtr lParam);
[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:
private bool EnumWindowCallBack(IntPtr hwnd, IntPtr lParam)
{
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;
}
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:
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:
private void regularListViewMenu_Opening(object sender, CancelEventArgs e)
{
EnumChildWindows(
listView1.Handle, new EnumWinCallBack(EnumWindowCallBack), IntPtr.Zero);
if (_headerRect.Contains(Control.MousePosition))
{
e.Cancel = true;
int xoffset = Control.MousePosition.X - _headerRect.Left;
int sum = 0;
foreach (ColumnHeader header in GetOrderedHeaders(listView1))
{
sum += header.Width;
if (sum > xoffset)
{
headerMenu.Tag = header;
headerMenu.Items[0].Text = "Command for Header " + header.Text;
headerMenu.Show(Control.MousePosition);
break;
}
}
}
else
{
}
}
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).
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#.