[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.