65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义ListView中的Header控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (26投票s)

2003年6月1日

5分钟阅读

viewsIcon

415911

downloadIcon

4569

本文详细介绍如何在 ListView 的详细信息模式下扩展默认的标题控件。

This is completely owner-draw header

图 1:FullyCustomHeader

Default implementation with images

图 2:默认 Windows 实现(带图像)

Default implementation with images

图 3:默认 Windows 实现(带图像且第一个列设置为所有者绘制)

引言

一切都始于在一个应用程序中,我需要 ListView 中的列标题显示一个图像。令我惊讶的是,.NET 对 HeaderControl 公共控件的支持非常有限。从通用控件 4.70 及更高版本开始,列标题支持来自 ImageList 和位图的图像。.NET 框架至少需要 IE 5.0,所以该控件版本应该是可用的。默认 .NET 标题中未实现的另一件事是,当用户开始拖动列或结束拖动列时发送给父列表视图的通知。我觉得这非常烦人,因为在一个应用程序中,我想要一个隐藏的组合框,并在选中项出现时显示它。但是为了正确响应列的重绘,我还需要调整组合框的大小。但是没有委托,我如何捕获事件……所以,我将所有这些缺失的功能整合到一个扩展的列表视图中(它只扩展了详细模式下的标题控件)。

基本思想

想法是创建一个新的 MyColumn 类,它具有新属性并支持图像和所有者绘制功能。我正在使用现有的 LVCOLUMN 结构,并简单地指定 ImageList 和图像索引。所有者绘制标志由 HDITEM 结构设置。为了正确添加列,我使用的是 LVM_INSERTCOLUMN 消息。通过重写 ListViewWndProc 来实现处理所有者绘制功能和跟踪列的事件。

使用代码

要使用此代码,只需将 CustomHeader.dll 添加到您的工具箱,然后将控件拖到窗体上。 Columns 属性是新的——它属于 MyHeaderCollection 类型,添加列时您会看到一些新属性出现。

  1. ImageIndex - 这是与标题关联的图像列表中的索引。
  2. OwnerDraw - 我添加了这个功能,以便您可以绘制自己的列。
  3. 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`

Default implementation with images

也许只有指定了位图时它才能正确工作……

历史

  • CustomHeader v. 1.0.0
© . All rights reserved.