![]() |
Desktop Development »
List Controls »
ListView controls
Intermediate
License: The Microsoft Public License (Ms-PL)
TreeListViewBy Thomas CaudalA custom control that ties a ListView and a TreeView together |
C#, Windows, .NET 1.0, Dev
|
|
Advanced Search |
|
|
|
||||||||||||||||

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:
TreeListView class that includes BeforeExpand, AfterExpand, BeforeCollapse, AfterCollapse events, TreeListViewItem class that includes Expand function, Collapse functions, IsExpanded property, 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, This class inherits from the ListView class.
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();}
}
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;
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);
}
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.
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.
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);
}
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.
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.
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);
}
}
}
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.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 31 Aug 2003 Editor: Sean Ewington |
Copyright 2002 by Thomas Caudal Everything else Copyright © CodeProject, 1999-2009 Web13 | Advertise on the Code Project |