
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;
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)
{
e.Graphics.DrawLine(pen, nCenterX - nSignPixels,
nCenterY, nCenterX + nSignPixels, nCenterY);
if ( ! Node.m_bExpanded )
{
e.Graphics.DrawLine(pen, nCenterX, nCenterY - nSignPixels,
nCenterX, nCenterY + nSignPixels);
}
e.Graphics.DrawRectangle(pen, nCenterX - nSignPixels - 2,
nCenterY - nSignPixels - 2, nSignPixels * 2 + 4, nSignPixels * 2 + 4);
if (e.Index != 0)
{
e.Graphics.DrawLine(pen, nCenterX, rectCol.Y,
nCenterX, nCenterY - nSignPixels - 2);
}
if (e.Index != lv.Items.Count - 1)
{
e.Graphics.DrawLine(pen, nCenterX, nCenterY +
nSignPixels + 2, nCenterX, rectCol.Y + rectCol.Width);
}
}
else
{
if (e.Index == 0)
{
e.Graphics.DrawLine(pen, nCenterX, nCenterY, nCenterX,
rectCol.Y + rectCol.Width);
e.Graphics.DrawLine(pen, nCenterX - nSignPixels - 2,
nCenterY, nCenterX + nSignPixels + 2, nCenterY);
}
else if (e.Index == lv.Items.Count - 1)
{
e.Graphics.DrawLine(pen, nCenterX, rectCol.Y, nCenterX, nCenterY);
e.Graphics.DrawLine(pen, nCenterX - nSignPixels - 2,
nCenterY, nCenterX + nSignPixels + 2, nCenterY);
}
else
{
e.Graphics.DrawLine(pen, nCenterX, rectCol.Y, nCenterX,
rectCol.Y + rectCol.Width);
}
if (Node.m_bLastChild)
{
e.Graphics.DrawLine(pen, nCenterX, nCenterY, nCenterX +
nSignPixels + 2, nCenterY);
}
}
pen.Dispose();
rectCol.X += ch.Width;
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;
}
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);
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) :{
Win32.DRAWITEMSTRUCT dis = (Win32.DRAWITEMSTRUCT)Marshal.PtrToStructure(
m.LParam,typeof(Win32.DRAWITEMSTRUCT));
Graphics g = Graphics.FromHdc(dis.hdc);
DrawItemState d = DrawItemState.Default;
if((dis.itemState & (int)Win32.ODS.ODS_SELECTED) > 0)
d = DrawItemState.Selected;
Rectangle r = new Rectangle(dis.rcItem.left, dis.rcItem.top,
dis.rcItem.right - dis.rcItem.left, dis.rcItem.bottom - dis.rcItem.top);
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);
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;
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);
}
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);
}
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
{
g.FillRectangle(m_BkBrush,this.ClientRectangle);
}
g.Dispose();
}
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.