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);
}
}
public EditItemInformations EditedItem
{
get{return _editeditem;}
}
public bool InEdit
{
get{return _inedit;}
}
public bool ShowPlusMinus
{
get{return _showplusminus;}
set{if(_showplusminus == value) return;
_showplusminus = value;
if(Created) Invoke(new VoidHandler(VisChanged));}
}
public Color PlusMinusLineColor
{
get{return _plusMinusLineColor;}
set{_plusMinusLineColor = value;
if(Created) Invalidate();}
}
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"));
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();
}
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"));
if(Visible && _isexpanded && ListView != null)
{
TreeListViewCancelEventArgs e = new TreeListViewCancelEventArgs(
this, TreeListViewAction.Collapse);
ListView.RaiseBeforeCollapse(e);
if(e.Cancel) return;
}
if(this.Visible)
foreach(TreeListViewItem item in Items)
item.Hide();
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"));
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);
if(treelistviewindex > -1)
{
ListView listview = (ListView) TreeListView;
listview.Items.Insert(treelistviewindex, (ListViewItem) item);
if(item.IsExpanded) item.Expand();
item.SetIndentation();
}
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"));
TreeListViewItem[] thisarray = ToArray();
Clear();
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);
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;
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;
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;
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