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.
Married with children, currently living in Corvallis, Oregon. I like to fish and coach soccer in my spare time.