5,696,576 members and growing! (16,346 online)
Email Password   helpLost your password?
Desktop Development » List Controls » ListView controls     Intermediate

TreeListView

By Thomas Caudal

A custom control that ties a ListView and a TreeView together
C#, Windows, .NET 1.0, .NETVisual Studio, VS.NET2002, Dev

Posted: 20 Nov 2002
Updated: 31 Aug 2003
Views: 351,102
Bookmarked: 297 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
125 votes for this Article.
Popularity: 9.41 Rating: 4.49 out of 5
8 votes, 6.8%
1
3 votes, 2.5%
2
5 votes, 4.2%
3
16 votes, 13.6%
4
86 votes, 72.9%
5

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.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

About the Author

Thomas Caudal



Occupation: Web Developer
Location: France France

Other popular List Controls articles:

Article Top
Sign Up to vote for this article
You must Sign In to use this message board.
FAQ FAQ Noise ToleranceSearch Search Messages 
 Layout  Per page   
 Msgs 1 to 25 of 414 (Total in Forum: 414) (Refresh)FirstPrevNext
GeneralRemove flicker on TreeListView while updating a subitemmemberdoshe2:38 25 Nov '08  
GeneralI want to use it under VC6. what should I do?membertongzone6:04 16 Nov '08  
GeneralHow i can move items up or downmemberobelix20083:52 13 Nov '08  
GeneralSome fixes for scrolling problemsmemberrichardwhitehead3:25 31 Oct '08  
GeneralHow o change row's height?memberfenixproductions15:22 16 Oct '08  
QuestionWith .NET 2.0 is it still needed to use the APIs2002 subprojectmembertho_wa3:07 7 Oct '08  
Questiondisable single check boxmembergeli23:48 27 Aug '08  
QuestionFocusedItem focuses the false item [modified]membergeli22:28 26 Aug '08  
Generallicense detailmemberMember 104286214:48 21 Aug '08  
GeneralRe: license detailmemberSunil Jampa20:26 28 Sep '08  
GeneralRe: license detailmemberMember 56468679:50 9 Nov '08  
GeneralRe: license detailmemberkuba532803:53 28 Nov '08  
GeneralTreeListView searchmembergeli1:04 13 Aug '08  
GeneralRepaint issue on vertical scrollmemberMark Downs6:24 8 Aug '08  
QuestionVertical lines and +/- [modified]memberNestor Sulikowski8:43 14 Jul '08  
GeneralPerformance improvement to Add functionmemberIanlo7:09 17 Jun '08  
GeneralNew VB.NET VersionmemberTosberg20:03 10 Jun '08  
GeneralDouble Buffer and Plus Minus ProblemmemberElliot Knight23:39 19 May '08  
GeneralRemove timeout of Combobox appearancememberionutzu2:01 1 May '08  
GeneralMultiline editmemberLuis Carreira8:32 29 Feb '08  
Generaldisplay manually the custom control for editmemberdeyd3:30 25 Oct '07