一、引言
树形控件是一种将具有一定层次结构的数据进行分级显示的控件,其所含项目以相互关联的方式显示在控件中,通过点击位于某个层次的项目结点展开或合拢下一层次中从属于该结点的所有子项目。树形控件非常适合于管理那些层次较多且相互间隶属关系较为清晰的项目元素,如资源管理器中的磁盘目录、单位的组织机构、产品的类别等,整个结构就像目录树一样,方便用户在其中进行各种操作。
在开发某管理信息系统时,多处需要使用到CTreeCtrl来显示不同类型、不同层次的数据信息,而且不同类型的数据,都各有不同的层次标准。考虑到代码的可重用性和可移植性,笔者设计了一个通用程序,适合于不同层次标准的数据显示,在项目中得到了应用,满足了实际开发中的需求。
二、实现方法
在树形控件中每一个结点都有一个句柄(HTREEITEM),同时添加结点时必须提供的参数就是该结点的父结点句柄,利用HTREEITEM InsertItem( LPCTSTR lpszItem, HTREEITEM
hParent = TVI_ROOT, HTREEITEM hInsertAfter = TVI_LAST )方法可以添加一个结点,其中lpszItem为显示的数据,hParent表示父结点的句柄(默认为TVI_ROOT),hInsertAfter表示当前添加的结点会按照什么样的方式排列在兄弟结点中(默认为TVI_LAST),方法的返回值为当前创建结点的句柄。
另一方面,从数据的结构,不难看出其数据层次有一定的规律性,即数据之间有着某种关联。因此,先将数据按照编码或关键字进行排序,排序后数据的结构便十分清晰了。排序后的数据,存在以下三种关系:第一种是数据之间存在上下级的关系;第二种是同一级的关系;第三种是完全不相关的关系,即两数据间没必然的联系。
对于第一种情况,我们可以使用树形控件的InsertItem()方法,另外,再给出要插入结点的父结点句柄,就可将数据插入到相应的位置。
对于第二种情况,和第一种情况类似,同样使用树形控件的InsertItem()方法,不过这里的父结点句柄变成了当前结点的父结点句柄而已,即和当前句柄共享同一个父结点句柄。最后一种情况,比前面的情况稍微有些复杂,因为它和当前结点没有必然的联系,而且还要试图找到自己父结点的句柄,才能使用树形控件的InsertItem()方法,将其插入到相应位置。如何才能找到自己父结点的句柄呢?这就涉及到句柄的回溯了,从当前结点的句柄向上层层回溯,直到找到对应的父结点为止。
按照这种思路,我们就可以编写自动创建树形结构的程序了。
三、主程序
通过以上对问题的分析及对树形控件相关方法的了解,我们已经了解程序的实现原理,下面便是相应的实现过程。
首先,给出层次的标准,即数据是按照什么标准划分的。
#define standard 2 //笔者这里使用每两位就是一个等级的标准
接着,再写一个建立结点的方法,并返回当前结点的句柄,实现代码如下:
HTREEITEM CtreeView::createNode(CString id, CString name, HTREEITEM pNode)
{
HTREEITEM tempItem;
tempItem = m_TREE.InsertItem(TVIF_TEXT|TVIF_PARAM, name, 0, 0, 0, 0, (DWORD)(atoi(id)), pNode, 0);
return tempItem;
}
最后,是主程序的代码:
//声明句柄变量,表示当前结点的句柄
HTREEITEM pItem;
pItem = TVI_ROOT;
//声明编码长度变量,分别表示上一结点和当前结点的长度
int oldLength , curLength;
oldLength = standard ;
//创建树形列表
void CzbjsView::initTree(CString id , CString name)
{
curLength = id.GetLength();
if (curLength > oldLength)
{//下一层次的数据
pItem = createNode(id, name, pItem);
}
else if (curLength == oldLength)
{
if (oldLength == standard )
{
//第一层次的数据
pItem = createNode(id, name, TVI_ROOT);
}
else
{
//同一层次的数据
pItem = m_TREE.GetNextItem(pItem, TVGN_PARENT);
pItem = createNode(id, name, pItem);
}
}
else
{
//第一层次的数据
if (curLength == standard )
{
pItem = createNode(id, name, TVI_ROOT);
}
else
{
//句柄的回溯,直到对应的父结点为止
for (int i = 0; i <= ( oldLength - curLength ) / standard ; i++)
{
pItem = m_TREE.GetNextItem(pItem, TVGN_PARENT);
}
pItem = createNode(id, name, pItem);
}
}
oldLength = curLength;
}
其运行结果如下图1所示:
图1
四、性能优化
根据以上我们对问题的分析与实现,问题似乎已经解决了,然而在实际应用时,另外一个问题却又出现了。整个过程,我们又忽略了什么?再来仔细回顾我们的实现原理,问题出现了。原来,我们只是解决了数据的自动创建,却忽略了数据的显示,即用户最终看到的界面。当显示的数据量非常少时,这种问题并不明显,甚至可以忽略,然而,当数据量达到成千上万时,这种问题就变得非常明显了。
原来,在实现过程中,我们是将所有的数据一次性地创建,全部数据都被读出并被加载到树形控件中,当数据量非常大时,就出现了数据显示的延迟,而且数据量越大,这种现象也就越明显,用户的交互性就被忽略了。
解决数据延迟的方法多种多样,这里介绍一种行之有效的方法。
首先,在初始化树形控件时,自动创建最上面两个层次的数据,这时的数据量相对较少,当然延迟也就会小很多;接着,便是根据用户的需求动态地读出并创建数据,这里的动态创建是隔层创建的,例如,当用户展开第一层的数据时,程序会自动地创建第三层的数据,而且这种创建只需一次,依此类推,直到数据全部显示,这样更易于用户的操作,且减少了数据的延迟;这里的解决方法主要是将大量的数据分层次、动态地创建,将总延迟分割为若干个用户可以接受的子延迟。
解决数据延迟使用的是树形控件的TVN_ITEMEXPANDED 消息。
其实现代码如下:
void CtreeTree::OnTvnItemexpandedTree1(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
//当树形控件被展开时
if(pNMTreeView->action == TVE_EXPAND)
{//得到当前选择结点的句柄
CPoint point,p;
TVHITTESTINFO HitTestInfo;
GetCursorPos(&point);
m_TREE.ScreenToClient(&point);
HitTestInfo.pt = point;
pItem = m_TREE.HitTest(&HitTestInfo);
//当前结点有下一级结点时
if (m_TREE.ItemHasChildren(pItem))
{
pItem = m_TREE.GetChildItem(pItem);
HTREEITEM tempItem;
//隔层的结点已被创建时
if (m_TREE.GetChildItem(pItem))
{
*pResult = 0;
return;
}
CString key;
//隔层创建子结点
while (pItem != NULL)
{
tempItem = pItem;
key.Format("%d",m_TREE.GetItemData(pItem));
key = key + "__";
CString sql = "select id, name from shebei where id like '"+key+"' order by id ";
//调用initTree()方法
//当前结点同一层次的结点
pItem = m_TREE.GetNextSiblingItem(tempItem );
}
}
}
*pResult = 0;
}
其运行结果如下图所示:
图2
五、结语
本文介绍了在VC++ 6.0下如何实现自动创建树形结构,提高了代码的可重用性和可移植性,并对数据显示的延迟给出了行之有效的方法,对其性能地进行了优化,增强了用户的交互性,其设计思想也具有普遍的通用性。
(收稿日期:2008年7月19日)
|