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

TreeListView

0.00/5 (No votes)
31 Aug 2003 187  
A custom control that ties a ListView and a TreeView together

Introduction

The System.Windows.Forms namespace provides the ListView and TreeView controls. But there is no control that allows you to use a tree and columns together.

I made such a control that enables this features:

  • Ties a ListView and a TreeView together in a TreeListView class that includes BeforeExpand, AfterExpand, BeforeCollapse, AfterCollapse events,
  • Uses a TreeListViewItem class that includes Expand function, Collapse functions, IsExpanded property,
  • Uses a TreeListViewItemCollection that replaces the ListViewItemCollection class used in the ListView control. This class is also used for the Items property in the TreeListViewItem class that contains the childs of an item. TreeListViewItemCollection adds the Sort function capability,
  • Subitem edit with custom control (EditBox, ComboBox, etc...),
  • XP-Style selection,
  • Plus-minus boxes and lines,
  • Indeterminate state item.

TreeListView control

This class inherits from the ListView class.

Properties

Some properties have been changed:

For example, the View property of the ListView control is no more used and must always be equal to View.Details. The Items is now a TreeListViewItemCollection. The SelectedItems and CheckedItems are now TreeListViewItem arrays (I don't have make special collection like SelectedListViewItemCollection and CheckedListViewItemCollection for the original ListView class...).

public new TreeListView.TreeListViewItemCollection Items{
    get{return(_items);}}

new public SelectedTreeListViewItemCollection SelectedItems
{
    get
    {
          SelectedTreeListViewItemCollection sel = new 
                                     SelectedTreeListViewItemCollection(this);
        return(sel);
    }
}
public new TreeListViewItem[] CheckedItems
{
    get
    {
        TreeListViewItem[] array = new 
                                  TreeListViewItem[base.CheckedIndices.Count];
        for(int i = 0 ; i < base.CheckedIndices.Count ; i++)
            array[i] = (TreeListViewItem) base.CheckedItems[i];
        return(array);
    }
}

// Gets the informations of the current edited item
public EditItemInformations EditedItem
{
    get{return _editeditem;}
}

// Gets wether an item is currently edited
public bool InEdit
{
    get{return _inedit;}
}

// Gets or sets a value indicating whether plus-sign (+) and minus-sign (-) 
// buttons are displayed next to TreeListView that contain child TreeListViews
public bool ShowPlusMinus
{
    get{return _showplusminus;}
    set{if(_showplusminus == value) return;
        _showplusminus = value;
        if(Created) Invoke(new VoidHandler(VisChanged));}
}

// Gets or Sets the color of the lines if ShowPlusMinus property is enabled
public Color PlusMinusLineColor
{
    get{return _plusMinusLineColor;}
    set{_plusMinusLineColor = value;
        if(Created) Invalidate();}
}

// Gets or Sets whether the control draw XP-Style highlight color
public bool UseXPHighlightStyle
{
    get{return _useXPHighLightStyle;}
    set{_useXPHighLightStyle = value;
        if(Created) Invalidate();}
}

Expand and collapse events

Like in the TreeView, when an item is expanded or collapsed, an event is raised before and after.
New events replace the AfterLabelEdit and BeforeLabelEdit of the standard ListView class. They allow you to modify the column to edit and the control that is used to edit the subitem (TextBox, ComboBox for example).

Here is a list of the new events (the handlers and arguments are not described here) :

[Description("Occurs before the tree node is collapsed")]
public event TreeListViewCancelEventHandler BeforeExpand;

[Description("Occurs before the tree node is collapsed")]
public event TreeListViewCancelEventHandler BeforeCollapse;

[Description("Occurs after the tree node is expanded")]
public event TreeListViewEventHandler AfterExpand;

[Description("Occurs after the tree node is collapsed")]
public event TreeListViewEventHandler AfterCollapse;

[Description("Occurs when the label for an item is edited by the user.")]
public new event TreeListViewLabelEditEventHandler AfterLabelEdit;

[Description("Occurs when the user starts editing the label of an item.")]
public new event TreeListViewBeforeLabelEditEventHandler BeforeLabelEdit;

TreeListViewItem class

This class inherits from the ListViewItem class. Some methods and properties have been changed like in the TreeListView class but the most important functions are the Expand and Collapse functions.

The Expand function adds each child of the collection in descending order just after this TreeListViewItem, so that the first item of the child appears just after the parent, the second after the first, etc.. because the expansion adds each child just after the parent item.

The Collapse function removes each child in the items from the ListView.

private bool _isexpanded;
public bool IsExpanded{
    get{return(_isexpanded);}}

public void Expand()
{
    if(ListView != null)
        if(ListView.InvokeRequired)
            throw(new Exception("Invoke Required"));
    // The item wasn't expanded -> raise an event
    if(Visible && !_isexpanded && ListView != null)
    {
         TreeListViewCancelEventArgs e = new TreeListViewCancelEventArgs(
            this, TreeListViewAction.Expand);
         ListView.RaiseBeforeExpand(e);
         if(e.Cancel) return;
    }
    if(Visible)
        for(int i = Items.Count - 1 ; i >= 0 ;i--)
        {
            TreeListViewItem item = this.Items[i];
            if(!item.Visible)
            {
                ListView LView = this.ListView;
                LView.Items.Insert(
                    this.Index + 1,
                    item);
                item.SetIndentation();
            }
            if(item.IsExpanded)
                item.Expand();
        }
    // The item wasn't expanded -> raise an event
    if(Visible && !_isexpanded && ListView != null)
    {
        this._isexpanded = true;
        TreeListViewEventArgs e = new TreeListViewEventArgs(
            this, TreeListViewAction.Expand);
        ListView.RaiseAfterExpand(e);
        if(AfterExpand != null) AfterExpand(this);
    }
    this._isexpanded = true;
}

public void Collapse()
{
    if(ListView != null)
        if(ListView.InvokeRequired)
            throw(new Exception("Invoke Required"));
    // The item was expanded -> raise an event
    if(Visible && _isexpanded && ListView != null)
    {
        TreeListViewCancelEventArgs e = new TreeListViewCancelEventArgs(
            this, TreeListViewAction.Collapse);
        ListView.RaiseBeforeCollapse(e);
        if(e.Cancel) return;
    }

    // Collapse
    if(this.Visible)
        foreach(TreeListViewItem item in Items)
                item.Hide();
    
    // The item was expanded -> raise an event
    if(Visible && _isexpanded && ListView != null)
    {
        this._isexpanded = false;
        TreeListViewEventArgs e = new TreeListViewEventArgs(
            this, TreeListViewAction.Collapse);
        ListView.RaiseAfterCollapse(e);
        if(AfterCollapse != null) AfterCollapse(this);
    }
    this._isexpanded = false;
}

The item is indented with the SetIndentation function that uses the Level property (gets the level of the item by getting the number of parents in the hierarchy) :

public int Level
{
    get{return(this.Parent == null ? 0 : this.Parent.Level + 1);}
}

public void SetIndentation()
{
    if(this.ListView == null) return;
    LV_ITEM lvi = new LV_ITEM();
    lvi.iItem = this.Index;
    lvi.iIndent = this.Level;
    if(TreeListView.ShowPlusMinus) lvi.iIndent++;
    lvi.mask = ListViewMessages.LVIF_INDENT;
    SendMessage(
        this.ListView.Handle,
        ListViewMessages.LVM_SETITEM,
        0,
        ref lvi);
}

TreeListViewItemCollection class

This class works like the TreeNodeCollection does with some changes. To manipulate items that are in a TreeListView, you must use the Invoke function if you are using multiple threads as with a standard ListView control.

Properties

The class has a Parent property (TreeListViewItem) and a Owner property (TreeListView). These properties can not have both a not null value at the same time because a collection enumerates the children of a TreeListView or enumerates the children of a TreeListViewItem. Both properties allow only to Get the value because these values are only changed internal when the constructor is called.

There are two properties SortOrder and SortOrderRecursively that get or set the sort order of the collection and automatically call the Sort function that is described below if the sort order is changed. The SortOrderRecursively property calls the SortOrder property of each TreeListViewItemCollection in the items of this collection recursively.

Functions

The Add function inserts an item in the collection at an index depending on the SortOrder value. This index is calculated with the GetInsertCollectionIndex function that is not given here. The item is also naturally inserted in the TreeListView if necessary at an index calculated with the GetInsertTreeListViewIndex function.

public virtual int Add(TreeListViewItem item)
{
    if(TreeListView != null)
        if(TreeListView.InvokeRequired)
            throw(new Exception("Invoke required"));
    // Do not add the item if the collection owns a TreeListView 
    // recursively and the item already owns a TreeListView
    if(TreeListView != null && item.ListView != null)
        throw(new Exception("The Item is already in a TreeListView"));
    int index = GetInsertCollectionIndex(item);
    if(index == -1) return(-1);
    if(Parent != null) item.SetParent(Parent);
    item.Items.Comparer = this.Comparer;
    int treelistviewindex = GetInsertTreeListViewIndex(item, index);
    // Insert in the ListView
    if(treelistviewindex > -1)
    {
        ListView listview = (ListView) TreeListView;
        listview.Items.Insert(treelistviewindex, (ListViewItem) item);
        if(item.IsExpanded) item.Expand();
        item.SetIndentation();
    }
    // Insert in this collection
    if(index > -1) List.Insert(index, item);
    if(index > -1) OnItemAdded(new TreeListViewEventArgs(item, 
                                                 TreeListViewAction.Unknown));
    return(index);
}

The Remove function removes an item from the collection.

public virtual void Remove(TreeListViewItem item)
{
    if(TreeListView != null)
        if(TreeListView.InvokeRequired)
            throw(new Exception("Invoke required"));
    int index = GetIndexOf(item);
    if(index == -1) return;
    RemoveAt(index);
}
public new void RemoveAt(int index)
{
    if(TreeListView != null)
        if(TreeListView.InvokeRequired)
            throw(new Exception("Invoke required"));
    TreeListViewItem item = this[index];
    if(this[index].Visible && this.TreeListView != null) item.Hide();
    List.RemoveAt(index);
    item.SetParent(null);
    OnItemRemoved(new TreeListViewEventArgs(item, 
                                                 TreeListViewAction.Unknown));
}

Other functions that not described here : AddRange, Clear, Contain.

The Sort function sorts the items in the collection and also change the order in the TreeListView if the items were visible. The items are stored in an array, the items in the array are sorted using the TreeListViewItemCollectionComparer class (not described here), the items are removed from the collection, and the items are copied from the sorted array to the collection.

public void Sort(bool recursively)
{
    if(TreeListView != null)
        if(TreeListView.InvokeRequired)
            throw(new Exception("Invoke required"));
    // Gets an array of the items
    TreeListViewItem[] thisarray = ToArray();
    // Removes the items
    Clear();
    // Adds the items
    foreach(TreeListViewItem item in thisarray)
        Add(item);
    if(recursively)
        foreach(TreeListViewItem item in thisarray)
            item.Items.Sort(true);
}

Other functions description

Subitem edit

To edit subitems, the control handles the LVN_BEGINLABELEDIT and LVN_ENDLABELEDIT messages in the WndProc :

case APIsEnums.ListViewNotifications.BEGINLABELEDIT:
    System.Diagnostics.Debug.Assert(FocusedItem != null);
    // Cancel label edit if the message is sent just after a double click
    if(_lastdoubleclick.AddMilliseconds(450) > DateTime.Now)
    {
        Message canceledit = Message.Create(Handle, 
                             (int) APIsEnums.ListViewMessages.CANCELEDITLABEL, 
                             IntPtr.Zero, IntPtr.Zero);
        WndProc(ref canceledit);
        m.Result = (IntPtr) 1;
        return;
    }
    item = FocusedItem;
    int column = 0;
    if(_lastitemclicked.Item == item &&
        _lastitemclicked.CreationTime.AddMilliseconds(
                          2*SystemInformation.DoubleClickTime) > DateTime.Now)
        column = _lastitemclicked.ColumnIndex;
    if(column == -1) column = 0;
    // Add subitems if needed
    while(item.SubItems.Count-1 < column) item.SubItems.Add("");
    TreeListViewBeforeLabelEditEventArgs beforeed = new 
                                         TreeListViewBeforeLabelEditEventArgs(
                                         FocusedItem, column, 
                                         item.SubItems[column].Text);
    OnBeforeLabelEdit(beforeed);
    if(beforeed.Cancel)
    {
        Message canceledit = Message.Create(Handle, 
                              (int)APIsEnums.ListViewMessages.CANCELEDITLABEL, 
                              IntPtr.Zero, IntPtr.Zero);
        WndProc(ref canceledit);
        m.Result = (IntPtr) 1;
        return;
    }
    _inedit = true;
    // Get edit handle
    Message mess = Message.Create(Handle, 
                               (int)APIsEnums.ListViewMessages.GETEDITCONTROL, 
                               IntPtr.Zero, IntPtr.Zero);
    WndProc(ref mess);
    IntPtr edithandle = mess.Result;
    _customedit = new CustomEdit(edithandle, this, beforeed.Editor);
    _editeditem = new EditItemInformations(
                     FocusedItem, beforeed.ColumnIndex, 
                             FocusedItem.SubItems[beforeed.ColumnIndex].Text);
    m.Result = IntPtr.Zero;
    return;
case APIsEnums.ListViewNotifications.ENDLABELEDIT:
    if(_customedit != null)
        _customedit.HideEditControl();
    _customedit = null;
    _inedit = false;
    _editeditem = new EditItemInformations();
    m.Result = IntPtr.Zero;
    return;

The CustomEdit class is not described here. This class will receive all messages sent to the edit box. It handles the messages and show the custom control instead of the standard edit box of the ListView.

XP-style selection

When the LVN_CUSTOMDRAW message is sent, the function CustomDraw is called to customize the draw behavior. At the pre-paint state, the select flag is set to false and the back color of the selected item is modified so that it will be displayed with a custom color.

private void CustomDraw(ref Message m)
{
  int iRow, iCol; bool bSelected;
  unsafe
  {
    APIsStructs.NMLVCUSTOMDRAW * nmlvcd = 
                          (APIsStructs.NMLVCUSTOMDRAW *)m.LParam.ToPointer();

     switch((APIsEnums.CustomDrawDrawStateFlags)nmlvcd->nmcd.dwDrawStage)
     {
       case APIsEnums.CustomDrawDrawStateFlags.PREPAINT:
              m.Result = 
                       (IntPtr)APIsEnums.CustomDrawReturnFlags.NOTIFYITEMDRAW;
              break;
       case APIsEnums.CustomDrawDrawStateFlags.ITEMPREPAINT:
              m.Result =
                    (IntPtr)APIsEnums.CustomDrawReturnFlags.NOTIFYSUBITEMDRAW;
              break;

       case APIsEnums.CustomDrawDrawStateFlags.ITEMPREPAINT |
              APIsEnums.CustomDrawDrawStateFlags.SUBITEM:
              iRow = (int)nmlvcd->nmcd.dwItemSpec;
              iCol = (int)nmlvcd->iSubItem;
              bSelected = base.Items[iRow].Selected;// && this.Focused;
              if(bSelected && _useXPHighLightStyle)
              {
                Color color = Focused ? ColorUtil.VSNetSelectionColor : 
                                       ColorUtil.VSNetSelectionUnfocusedColor;
                if(HideSelection && !Focused) color = BackColor;
                if(FullRowSelect || iCol == 0)
                   nmlvcd->clrTextBk = (int)ColorUtil.RGB(color.R, color.G, 
                                         color.B);
                   nmlvcd->nmcd.uItemState &= 
                           ~(uint)APIsEnums.CustomDrawItemStateFlags.SELECTED;
                if(iCol == 0) DrawSelectedItemFocusCues(iRow);
              }
              if(iCol == 0)
              {
                 DrawIntermediateStateItem((TreeListViewItem)base.Items[iRow]);
                 DrawPlusMinusItemLines((TreeListViewItem)base.Items[iRow]);
                 DrawPlusMinusItem((TreeListViewItem)base.Items[iRow]);
              }
              m.Result = (IntPtr)APIsEnums.CustomDrawReturnFlags.NEWFONT;
              break;
        }
    }
}

The ColorUtil class has been written by Carlos H. Perez.

The DrawIntermediateStateItem, DrawPlusMinusItemLines, DrawPlusMinusItem are not described here.

Indeterminate state item

The property Checkable avoids checking an item. An item that has this property set to true will display a grey box if at least one of the sub items is checked.

new public CheckState Checked
{
   get
   {
     if(!Checkable) return HasCheckedChild ? CheckState.Indeterminate : 
                                                         CheckState.Unchecked;
     else return(base.Checked ? CheckState.Checked : CheckState.Unchecked);
   }
   set
   {
     if(ListView != null)
     if(ListView.FreezeCheckBoxes) return;
     if(Checkable && value == CheckState.Indeterminate) 
        throw(new Exception(
              "A checkable item can only have checked and unchecked values"));
        if(Checkable)
    try{base.Checked = value == CheckState.Checked;}
    catch{}
    if(Items.Count > 0 && !Checkable)
    {
      CheckState checkvalue = value == CheckState.Unchecked ?
               CheckState.Unchecked : CheckState.Checked;
        if(ListView != null)
        {ListView.Invoke(
             new ChangeChildrenCheckStateRecursivelyHandler(
                                         ChangeChildrenCheckStateRecursively),
                                 new object[]{checkvalue});}
        else
             ChangeChildrenCheckStateRecursively(checkvalue);
        }
    }
}

Conclusion

Most of the functionalities have been described here.

I do not guarantee that this control works 100%. If you find mistakes, you can correct them. This control is not a final release and you can send me mails with explains if you find bugs or mistakes and join the modifications if you did them.

The objective is to give a good starting point for those which want to have a control which mixes the TreeView and the ListView controls.

History

  • 1 Sep 03
    • Multiselect added
    • PathSeparator added
    • Better key integration (for Multiselect and checkboxes)
    • ImageList bug fixed (better integration in the designer)
    • Scrollable bug fixed
    • Minor bugs fixed
  • 14 July 03 - updated source code
  • 7 July 2003 - updated source code.
  • 24 June 2003 - updated source code.
  • 3 Jun 2003:
    • many bugs fixed,
    • Subitem edit with custom control (EditBox, ComboBox, etc...) function,
    • XP-Style selection function,
    • Plus-minus boxes and lines function,
    • Indeterminate state item function.
  • 26 Nov 2002 - updated downloads

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