自定义ListView中的Header控件






4.79/5 (26投票s)
2003年6月1日
5分钟阅读

415911

4569
本文详细介绍如何在 ListView 的详细信息模式下扩展默认的标题控件。
图 1:FullyCustomHeader
图 2:默认 Windows 实现(带图像)
图 3:默认 Windows 实现(带图像且第一个列设置为所有者绘制)
引言
一切都始于在一个应用程序中,我需要 ListView
中的列标题显示一个图像。令我惊讶的是,.NET 对 HeaderControl
公共控件的支持非常有限。从通用控件 4.70 及更高版本开始,列标题支持来自 ImageList
和位图的图像。.NET 框架至少需要 IE 5.0,所以该控件版本应该是可用的。默认 .NET 标题中未实现的另一件事是,当用户开始拖动列或结束拖动列时发送给父列表视图的通知。我觉得这非常烦人,因为在一个应用程序中,我想要一个隐藏的组合框,并在选中项出现时显示它。但是为了正确响应列的重绘,我还需要调整组合框的大小。但是没有委托,我如何捕获事件……所以,我将所有这些缺失的功能整合到一个扩展的列表视图中(它只扩展了详细模式下的标题控件)。
基本思想
想法是创建一个新的 MyColumn
类,它具有新属性并支持图像和所有者绘制功能。我正在使用现有的 LVCOLUMN
结构,并简单地指定 ImageList
和图像索引。所有者绘制标志由 HDITEM
结构设置。为了正确添加列,我使用的是 LVM_INSERTCOLUMN
消息。通过重写 ListView
的 WndProc
来实现处理所有者绘制功能和跟踪列的事件。
使用代码
要使用此代码,只需将 CustomHeader.dll 添加到您的工具箱,然后将控件拖到窗体上。 Columns
属性是新的——它属于 MyHeaderCollection
类型,添加列时您会看到一些新属性出现。
ImageIndex
- 这是与标题关联的图像列表中的索引。OwnerDraw
- 我添加了这个功能,以便您可以绘制自己的列。ImageOnRight
- 这仅在OwnerDraw
属性设置为true
时有效,因为默认的 Windows 实现无法正确绘制图像。
新的 ListView
属性是:
HeaderImageList
- 使用此属性设置标题的ImageList
。FullyCustomHeader
- 使用此属性绘制整个标题,而不仅仅是列——在这种情况下,我将整个控件表面提供给您进行绘制。DefaultCustomDraw
- 您可以将其与FullyCustomHeader
结合使用,获得完全自定义绘制的标题(页面顶部的截图就是这种情况)。HeaderHandle
- 获取标题控件的句柄。IncreaseHeaderHeight
- 使用此属性增加标题控件的默认高度。这实际上是通过设置更大的字体来完成的(我尝试了许多其他解决方案,如SetWindowPos
和重写HDM_LAYOUT
,但都没有正确工作)。所以这个属性只是增加了指定量的标题字体高度。这仅在MyColumn.OwnerDraw
设置为true
时有用,因为设置更大的字体然后让 Windows 进行绘制不太好看)。HeaderHeight
- 获取标题的高度(以像素为单位)。
新的事件是:
public event DrawItemEventHandler DrawColumn
我使用 `DrawItemEventHandler` 类型的委托,因为它完全满足我的需求。当 `MyColumn` 对象的 `OwnerDraw` 属性设置为 `true` 时,使用此事件进行绘制。
public event DrawHeaderEventHandler DrawHeader
这是一个自定义委托类型,声明如下:
public delegate void DrawHeaderEventHandler(DrawHeaderEventArgs e)
当将 `MyListView.FullyCustomHeader` 设置为绘制标题的整个表面时,使用此事件。
public event HeaderEventHandler BeginDragHeaderDivider; public event HeaderEventHandler DragHeaderDivider; public event HeaderEventHandler EndDragHeaderDivider;
事件的名称不言自明。它们是:
public delegate void HeaderEventHandler(object sender, HeaderEventArgs e);
DrawHeaderEventArgs
`DrawHeaderEventArgs` 类继承自 `EventArgs` 类,声明如下:
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
我通过 `DrawHeaderEventArgs` 类提供了边界、高度和要绘制的图形对象。`HeaderEventArgs` 类也继承自 `EventArgs`,声明如下:
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 方法
此事件中最重要部分是触发事件的列的索引——这由 `ColumnIndex` 属性提供。
如何正确设置 `MyColumn` 对象
void InsertColumns()
{
int counter = 0;
foreach(MyColumn m in myColumns)
{
Win32.LVCOLUMN lvc = new Win32.LVCOLUMN();
//LVCF_FMT|LVCF_SUBITEM|LVCF_WIDTH|LVCF_TEXT
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;//LVCF_IMAGE
lvc.iImage = m.ImageIndex;
lvc.fmt |= 0x0800;//LVCFMT_IMAGE
if(m.ImageOnRight)
lvc.fmt |= 0x1000;
}
//Send message LVM_INSERTCOLUMN
Win32.SendMessage(this.Handle,0x1000+97,counter,ref lvc);
//Check if column is set to owner-draw
//If so - send message HDM_SETITEM with HDF_OWNERDRAW flag set
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++;
}
}
根据 `MyColumn` 的属性,我填充一个 `LVCOLUMN` 结构,然后发送 `LVM_INSERTCOLUMN` 消息。我们需要检查 `OwnerDraw` 属性是否设置为 `true` —— 如果是,则用新的标志——所有者绘制——修改现有的 `HDITEM` 结构。
HeaderControl 类
`HeaderControl` 类起着重要的作用。它继承自 `NativeWindow`,其唯一目的是接收发送到标题控件的消息。
internal class HeaderControl : NativeWindow
{
MyListView parent;
bool mouseDown;
public HeaderControl(MyListView m)
{
parent = m;
//Get the header control handle
IntPtr header = Win32.SendMessage(parent.Handle,
(0x1000+31), IntPtr.Zero, IntPtr.Zero);
this.AssignHandle(header);
}
protected override void WndProc(ref Message m)
{
//.................
}
}
标题句柄通过 `LVM_GETHEADER` 消息获得。获得句柄后,将其分配给 `HeaderControl` 类,以便我们可以重写其 `WndProc`。
HeaderControl 重写的 WndProc
protected override void WndProc(ref Message m)
{
switch(m.Msg)
{
case 0x000F://WM_PAINT
if(parent.FullyCustomHeader)
{
Win32.RECT update = new Win32.RECT();
if(Win32.GetUpdateRect(m.HWnd,ref update,
false)==0)
break;
//Fill the paintstruct
Win32.PAINTSTRUCT ps = new
Win32.PAINTSTRUCT();
IntPtr hdc = Win32.BeginPaint(m.HWnd, ref ps);
//Create graphics object from the hdc
Graphics g = Graphics.FromHdc(hdc);
//Get the non-item rectangle
int left = 0;
Win32.RECT itemRect = new Win32.RECT();
for(int i=0; i<parent.Columns.Count; i++)
{
//HDM_GETITEMRECT
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 we have a valid event handler - call it
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));
//Now we have to check if we have
//owner-draw columns and fill
//the DRAWITEMSTRUCT appropriately
int counter = 0;
foreach(MyColumn mm in parent.Columns)
{
if(mm.OwnerDraw)
{
Win32.DRAWITEMSTRUCT dis =
new Win32.DRAWITEMSTRUCT();
dis.ctrlType = 100;//ODT_HEADER
dis.hwnd = m.HWnd;
dis.hdc = hdc;
dis.itemAction = 0x0001;//ODA_DRAWENTIRE
dis.itemID = counter;
//Must find if some item is pressed
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 clicked on a divider -
//we don't have hot item
if(hi.flags == 0x0004 || hotItem != counter)
hotItem = -1;
if(hotItem != -1 && mouseDown)
dis.itemState = 0x0001;//ODS_SELECTED
else
dis.itemState = 0x0020;
//HDM_GETITEMRECT
Win32.SendMessage(m.HWnd, 0x1200+7,
counter, ref itemRect);
dis.rcItem = itemRect;
//Send message WM_DRAWITEM
Win32.SendMessage(parent.Handle,
0x002B,0,ref dis);
}
counter++;
}
Win32.EndPaint(m.HWnd, ref ps);
}
else
base.WndProc(ref m);
break;
case 0x0014://WM_ERASEBKGND
//We don't need to do anything here
//in order to reduce flicker
if(parent.FullyCustomHeader)
break;
else
base.WndProc(ref m);
break;
case 0x0201://WM_LBUTTONDOWN
mouseDown = true;
base.WndProc(ref m);
break;
case 0x0202://WM_LBUTTONUP
mouseDown = false;
base.WndProc(ref m);
break;
case 0x1200+5://HDM_LAYOUT
base.WndProc(ref m);
break;
case 0x0030://WM_SETFONT
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;
}
}
在这里,我检查 `MyListView.FullyCustomHeader` 属性是否设置为 `true`。如果是,则触发 `DrawHeader` 事件,然后检查所有者绘制的列——如果我们有,则相应地填充 `DRAWITEMSTRUCT` 并将 `WM_DRAWITEM` 消息发送到 `MyListView`。如果我们没有 `FullyCustomHeader` —— 则调用 `base.WndProc`。
通过 MyListView WndProc 触发 DrawColumn 事件
我们需要重写 `MyListView` 的 `WndProc` 并捕获 `WM_DRAWITEM` 消息,然后相应地填充 `DrawItemEventArgs`,如果我们有一个有效的事件处理程序——调用它!
//.............................................................
case 0x002B://WM_DRAWITEM
//Get the DRAWITEMSTRUCT from the LParam of the message
Win32.DRAWITEMSTRUCT dis = (Win32.DRAWITEMSTRUCT)Marshal.PtrToStructure(
m.LParam,typeof(Win32.DRAWITEMSTRUCT));
//Check if this message comes from the header
if(dis.ctrlType == 100)//ODT_HEADER - it do comes from the header
{
//Get the graphics from the hdc field of the DRAWITEMSTRUCT
Graphics g = Graphics.FromHdc(dis.hdc);
//Create a rectangle from the RECT struct
Rectangle r = new Rectangle(dis.rcItem.left,
dis.rcItem.top, dis.rcItem.right -
dis.rcItem.left, dis.rcItem.bottom - dis.rcItem.top);
//Create new DrawItemState in its default state
DrawItemState d = DrawItemState.Default;
//Set the correct state for drawing
if(dis.itemState == 0x0001)
d = DrawItemState.Selected;
//Create the DrawItemEventArgs object
DrawItemEventArgs e = new
DrawItemEventArgs(g,this.Font,r,dis.itemID,d);
//If we have a handler attached call
//it and we don't want the default drawing
if(DrawColumn != null && !defaultCustomDraw)
DrawColumn(this.Columns[dis.itemID], e);
else if(defaultCustomDraw)
DoMyCustomHeaderDraw(this.Columns[dis.itemID],e);
//Release the graphics object
g.Dispose();
}
break;
//..................................................................
触发 BeginDragHeaderDivider, DragHeaderDivider 和 EndDragHeaderDivider 事件
这些事件作为通知从标题控件发送给我们。我们所要做的就是确保我们有一个有效的处理程序来调用它。
//----------
case 0x004E://WM_NOTIFY
base.WndProc(ref m);
Win32.NMHDR nmhdr = (Win32.NMHDR)m.GetLParam(typeof(Win32.NMHDR));
switch(nmhdr.code)
{
case (0-300-26)://HDN_BEGINTRACK
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)://HDN_ITEMCHANGING
nm=(Win32.NMHEADER)m.GetLParam(typeof(Win32.NMHEADER));
//Adjust the column width
Win32.RECT rect = new Win32.RECT();
//HDM_GETITEMRECT
Win32.SendMessage(header.Handle, 0x1200+7, nm.iItem, ref rect);
//Get the item height which is actually header's height
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)://HDN_ENDTRACK
nm=(Win32.NMHEADER)m.GetLParam(typeof(Win32.NMHEADER));
if(EndDragHeaderDivider != null)
EndDragHeaderDivider(this.Columns[nm.iItem],
new HeaderEventArgs(nm.iItem, nm.iButton));
break;
}
break;
//----------
关注点
我还在考虑扩展 `MyListView`,使其支持自定义绘制和用户定义的选中颜色、边框颜色和列颜色,但正如我在 CodeProject 的文章部分所发现的那样,有些人已经做到了这一点,所以我只关注标题控件。我没有改动 `LVCOLUMN` 结构中的 `bitmap` 字段。但令我非常惊讶的是,如果我们设置了默认 Windows 实现的 `LVCFMT_BITMAP_ON_RIGHT` 标志,图像就没有正确绘制!!!您看到了吗?
默认 Windows 实现的 `LVCFMT_BITMAP_ON_RIGHT`
也许只有指定了位图时它才能正确工作……
历史
- CustomHeader v. 1.0.0