Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Simple Tree List View

0.00/5 (No votes)
3 Oct 2014 1  
A simple tree list view .NET WinForms control

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:

  • Tree List View (TLV) - A control like the conventional ListView (in its Details mode), which offers the facility of adding items as child to other items in the control so that a tree structure can be established. The items can be decorated with minor things like check boxes or images. This type of control does not offer the facility of in-place editing. That means it does not offer the facility of popping up a corresponding or associated control with each (sub)item for modifying the value associated with the (sub)item; if the control could be placed in edit mode.
  • Tree Grid - I believe, by now, you would have understand what this type of control has to offer - everything that the Tree List View does and does not offer. You could compare a tree grid with a conventional data grid, in which elements could be added to establish a tree hierarchy.

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.

The 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's it. We got our control working.

Points Of Interest

  • This control will work its magic only in the Details view and when the OwnerDraw is set to true. Otherwise, it is nothing more than a normal ListView. So, for instance, you could switch off the OwnerDraw and show the items flattened out; which was needed in my case then.
  • As of this writing, column re-ordering is not supported but it is possible to support column re-ordering.
  • As of this writing, column resizing is not supported. The column width is sized to fit the longest content. The resizing can be enabled from code by modifying the OnColumnWidthChanging event handler. However, the 'size to fit content' resizing (double clicking on the column header border) cannot be achieved because the control fires the ColumnWidthChanging event both when resizing by dragging the column header or double clicking the column header border. Since the distinction cannot be made, it is not possible to programmatically set the column width.

History

  • October 1, 2014 - First draft

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