Click here to Skip to main content
Click here to Skip to main content

TreeListView

, 31 Aug 2003
Rate this:
Please Sign up or sign in to vote.
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, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Thomas Caudal
Web Developer
France France
No Biography provided

Comments and Discussions

 
QuestionGrouping and Full Row Select PinmemberBrendt.w3-May-14 9:18 
QuestionNot Working in Virtual Mode Pinmembergoud12323-Apr-14 20:17 
QuestionItemsCount PinmemberMember 1076063119-Apr-14 6:35 
QuestionVirtual Mode Issue Pinmembergoud12315-Apr-14 3:51 
GeneralMy vote of 5 PinmemberMember 1052040712-Jan-14 22:08 
SuggestionFix for control drawing more items than needed. [modified] PinmemberMember 866008828-Nov-13 12:11 
SuggestionRe: Fix for control drawing more items than needed. PinmemberRoland Bär11-Mar-14 1:05 
QuestionIndent Problem when used it in different DPI (120 and above) [modified] Pinmemberfhsong12-Nov-13 22:34 
AnswerRe: Indent Problem when used it in different DPI (120 and above) PinmemberLiu Junfeng14-Feb-14 0:39 
QuestionChild Nodes Disappear! PinmemberMember 101923614-Oct-13 7:19 
Questionhow can i use this in my project? Pinmembermariuskraemer1-Jun-12 2:46 
how can i use this in my project?
QuestionPlease give me a hand. Thanks. PinmemberChi_Chan4-Apr-12 22:16 
QuestionTextBox instead of ComboBox PinmemberJANARDHAN GURRAM20-Mar-12 7:56 
BugBug when use AfterLabelEdit Pinmemberconecine12-Jan-12 14:47 
Bugimages referred by imagekey won't be displayed if the item is added before the control is shown PinmemberAlex968725-Nov-11 4:50 
Question64-bit compatibility? PinmemberRobin Reyes18-Aug-11 9:18 
AnswerRe: 64-bit compatibility? PinmemberRobin Reyes18-Aug-11 9:31 
GeneralRe: 64-bit compatibility? Pinmemberyinyunpanwutong27-Aug-14 3:04 
QuestionHow to change the color of the column Header PinmemberNorth 20094-Aug-11 1:42 
QuestionHow to show vertical scrollbars at right side. PinmemberNorth 20092-Aug-11 0:09 
GeneralThe plus/minus signs not shown with the program if I rebuild it [modified] PinmemberMember 27711384-Apr-11 15:04 
GeneralRe: The plus/minus signs not shown with the program if I rebuild it PinmemberNorth 200931-Jul-11 19:32 
GeneralRe: The plus/minus signs not shown with the program if I rebuild it PinmemberNorth 20093-Aug-11 22:31 
GeneralRe: The plus/minus signs not shown with the program if I rebuild it PinmemberKentarion9-Dec-13 0:02 
AnswerRe: The plus/minus signs not shown with the program if I rebuild it [modified]--Resolved Pinmemberpurab15-Dec-11 21:11 
GeneralRe: The plus/minus signs not shown with the program if I rebuild it [modified]--Resolved PinmemberLiu Junfeng13-Feb-14 19:45 
QuestionHow to select a parent item during collapsing PinmemberSkynet8723-Mar-11 0:41 
GeneralBUG while changing system Time Pinmembermansshah8-Mar-11 6:29 
GeneralProblem with EndEdit PinmemberArchan_gel@mail.ru29-Sep-10 23:45 
QuestionListviewItem do not have indent as treeview and show as a regualr listview PinmemberJacky Chi Ho Lau22-Jul-10 3:35 
Questiontlv1.items.add does nothing [modified] PinmemberFecotrentini27-Apr-10 2:47 
QuestionItem flickering PinmemberSauls30-Mar-10 12:01 
AnswerRe: Item flickering PinmemberPaw Jershauge30-Mar-10 12:48 
GeneralRe: Item flickering PinmemberSauls31-Mar-10 3:37 
GeneralRe: Item flickering PinmemberPaw Jershauge31-Mar-10 11:26 
GeneralRe: Item flickering PinmemberSauls1-Apr-10 3:18 
GeneralRe: Item flickering PinmemberPaw Jershauge1-Apr-10 3:30 
GeneralRe: Item flickering PinmemberSauls1-Apr-10 5:39 
GeneralRe: Item flickering PinmemberSauls7-Apr-10 4:07 
GeneralRe: Item flickering PinmemberPaw Jershauge8-Apr-10 1:27 
GeneralRe: Item flickering PinmemberSauls8-Apr-10 13:21 
GeneralRe: Item flickering PinmemberPaw Jershauge11-Apr-10 23:44 
GeneralRe: Item flickering PinmemberSauls12-Apr-10 6:58 
AnswerRe: Item flickering PinmemberTarek Najem11-Apr-10 2:08 
GeneralThanks! PinmemberAlex_hbg3-Mar-10 10:36 
QuestionIcons in imageList1 cannot be displayed Pinmembervictorguo161325-Feb-10 22:15 
GeneralCancel Edit label and Drag'N'Drop Pinmembermushik3-Feb-10 5:42 
Generalhide column Pinmemberlonguard12-Jan-10 4:39 
GeneralRe: hide column PinmemberTarek Najem23-Feb-10 2:01 
AnswerSearch Feature In TreeListView PinmemberTarek-Najem9-Nov-09 21:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140902.1 | Last Updated 1 Sep 2003
Article Copyright 2002 by Thomas Caudal
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid