Figure 1: FullyCustomHeader
Figure 2: Default Windows implementation (with images)
Figure 3: Default Windows implementation (with images and first column set to owner-draw)
Introduction
It all began when in an application I needed the column header in a ListView
to display an image. I was surprised to find that .NET provides very limited implementation of the HeaderControl
common control. With common controls version 4.70 and later, column header supports images from an ImageList
and bitmaps. Well, .NET framework requires IE 5.0 at least, so that control version is meant to be available. Another thing not implemented in the default .NET header is the notifications sent to parent list view when the user starts dragging a column or ends dragging a column. I found that very annoying because in an application I wanted to have a hidden combo box which appears with the selected item. But to correspond properly on the column resize, I needed to resize the combo box also. But how to catch an event when there is no delegate for it... So I put all these missing features in one extended list view (it only extends the header control in detailed mode).
Basic idea
The idea is creating a new class MyColumn
which has new properties and supports images and owner-draw feature. I am using the existing LVCOLUMN
structure and simply specifying an ImageList
, image index. The owner-draw flag is set set by HDITEM
structure. To add the columns correctly I am using the LVM_INSERTCOLUMN
message. The events for handling owner-draw feature and for tracking columns are achieved by overriding the WndProc
of the ListView
.
Using the code
To use the code you just have to add the CustomHeader.dll to your toolbox and then drag the control on the form. The Columns
property is new - it is of type MyHeaderCollection
and when adding a column you will see some new properties to appear:
ImageIndex
- this is the index from an image list associated with the header.
OwnerDraw
- I have added this feature so you can draw your own columns.
ImageOnRight
- this works only with OwnerDraw
property set to true
because the default Windows implementation is not drawing the image correctly.
The new ListView
properties are:
HeaderImageList
- use this to set the ImageList
for the header.
FullyCustomHeader
- use this to draw the entire header, not only the columns - in this case I am giving you the whole surface of the control for drawing.
DefaultCustomDraw
- you can combine this with FullyCustomHeader
and get a completely custom drawn header (the screenshot at the top of the page is that case).
HeaderHandle
- gets the header control handle.
IncreaseHeaderHeight
- use this to increase the header control default height. This is actually done by setting bigger font (I tried many other solutions like SetWindowPos
and overriding HDM_LAYOUT
but none worked correct). So this property simply increases the header's font height with the specified amount. This is useful only with MyColumn.OwnerDraw
set to true
, because it is not nice to set bigger font and let Windows do the drawing:)).
HeaderHeight
- gets the header height in pixels.
The new events are:
public event DrawItemEventHandler DrawColumn
I use the delegate of type DrawItemEventHandler
as it suits my needs completely. Use this event to draw a MyColumn
object with OwnerDraw
property set to true
.
public event DrawHeaderEventHandler DrawHeader
This is a custom delegate type and it is declared like that:
public delegate void DrawHeaderEventHandler(DrawHeaderEventArgs e)
Use this event when setting the MyListView.FullyCustomHeader
to draw the entire surface of the header.
public event HeaderEventHandler BeginDragHeaderDivider;
public event HeaderEventHandler DragHeaderDivider;
public event HeaderEventHandler EndDragHeaderDivider;
The names of the events are self-explanatory. They are of type:
public delegate void
HeaderEventHandler(object sender, HeaderEventArgs e);
DrawHeaderEventArgs
The DrawHeaderEventArgs
class inherits from EventArgs
class and it is declared like that:
public class DrawHeaderEventArgs : EventArgs
{
Graphics graphics;
Rectangle bounds;
int height;
public DrawHeaderEventArgs(Graphics dc, Rectangle rect, int h)
{
graphics = dc;
bounds = rect;
height = h;
public Graphics Graphics
{
get{return graphics;}
}
public Rectangle Bounds
{
get{return bounds;}
}
public int HeaderHeight
{
get{return height;}
}
}
HeaderEventArgs
I am providing to you the bounds, height and the graphics object to draw on with this DrawHeaderEventArgs
class. The HeaderEventArgs
class also inherits from EventArgs
and is declared like that:
public class HeaderEventArgs : EventArgs
{
int columnIndex;
int mouseButton;
public HeaderEventArgs(int index, int button)
{
columnIndex = index;
mouseButton = button;
}
public int ColumnIndex
{
get{return columnIndex;}
}
public int MouseButton
{
get{return mouseButton;}
}
}
InsertColumns method
The most significant part of this event is the index of the column that fires the event - this is provided by the ColumnIndex
property.
How the MyColumn
objects are set correctly:
void InsertColumns()
{
int counter = 0;
foreach(MyColumn m in myColumns)
{
Win32.LVCOLUMN lvc = new Win32.LVCOLUMN();
lvc.mask = 0x0001|0x0008|0x0002|0x0004;
lvc.cx = m.Width;
lvc.subItem = counter;
lvc.text = m.Text;
switch(m.TextAlign)
{
case HorizontalAlignment.Left:
lvc.fmt = 0x0000;
break;
case HorizontalAlignment.Center:
lvc.fmt = 0x0002;
break;
case HorizontalAlignment.Right:
lvc.fmt = 0x0001;
break;
}
if(headerImages != null && m.ImageIndex != -1)
{
lvc.mask |= 0x0010;
lvc.iImage = m.ImageIndex;
lvc.fmt |= 0x0800;
if(m.ImageOnRight)
lvc.fmt |= 0x1000;
}
Win32.SendMessage(this.Handle,0x1000+97,counter,ref lvc);
if(m.OwnerDraw)
{
Win32.HDITEM hdi = new Win32.HDITEM();
hdi.mask = (int)Win32.HDI.HDI_FORMAT;
hdi.fmt = (int)Win32.HDF.HDF_OWNERDRAW;
Win32.SendMessage(header.Handle,0x1200+12,counter,ref hdi);
}
counter++;
}
}
According to MyColumn
's properties I am filling a LVCOLUMN
structure and then send message LVM_INSERTCOLUMN
. We need to check if we have OwnerDraw
property set to true
- if so, modify the existing HDITEM
structure with the new flag - owner-draw.
HeaderControl class
The HeaderControl
class has an important role. It inherits from NativeWindow
and its only purpose is to receive messages sent to the header control.
internal class HeaderControl : NativeWindow
{
MyListView parent;
bool mouseDown;
public HeaderControl(MyListView m)
{
parent = m;
IntPtr header = Win32.SendMessage(parent.Handle,
(0x1000+31), IntPtr.Zero, IntPtr.Zero);
this.AssignHandle(header);
}
protected override void WndProc(ref Message m)
{
}
}
The header handle is received via the LVM_GETHEADER
message. After getting this handle assign it to the HeaderControl
class, so we can override its WndProc
.
HeaderControl overriden WndProc
protected override void WndProc(ref Message m)
{
switch(m.Msg)
{
case 0x000F:
if(parent.FullyCustomHeader)
{
Win32.RECT update = new Win32.RECT();
if(Win32.GetUpdateRect(m.HWnd,ref update,
false)==0)
break;
Win32.PAINTSTRUCT ps = new
Win32.PAINTSTRUCT();
IntPtr hdc = Win32.BeginPaint(m.HWnd, ref ps);
Graphics g = Graphics.FromHdc(hdc);
int left = 0;
Win32.RECT itemRect = new Win32.RECT();
for(int i=0; i<parent.Columns.Count; i++)
{
Win32.SendMessage(m.HWnd, 0x1200+7, i,
ref itemRect);
left += itemRect.right-itemRect.left;
}
parent.headerHeight =
itemRect.bottom-itemRect.top;
if(left >= ps.rcPaint.left)
left = ps.rcPaint.left;
Rectangle r = new Rectangle(left,
ps.rcPaint.top,
ps.rcPaint.right-left,
ps.rcPaint.bottom-ps.rcPaint.top);
Rectangle r1 = new Rectangle(ps.rcPaint.left,
ps.rcPaint.top,
ps.rcPaint.right-left,
ps.rcPaint.bottom-ps.rcPaint.top);
g.FillRectangle(new
SolidBrush(parent.headerBackColor),r);
if(parent.DrawHeader != null &&
!parent.DefaultCustomDraw)
parent.DrawHeader(new
DrawHeaderEventArgs(g,r,
itemRect.bottom-itemRect.top));
else
parent.DrawHeaderBorder(new
DrawHeaderEventArgs(g,r,
itemRect.bottom-itemRect.top));
int counter = 0;
foreach(MyColumn mm in parent.Columns)
{
if(mm.OwnerDraw)
{
Win32.DRAWITEMSTRUCT dis =
new Win32.DRAWITEMSTRUCT();
dis.ctrlType = 100;
dis.hwnd = m.HWnd;
dis.hdc = hdc;
dis.itemAction = 0x0001;
dis.itemID = counter;
Win32.HDHITTESTINFO hi = new
Win32.HDHITTESTINFO();
hi.pt.X =
parent.PointToClient(MousePosition).X;
hi.pt.Y =
parent.PointToClient(MousePosition).Y;
int hotItem = Win32.SendMessage(m.HWnd,
0x1200+6, 0, ref hi);
if(hi.flags == 0x0004 || hotItem != counter)
hotItem = -1;
if(hotItem != -1 && mouseDown)
dis.itemState = 0x0001;
else
dis.itemState = 0x0020;
Win32.SendMessage(m.HWnd, 0x1200+7,
counter, ref itemRect);
dis.rcItem = itemRect;
Win32.SendMessage(parent.Handle,
0x002B,0,ref dis);
}
counter++;
}
Win32.EndPaint(m.HWnd, ref ps);
}
else
base.WndProc(ref m);
break;
case 0x0014:
if(parent.FullyCustomHeader)
break;
else
base.WndProc(ref m);
break;
case 0x0201:
mouseDown = true;
base.WndProc(ref m);
break;
case 0x0202:
mouseDown = false;
base.WndProc(ref m);
break;
case 0x1200+5:
base.WndProc(ref m);
break;
case 0x0030:
if(parent.IncreaseHeaderHeight > 0)
{
System.Drawing.Font f =
new System.Drawing.Font(parent.Font.Name,
parent.Font.SizeInPoints +
parent.IncreaseHeaderHeight);
m.WParam = f.ToHfont();
}
base.WndProc(ref m);
break;
default:
base.WndProc(ref m);
break;
}
}
Here I am checking if we have MyListView.FullyCustomHeader
property set to true
. If so - fire the DrawHeader
event and then check for owner-draw columns - if we have ones, fill a DRAWITEMSTRUCT
appropriately and send WM_DRAWITEM
message to MyListView
. If we haven't FullyCustomHeader
- call base.WndProc
.
Firing the DrawColumn event through the MyListView WndProc
We have to override the MyListView
's WndProc
and catch the WM_DRAWITEM
message, then fill the DrawItemEventArgs
appropriately and if we have a valid event handler - call it!
case 0x002B:
Win32.DRAWITEMSTRUCT dis = (Win32.DRAWITEMSTRUCT)Marshal.PtrToStructure(
m.LParam,typeof(Win32.DRAWITEMSTRUCT));
if(dis.ctrlType == 100)
{
Graphics g = Graphics.FromHdc(dis.hdc);
Rectangle r = new Rectangle(dis.rcItem.left,
dis.rcItem.top, dis.rcItem.right -
dis.rcItem.left, dis.rcItem.bottom - dis.rcItem.top);
DrawItemState d = DrawItemState.Default;
if(dis.itemState == 0x0001)
d = DrawItemState.Selected;
DrawItemEventArgs e = new
DrawItemEventArgs(g,this.Font,r,dis.itemID,d);
if(DrawColumn != null && !defaultCustomDraw)
DrawColumn(this.Columns[dis.itemID], e);
else if(defaultCustomDraw)
DoMyCustomHeaderDraw(this.Columns[dis.itemID],e);
g.Dispose();
}
break;
Firing the BeginDragHeaderDivider, DragHeaderDivider and EndDragHeaderDivider events
These events comes to us as notifications from the header control. All we have to do is make sure we have a valid handler to call it:
case 0x004E:
base.WndProc(ref m);
Win32.NMHDR nmhdr = (Win32.NMHDR)m.GetLParam(typeof(Win32.NMHDR));
switch(nmhdr.code)
{
case (0-300-26):
nm=(Win32.NMHEADER)m.GetLParam(typeof(Win32.NMHEADER));
if(BeginDragHeaderDivider != null)
BeginDragHeaderDivider(this.Columns[nm.iItem],
new HeaderEventArgs(nm.iItem, nm.iButton));
break;
case (0-300-20):
nm=(Win32.NMHEADER)m.GetLParam(typeof(Win32.NMHEADER));
Win32.RECT rect = new Win32.RECT();
Win32.SendMessage(header.Handle, 0x1200+7, nm.iItem, ref rect);
this.headerHeight = rect.bottom-rect.top;
this.Columns[nm.iItem].Width = rect.right - rect.left;
if(DragHeaderDivider != null)
DragHeaderDivider(this.Columns[nm.iItem],
new HeaderEventArgs(nm.iItem, nm.iButton));
break;
case (0-300-27):
nm=(Win32.NMHEADER)m.GetLParam(typeof(Win32.NMHEADER));
if(EndDragHeaderDivider != null)
EndDragHeaderDivider(this.Columns[nm.iItem],
new HeaderEventArgs(nm.iItem, nm.iButton));
break;
}
break;
Points of interest
Well, I was thinking also of extending MyListView
, so it will support custom drawing and user-defined selection color, border color and column color, but as I found in the CodeProject's articles section that, some people have already made that, so I worked only on the header control. I haven't played with the bitmap field of the LVCOLUMN
structure. But to my great surprise if we have the default Windows implementation of the LVCFMT_BITMAP_ON_RIGHT
flag set to the column, the image is not drawn correctly!!! Do you see:
Default Windows implementation for LVCFMT_BITMAP_ON_RIGHT
:
May be it works correctly only when a bitmap is specified...
History