在使用C++ Builder的时候,VCL控件为我们提供了非常方便的重用,但往往第三方提供的控件未必能全部满足自己的需求。所以有时候自己想为第三方控件扩展一些行为以便自己重用。有什么办法可以做到呢?
为VCL控件的行为做扩展当然最好的方法是改VCL的代码,但是如果对PASCAL语言不熟悉,或者想尝试利用C++ 的方法来做的话(C++ Builder命名为C++,当然能支持C++的语法)。
所以下面我们就尝试使用C++的方法来对第三方的控件做扩展。
这里选用的第三方的控件是SUI的控件套装。这里先来介绍一下这个套装:这个控件套装主要的功能是为我们的应用程序提供了一套对基本控件的扩展(包括功能上和皮肤上的),而且将所有的控件都集中给一个皮肤控件控制,如果要改变应用程序的皮肤,则变更这一个控件的SKIN文件,整个的界面就变化了,你可以使用SUI公司的专业皮肤方案,也可以自己去制作SKIN。非常方便。能帮助我们完成不少界面上的工作。
我们使用SUI的TREE控件来做一个实例,使用C++ 的一些技术来扩展它,并达到重用的目的。
我们首先确定目标,就是我们要扩展的功能,有三个:
1、 让树可以存储每个节点的值。
2、 让树可以装载每个节点的值。
3、 递归删除节点。
为什么会有上面的三个目标呢?试想一下我们平时使用TREE控件的时候,每个节点都有一些自己需要存储的值,这些值是要我们自己定义的,并可能在树控件销毁的时候永久保存(数据库、文件。。。),并在树控件初始化的时候把这些值重新装载进节点。再有在我们删除节点的时候,可能需要对这些值做处理,以免产生内存泄漏等副作用。每次我们用树控件的时候都碰到这个问题,为什么我们不一劳永逸,想个可重用的方法把它封装成自己可用的控件呢?
一个经典的实例是:我们在应用中需要分开节点的表示,例如要分开文件夹和文件两种类型。
这经常就是使用enum类型来表示,并分配到每个节点里去,以便在使用中对其进行判断。
我们先考虑上面要实现的三个功能要使用C++的什么技术来实现:
对于1、2两个功能,因为事先不知道DATA的类型,所以可以用模板技术来解决。对于第三个功能,因为考虑到节点被删除前,客户可能还要做一些事情,这里可以使用回调函数来解决。
这里给出下面的例子:
第一个函数SaveTreeViewToFileTemplate实现树把每个节点的值存储到文件中:
// 因为事先不知道要储存的DATA的大小,所以这里用模板技术来解决
// 储存树的DATA值到文件中
template<class Type>
void SaveTreeViewToFileTemplate(TsuiTreeView *tvw, AnsiString &strFile,
Type tNoUsed)
{
tvw->SaveToFile(strFile);
int nFileHandle = 0;
AnsiString strFileName;
strFileName.sprintf("%sData", strFile);
if( FileExists(strFileName) )
{
DeleteFile(strFileName);
}
nFileHandle = FileCreate(strFileName);
if( nFileHandle == -1 )
{
AnsiString strMsg;
strMsg.sprintf("%s文件创建失败.", strFileName);
ShowMessage(strMsg);
return;
}
else
{
// 存储节点的DATA值
TTreeNode *pNode;
pNode = tvw->TopItem;
while(pNode)
{
FileWrite(nFileHandle, (void *)(pNode->Data), sizeof(Type) );
pNode = pNode->GetNext();
}
}
FileClose(nFileHandle);
}
第二个函数LoadTreeViewFromFileTemplate实现树在装载的时候把节点从文件中装载回来,注意这里还提供了一个回调函数在树结点装载回来的时候做一些用户要做的事情:
/*****************************************************************************/
* 把树的DATA值从文件中LOAD出来
* 这里还要提供回调函数给客户在LOADITEM的时候做些事情
/*****************************************************************************/
// 回调函数的原形:
typedef void (CALLBACK *LoadNodeDef)(TTreeNode *tn);
template<class Type>
void LoadTreeViewFromFileTemplate(TsuiTreeView *tvw, AnsiString &strFile,
Type tNoUsed, LoadNodeDef LoadNodeFunc)
{
tvw->LoadFromFile(strFile);
int nFileHandle = 0;
AnsiString strFileName;
strFileName.sprintf("%sData", strFile);
if( !FileExists(strFileName) )
{
return;
}
else
{
nFileHandle = FileOpen(strFileName, fmOpenRead);
FileSeek(nFileHandle, 0, 0);
Type *data;
TTreeNode *pNode;
pNode = tvw->TopItem;
while(pNode)
{
data = new Type();
FileRead( nFileHandle, data, sizeof(Type) );
pNode->Data = data;
if(LoadNodeFunc != NULL)
LoadNodeFunc(pNode);
pNode = pNode->GetNext();
}
}
FileClose(nFileHandle);
}
第三个函数实现递归删除树结点:
/*****************************************************************************/
* 递归删除节点
/*****************************************************************************/
void __fastcall TSuiTreeViewEx::DeleteFolderRecursive(TsuiTreeView *tvw,
TTreeNode *tn, DeleteNodeDef DeleteNodeFunc)
{
BLNode(tn, DeleteNodeFunc);
tvw->Items->Delete(tn);
}
BLNode的函数原型是这样的:
/*****************************************************************************/
* 遍历树节点
/*****************************************************************************/
void __fastcall TSuiTreeViewEx::BLNode(TTreeNode *tn, DeleteNodeDef DeleteNodeFunc)
{
if( tn->Data != NULL )
{
//ShowMessage(tn->Text);
DeleteNodeFunc(tn);
vSafeDelete( tn->Data );
}
TTreeNode *nodeTmp;
nodeTmp = tn->getFirstChild();
if( nodeTmp == NULL)
return;
//ShowMessage(nodeTmp->Text);
DeleteNodeFunc(nodeTmp);
vSafeDelete( nodeTmp->Data);
while( nodeTmp != tn->GetLastChild() )
{
nodeTmp = tn->GetNextChild(nodeTmp);
//ShowMessage(nodeTmp->Text);
DeleteNodeFunc(nodeTmp);
vSafeDelete( nodeTmp->Data);
BLNode(nodeTmp, DeleteNodeFunc);
}
}
最后还可以提供一个函数作清除的工作:
/*****************************************************************************/
* 把树所有的节点的DATA值都清除一遍,避免资源泄漏
/*****************************************************************************/
void __fastcall TSuiTreeViewEx::DeleteAllNodeData(TsuiTreeView *tvw)
{
TTreeNode *tn = tvw->TopItem;
while(tn)
{
vSafeDelete(tn->Data);
tn = tn->GetNext();
}
}
上面的函数需要写在C++文件的头文件和CPP文件中。
那我们的使用就变得非常方便了,如下:
// 存储命令树
void __fastcall TSingleFuncUILogic::SaveTreeViewToFile(TsuiTreeView *tvw, AnsiString &strFile)
{
eNodeType nodeType = Cmd;
TSuiTreeViewEx::SaveTreeViewToFileTemplate( tvw, strFile, nodeType );
}
// 装载命令树
// 回调函数,在转载的时候做些指定IMAGEINDEX的事情
void CALLBACK TSingleFuncUILogic::LoadNodeCallBack(TTreeNode *tn)
{
if( GetNodeType(tn) == Folder)
{
if(tn->Expanded)
{
tn->ImageIndex = 1;
tn->SelectedIndex = 1;
}
else
{
tn->ImageIndex = 0;
tn->SelectedIndex = 0;
}
}
if( GetNodeType(tn) == Cmd )
{
tn->ImageIndex = 2;
tn->SelectedIndex = 2;
}
}
void __fastcall TSingleFuncUILogic::LoadTreeViewFromFile(TsuiTreeView *tvw, AnsiString &strFile)
{
eNodeType nodeType = Cmd;
TSuiTreeViewEx::LoadTreeViewFromFileTemplate( tvw, strFile, nodeType, LoadNodeCallBack );
}
写到这里,相信有心人肯定明白了我这个方法是如何实现对控件的功能扩展的。
当然对控件进行封装重用的方法很多(例如可对每个节点类型封装成一个类,在生成结点的时候生成对象并赋值到结点),这里我只是给出自己的一个方法,这个方法也有自己的一些缺点,例如每次使用都必须把这个写好的C++文件包含到工程里去。这是物理上的缺陷(就是使用起来还不够方便)。这里只是一种思路,有更好方法的朋友欢迎探讨。