Click here to Skip to main content
15,885,546 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

 
QuestionNeed Contact Information Pin
Member 120390676-Oct-15 22:11
Member 120390676-Oct-15 22:11 
AnswerRe: Need Contact Information Pin
Member 1203906715-Oct-15 22:37
Member 1203906715-Oct-15 22:37 
Generalrow height Pin
nlt1814-Jun-07 23:31
nlt1814-Jun-07 23:31 
GeneralMaking it work in .NET 2.0 Pin
Riaan Lehmkuhl14-May-07 6:13
professionalRiaan Lehmkuhl14-May-07 6:13 
Hi,

I ran into an error while trying to use the control in .NET 2.0

Managed Debugging Assistant 'PInvokeStackImbalance' has detected a problem in 'C:\Projects\MyApp.vshost.exe'.<br />
Additional Information: A call to PInvoke function 'SharpListView!SharpListView.Win32::SetWindowLong' has unbalanced the stack. <br />
This is likely because the managed PInvoke signature does not match the unmanaged target signature. <br />
Check that the calling convention and parameters of the PInvoke signature match the target unmanaged signature.


To fix this required two small changes...

Index: C:/Projects/SharpListView/Win32.cs
===================================================================
--- C:/Projects/SharpListView/Win32.cs	(revision 222)
+++ C:/Projects/SharpListView/Win32.cs	(revision 223)
@@ -71,7 +71,7 @@
 		public static extern long GetWindowLong(IntPtr hwnd, int nIndex);
 
 		[DllImport("User32.dll",CharSet = CharSet.Auto)]
-		public static extern long SetWindowLong(IntPtr hwnd, int nIndex, long dwNewLong);
+		public static extern long SetWindowLong(IntPtr hwnd, int nIndex, uint dwNewLong);
 
 
 		#endregion


Index: C:/Projects/SharpListView/SharpListView.cs
===================================================================
--- C:/Projects/SharpListView/SharpListView.cs	(revision 222)
+++ C:/Projects/SharpListView/SharpListView.cs	(revision 223)
@@ -90,7 +90,7 @@
 			//#define LVS_OWNERDRAWFIXED      0x0400
 			//#define LVS_REPORT              0x0001
 			//#define GWL_STYLE           (-16)
-			long lStyle = Win32.GetWindowLong(this.Handle, -16);
+			uint lStyle = (uint)Win32.GetWindowLong(this.Handle, -16);
 			lStyle |= (0x0400 | 0x0001);
 			long lRet = Win32.SetWindowLong(this.Handle, -16, lStyle);
 		}


Hope this saves someone some time.

Cheers
QuestionIs SharpListView easy to convert to web control? Pin
JamesZhang28-Jun-06 6:09
JamesZhang28-Jun-06 6:09 
AnswerRe: Is SharpListView easy to convert to web control? Pin
Bill Pfeil28-Jun-06 6:51
Bill Pfeil28-Jun-06 6:51 
GeneralRe: Coloured Header Pin
Bill Pfeil3-Apr-06 7:12
Bill Pfeil3-Apr-06 7:12 
Generalword highlighting in Cell Pin
Ecologic25-Dec-05 20:22
Ecologic25-Dec-05 20:22 
Generalm.Msg constants Pin
Acid Cool31-Aug-05 16:15
Acid Cool31-Aug-05 16:15 
GeneralRe: m.Msg constants Pin
Bill Pfeil1-Sep-05 8:02
Bill Pfeil1-Sep-05 8:02 
QuestionUse in modified View.List mode? Pin
Gery Dorazio27-Dec-04 21:27
Gery Dorazio27-Dec-04 21:27 
AnswerRe: Use in modified View.List mode? Pin
Bill Pfeil28-Dec-04 9:47
Bill Pfeil28-Dec-04 9:47 
QuestionIs there a way to paint images instead of gradients? Pin
Andrew Jo30-Aug-04 5:22
Andrew Jo30-Aug-04 5:22 
AnswerRe: Is there a way to paint images instead of gradients? Pin
Bill Pfeil30-Aug-04 6:51
Bill Pfeil30-Aug-04 6:51 
GeneralRe: Is there a way to paint images instead of gradients? Pin
Andrew Jo30-Aug-04 7:19
Andrew Jo30-Aug-04 7:19 
GeneralRe: Is there a way to paint images instead of gradients? Pin
Bill Pfeil30-Aug-04 13:32
Bill Pfeil30-Aug-04 13:32 
GeneralField Clipping Pin
Joel Matthias24-Aug-04 4:40
Joel Matthias24-Aug-04 4:40 
GeneralRe: Field Clipping Pin
Bill Pfeil24-Aug-04 7:14
Bill Pfeil24-Aug-04 7:14 
GeneralOnly Painting Some Items Pin
Joel Matthias23-Aug-04 15:04
Joel Matthias23-Aug-04 15:04 
GeneralRe: Only Painting Some Items Pin
Bill Pfeil24-Aug-04 7:22
Bill Pfeil24-Aug-04 7:22 
GeneralRe: Only Painting Some Items Pin
tupacs0120-Oct-04 6:33
tupacs0120-Oct-04 6:33 
GeneralRe: Only Painting Some Items Pin
Joel Matthias20-Oct-04 6:41
Joel Matthias20-Oct-04 6:41 
QuestionThe last one? Pin
csmba2-Jul-04 8:03
csmba2-Jul-04 8:03 
AnswerRe: The last one? Pin
Bill Pfeil2-Jul-04 13:19
Bill Pfeil2-Jul-04 13:19 
GeneralRe: The last one? Pin
csmba2-Jul-04 13:35
csmba2-Jul-04 13:35 

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.