作者:星轨(oRbIt)
E_Mail :inte2000@163.com
转载请注明原作者,否则请勿转载
在下拉列表框中显示树状结构 下拉列表框(或称组合列表框)是在Windows系统下开发软件最常用的控件之一,该控件适用性强且使用简单,因而深受开发人员喜爱。一个典型的用户界面就是使用下拉列表框(或称组合列表框)显示数据库中相关表名或某个表中的某个字段的全部数据,用户通过鼠标选择某一项后进行相关操作。在这种情况下使用下拉列表框对数据选择可以简化操作,用户也比较喜欢这种方式,但是简单的下拉列表框无法表达数据之间的相互关系,比如两个数据项之间的父子关系或包容关系等。开发人员通常希望能在简化用户操作的同时向用户提供这些信息以方便用户决定下一步操作。针对这种情况我们使用MFC开发了一个支持显示树状结构的CComboBox派生类CWzComboBox,并且该派生类已经成功应用到我们的软件中,现在我们将代码公开,希望对广大开发人员有所帮助,也希望大家共同完善它。(演示程序运行示例如图1所示)
图1 演示程序运行实例
首先我们对CwzComboBox类的自定义函数和相关的数据结构做简要的说明。要使列表框能够显示树状结构,首先应该设计一个数据结构用于记录列表中各个项之间的关系。我们定义了一个结构用于存储节点之间的关系,不妨将之称为信息数据结构,如下所示:
typedef struct tagITEMDATA
{
WORD wParantOriginIdx;
WORD wOriginIdx;
BYTE cType;
BYTE cLevel;
}ITEMDATA,*LPITEMDATA;
结构成员wParantOriginIdx和wOriginIdx分别用以存储该项的父项和该项本身在列表中的原始索引位置,因为添加或删除列表项时会导致列表项之间的顺序改变,也就是索引位置会改变,所以只能通过各项的原始索引位置确定它们之间的关系。成员cType用于存储节点对应的图标在图标列表(ImageList)中的索引位置,成员cLevel用于存储该项的级别,处于根结点的项cLevel是0,子项的cLevel都比父项的大,该成员主要用在显示列表项时确定项的水平起始位置,从而使各项看起来呈树状结构。
下面就详细说明一下类CWzComboBox的设计。基类CComboBox的原来的公有方法 AddString()和InsertString()不支持附加信息不能再使用,DeleteString()无法正确的释放附加信息所占用的内存所以也不能使用,为了防止开发人员误用他们引起代码崩溃,我们重载了这三个函数:
virtual int AddString(LPCTSTR lpszString) { return -1; }
virtual int InsertString(int nIndex, LPCTSTR lpszString) { return -1; }
virtual int DeleteString(int nIndex) { return -1; }
仅仅返回-1表示函数执行失败。手工加入AddCTString()和DeleteCTString()两个函数用于列表项的添加和删除。AddCTString()向第一个参数wParentIdx指定的项添加一个子项,函数运行时首先判断wParentIdx是否是nRootIndex(定义常量nRootIndex等于(WORD)-1),如果是nRootIndex就将该项设为根项,即令cLevel为0。
WORD CWzComboBox::AddCTString(WORD wParentIdx,BYTE cType,LPCTSTR lpszString)
{
int idx = -1;
if(wParentIdx == nRootIndex)//添加根项
{
idx = CComboBox::AddString(lpszString);//调用基类的函数添加该项到列表中
LPITEMDATA pData = new ITEMDATA;//建立相应的项信息结构
ASSERT(pData != NULL); pData->cLevel = 0;//根为0级
pData->cType = cType;
pData->wOriginIdx = (WORD)idx;//该项当前的索引位置
pData->wParantOriginIdx = wParentIdx;//父项的索引位置,就是nRootIndex
SetItemData(idx,(DWORD)pData);
}
else//不是添加根项
{
int ParentCurrentIdx = CurrentIdxFromOriginIdx((int)wParentIdx);//根据父项的原始索引 位置找到父项当前的索引位置
LPITEMDATA pParentData = (LPITEMDATA)GetItemData(ParentCurrentIdx);//读出项信息结构
int count = GetChildCount(pParentData->wOriginIdx);//得到父项的子项个数
int pos = wParentIdx + count + 1;//子项增加一个
idx = CComboBox::InsertString(pos,lpszString);
LPITEMDATA pData = new ITEMDATA;//建立子项的信息结构
ASSERT(pData != NULL);
pData->cLevel = pParentData->cLevel + 1;//比父项的级数大1
pData->cType = cType;
pData->wOriginIdx = (WORD)idx;
pData->wParantOriginIdx = pParentData->wOriginIdx;
SetItemData(idx,(DWORD)pData);
}
return (WORD)idx;//返回索引位置
}
删除某项时首先遍历整个列表,删除所有子项,释放为子项信息结构分配的内存,最后删除该项并释放该项信息结构占用的内存。根据树状结构的特性,某项的所有子项的索引位置都在该项和该项的下一个兄弟项之间,所以删除函数的任务就是删除该项后所有级数比该项高的项,直到遇到第一个级数与该项相同或更小的项。外层的while循环用来删除子项,直到标志bFind是FALSE才退出,内层的for循环从当前项的索引位置开始到列表的结尾搜索当前项的子项,只要找到就将bFind赋值为TRUE并删除该项,然后跳出for循环,继续外层的while循环。
int CWzComboBox::DeleteCTString(int index)//参数为当前项索引位置
{
LPITEMDATA pdata = (LPITEMDATA)GetItemData(index);
BOOL bFind = TRUE; //删除该项的所有子项,包括子项的子项
while(bFind)
{
bFind = FALSE;//只要找到子项就设为TRUE,结束或遇到一个级数相同的项就保持FALSE,结束标 志
int count = GetCount();//得到总的项数
for(int i = index + 1; i < count; i++)//子项的索引位置总在该项之后
{
LPITEMDATA p = (LPITEMDATA)GetItemData(i);
if(p->cLevel > pdata->cLevel)//子项的Level比父项的Level大
{
bFind = TRUE;//找到一个子项,可能还有
CComboBox::DeleteString(i);
break;//删除一个就跳出循环
}
}
}
//现在删除当前项
CComboBox::DeleteString(index);
return GetCount();
}
在删除函数中用到了GetChildCount()和CurrentIdxFromOriginIdx()两个函数,这两个函数被声明为保护成员,不允许直接访问。GetChildCount()返回指定索引位置的项的所有子项,包括子项的子项,实现的方法很简单,就是利用前面提到的树状结构的特性,统计该项后所有级数比该项高的项的个数,直到遇到第一个级数与该项相同或更小的项。CurrentIdxFromOriginIdx()函数根据某项的原始索引位置查找并返回其当前位置索引位置,实现的方法也很简单,就是遍历整个列表,比较每一项的原始索引位置,找到相同的就返回其在列表中的当前索引位置。 以上是对CwzComboBox类的自定义函数和相关的数据结构的说明,现在要解释一下与MFC有关的函数。首先就自绘控件做简要的说明,事实上所有的控件都是“自绘”的,Windows系统的窗口在需要重画窗口内的控件时会向该控件发送WM_DRAWITEM消息,该消息有两个参数,第一个参数是控件的ID,第二个参数是一个指向DRAWITEMSTRUCT结构的指针,DRAWITEMSTRUCT结构含有控件绘制自己所需的显示设备上下文、位置、控件的状态和其他属性,控件就利用这些参数绘制自己。MFC对控件进行了封装,将WM_DRAWITEM消息其映射为虚函数DrawItem,从而简化了编程的复杂性。所以,要实现列表框显示自定义的树状结构只需重载DrawItem函数就可以了。当系统需要绘制列表框时就会多次调用DrawItem来画列表框的每一项。CwzComboBox类重载的DrawItem仅仅就是根据各项的信息结构在相应的位置画出该项,所以不需做太多说明。另一个需要重载的函数是DeleteItem,这也是基类的虚函数,重载此函数的目的是为了使系统删除列表项的同时能释放为该项信息结构分配的存储空间。我们自定义删除函数并没有直接释放信息结构占用的存储空间而是调用了基类的DeleteString函数,由DeleteString函数触发DeleteItem,这样做就简化了开发人员对列表项的维护工作。当下拉列表框被打开时会触发CBN_DROPDOWN事件,CwzComboBox类响应这个事件,计算使列表框中所有项能够完全显示所需的最小宽度值,并根据这个最小宽度值重设列表框的宽度,从而使其看起来有视觉效果好一些。
CwzComboBox类的的使用非常简单,看一下例子代码就知道了。此代码最早是发布在www.codeproject.com网站,所以项目的资源都英文的,注释也是英文的。www.vchelp.net网站也有转载,另外还可以使用以下URL:http://www.winmsg.com/download/cwzcombox.zip下载代码和演示程序。