Tree List View

Article published on CodeProject

Introduction

Tree List View

How about a control like that? Isn’t it cool? Unfortunately, you don’t get that with the Windows Forms controls collection. But you can get yourself one; read on.

We might have seen such types of controls and they are called by different names. In the context of the article (and in general I believe), such controls may be classified into two types. This categorization is primarily based on the functionality offered than the view itself. So the two types of the control are as follows:-

So that is a Tree List View control. Let us see how to build one.

Implementation Plan

What we will be doing is derive from the existing ListView class, call it TreeListView. So our Tree List View is basically a control with all the capabilities of the list view and exactly same in its vanilla state. Not only that, we will have to capture the hierarchy information among the list view items. To do that, we will derive from the existing ListViewItem class, call it ListViewItem2. Assuming any instance of ListViewItem2 to be a parent item (at any level), we should be able to add child list view items. In other words, an instance of ListViewItem2 is a container of its child items, and a cue for our custom rendering logic to render it as a hierarchy.

Thus the hierarchy is captured. Rest of it is rendering this hierarchy.

Taking Control Of Rendering

Yes, we will have to take control of the painting logic for such a control. We will set the OwnerDraw to true and override DrawItem and DrawSubItem to implement the custom logic to render appropriately.

There are several things which are part of the rendering logic. Each item in the list view can have a checkbox or an image. We have to show\hide items depending on whether their parent is expanded or collapsed. Besides, it will also show a plus (+) image if it has child items and if it is expanded; or a minus (-) image if it has child items and if it is collapsed. An item with children should expand when clicked on the collapsed (+) image, and collapse when clicked on the expanded (-) minus image. And depending on the depth, the text for the first sub-item of each list view item must be spaced\tabbed accordingly. We should take care of auto adjusting the length of the header item when double clicked on the header seam lines. Our custom logic has to take care of all these things to render.

Following snippet is worth a thousand words of the core rendering logic. Please refer to the source code attached for further details.-

private void OnDrawSubItem(object sender, DrawListViewSubItemEventArgs e)
{
   SuspendLayout();
 
   var lvItem = e.Item as ListViewItem2;
   if (lvItem == null || lvItem.IsEmpty)
   {
      return;
   }
 
   var txtMetrics = Helpers.GetTextMetrics(e.Graphics);      
   int yFactor = (e.Bounds.Height - txtMetrics.tmHeight) / 2;
 
   bool hasChildren = lvItem.HasChildren;
   int xBound = e.Bounds.X + 5;
 
   if (e.SubItem == e.Item.SubItems[0])
   {
      int iLevel = lvItem.GetIndentLevel();
      bool hasParent = lvItem.Parent == null ? false : true;
 
      xBound += hasParent ? iLevel * 14 : 0;
 
      if (hasChildren)
      {
         var imageLocation = new Point(xBound, e.Bounds.Y + yFactor + 1);
         
         lvItem.PlusMinusLocation = imageLocation;
         
         var image = 
         		lvItem.Expanded 
         		? TreeListView.MinusImage 
         		: TreeListView.PlusImage

         e.Graphics.DrawImage(image, imageLocation);
         xBound += (TreeListView.PlusImage.Width + TreeListView.GeneralGapWidth);
      }
 
      if (this.CheckBoxes)
      {
				Size cbSize = CalculateCheckBoxSize(e.SubItem);
				Rectangle cbBounds = new Rectangle(new Point(xBound, e.Bounds.Y), cbSize);

				ControlPaint.DrawCheckBox(
					e.Graphics,
					cbBounds,
					(lvItem.Checked ? ButtonState.Checked : ButtonState.Normal) | ButtonState.Flat
				);
 
         lvItem.CheckBoxBounds = cbBounds;
         xBound += cbBounds.Width + TreeListView.GeneralGapWidth;
      }
 
      if (this.SmallImageList != null
	      		&& e.Item.ImageIndex >= 0
	      		&& e.Item.ImageIndex < this.SmallImageList.Images.Count)
      {
        Image img = e.Item.ImageList.Images[e.Item.ImageIndex];
        int imageWidth = img.Width;
        int imageHeight = img.Height - 2;

        e.Graphics.DrawImage(img, new Rectangle(xBound, e.Bounds.Y + 1, imageWidth, imageHeight));
        xBound += imageWidth + TreeListView.GeneralGapWidth;
      }
   }

	PointF drawPoint = new PointF(xBound, e.Bounds.Y + yFactor);
	SizeF drawBound = new SizeF(e.Bounds.X + e.Bounds.Width - xBound, e.Bounds.Height);
	RectangleF drawRect = new RectangleF(drawPoint, drawBound);

	StringFormat txtFormat = new StringFormat();
	txtFormat.Trimming = StringTrimming.EllipsisCharacter;
	txtFormat.LineAlignment = ToStringAlignment(e.Header.TextAlign);

  e.Graphics.DrawString(
			e.SubItem.Text,
      e.Item.Font,
      new SolidBrush(e.Item.ForeColor),
      drawRect,
      txtFormat
  );
 
	ResumeLayout(true);
} 

That is it. We got our control working.

Points Of Interest

History