Click here to Skip to main content
15,888,610 members
Articles / Programming Languages / C#
Article

SharpListView

Rate me:
Please Sign up or sign in to vote.
4.00/5 (18 votes)
21 Jun 20042 min read 185.4K   726   61   45
Fast and easy C# owner-drawn ListView

Image 1

Introduction

SharpListView. The last .NET owner-draw listview you'll ever need ;)

How does one create an owner-draw listview in .NET? Not very easily! You could call Control.SetStyle method, setting the style for ControlStyles.UserPaint, and then override the Control.OnPaint method. This works, of course, but MS left it to you to figure out how to paint the entire control, as well as how to optimize that paint. Remember the way listview used to work? After I discovered that the new ListView simply wrapped the old common control, I set out to "unwrap" that functionality.

After a number of google searches, the closest I could come was the CustomHeader project by Georgi Atanasov (www.codeproject.com/cs/miscctrl/customheader.asp). Thanks, Georgi! From his code I re-discovered the joys of working with a WndProc . First time in about a decade.

Anyway, if you want an owner-draw listview that works just like the common control it wraps, look no further. And I wouldn't be too worried about Avalon, either. According to msdn.microsoft.com, the Avalon ListView will function pretty much like this one.

Using the code

Using the class is trivial. Simply add the SharpListView class to your form, then write code to handle a few events. You will need, at minimum, to handle the DrawItemEvent. My Form creates a simple 2-level tree-in-a-list. Here is all the drawing that occurs in my Form.lv_DrawItem method:

C#
private void lv_DrawItem(object sender, 
  System.Windows.Forms.DrawItemEventArgs e)
{
 if (e.Index >= lv.Items.Count) return;
 ListViewItem lvi = lv.Items[e.Index];
 if (lvi == null) return;
 TreeNode Node = (TreeNode) lvi.Tag;
 if (Node == null) return;

 if (lvi.Selected)
  e.Graphics.FillRectangle(Brushes.DarkBlue,e.Bounds);
 else
  e.Graphics.FillRectangle(lv.BkBrush,e.Bounds);

 StringFormat s = new StringFormat();
 s.FormatFlags = StringFormatFlags.NoWrap;
 s.Trimming = StringTrimming.EllipsisCharacter;
 s.Alignment = StringAlignment.Near;

 Rectangle rectCol = e.Bounds;

 /////////////////////////////////////////
 // (+/-) Column
 //////////////////////////////////////////
 ColumnHeader ch = lv.Columns[0];
 rectCol.Width = ch.Width;

 int nHalfW = rectCol.Width / 2;
 int nHalfH = rectCol.Height / 2;
 int nCenterX = rectCol.X + nHalfW;
 int nCenterY = rectCol.Y + nHalfH;
 int nSignPixels = 2;

 Pen pen = new Pen(Brushes.Yellow);

 if (Node.m_arSubItems.Count > 0)
 {
  // Draw the plus or minus
  e.Graphics.DrawLine(pen, nCenterX - nSignPixels, 
    nCenterY, nCenterX + nSignPixels, nCenterY);
  if ( ! Node.m_bExpanded )
  {
   e.Graphics.DrawLine(pen, nCenterX, nCenterY - nSignPixels,
      nCenterX, nCenterY + nSignPixels);
  }

  // Draw a box around the plus or minus
  e.Graphics.DrawRectangle(pen, nCenterX - nSignPixels - 2,
    nCenterY - nSignPixels - 2, nSignPixels * 2 + 4, nSignPixels * 2 + 4);

  // Is this the very first item?
  if (e.Index != 0)
  {
   // Draw a line from the top to the box
   e.Graphics.DrawLine(pen, nCenterX, rectCol.Y, 
    nCenterX, nCenterY - nSignPixels - 2);
  }

  // Is this the very last item?
  if (e.Index != lv.Items.Count - 1)
  {
   // Draw a line from the bottom to the box
   e.Graphics.DrawLine(pen, nCenterX, nCenterY + 
    nSignPixels + 2, nCenterX, rectCol.Y + rectCol.Width);
  }
 }
 else
 {
  // Is this the very first item?
  if (e.Index == 0)
  {
   // Draw a line through the lower half of the item
   e.Graphics.DrawLine(pen, nCenterX, nCenterY, nCenterX, 
    rectCol.Y + rectCol.Width);
   // Draw a line indicating the start of the list
   e.Graphics.DrawLine(pen, nCenterX - nSignPixels - 2, 
    nCenterY, nCenterX + nSignPixels + 2, nCenterY);
  }
   // Is this the very last item?
  else if (e.Index == lv.Items.Count - 1)
  {
   // Draw a line through half the item
   e.Graphics.DrawLine(pen, nCenterX, rectCol.Y, nCenterX, nCenterY);
   // Draw a line indicating the end of the list
   e.Graphics.DrawLine(pen, nCenterX - nSignPixels - 2,
     nCenterY, nCenterX + nSignPixels + 2, nCenterY);
  }
  else
  {
   // Draw a line all the way through the item
   e.Graphics.DrawLine(pen, nCenterX, rectCol.Y, nCenterX, 
    rectCol.Y + rectCol.Width);
  }

  if (Node.m_bLastChild)
  {
   // Draw a line indicating the end of the expanded region
   e.Graphics.DrawLine(pen, nCenterX, nCenterY, nCenterX + 
    nSignPixels + 2, nCenterY);
  }
 }
 pen.Dispose();
 
 rectCol.X += ch.Width;

 /////////////////////////////////////////////////
 // Node type column
 /////////////////////////////////////////////////
 ch = lv.Columns[1];
 rectCol.Width = ch.Width;
 if (Node.m_arSubItems.Count > 0)
  e.Graphics.DrawString("Container",lv.Font,Brushes.Yellow,rectCol,s);
 else
  e.Graphics.DrawString("Node",lv.Font,Brushes.Yellow,rectCol,s);
 rectCol.X += ch.Width;

 ///////////////////////////////////////////////////
 // Remaining columns
 ///////////////////////////////////////////////////
 // ... And so on...
}

Points of Interest

By the method of trial and error, I discovered that if you set the proper window styles on the ListView common control, you will receive the reflected WM_DRAWITEM message. Here I set the styles in the SharpListView.OnHandleCreated override:

C#
protected override void OnHandleCreated(EventArgs e)
{
 base.OnHandleCreated(e);

 // In order to receive the WM_DRAWITEM message, 
 // the ListView common control
 // must have the following styles:
 //#define LVS_OWNERDRAWFIXED   0x0400
 //#define LVS_REPORT       0x0001
 //#define GWL_STYLE      (-16)
 long lStyle = Win32.GetWindowLong(this.Handle, -16);
 lStyle |= (0x0400 | 0x0001);
 long lRet = Win32.SetWindowLong(this.Handle, -16, lStyle);

 BkBrush = new SolidBrush(this.BackColor);
}

I handle the reflected WM_DRAWITEM in the SharpListView WndProc and call the form's event handler:

case (int) (Win32.WM.WM_DRAWITEM | 
  Win32.WM.WM_REFLECT) :// reflected WM_DRAWITEM
{
 //Get the DRAWITEMSTRUCT from the LParam of the message
 Win32.DRAWITEMSTRUCT dis = (Win32.DRAWITEMSTRUCT)Marshal.PtrToStructure(
  m.LParam,typeof(Win32.DRAWITEMSTRUCT));

 //Debug.WriteLine(dis.itemID.ToString());

 //Get the graphics from the hdc field of the DRAWITEMSTRUCT
 Graphics g = Graphics.FromHdc(dis.hdc);

 //Create new DrawItemState in its default state     
 DrawItemState d = DrawItemState.Default;
 //Set the correct state for drawing
 if((dis.itemState & (int)Win32.ODS.ODS_SELECTED) > 0)
  d = DrawItemState.Selected;
 //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 the DrawItemEventArgs object
 DrawItemEventArgs e = new DrawItemEventArgs(g,this.Font,r,dis.itemID,d);
 OnDrawItem(e);

 g.Dispose();  

 break;
}

Finally, I needed to handle the WM_ERASEBKGND message, and draw other areas besides my list items. I don't propagate this message because then the .NET ListView class erases the entire background. One final note: If you are resizing the form, you will not have good results unless you make a call to UpdateBounds(). Apparently this call results in the .NET ListView wrapper synchronizing its bounds with the common control.

C#
case (int)Win32.WM.WM_ERASEBKGND:
{
 if ((int)m.WParam != 0)
 {
  Graphics g = Graphics.FromHdc(m.WParam);

  // We don't want to paint the entire background, just 
  // any area to the right of the last column and also 
  // the little slices at the top and bottom of our list.

  // If you don't call this, you get yesterdays 
  // bounds. Very bad if resizing.
  this.UpdateBounds();

  if (oldBounds != this.ClientRectangle)
  {
   oldBounds = this.ClientRectangle;
   if (m_bUseGradient)
   {
    m_BkBrush = new LinearGradientBrush(
     this.ClientRectangle,
     m_GradientColorBegin,
     m_GradientColorEnd,
     LinearGradientMode.Horizontal);
   }
   else
   {
    m_BkBrush = new SolidBrush(this.BackColor);
   }

  }
  
  if (this.Items.Count > 0)
  {
   Rectangle r = this.GetItemRect(this.TopItem.Index);
   int nTotalWidth = 0;
   foreach (ColumnHeader col in this.Columns)
    nTotalWidth += col.Width;


   // Paint the top slice
   if (r.Top > this.ClientRectangle.Top)
   {
    Rectangle rect = new Rectangle(
     this.ClientRectangle.Left, 
     this.ClientRectangle.Top, 
     nTotalWidth, 
     r.Top - this.ClientRectangle.Top);
    g.FillRectangle(m_BkBrush,rect);
   }

   // Paint any visible area to the right of the columns
   if (r.Right < this.ClientRectangle.Right)
   {
    Rectangle rect = new Rectangle(
     this.ClientRectangle.Left + nTotalWidth, 
     this.ClientRectangle.Top, 
     this.ClientRectangle.Width - nTotalWidth, 
     this.ClientRectangle.Height);
    g.FillRectangle(m_BkBrush,rect);
   }

   // Paint the bottom slice, but only if visible
   r = this.GetItemRect(this.Items.Count - 1);
   if (r.Bottom < this.ClientRectangle.Bottom)
   {
    Rectangle rect = new Rectangle(
     this.ClientRectangle.Left, 
     r.Bottom, 
     nTotalWidth, 
     this.ClientRectangle.Bottom - r.Bottom);
    g.FillRectangle(m_BkBrush,rect);
   }
  }
  else
  {
   // We have no items. Paint the whole thing.
   g.FillRectangle(m_BkBrush,this.ClientRectangle);
  }

  g.Dispose();
 }
 // We don't want the .net listview to draw the background (causes flashing).
 m.Msg = (int) Win32.WM.WM_NULL;
 break;
}

That's it. Enjoy!

History

  • 02-27-04 v1.0 SharpListView
  • 03-18-04 v1.1 Added designer support. Added gradient background support.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United States United States
Married with children, currently living in Corvallis, Oregon. I like to fish and coach soccer in my spare time.

Comments and Discussions

 
QuestionSample usage?? Pin
jaxterama21-Mar-04 0:55
jaxterama21-Mar-04 0:55 
AnswerStill can't get it to work... Pin
jaxterama25-Mar-04 17:12
jaxterama25-Mar-04 17:12 
GeneralRe: Still can't get it to work... Pin
Bill Pfeil26-Mar-04 4:53
Bill Pfeil26-Mar-04 4:53 
GeneralRe: Still can't get it to work... Pin
Bill Pfeil26-Mar-04 5:03
Bill Pfeil26-Mar-04 5:03 
GeneralRe: Still can't get it to work... Pin
jaxterama29-Mar-04 6:03
jaxterama29-Mar-04 6:03 
GeneralRe: Still can't get it to work... Pin
Bill Pfeil29-Mar-04 7:31
Bill Pfeil29-Mar-04 7:31 
GeneralRe: Still can't get it to work... Pin
jaxterama29-Mar-04 12:07
jaxterama29-Mar-04 12:07 
QuestionAnd the test app? Pin
Darren Schroeder19-Mar-04 2:58
Darren Schroeder19-Mar-04 2:58 
How about including SharpListViewTest as the solution indicates?
AnswerRe: And the test app? Pin
Bill Pfeil19-Mar-04 4:25
Bill Pfeil19-Mar-04 4:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.