Click here to Skip to main content
15,860,972 members
Articles / Programming Languages / C#

TreeListView

Rate me:
Please Sign up or sign in to vote.
4.79/5 (147 votes)
31 Aug 2003Ms-PL5 min read 1.5M   38.3K   436   524
A custom control that ties a ListView and a TreeView together

Image 1

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

C#
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) :

C#
[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.

C#
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) :

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

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

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

C#
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 :

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

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

C#
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)


Written By
Web Developer
France France
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionAPIsEnums.WindowMessages.PAINT message is not executed Pin
vijay anjur18-Feb-20 8:50
vijay anjur18-Feb-20 8:50 
AnswerRe: APIsEnums.WindowMessages.PAINT message is not executed Pin
kreiseder29-Oct-20 22:04
kreiseder29-Oct-20 22:04 
QuestionHow do I disable a single checkbox ? Pin
Member 107903486-Sep-19 2:55
Member 107903486-Sep-19 2:55 
QuestionControl is not loading in Windows 2010 properly Pin
Sudheer gadde18-Jun-19 23:34
Sudheer gadde18-Jun-19 23:34 
QuestionAdd column as checkbox in TryTreeListView and download the latest version Pin
lecntp4-May-19 14:30
lecntp4-May-19 14:30 
QuestionFixing the problem with "plus" / "minus" display Pin
Member 1402413118-Oct-18 22:55
Member 1402413118-Oct-18 22:55 
AnswerRe: Fixing the problem with "plus" / "minus" display Pin
ybao20007-Mar-19 12:03
ybao20007-Mar-19 12:03 
AnswerRe: Fixing the problem with "plus" / "minus" display Pin
vwu7-Jun-19 11:43
vwu7-Jun-19 11:43 
GeneralRe: Fixing the problem with "plus" / "minus" display Pin
simonccheng13-Feb-23 15:12
simonccheng13-Feb-23 15:12 
BugKeyDown Event not fired when Control, Alt Or Shift Key pressed Pin
Célio20-Apr-18 3:25
Célio20-Apr-18 3:25 
QuestionThe type of "idFrom" field in the NMHDR structure should be IntPtr Pin
Member 1296531931-Mar-17 23:55
Member 1296531931-Mar-17 23:55 
QuestionReg: Work Document Pin
Sudheer gadde21-Dec-16 20:00
Sudheer gadde21-Dec-16 20:00 
QuestionWorking fine.Thanks a lot,this is what i want. Pin
Member 1245034712-Apr-16 15:40
Member 1245034712-Apr-16 15:40 
GeneralMy vote of 5 Pin
vishal25927-Sep-15 7:37
vishal25927-Sep-15 7:37 
Questionhow to remove checkboxes or to make them invisible Pin
Member 1176816616-Jun-15 17:56
Member 1176816616-Jun-15 17:56 
AnswerRe: how to remove checkboxes or to make them invisible Pin
Andrew Ellis27-Aug-15 1:05
Andrew Ellis27-Aug-15 1:05 
QuestionExpand Takes a long time Pin
Member 113992374-Feb-15 7:28
Member 113992374-Feb-15 7:28 
QuestionGrouping and Full Row Select Pin
Brendt.w3-May-14 9:18
Brendt.w3-May-14 9:18 
QuestionNot Working in Virtual Mode Pin
goud12323-Apr-14 20:17
goud12323-Apr-14 20:17 
QuestionItemsCount Pin
Alde Oma19-Apr-14 6:35
Alde Oma19-Apr-14 6:35 
QuestionVirtual Mode Issue Pin
goud12315-Apr-14 3:51
goud12315-Apr-14 3:51 
GeneralMy vote of 5 Pin
Member 1052040712-Jan-14 22:08
Member 1052040712-Jan-14 22:08 
SuggestionFix for control drawing more items than needed. Pin
BanksyHF28-Nov-13 12:11
BanksyHF28-Nov-13 12:11 
SuggestionRe: Fix for control drawing more items than needed. Pin
Roland Bär11-Mar-14 1:05
Roland Bär11-Mar-14 1:05 
QuestionIndent Problem when used it in different DPI (120 and above) Pin
fhsong12-Nov-13 22:34
fhsong12-Nov-13 22:34 

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

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