[section_title title=Details]

2008-1-16. Brian Rinebarger found an issue with the original code where it relied on a possible bug in the version of Common Controls that is part of Windows Vista. The problem was that any subsequent calls to HDM_GETITEM would always return just HDF_STRING as the format instead of the additional HDF_SORTUP/HDF_SORTDOWN bits. This worked fine on Windows Vista, but not on Windows XP, where the icon would get stuck once the column was sorted in the ascending order. I have changed the code, as suggested by Brian, so that it doesn’t rely on this bug.

As mentioned in the run-down, we will be using HDM_GETITEM and HDM_SETITEM messages to get and set the icons. Let’s dive in and look at the C# code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void SetSortIcons(int previouslySortedColumn, int newSortColumn)
{
    IntPtr hHeader = SendMessage(_listView.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);
    IntPtr newColumn = new IntPtr(newSortColumn);
    IntPtr prevColumn = new IntPtr(previouslySortedColumn);
    HDITEM hdItem;
    IntPtr rtn;

    // Only update the previous item if it existed and if it was a different one.
    if (previouslySortedColumn != -1 && previouslySortedColumn != newSortColumn)
    {
        // Clear icon from the previous column.
        hdItem = new HDITEM();
        hdItem.mask = HDI_FORMAT;
        rtn = SendMessageITEM(hHeader, HDM_GETITEM, prevColumn, ref hdItem);
        hdItem.fmt &= ~HDF_SORTDOWN & ~HDF_SORTUP;
        rtn = SendMessageITEM(hHeader, HDM_SETITEM, prevColumn, ref hdItem);
    }

    // Set icon on the new column.
    hdItem = new HDITEM();
    hdItem.mask = HDI_FORMAT;
    rtn = SendMessageITEM(hHeader, HDM_GETITEM, newColumn, ref hdItem);
    if (_listView.Sorting == SortOrder.Ascending)
    {
        hdItem.fmt &= ~HDF_SORTDOWN;
        hdItem.fmt |= HDF_SORTUP;
    }
    else
    {
        hdItem.fmt &= ~HDF_SORTUP;
        hdItem.fmt |= HDF_SORTDOWN;
    }
    rtn = SendMessageITEM(hHeader, HDM_SETITEM, newColumn, ref hdItem);
    _previouslySortedColumn = newSortColumn;
}

Before we look into what this is doing, please note that, one, this code is extracted from a sorting class that I wrote to encompass most of the ListView sorting related functions, and, two, there are some declarations that you will have to include before this code will work (e.g. using System.Runtime.InteropServices;). The declarations are at the end of this section.

Now. First we get the pointer to the Header control. Pretty simple.

1
IntPtr hHeader = SendMessage(_listView.Handle, LVM_GETHEADER, IntPtr.Zero, IntPtr.Zero);

The next few IntPtr declarations are necessary for passing the C# integers to InteropServices. After that comes the fun part.

1
2
3
4
hdItem = new HDITEM();
hdItem.mask = HDI_FORMAT;  // <-- This tells HDM_GETITEM that we are only looking for the format
rtn = SendMessageITEM(hHeader, HDM_GETITEM, prevColumn, ref hdItem);
hdItem.fmt &= ~HDF_SORTDOWN & ~HDF_SORTUP;   // <-- Using the AND (&) and NOT (~) operators we ensure that icons are turned off.

If you know that your columns will always be left aligned and displaying strings then you can skip the first call and just set hdItem.fmt to (HDF_LEFT | HDF_STRING) & ~HDF_SORTUP & ~HDF_SORTDOWN instead of getting what it’s already set to. But, here instead I chose to be more generic and only change what I need to change. So, we have to first get the current format and then turn off the bits that denote the sorting icons.

Once we have changed the format to exclude the icons then we can go ahead and actually set the format using the HDM_SETITEM message.

1
rtn = SendMessageITEM(hHeader, HDM_SETITEM, prevColumn, ref hdItem);

The second part of this code does the same thing except this time it sets the icons based on the chosen sort criteria.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
if (_listView.Sorting == SortOrder.Ascending)
{
    hdItem.fmt &= ~HDF_SORTDOWN;   // First unset the opposite direction
    hdItem.fmt |= HDF_SORTUP;      // Then set the current direction
}
else
{
    hdItem.fmt &= ~HDF_SORTUP;
    hdItem.fmt |= HDF_SORTDOWN;
}

Here, first we unset the bit for the opposite direction, and then we use the OR (|) operator to only set the appropriate sorting icon bits, i.e. not changing any of the other formatting.

So, that is how the actual sorting icons are displayed.

Now, let’s talk about the declarations necessary for getting the above code to work.

1. Add using System.Runtime.InteropServices; to your class.

2. Add the necessary function declarations.

The following code defines the HDITEM structure and the several constants.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[StructLayout(LayoutKind.Sequential)]
public struct HDITEM
{
    public Int32 mask;
    public Int32 cxy;
    [MarshalAs(UnmanagedType.LPTStr)]
    public String pszText;
    public IntPtr hbm;
    public Int32 cchTextMax;
    public Int32 fmt;
    public Int32 lParam;
    public Int32 iImage;
    public Int32 iOrder;
};

// Parameters for ListView-Headers
public const Int32    HDI_FORMAT = 0x0004;
public const Int32      HDF_LEFT = 0x0000;
public const Int32    HDF_STRING = 0x4000;
public const Int32    HDF_SORTUP = 0x0400;
public const Int32  HDF_SORTDOWN = 0x0200;
public const Int32 LVM_GETHEADER = 0x1000 + 31;  // LVM_FIRST + 31
public const Int32   HDM_GETITEM = 0x1200 + 11;  // HDM_FIRST + 11
public const Int32   HDM_SETITEM = 0x1200 + 12;  // HDM_FIRST + 12

[DllImport("user32.dll")]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

[DllImport("user32.dll", EntryPoint = "SendMessage")]
private static extern IntPtr SendMessageITEM(IntPtr Handle, Int32 msg, IntPtr wParam, ref HDITEM lParam);

3. Finally, add the message calls as shown above.

Once you have added the using statement and the declarations then you can do the SendMessageITEM and SendMessage calls from that class.