----在实际的MIS系统中,远程数据库访问大多通过Modem连接,出于通信费用及速度方面的考虑,往往采用先将数据保存在本地,然后集中传送到远端的办法。远程数据传送可以有多种方案,最常见的是先将要传送的数据打包成文件,在利用文件传输形式传送到目的地,在目的地对数据恢复后添加到本地数据库中。这种方法普遍地应用于证券交易系统,其优点是速度快,并且可事先对数据压缩,更大限度地节约传送时间及费用。但这种方案也有其不足之处:由于利用文件传输机制,无法利用数据库本身的特性如完整性约束、数据一致性、回滚机制等,因此在比较复杂的数据库系统中较少采用。另一种方法是直接将两端处理成"客户/服务器"模式,将数据传送看成是向Server提交数据。由于这种方案充分利用了数据库服务器的特性,并且实际操作基本与局域网方式一致,因此本文将详细介绍这种方案。另外本文的部分内容是基于Delphi/CBuilder的。
----由于传输速度的原因,当传送大量数据时绝对不赞成逐条记录地向服务器提交数据,而应批量地向Server提交,Delphi/CBuilder中提供了一个TBatchMove控件专门用于批量传送数据,利用它可极大减少网络负担,提高传送速度。遗憾的是,TBatchMove控件只提供了简单的错误控制功能,没有提供显示传送进度、用户终止传送等重要功能。然而TBatchMove所依赖的BDE却提供了一种"回调机制"可以完成上述两个功能。所谓"回调"过程是这样的:当BDE执行某种操作时,比如从一张表向另一张表拷贝大量数据的过程中,每过一段时间(如需要显示拷贝进度时),BDE会调用一段你自己写的函数(回调函数),以帮助你更完全地控制程序。这种做法有点想DLPHI中的Event(事件)及事件处理函数--某个具体的操作动作会让VCL触发某个事件,从而调用一段你写好的事件处理函数,不同的事件会触发不同的处理函数。
----为了让BDE能正确地与你的函数协同工作,你必须事先"注册"你的函数,让BDE知道某个事件发生时应调用(回调)你的某段代码。BDE提供了一个DbiRegisterCallBack注册函数,不幸的是,BDE的联机帮助中的说明不能适合于Delphi/CBuilder,按照该说明编写的程序根本不能通过编译!笔者通过实践找到了正确使用BDE回调函数的方法,下面将详细介绍该机制的使用。BDE回调机制包含以下几个步骤:
----1)按BDE的预定格式编写你的回调函数
----2)调用DbiRegisterCallBack函数注册你的回调函数,这样当你执行相关数据库操作时就自然地触发你的回调函数。
----3)执行相关数据库操作,比如BatchMove1->Exectue();
----4)注销该回调函数
----其中最关键的是正确注册你的回调函数,因此先介绍第二步。(注册与注销都调用同一函数,只是最后一个参数略有不同)
----首先你应知道在哪类"事件"发生时调用你的回调函数,其次你应明白与该事件相关的参数及数据结构--这一切都发生在调用DbiRegisterCallBack函数注册时,所以下面先介绍DbiRegisterCallBack的正确用法及说明:
----在原BDE帮助中该函数的原形(C)是这样的
DBIResult DBIFN DbiRegisterCallBack (hCursor, ecbType, iClientData, iCbBufLen, pCbBuf, pfCb);
----要使用该函数必须include头文件,问题是Delphi/CBuilder中根本没有提供该文件,取而代之的是"BDE.HPP",但是在包含进该文件后程序仍然不能编译通过,因为该文件中没有DBIFN等的说明。一个简单的方法是在代码中去掉DBIFN。函数中各参数解释如下:hCursor是一个BDE中对象的句柄,如果这个参数为NULL,则表示注册的回调函数适合于所有BDE任务;第二个参数ecbType是指回调函数的触发条件的类别,有很多种类型可以选择,其中cbGENPROGRESS表示当需要显示一个长操作的进度时触发这个回调函数;第三个参数iClientData是传递给回调函数的某个数据结构的指针,在我们的例子中为NULL;第四个参数iCbBufLen是指回调Buffer的大小,该大小随第二个参数的不同而不同,比如sizeof(CBPROGRESSDesc);第五个参数pCbBuf是回调Buffer的指针,该指针类型随第二个参数变化,比如cbGENPROGRESS的数据结构是CBPROGRESSDesc;最后一个参数是回调函数的地址指针,当该参数为NULL时表示注销该类型的回调函数。关于回调函数将在稍后详细介绍。下面是注册执行长操作时显示进度的回调函数的格式:
int rst= DbiRegisterCallBack (NULL,
//适合于任何进程
cbGENPROGRESS, //回调类型:显示长操作的进度
NULL, //没有数据
sizeof(CBPROGRESSDesc), //数据结构的大小
&aCBBuf, //数据的内存地址
ApiCallBackFun //回调函数的地址
);
----接下来就应该完成第一步:编写回调函数
----在C中,回调函数应如下声明:
CBRType__stdcallApiCallBackFun(
CBTyp eecbType,//回调类型
int iClientData,//回调数据(指针)
void *pCbInfo//回调数据结构指针
)
----第一个参数是回调类型;第二个参数是回调数据,其解释同DbiRegisterCallBack的第三个参数;第三个是回调数据的指针,该数据的结构随回调类型的不同而不同。比如进度指示cbGENPROGRESS的数据结构是CBPROGRESSDesc,其定义如下:
struct CBPROGRESSDesc {
short iPercentDone; //进度的百分比
char szMsg[128]; //进度的文本信息
};
----该结构的两个域同时只有一个起作用,第一个表示操作的进度百分比,当其为-1时表示第二个域起作用。第二个域用字符串表示进度信息,其格式为<String><:><Value>,比如:RecordsCopied:125
----本文主要在回调函数中完成两个工作:
----1)显示数据拷贝(BatchMove)进度
----2)提供让用户终止长时间拷贝的机制
----显示拷贝进度的代码如下:
CBRType __stdcall ApiCallBackFun(
CBType ecbType, // Callback type
int iClientData, // Client callback data
void * pCbInfo // Call back info/Client)
{ AnsiString str;
if(ecbType==cbGENPROGRESS)
{
int j= StrToInt( ((CBPROGRESSDesc*)
pCbInfo)- >iPercentDone);
if(j< 0)
//如果iPercentDone为-1,则分析szMsg的信息
{
str=((CBPROGRESSDesc*)pCbInfo)- >szMsg;
int pos=str.AnsiPos(":")+1;
//提取出拷贝的记录数
//下面的代码用来在一个Form中显示拷贝进度及拷贝数量
Form1- >Label2- >Caption= str.SubString(pos,100);
Form1- >Label2- >Update();
Form1- >ProgressBar1- >Position=
int((str.SubString(pos,100).
ToDouble()/Form1- >TransNum)*100);
Form1- >ProgressBar1- >Update();
}
else
{Form1- >ProgressBar1- >Position=j;
Form1- >ProgressBar1- >Update();
}
return cbrCONTINUE;
//必须返回cbrCONTINUE以便让BatchMove继续
//若返回cbrABORT则终止拷贝
}
----一切完成以后,每当调用长时间BDE操作(比如BatchMove1->Exectue())时都会触发该回调函数,注意在不需要时应"注销"这个回调函数。
----如果批量传送数据时间很长,则必须为用户提供终止该操作的机会,前面提到,若回调函数返回cbrABORT,则BatchMove过程立即终止。可以在Form上加上一个"停止"按钮和一个全局布尔变量isContinue,当开始拷贝时设该变量为true,当按钮按下后,设该变量为false,每次调用回调函数时检查isContinue的值,若为true则回调函数返回cbrCONTINUE让拷贝继续,否则返回cbrABORT终止拷贝。但是问题在于一旦拷贝过程开始,该进程内所有消息将被阻塞,应用程序在拷贝结束之前没有机会响应键盘、鼠标等一切消息,连屏幕刷新都不能完成,因此必须找到一种避免消息阻塞的方法。
----大家知道,Windows是靠事件(消息)驱动的,在WIN32系统中有两种消息队列:系统队列和应用程序队列,当一个程序进行一个长时间操作时,系统分配给该程序的时间片将完全用于处理该操作,换句话说,应用程序没有从它的应用程序队列中取出消息并处理的机会,这样该程序将停止一切对外部事件的响应直到该操作完成为止。具体到本文中就是程序必须等到BatchMove1->Execute()执行完毕后才能响应用户操作,因此用户将完全没有机会终止拷贝过程。
----解决的办法是:在回调函数中取出消息队列中的消息,并后台处理它们,这样用户将有机会按下终止按钮。实现的代码很简单,在回调函数中最后加入以下代码即可
CBRType __stdcall ApiCallBackFun(…)
{
……
MSG amsg;
while(PeekMessage(&amsg,NULL,0,0,PM_REMOVE))
//从队列中取消息
{
TranslateMessage(&amsg); //翻译消息
DispatchMessage(&amsg); //分发消息
}
if (isContinue)
return cbrCONTINUE;
else
return cbrABORT;
}
----以上的代码虽然都用CBuilder编写,但是其原理同样适用于DELPHI
----由于传输速度的原因,当传送大量数据时绝对不赞成逐条记录地向服务器提交数据,而应批量地向Server提交,Delphi/CBuilder中提供了一个TBatchMove控件专门用于批量传送数据,利用它可极大减少网络负担,提高传送速度。遗憾的是,TBatchMove控件只提供了简单的错误控制功能,没有提供显示传送进度、用户终止传送等重要功能。然而TBatchMove所依赖的BDE却提供了一种"回调机制"可以完成上述两个功能。所谓"回调"过程是这样的:当BDE执行某种操作时,比如从一张表向另一张表拷贝大量数据的过程中,每过一段时间(如需要显示拷贝进度时),BDE会调用一段你自己写的函数(回调函数),以帮助你更完全地控制程序。这种做法有点想DLPHI中的Event(事件)及事件处理函数--某个具体的操作动作会让VCL触发某个事件,从而调用一段你写好的事件处理函数,不同的事件会触发不同的处理函数。
----为了让BDE能正确地与你的函数协同工作,你必须事先"注册"你的函数,让BDE知道某个事件发生时应调用(回调)你的某段代码。BDE提供了一个DbiRegisterCallBack注册函数,不幸的是,BDE的联机帮助中的说明不能适合于Delphi/CBuilder,按照该说明编写的程序根本不能通过编译!笔者通过实践找到了正确使用BDE回调函数的方法,下面将详细介绍该机制的使用。BDE回调机制包含以下几个步骤:
----1)按BDE的预定格式编写你的回调函数
----2)调用DbiRegisterCallBack函数注册你的回调函数,这样当你执行相关数据库操作时就自然地触发你的回调函数。
----3)执行相关数据库操作,比如BatchMove1->Exectue();
----4)注销该回调函数
----其中最关键的是正确注册你的回调函数,因此先介绍第二步。(注册与注销都调用同一函数,只是最后一个参数略有不同)
----首先你应知道在哪类"事件"发生时调用你的回调函数,其次你应明白与该事件相关的参数及数据结构--这一切都发生在调用DbiRegisterCallBack函数注册时,所以下面先介绍DbiRegisterCallBack的正确用法及说明:
----在原BDE帮助中该函数的原形(C)是这样的
DBIResult DBIFN DbiRegisterCallBack (hCursor, ecbType, iClientData, iCbBufLen, pCbBuf, pfCb);
----要使用该函数必须include头文件,问题是Delphi/CBuilder中根本没有提供该文件,取而代之的是"BDE.HPP",但是在包含进该文件后程序仍然不能编译通过,因为该文件中没有DBIFN等的说明。一个简单的方法是在代码中去掉DBIFN。函数中各参数解释如下:hCursor是一个BDE中对象的句柄,如果这个参数为NULL,则表示注册的回调函数适合于所有BDE任务;第二个参数ecbType是指回调函数的触发条件的类别,有很多种类型可以选择,其中cbGENPROGRESS表示当需要显示一个长操作的进度时触发这个回调函数;第三个参数iClientData是传递给回调函数的某个数据结构的指针,在我们的例子中为NULL;第四个参数iCbBufLen是指回调Buffer的大小,该大小随第二个参数的不同而不同,比如sizeof(CBPROGRESSDesc);第五个参数pCbBuf是回调Buffer的指针,该指针类型随第二个参数变化,比如cbGENPROGRESS的数据结构是CBPROGRESSDesc;最后一个参数是回调函数的地址指针,当该参数为NULL时表示注销该类型的回调函数。关于回调函数将在稍后详细介绍。下面是注册执行长操作时显示进度的回调函数的格式:
int rst= DbiRegisterCallBack (NULL,
//适合于任何进程
cbGENPROGRESS, //回调类型:显示长操作的进度
NULL, //没有数据
sizeof(CBPROGRESSDesc), //数据结构的大小
&aCBBuf, //数据的内存地址
ApiCallBackFun //回调函数的地址
);
----接下来就应该完成第一步:编写回调函数
----在C中,回调函数应如下声明:
CBRType__stdcallApiCallBackFun(
CBTyp eecbType,//回调类型
int iClientData,//回调数据(指针)
void *pCbInfo//回调数据结构指针
)
----第一个参数是回调类型;第二个参数是回调数据,其解释同DbiRegisterCallBack的第三个参数;第三个是回调数据的指针,该数据的结构随回调类型的不同而不同。比如进度指示cbGENPROGRESS的数据结构是CBPROGRESSDesc,其定义如下:
struct CBPROGRESSDesc {
short iPercentDone; //进度的百分比
char szMsg[128]; //进度的文本信息
};
----该结构的两个域同时只有一个起作用,第一个表示操作的进度百分比,当其为-1时表示第二个域起作用。第二个域用字符串表示进度信息,其格式为<String><:><Value>,比如:RecordsCopied:125
----本文主要在回调函数中完成两个工作:
----1)显示数据拷贝(BatchMove)进度
----2)提供让用户终止长时间拷贝的机制
----显示拷贝进度的代码如下:
CBRType __stdcall ApiCallBackFun(
CBType ecbType, // Callback type
int iClientData, // Client callback data
void * pCbInfo // Call back info/Client)
{ AnsiString str;
if(ecbType==cbGENPROGRESS)
{
int j= StrToInt( ((CBPROGRESSDesc*)
pCbInfo)- >iPercentDone);
if(j< 0)
//如果iPercentDone为-1,则分析szMsg的信息
{
str=((CBPROGRESSDesc*)pCbInfo)- >szMsg;
int pos=str.AnsiPos(":")+1;
//提取出拷贝的记录数
//下面的代码用来在一个Form中显示拷贝进度及拷贝数量
Form1- >Label2- >Caption= str.SubString(pos,100);
Form1- >Label2- >Update();
Form1- >ProgressBar1- >Position=
int((str.SubString(pos,100).
ToDouble()/Form1- >TransNum)*100);
Form1- >ProgressBar1- >Update();
}
else
{Form1- >ProgressBar1- >Position=j;
Form1- >ProgressBar1- >Update();
}
return cbrCONTINUE;
//必须返回cbrCONTINUE以便让BatchMove继续
//若返回cbrABORT则终止拷贝
}
----一切完成以后,每当调用长时间BDE操作(比如BatchMove1->Exectue())时都会触发该回调函数,注意在不需要时应"注销"这个回调函数。
----如果批量传送数据时间很长,则必须为用户提供终止该操作的机会,前面提到,若回调函数返回cbrABORT,则BatchMove过程立即终止。可以在Form上加上一个"停止"按钮和一个全局布尔变量isContinue,当开始拷贝时设该变量为true,当按钮按下后,设该变量为false,每次调用回调函数时检查isContinue的值,若为true则回调函数返回cbrCONTINUE让拷贝继续,否则返回cbrABORT终止拷贝。但是问题在于一旦拷贝过程开始,该进程内所有消息将被阻塞,应用程序在拷贝结束之前没有机会响应键盘、鼠标等一切消息,连屏幕刷新都不能完成,因此必须找到一种避免消息阻塞的方法。
----大家知道,Windows是靠事件(消息)驱动的,在WIN32系统中有两种消息队列:系统队列和应用程序队列,当一个程序进行一个长时间操作时,系统分配给该程序的时间片将完全用于处理该操作,换句话说,应用程序没有从它的应用程序队列中取出消息并处理的机会,这样该程序将停止一切对外部事件的响应直到该操作完成为止。具体到本文中就是程序必须等到BatchMove1->Execute()执行完毕后才能响应用户操作,因此用户将完全没有机会终止拷贝过程。
----解决的办法是:在回调函数中取出消息队列中的消息,并后台处理它们,这样用户将有机会按下终止按钮。实现的代码很简单,在回调函数中最后加入以下代码即可
CBRType __stdcall ApiCallBackFun(…)
{
……
MSG amsg;
while(PeekMessage(&amsg,NULL,0,0,PM_REMOVE))
//从队列中取消息
{
TranslateMessage(&amsg); //翻译消息
DispatchMessage(&amsg); //分发消息
}
if (isContinue)
return cbrCONTINUE;
else
return cbrABORT;
}
----以上的代码虽然都用CBuilder编写,但是其原理同样适用于DELPHI