15 June 2010

HDS_FILTERBAR

There’s a ListViewFilter control in C# on codeproject from 2003. It displays a filter bar in the column headers by setting HDS_FILTERBAR.

If we start off with a ListView showing some random data in a few columns:

Filterbar3

And set the HDS_FILTERBAR style, then it will display the filter bar:

Filterbar2

But there’s a problem – it hasn’t resized the header controls – they need to be taller to fit the text in properly. The codeproject code makes the column resize using a hack. Here’s how it shows the filter bar:

// set/reset the flag for the filterbar
if ( hdr_filter ) style |= HDS_FILTR;
else style ^= HDS_FILTR;
SetWindowLong( Handle, W32_GWL.GWL_STYLE, style );

And here’s the self-confessed kludge that gets it to resize:

// now we have to resize this control.  we do this by sending
// a set item message to column 0 to change it's size.  this
// is a kludge but the invalidate and others just don't work.
hdr_hditem.mask = W32_HDI.HDI_HEIGHT;
SendMessage( Handle, W32_HDM.HDM_GETITEMW, 0, ref hdr_hditem );
hdr_hditem.cxy += ( hdr_filter ) ? 1 : -1;
SendMessage( Handle, W32_HDM.HDM_SETITEMW, 0, ref hdr_hditem );

It’s sending a message to alter a property of the header – using HDI_HEIGHT. If we look at the windows header files to see how it’s defined:

#define HDI_WIDTH               0x0001
#define HDI_HEIGHT              HDI_WIDTH

It’s actually the same as the width. The kludge increases the width of the column header by 1 when the filter is shown, and decreases it when the filter is not shown. Altering the width must be enough to make windows resize the control.

Searching for alternate solutions I found one that mentions using MoveWindow to resize the header control, so I tried that. Unfortunately when you resize the header it then overlaps the parent listview, hiding one or two of the ListViewItems by overlapping them.

The info needed to do it properly is in the “Header Controls” topic in the MSDN library, in the “header control size and position” paragraph. We have to send the HDM_LAYOUT message to the control, specifying the bounds of the parent. On return we get a WINDOWPOS structure which tells us the best bounds for the header so that it will sit in the bounds of the parent. All we then have to do is to resize the header control using SetWindowPos, and alter the bounds of the parent control so that its top lies below the header and its height isn’t too big.

HDLAYOUT layout = new HDLAYOUT();
RECT rect = new RECT();
rect.Right = parent.ClientSize.Width;
rect.Bottom = parent.ClientSize.Height;
layout.prc = Marshal.AllocHGlobal(Marshal.SizeOf(rect));
Marshal.StructureToPtr(rect, layout.prc, true);
layout.pwpos = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(WINDOWPOS)));
lresult = NativeMethods.SendMessage(Handle, HDM_LAYOUT, IntPtr.Zero, ref layout);
Marshal.FreeHGlobal(layout.prc);
WINDOWPOS pos = (WINDOWPOS)Marshal.PtrToStructure(layout.pwpos, typeof(WINDOWPOS));
Marshal.FreeHGlobal(layout.pwpos);
bool res = NativeMethods.SetWindowPos(Handle, IntPtr.Zero, pos.X, pos.Y, pos.Width, pos.Height, 
    SetWindowPosFlags.NoMove | SetWindowPosFlags.NoZOrder | SetWindowPosFlags.FrameChanged);
res = NativeMethods.SetWindowPos(parent.Handle, IntPtr.Zero, 0, pos.Height, parent.Width, parent.Height - pos.Height, 
    SetWindowPosFlags.NoMove | SetWindowPosFlags.NoZOrder | SetWindowPosFlags.FrameChanged);
ClearAllFilters();  

Now the filterbar is shown with the correct size, it doesn’t overlap the top of the parent listview, and it resizes correctly when the filter bar is removed.

Filterbar1