SharpListView





4.00/5 (17 votes)
Mar 4, 2004
2 min read

187304

731
Fast and easy C# owner-drawn ListView
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:
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:
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.
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.