您的位置:首页 > 编程语言 > Delphi

使用package(包功能)实现DLL与主程序的数据库连接共享(Delphi版)

2015-02-09 23:58 639 查看
1、使用Package实现

如何使用Delphi开发大型主从架构系统-Package的秘密和威力

相信许多人和我一样,在使用Delphi开发应用系统的时候,一定会想到如何的切割整个应用系统。是把所有的子系统撰写成一个很大(可能会有数M.Bytes的大小)的EXE檔呢?还是应该把每一个模组撰写在不同的EXE檔案之中的好。

事实上这两种方法都有它们自己的问题。如果是把所有的模组撰写在一个EXE檔案之中的话,那么不但执行檔太大,不易更新和维护。在开发时也不甚方便,因为要让数个人撰写同一支应用程式的确比较麻烦。那么如果我们把每一个模组让不同的程式师撰写成独立的EXE檔案,再由一个主程式分别启动不同的EXE檔不就好了吗?没错这是许多人使用的方法(包括我在内),但是这样切割应用系统有一个问题,那就是如果每一个独立的EXE模组,都需要使用资料库的话,那么当主程式启动个别的EXE檔案时,每一个EXE都必须重新再连结到资料库,开启资料库表格,再取得它需要的资料。这个过程通常都需要不少的时间。例如连结到Oracle并且开启一个资料库表格的话,通常需要五到十秒。如果EXE开启的资料库表格或是查询比较多的话,那么主程式在启动独立的EXE檔案时,通常需要30几秒到一分钟不等。这对于许多的使用者而言是非常不方便的。这样的状状甚至会造成你的专案无法交货(例如使用者要求在五秒之内EXE程式的画面必须出现)。除此之外,每一个独立的EXE又使用了额外的连结以便存取资料库,造成了资源的浪费。面对这种二难的局面,你现在的选择是什么呢?

事实上这个问题在我的心中也盘旋了许久。因为这一是我想要解决的问题,只是由于工作的繁忙让我一直无法花时间解决它。最近在手上的事情告一段落之后,又接到许多朋友的询问,所以决定花一些时间试着解决这个重要的问题。

增加应用程式载入的效率

如果我们仔细的思考这个问题的话,就可以发现问题出在每一支独立的EXE都需要重复的连结资料库所至。所以如果我们可以让连结到资料库的次数减少的话,不就可以加快应用程式载入的效率了吗?

这个想法当然很简单,但是问题是要如何的减少应用程式连结资料库的次数呢?事实上这个想法也很简单,最好是让应用系统连结资料库的次数变成一次,如此一来除了主程式需要连结资料库之外,其他的应用模组都能够公同使用由主程式载入的资料模组的话,那么一切问题不都解决了吗?请注意,在这里我所说的其他模组代表独立的EXE或是其他形式的应用程式,而不是指在单一一个EXE之中不同的表格或是子系统。

我们可以使用图一来表示这个想法。在这个构想中,我希望由应用主程式先负责载入公用的资料模组。在这个资料模组之中有其他子系统需要使用各个资料库表格,如此一来当主程式启动其他的子系统时,就不需要再让每一个子系统再连结,开启资料库表格了。





图一 公用资料模组示意图

当这样还有一些设计上的问题,我们稍后再回来讨论这个问题,现在先我们看看如何的把这个构想实做出来,并且测试一下实际的结果是不是真的比较有效率。

要实做这个构想,我们必须想办法让公用的资料模组能够让每一个子系统存取到,并且不再需要每一个子系统都分别的和资料库建立一个连结的Session。我的第一个想法是使用DLL来解决这个问题。但是事实上使用DLL无法解决这个问题。在我花费了许多的时间之后,使用DLL仍然有会有Access Violation的错误。我也曾在网路上搜寻相关的问题或是资料,我在宝兰的Discuss Forum中也看到有人提出类似的问题,但是似乎都没有人确实的回答这个问题。我也想过干脆拿出我的Soft-Ice和Bounds-Checker看看为什么使用DLLAssembly打交道,这实在不是件好玩的事情。正打算放弃之时,突然想到Delphi
3.0之中的Package不正是解决这个问题的好方法吗?于是我就决定试试看,果然一击中地,顺利的解决了这个问题。当然,要知道为什么使用Package可以解决这个问题,你需要知道Package和DLL的异同。

DLL和Package

为什么在我一开始使用DLL时无法解决多个模组共用一个资料模组DLL的问题呢?这主要是因为在Win 95/NT中当每一个模组载入DLL时,对于每一个DLL之中的全域变数而言,每一个模组都会有一份独立的变数。这是什么意思呢?我们可以使用图二来说明。


图二Win 95/NT中全域变数和模组的关系

当图二中的模组一和模组二分别的载入它们共用的DLL时,虽然在这个共用的DLL中有一个全域变数gAccount。但是模组一和模型二会分别的拥有一个gAccount变数。这也就是说模组一对于gAccount变数数值所做的修改并不会影响模组二中的gAccount变数数值。在Wn 95/NT中的DLL行为是和Win 3.x不同的,因为在Win 3.x中所有的模组都使是共用一份DLL中的全域变数。

由于Win 95/NT中DLL全域变数的这种特性,所以当你想把资料模组撰写在DLL之中让多个模组共同使用时,问题就来了。因为在这种情形下,每一个模组都会有一份它自己的资料模组。所以当每一个应用程式模组载入资料模组的DLL时,它仍然会连结资料库一次,所以你并无法减少连结资料库的次数。

那么使用Package有什么不同吗?在回答这个问题之前,请你先回想一下。在Delphi 3.x中它允许你使用Package的功能来编译你的应用程式,如图三所示。



图三Delphi 3.x允许你使用Package的功能编译应用程序

使用Package编译应用程式的好处除了可以减少应用程式的大小之外,事实上Package还有一个很重要的特性,那就是Package允许多个模组共用所有的全域变数。

我们可以使用图四来说明Package的特性。请你想一想,当你的应用程式使用Package的功能时,事实上它必须载入许多不同的Packages。不管你的应用程式是否使用了全域变数Application,许多的Packages都会使用Application这个全域变数。由于全域变数Application是存在于VCL.DPL之中,所以如果Application会对于每一个载入它的模组都产生一份独立的全域变数的话,那么整个应用程式便会产生不正确的结果。所以由这个说明我们可以知道,在图四中的Application和Screen等全域变数对于所有使用它的模组而言一定是只有一份全域变数。


图四Package中全域变数的特性

事实上在Forms.PAS之中的程式码也透露着这些蛛丝马迹。例如下面便是Forms.PAS宣告Application和Screen这二个变数的程式码。从它们的注释中我们可以清楚的看到,它们是全域物件,即使是编译成Package时也是一样。

{Globalobjects}

var

Application :TApplication

Screen :TScreen

Ctl3DBtnWndProc :Pointer=nil;

由于Package能够自动的将其中的全域变数编译成所有使用它的模组都共用一份的特性。所以我们就可以使用这个特性重新的建构图一的架构成为图五的形式 。


图五改良过的公用资料模组示意图

在图五中我们可以把所有模组需要共同使用的资料模组撰写在一个Package之中,然后再把每一个子系统撰写成独立的Package。只有主程式是EXE,它负责载入共用的资料模组,以及在使用者启动每一个子系统时再载入相对应的子系统Package。

使用这种架构有许多的好处。第一个便是共用的资料模组只需要载入一次即可。而这个好处便是我们前面讨论需要解决的问题。如此一来在每一个子系统载入时便可以加快其执行的速度。第二个好处是在开发这整个系统时,我们仍然可以让不同的程式师负责发展不同的子系统,这样可以就可以解决前面讨论的分工的问题。此外如果一个子系统很庞大的话,你也可以再次的切割这个子系统成为更多的小系统。例如你可以再把图五中的会计子系统Package再分为一般会计,成本会计,和管理会计等不同的Package。第三个好处是使用Package就像是使用DLL一样,主程式可以在需要的时候才载入一个Package,在使用完毕之后可以立刻的释放这个Package,这样可以让你对于系统资源有更好的控制能力。最后一个好处是使用Package可以让你发展出Plug-and-Play的模组,当然这需要藉由结合虚拟介面或是虚拟类别的功能。藉由使用Package和虚拟介面的能力,你可以任意的替换应用系统之中的模组而不会影响系统的执行。这个功能对于许多使用Delphi开发套装软体的程式师来说是一个非常重要的功能。

现在你应该对于DLL和Package的差异有了基本的瞭解,现在是让我们看看如何使用Package的特性解决我们面对的问题的时候了。下一小节就让我们实际的撰写一个范例来证明Package对于全域变数的处理方式以及使用Package的确能够加快应用程式的载入速度。

实际的范例

由于平日我大部份的时间都是使用Oracle,MSSQLServer和InterBase(许多的读者都询问我为什么不使用Sybase或是Informix做为范例说明,这实在是因为我比较少使用它们,并没有其他的意思,所以请使用Sybase,Informix和DB2的读者见谅。不过我相信我们讨论的东西都可以使用在这些资料库之上)。在这三个资料库中,Oracle的连结速度一直都是令我非常头大的,因为在这三者之中,Orcale连结资料库和开启资料库表格的时间最久。所以本节的范例就以Oracle资料库为范例,看看使用了Package之后会不会有任何明显的改善。

首先请你先建立一个Package,并且在这个Package之中产生一个资料模组,并且使用Database和Query元件连结到Oracle的资料库如图六所示。


图六存在于Package之中的资料

模组使用Database连结到Oracle

在成功的编译了这个Package之后,再让我们设计范例程式的主表格如图七一样。


图七使用公用资料模组的主表格

在主表格中有一个DataSource元件。这个DataSource组件会在资料模组的Package载入之后,连结到资料模组之中的Query元件以便显示Oracle资料库之中的资料。现在剩下的工作便是撰写程式码载入资料模组Package和显示资料库的资料。

首先在主表格启动时,它必须在FormActivate事件处理函数中载入资料模组Package。它呼叫LoaddbPackage这个程序。

procedure TMainForm.FormActivate(Sender: TObject);

begin

LoaddbPackage;

end;

LoaddbPackage是真正负责载入Package的程序。它在一个try except程式区块中呼叫Delphi的LoadPackge函数载入指定名称的Package。这个函数在成功执行后会回传一个Package的handle值。我们的程式必须储存这个handle值以便稍后使用它,并且在最后藉由这个handle值释放载入的Package。如果呼叫LoadPackage成功的话,程式就呼叫LoadDataModule从Package中取得前面介绍的资料模组,否则在except程式区块中ShowMessage会显示发生错误的原因。

procedureTMainForm.LoaddbPackage;

begin             //我们必须加载数据库Package以便连结到数据库

try

aDBConnect:=LoadPackage(DBPackages);

LoadDataModule;

excepton E:Exception do

begin

    MessageBeep(Word(-1));

    ShowMessage(E.Message);

    Application.Terminate;

end;

end;

end;

在LoadDataModule中我们必须先从资料模组Package之中取得资料模组的真正Meta-Class名称,然后使用这个Meta-Class建立真正的资料模组物件。所以LoadDataModule一开始会呼叫GetClass向Windows取得特定类别名称的Meta-Class。而GetClass传入的参数『TConcreteDataModule』,便是前面我们建立的资料模组的真正的类别名称。

由于一个独立的EXE要能够取得Package之中的Meta-Class必须使用指定的类别名称。所以当你在Package中撰写任何的类别时,你必须确定这个类别名称在所有Delphi或是应用程式载入的Package中都是唯一的。否则如果有在所有载入的Pakcage中有相同的类别名称时,Delphi或是应用程式会产生一个例外。

在成功的从Package取得了Meta-Class之后,你就必须使用这个Meta-Class产生真正的资料模组物件。请注意在下面的程式码中,我们使用了强制型态转换把TComponentClass的物件转换为TDataModule的物件。

在建立了TDataModule物件之后,我们就可以一一的搜寻资料模组之中的元件并且找到我们需要的Query元件,并且把主表格中DataSource的DataSet特性值设定为找到的Query元件。

procedure TMainForm.LoadDataModule;

var iCounter:Integer;

begin      {NotethatTApplication"owns"this form and thus it must be freed priorto unloading the package } dataModuleClass := GetClass('TConcreteDataModule');

if dataModuleClass <> nil then

    begin

     admGlobal :=TDataModule(TComponentClass(dataModuleClass).Create(Application));

     for iCounter := 0 to admGlobal.ComponentCount - 1 do

     begin

      if UpperCase(admGlobal.Components[iCounter].ClassName) = 'TQUERY' then

      begin

       aQuery := TQuery(admGlobal.Components[iCounter]);

       DataSource1.DataSet := aQuery;

       break;

      end;

     end;

    end;

end;

由于在上面的程式码中我们使用了GetClass以取得特定名称的Meta-Class,所以你的资料模组Package必须在它的initialization程式区块中先注册它自己。下面的程式码便是资料模组Package注册的程式码。

initializationRegisterClass(TConcreteDataModule)

现在就可以执行范例程式了,在主程式执行之后,你可以看到类似图八的画面。从主表格中我们可以证明的确可以藉由资料模组Package存取到Oracle的资料。



图八主表格执行的画面,它果然可以藉由资料模组Package存取资料

到这里为止我们只是证明了使用资料模组Package可以让应用程式正确的执行,但是主程序启动的时间和一般独立的EXE没有什么不同。但是接下来的情形就比较有趣了。因为我们要藉由接下来的程式码来证明使用Package可以大幅加快子系统的载入速度。

在主表格中有一个按钮『载入第二个UI Package』。这个按钮的功能就是模拟载入子系统的功能。当使用者点选这个按钮之后,范例程式会载入另外一个只有使用者介面的Package,而且这个Package必须再使用刚才载入的资料模组Package来显示资料。藉由这个模拟的子系统功能,我们要证明资料模组Package可以在不同的模组中共用而且可以加快模组启动的时间。

这个按钮的OnClick事件处理函数和主表格的FormActvate事件处理函数非常的类似,它也是呼叫LoadUIPackage程序以便载入一个使用者介面Package。

procedure TMainForm.Button1Click(Sender: TObject);

begin

LoadUIPackage;

end;

LoadUIPackage 和 LoaddbPackage几乎一模一样,它也是呼叫Delphi的LoadPackage和GetClass载入Package,取得使用者介面表格的Meta-Class,建立使用者介面表格对象,搜寻资料模组中的Query元件,然后显示资料库的资料。

procedure TMainForm.LoadUIPackage;

begin //我们必须加载使用者接口Package以便连结到数据库

try

    UIConnect := LoadPackage(UIPackages);

    LoadUIModule;

excepton E :Exception do

begin

    MessageBeep(Word(-1));

    ShowMessage(E.Message);

    Application.Terminate;

end;

end;

end;

procedure TMainForm.LoadUIModule;

var

iCounter : Integer;

aDS :TDataSource;

begin        { Note that TApplication "owns" this form and thus it must be freed priorto unloading the package }

pkgModuleClass := GetClass('TUIPackageForm');

if pkgModuleClass <> nil then

beginaPkgForm := TCustomForm(TComponentClass(pkgModuleClass).Create(Application));

for iCounter := 0 to aPkgForm.ComponentCount - 1 do

begin

if UpperCase(aPkgForm.Components[iCounter].ClassName) = 'TDATASOURCE' then

begin

    aDS := TDataSource(aPkgForm.Components[iCounter]);

    aDS.DataSet := aQuery;

    break;

end;

end;

aPkgForm.Visible := True;

end;

end;

当我们完成载入使用者介面Package的功能,再执行范例程式并且按下『载入第二个UIPackage』按钮之后,可以看到类似图九的画面。



图九第二个使用者介面Package启动的画面

令人惊讶的是,使用者介面Package之中的表格会立刻的显示出来,几乎完全不需要等待。表格一是我使用这个范例程式和使用二个独立的EXE应用程式比较的结果。从表格中你可以看到使用Package载入子系统比使用EXE载入子系统整整快了十倍。

                                     使用独立的EXE应用程式                    使用EXE加Package的功能

主程式启动时间               20                                                      20

其余模组启动时间           20                                                      2

表格一独立的EXE和使用Package的EXE执行的效率比较



图十使用Package的应用程式和一般应用程式执行效率的比较

从上面的结果我们就可以知道使用Package的好处,它不但可以让我们共用资源,也能够改善子系统启动的时间 。

当然应用程式在使用完毕之后,它必须释放动态载入的Package以便释放系统资源。在主表格的FormClose事件处理函数中它呼叫了UnLoadAddInPackage程序并且传递Package的handle值做为参数以便释放Package。

procedure TMainForm.FormClose(Sender: TObject; var Action: TCloseAction);

begin

UnLoadAddInPackage(dbConnect);

UnLoadAddInPackage(UIConnect);

end;

UnLoadAddInPackage先从全域物件Application中找到载入的资料模组和使用者介面表格,先释放这些动态载入,建立的物件,切断它们和Application和关系,再利用Package的handle值解除Package对Windows的注册,最后再呼叫UnLoadPackage释放载入到记忆体之中的Package。

procedure UnLoadAddInPackage(Module: THandle);

var i: Integer;

M: TMemoryBasicInformation;

begin

for i := Application.ComponentCount - 1 downto 0 do

begin

VirtualQuery(GetClass(Application.Components[i].ClassName), M, SizeOf(M));

if (Module = 0) or (HMODULE(M.AllocationBase) = Module) then

begin

    ShowMessage(Application.Components[i].ClassName);

    Application.Components[i].Free;

end;

end;

UnRegisterModuleClasses(Module);

UnLoadPackage(Module);

end;

当你结束主程式时,你应该会看到类似图十一和图十二的画面。这些画面证明了当主程式结束时,它能够正确的释放所有载入的Package以释放系统资源。



图十一范例程式结束时显示它正确的释放了所有使用的Package



图十二范例程式正确的释放了公用的资料模组Package

上面的范例程式证明了使用Package的确能够公用Package以及Package之中的全域资料,此外也能够帮助子系统加快载入和启动的时间。但是主程式在启动时仍然需要载入资料模组Package,连结资料库。但是对于许多的使用者而言,他们仍然会希望让主程式也能够很快的出现。要达成这个目标,你还需要藉助Delphi执行绪的能力。

加入多执行绪载入的能力


在上一节中你看到了使用Package的确可以大幅加快子系统载入的时间。但是我们在载入主程式时仍然需要20到30秒的时间,因为主程式还是需要载入共用的资料模组Package。但是对于许多案子来说,使用者经常会要求程式必须在10秒或是5秒之内第一个画面就必须出现。那么对于这种要求我们可不可以达到呢?

如果你想到Delphi中的多工执行绪功能Tthread物件的话,那么答案就出现了。我们只需要在主程序第一个表格出现之时,启动一个多执行绪物件让它负责载入公用的资料模组不就可以了吗?如此一来主表格可以立刻的出现在萤幕之上,而让执行绪在背景继续的载入资料模组。

为了要加入多执行绪的能力,你必须修改上一节的范例。在范例程式的FormActivate事件处理函数我们不再直接呼叫LoaddbPackage,而是建立一个Tthread物件,然后由这个Tthread物件真正的载入资料模组Package。

procedure TMainForm.FormActivate(Sender: TObject);

begin

DBThread := TOracleThread.Create(True);

aDBThread.OnTerminate := dbThreadTerminated;

aDBThread.Resume;

end;

在TOracleThread的Execute虚拟方法中,它呼叫了LoaddbPackage载入资料模组。在这里有一点非常重要的地方便是当你在执行绪中载入Package时,所有的handle值和物件参考值必须储存在主执行绪的变数之中。否则在这个执行绪执行完毕之后,这些handle值和物件参考值都会成为无效的数值。所以在下面的程式码中你可以看到执行绪在呼叫LoadPackage载入Package时会把LoadPackage回传的数值储存在主表格的变数之中。

procedure TOracleThread.Execute;

begin             {Placethreadcodehere }

LoaddbPackage;

end;

procedure TOracleThread.LoaddbPackage;

begin                   //我们必须加载数据库Package以便连结到数据库

try

MainForm.aDBConnect := LoadPackage(DBPackages);

LoadDataModule;

excepton E :Exception do

begin

MessageBeep(Word(-1));

ShowMessage(E.Message);

Application.Terminate;

end;

end;

end;

procedure TOracleThread.LoadDataModule;

var

iCounter : Integer;

begin           { Note that TApplication "owns" this form and thus it must be freed priorto unloading the package }

dataModuleClass := GetClass('TConcreteDataModule');

if dataModuleClass <> nil then

begin

MainForm.admGlobal:= TDataModule(TComponentClass(dataModuleClass).Create(Application));

end;

end;

当我们修改完范例程式之后,就可以试着再次的执行它,看看主程式的载入时间是不是有任何的改善。下面的表格和图形显示出当我们使用了执行绪的功能之后,第一支主程式启动的时间果然大幅减少为3秒钟。比起原来的20秒果然改善了许多,真是令人印象深刻。

                                   使用独立的EXE应用程式                                     使用EXE加Package的功能

主程式启动时间            20                                                                        3

其余模组启动时间        20                                                                        2

表格二使用执行绪后的范例程式执行效率



图十三加入执行绪后应用程式执行的效率

现在使用了执行绪功能之后,不但每一个子系统启动的时间加快了许多,主程式更是可以在瞬间出现,这样的执行速度应该是可以让大部份的人满意了吧。

应用程式,企业法则(企业物件)的切割

从上面的范例中我们可以知道,善用Delphi的Package和执行绪的功能可以让我们大幅的改善应用程式载入和执行的效率。但是上面的范例只是假设在很简单的状态之下。在许多实际的案子中,我们可能无法把所有的资料集元件放在一个单一的资料模组之中。在这种情形下,你可以把所有的资料集元件分别撰写在不同的资料模组Package之中,并且在每一个子系统需要特定的资料模组Package时再载入它们。

当我们从这个角度观察应用系统时,可以发现如何的切割资料集到不同的资料模组中似乎是一件非常重要的事情。因为这不但牵涉到应用系统执行的效率,更和系统资源的善用有很大的关系。事实上当我们开发N-Tier的应用系统时,你也会发现如何切割应用程式和企业物件对整个应用系统的架构有深远的影响。

所以应用程式和企业物件的切割似乎在未来新一代应用系统的开发中占有重要的地位。当然要能够适当,有效率的切割企业物件需要SA在分析系统时好好的做分析的工作,更需要SD能够通盘的设计整个应用系统运作的架构。

如果我们结合Delphi强大的N-Tier,Package以及执行绪功能的话,就可以使用如下的图形来表示。



图十四N-Tier,Package和执行绪结合使用的架构

请注意在图十四中说明了并不是只有资料模组可以存在于Package之中,我们也可以把应用逻辑或是企业物件封装在Package之中或是ActiveX之中,于应用程式需要时再载入执行它们。最后由于应用程式伺服器在大多数的情形下是执行在WindowsNTServer之中,所以我们可以更有效率的使用作业系统的执行绪能力来载入应用程式需要的Package。

更具威力的功能

就像我在前面说明的,我无法在一篇文章中为各位介绍所有有关Package的使用方法和各种功能。使用Package更高阶和更具威力的功能应该是和虚拟类别结合一起使用。此外Delphi3也提供了许多的程序和函数能够让你检查每一个Package使用了什么其他的Package,执行时期函式馆和全域函式。

结合Package和虚拟类别不但能够让你发展出Plug-and-Play的模组功能,更能够让你完全的控制Package中所有的物件和变数。更能够让你发展出一些自动化的工具来帮助你开发你的应用系统。我计划在以后的文章中再继续和各位讨论这些高等的主题。

结论

在本篇文章中我们看到了如何的使用Delphi3.0中的Package功能来解决应用载入时连结资料库效率的问题。许多人对于Package的认识只是它可以减少应用程式的檔案大小,除此之外似乎就没有什么其他的瞭解了。但是这篇文章就告诉你如何的发掘Package更有威力的一个应用。使用Packages可以立刻的降低模组启动需要的时间。

但是我们在这篇文章中就讨论了所有有关Package的功能来吗?那当然不,还需要许多更有威力的功能我并未讨论。例如你如果能够更进一步的结合Package,参考计数值以及抽象虚拟类别(AbstractVirtualClass)或是抽象介面的话,那么你就可以完全的控制Package中内含的类别以及表格,而不需要一定是Delphi内定的类别和表格。这样一来你可以设计一个Plug-and-Play的Package介面,以便让主程式能够载入任意的Package,存取它功能。此外你也可以取得Package中许多重要的资讯,例如这个Package输出了那些的方法,使用了那些其他的Package?Package和initialize以及finalization的关系等,也许有机会再让我们讨论这些更有意思的主题。

事实上从这篇文章中就可以出Delphi的多么的好用。如果今天你是使用VisualBasic或是PowerBuilder的话,那么抱歉,你绝对无法解决这个问题,也许再等到下一个VisualBasic或是PowerBuilder版本的话,就有『可能』解决这个问题吧。但是Delphi就不同了,它除了可以让你撰写一般的应用系统,但是在遇到特殊的情形时,Delphi也可以在短暂的时间内超越工具目前功能的限制,而解决这些问题,谁是比较好的主从架构开发工具就不言自明了。

2、阿朱大虾的经典文章
http://www.delphibbs.com/delphibbs/dispq.asp?lid=534762

3、http://www.delphibbs.com/delphibbs/dispq.asp?lid=1819824

根据Delphi提供的有关DLL编写和调用的帮助信息,你可以很快完成一般的DLL编写和调用的

应用程序。本文介绍的主题是如何编写和调用能够传递各种参数(包括对象实例)的DLL。例

如,主叫程序传递给DLL一个ADOConnection对象示例作为参数,DLL中的函数和过程调用通

过该对象实例访问数据库。

  需要明确一些基本概念。对于DLL,需要在主程序中包含exports子句,用于向外界提供调用

接口,子句中就是一系列函数或过程的名字。对于主叫方(调用DLL的应用程序或其它的DLL),

则需要在调用之前进行外部声明,即external保留字指示的声明。这些是编写DLL和调用DLL必须

具备的要素。

  另外需要了解Object Pascal中有关调用协议的内容。在Object Pascal中,对于过程和函数有

以下五种调用协议:

  指示字参数传递顺序参数清除者参数是否使用寄存器

  register 自左向右被调例程是

  pascal 自左向右被调例程否

  cdecl 自右向左调用者否

  stdcall 自右向左被调例程否

  safecall 自右向左被调例程否

  这里的指示字就是在声明函数或过程时附加在例程标题之后的保留字,默认为register,即是

唯一使用CPU寄存器的参数传递方式,也是传递速度最快的方式;

  pascal:调用协议仅用于向后兼容,即向旧的版本兼容;

  cdecl:多用于C和C++语言编写的例程,也用于需要由调用者清除参数的例程;

  stdcall:和safecall主要用于调用WindowsAPI函数;其中safecall还用于双重接口。

  在本例中,将使用调用协议cdecl,因为被调用的DLL中,使用的数据库连接是由主叫方传递

得到的,并且需要由主叫方处理连接的关闭和销毁。

  下面是 DLL完整源程序和主叫程序完整源程序。包括以下四个文件:

   Project1.DPR {主叫程序}

   Unit1.PAS {主叫程序单元}

   Project2.DPR {DLL}

   Unit2.PAS {DLL单元}

  {---------- DLL 主程序 Project2.DPR ----------}

  library Project2;

  uses

   SysUtils,

   Classes,

   Unit2 in ‘Unit2.pas‘ {Form1};

  {$R *.RES}

  { 下面的语句用于向调用该 DLL的程序提供调用接口 }

  exports

   DoTest; { 过程来自单元Unit2 }

  begin

  end.

{---------- DLL中的单元 Unit2.PAS ----------}

  unit Unit2;

  interface

  uses

   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

   Db, ADODB, StdCtrls, Menus;

  type

   TForm1 = class(TForm)

   ADOConnection1: TADOConnection;{ 本地数据库连接 }

   Memo1: TMemo; { 用于显示信息 }

   private

   public

   end;

  { 该过程向外提供 }

  procedure DoTest(H: THandle; { 获得调用者的句柄 }

   AConn: TADOConnection;{ 获得调用者的数据库连接 }

   S: string; { 获得一些文本信息 }

   N: Integer); { 获得一些数值信息 }

   cdecl; { 指定调用协议 }

  implementation

  {$R *.DFM}

  procedure DoTest(H: THandle; AConn: TADOConnection; S: string; N: Integer);

  begin

   Application.Handle := H; { 将过程的句柄赋值为调用者的句柄 }

   { 上面语句的作用在于, DLL的句柄和调用者的句柄相同,在任务栏中就不会 }

   { 各自出现一个任务标题了。 }

   with TForm1.Create(Application) do try{ 创建窗体 }

   Memo1.Lines.Append(‘成功调用‘); { 显示一行信息 }

   ADOConnection1 := AConn; { 获得数据库连接的实例 }

   Memo1.Lines.Append(

   ADOConnection1.ConnectionString +

   ‘ - ‘ + S + ‘ - ‘ + IntToStr(N)); { 根据得到的参数显示另一行信息 }

   ShowModal; { 模式化显示窗体 }

   finally

   Free; { 调用结束时销毁窗口 }

   end;

  end;

  end.

  {---------- 调用者 Project1.DPR,很普通的工程文件 ----------}

  program Project1;

  uses

  Forms,

   Unit1 in ‘Unit1.pas‘ {Form1};

  {$R *.RES}

  begin

   Application.Initialize;

   Application.CreateForm(TForm1, Form1);

   Application.Run;

  end.

  {---------- 调用者单元Unit1.PAS ----------}

  unit Unit1;

  interface

  uses

   Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,

   StdCtrls, Db, ADODB;

  type

   TForm1 = class(TForm)

   Button1: TButton; { 按此按钮进行调用 }

   ADOConnection1: TADOConnection; { 本地数据库连接,将传递给 DLL }

   procedure Button1Click(Sender: TObject);{ 调用 DLL}

   private

   public

   end;

  var

   Form1: TForm1;

  implementation

  {$R *.DFM}

  { 外部声明必须和 DLL中的参数列表一致,否则会运行时错误 }

  procedure DoTest(H: THandle; { 传递句柄 }

   AConn: TADOConnection; { 传递数据库连接 }

   S: string; { 传递文本信息 }

   N: Integer); { 传递数值信息 }

   cdecl; { 指定调用协议 }

   external ‘Project2.dll‘;{ 指定过程来源 }

  { 调用过程 }

  procedure TForm1.Button1Click(Sender: TObject);

  begin

   DoTest(Application.Handle,

   ADOConnection1,

   ‘Call OK‘,

   256);

  end;

  end.

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: