首页  编辑  

Delphi(C++Builder)包(Package)动态加载和动态注册类技术的深入探索

Tags: /超级猛料/IDE.集成开发环境/IDE 环境/   Date Created:

Delphi(C++Builder) 包(Package)动态加载和动态注册类技术的深入探索

http://www.delphibbs.com/delphibbs/dispq.asp?lid=1687832

来自:wr960204, 时间:2003-3-17 13:18:00, ID:1687832

Delphi 的包是Delphi IDE的核心技术,没有包也就没有了Delphi的可视化编程。包也可以用在我们开发的项目中,其好处是可以代码共享,减小工程尺寸,单纯通过替换包文件就能实现工程的升级和补丁。但是我们要加载包,就要知道包中已经存在的类。关于如何动态加载包的资料比比皆是我就不想就此问题讨论了。但是Delphi的IDE很是特殊,它无需事先知道你的包有哪些类就能注册组建,创建组建。但是Borland没有公开BPL文件的格式。我们自己是否可以实现IDE的功能呢?

首先我们知道。一个组件包想要能在IDE中使用就要进行注册也就是要创建一个过程例如:

Procedure Register;

Begin

  RegisterComponents(IDE中的页面, [组件类]);

End;

在IDE加载时就要调用这个过程进行注册。

其次我们通过Borland的文档又知道BPL只是一种特殊格式的DLL文件。那么既然IDE可以调用得到注册过程那么注册过程一定要是导出类型(exports)的才行。既然如此我们可以想办法弄明白。写一个包文件。里面包含Test、和TestBtn两个单元。两个单元分别都有注册过程,然后编译成BPL文件。好了我们可以用EXESCOPE这个工具来弄清楚其中的奥秘。

我们可以看到一个函数@Test@Register$qqrv。几乎可以肯定这个函数就是BPL把Test单元中的Register导出的注册函数,而那个@Testbtn@Register$qqrv就一定是Testbtn这个单元的注册函数。可以做一个实验来证明我们的想法,在Test单元的Register的函数中加上ShowMessage('你好,你调用了注册函数');

然后在我们来调用一下包中的函数@Test@Register$qqrv,随便写一个工程看看是不是可以调用得到Test单元中的Register过程。

var

 H                 : Integer;

 regproc           : procedure();

begin

 H := 0;

 H := LoadPackage('TestPackage.bpl');

 try

   if H <> 0 then

   begin

     RegProc := GetProcAddress(H,'@Test@Register$qqrv');//载入包中的函数

     if Assigned(RegProc) then

     begin

       regproc();//调用函数

     end;

   end;

 finally

   if H <> 0 then

   begin

     UnloadPackage(H);

     H := 0;

   end;

 end;

end;

调用的结果,果然调用到了包中Terst单元的Register过程。但是如何得到注册了哪些类呢?注册组件要用RegisterComponents函数。好在VCL体系的源代码是开放的,我们看看RegisterComponents是如何实现的吧。

在Classes单元我们可以看到:

procedure RegisterComponents(const Page: string;

 const ComponentClasses: array of TComponentClass);

begin

 if Assigned(RegisterComponentsProc) then

   RegisterComponentsProc(Page, ComponentClasses)

 else

   raise EComponentError.CreateRes(@SRegisterError);

end;

画线的是一个函数指针,Delphi的IDE就是在这个指针所指的函数里去作具体的工作。我们也可以利用它来实现我们的注册。

procedure MyRegComponentsProc(const Page: string;

 const ComponentClasses: array of TComponentClass);

var

 I                 : Integer;

 IDEInfo           : PIDEInfo;

begin

 for i := 0 to High(ComponentClasses) do

 begin

   RegisterClass(ComponentClasses[I]);

 end;

end;

然后一条语句RegisterComponentsProc:= @MyRegComponentsProc;似乎就解决问题了。

慢着!RegisterComponentsProc是在Classes单元。但是BPL中的Classes单元是在另一个运行时的包VCL.BPL里面。而我们工程所修改的RegisterComponentsProc的指针是编译在我们的工程中,空间是不同的。所以我们的工程一定要编译成带运行时包VCL.BPL的才行。但是这样一来的话我们也就只能载入和我们所用的编译器相同版本编译器编译出来的BPL文件了,也就是说Delphi6只能载入Delphi6或者BCB6编译出来的BPL文件以此类推。

但是还有一个问题没有解决,那就是如何知道一个包中到底有那些各单元呢?可以通过GetPackageInfo过程来获得。

我已经把加载包的过程封装到了一个类中。整个程序的代码如下:

{ *********************************************************************** }

{                                                                         }

{ 动态加载Package的类                                                     }

{                                                                         }

{ wr960204(王锐)2003-2-20                                                 }

{                                                                         }

{ *********************************************************************** }

unit UnitPackageInfo;

interface

uses

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

 Dialogs, StdCtrls;

type

 PIDEInfo = ^TIDEInfo;

 TIDEInfo = record

   iClass: TComponentClass;

   iPage: string;

 end;

type

 TPackage = class(TObject)

 private

   FPackHandle: THandle;

   FPackageFileName: string;

   FPageInfos: TList;

   FContainsUnit: TStrings;            //单元名

   FRequiresPackage: TStrings;         //需要的的包

   FDcpBpiName: TStrings;              //

   procedure ClearPageInfo;

   procedure LoadPackage;

   function GetIDEInfo(Index: Integer): TIDEInfo;

   function GetIDEInfoCount: Integer;

 public

   constructor Create(const FileName: string); overload;

   constructor Create(const PackageHandle: THandle); overload;

   destructor Destroy; override;

   function RegClassInPackage: Boolean;

   property IDEInfo[Index: Integer]: TIDEInfo read GetIDEInfo;

   property IDEInfoCount: Integer read GetIDEInfoCount;

   property ContainsUnit: TStrings read FContainsUnit;

   property RequiresPackage: TStrings read FRequiresPackage;

   property DcpBpiName: TStrings read FDcpBpiName;

 end;

implementation

var

 CurrentPackage    : TPackage;

procedure RegComponentsProc(const Page: string;

 const ComponentClasses: array of TComponentClass);

var

 I                 : Integer;

 IDEInfo           : PIDEInfo;

begin

 for i := 0 to High(ComponentClasses) do

 begin

   RegisterClass(ComponentClasses[I]);

   new(IDEInfo);

   IDEInfo.iPage := Page;

   IDEInfo.iClass := ComponentClasses[I];

   CurrentPackage.FPageInfos.Add(IDEInfo);

 end;

end;

procedure EveryUnit(const Name: string; NameType: TNameType; Flags: Byte; Param:

 Pointer);

begin

 case NameType of

   ntContainsUnit:

     CurrentPackage.FContainsUnit.Add(Name);

   ntDcpBpiName:

     CurrentPackage.FDcpBpiName.Add(Name);

   ntRequiresPackage:

     CurrentPackage.FRequiresPackage.Add(Name);

 end;

end;

{ TPackage }

constructor TPackage.Create(const FileName: string);

begin

 FPackageFileName := FileName;

 LoadPackage;

end;

procedure TPackage.ClearPageInfo;

var

 I:Integer;

 IDEInfo:PIDEInfo;

begin

 for i:=FPageInfos.Count-1 downto 0 do

 begin

   IDEInfo:=FPageInfos[I];

   Dispose(IDEInfo);

   FPageInfos.Delete(I);

 end;

 FPageInfos.Clear;

end;

constructor TPackage.Create(const PackageHandle: THandle);

begin

 FPackageFileName := GetModuleName(PackageHandle);

 LoadPackage;

end;

destructor TPackage.Destroy;

var

 I                 : Integer;

begin

 FContainsUnit.Free;

 FRequiresPackage.Free;

 FDcpBpiName.Free;

 if FPackHandle <> 0 then

 begin

   UnRegisterModuleClasses(FPackHandle);

   ClearPageInfo;

   FPageInfos.Free;

   UnloadPackage(FPackHandle);

   FPackHandle := 0;

 end;

 inherited Destroy;

end;

function TPackage.GetIDEInfoCount: Integer;

begin

 Result := FPageInfos.Count;

end;

function TPackage.GetIDEInfo(Index: Integer): TIDEInfo;

begin

 if (Index in [0..(FPageInfos.Count - 1)]) then

 begin

   Result := TIDEInfo(FPageInfos[Index]^);

 end;

end;

procedure TPackage.LoadPackage;

var

 Flags             : Integer;

 I                 : Integer;

 UnitName          : string;

begin

 FPageInfos := TList.Create;

 FContainsUnit := TStringList.Create;

 FRequiresPackage := TStringList.Create;

 FDcpBpiName := TStringList.Create;

 FPackHandle := SysUtils.LoadPackage(FPackageFileName);

 CurrentPackage := Self;

 GetPackageInfo(FPackHandle, @FPackHandle, Flags, EveryUnit);

end;

function TPackage.RegClassInPackage: Boolean;

//该函数只能在工程文件需要VCL,RTL两个包文件时才能用

//因为我们需要把全局的函数指针Classes.RegisterComponentsProc指向我们自己

//函数(该函数为IDE准备,IDE会为它设定函数而我们的程序也要模仿IDE为它设定函数)。

//如果不是带VCL和RTL两个包,那么我们设置的只是我们本身Classes单元的函数指针

//而不是包括Package的全局的。

//

//而有趣的是如果我们的工程不带包运行,那么我们基本上可以同时用它来查看最近几个版本的

//Borland编译器所产生的包文件而不会产生异常,但是控件不能够注册了。

var

 I                 : Integer;

 oldProc           : Pointer;

 RegProc           : procedure();

 RegProcName, UnitName: string;

begin

 oldProc := @Classes.RegisterComponentsProc;

 Classes.RegisterComponentsProc := @RegComponentsProc;

 FPageInfos.Clear;

 try

   try

     for i := 0 to FContainsUnit.Count - 1 do

     begin

       RegProc := nil;

       UnitName := FContainsUnit[I];

       RegProcName := '@' + UpCase(UnitName[1])

         + LowerCase(Copy(UnitName, 2, Length(UnitName))) + '@Register$qqrv';

       //后面这个字符串@Register$qqrv是Borland定死了的,Delphi5,6,7,BCB5,6都是这样子的

       //Delphi3是Name + '.Register@51F89FF7'。而Delphi4手里没有,不曾试验过

       RegProc := GetProcAddress(FPackHandle,

         PChar(RegProcName));

       if Assigned(RegProc) then

       begin

         CurrentPackage := Self;

         RegProc;

       end;

     end;

   except

     UnRegisterModuleClasses(FPackHandle);

     ClearPageInfo;

     Result := True;

     Exit;

   end;

 finally

   Classes.RegisterComponentsProc := oldProc;

 end;

end;

end.

调用如下

{ *********************************************************************** }

{                                                                         }

{ 程序主窗体单元                                                          }

{                                                                         }

{ wr960204(王锐)2003-2-20                                                 }

{                                                                         }

{ *********************************************************************** }

unit Unit1;

interface

uses

 UnitPackageInfo,

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

 Dialogs, StdCtrls, ExtCtrls;

type

 TForm1 = class(TForm)

   GroupBox1: TGroupBox;

   Panel1: TPanel;

   ListBox1: TListBox;

   Button1: TButton;

   Button2: TButton;

   OpenDialog1: TOpenDialog;

   Memo1: TMemo;

   procedure Button1Click(Sender: TObject);

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

   procedure Button2Click(Sender: TObject);

 private

   { Private declarations }

   FPack: TPackage;

   procedure FreePack;

 public

   { Public declarations }

 end;

var

 Form1             : TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);

var

 I                 : Integer;

begin

 if OpenDialog1.Execute then

 begin

   FreePack;

   FPack := TPackage.Create(OpenDialog1.FileName);

   FPack.RegClassInPackage;

 end;

 ListBox1.Items.Clear;

 for i := 0 to FPack.IDEInfoCount - 1 do

 begin

   ListBox1.Items.Add(FPack.IDEInfo[I].iClass.ClassName);

 end;

 Memo1.Lines.Clear;

 Memo1.Lines.Add('------ContainsUnitList:-------');

 for i := 0 to FPack.ContainsUnit.Count - 1 do

 begin

   Memo1.Lines.Add(FPack.ContainsUnit[I]);

 end;

 Memo1.Lines.Add('------DcpBpiNameList:-------');

 for i := 0 to FPack.DcpBpiName.Count - 1 do

 begin

   Memo1.Lines.Add(FPack.DcpBpiName[I]);

 end;

 Memo1.Lines.Add('--------RequiresPackageList:---------');

 for i := 0 to FPack.RequiresPackage.Count - 1 do

 begin

   Memo1.Lines.Add(FPack.RequiresPackage[I]);

 end;

end;

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

begin

 FreePack;

end;

procedure TForm1.Button2Click(Sender: TObject);

var

 Ctrl              : TControl;

begin

 if (ListBox1.ItemIndex <> -1) and (FPack <> nil) then

 begin //判断如果不是TControl的子类创建了也看不见,就不创建了

   if (FPack.IDEInfo[ListBox1.ItemIndex].iClass.InheritsFrom(TControl)) then

   begin

     Ctrl := nil;

     try

       Ctrl := TControl(FPack.IDEInfo[ListBox1.ItemIndex].iClass.Create(Self));

       Ctrl.Parent := Panel1;

       Ctrl.SetBounds(0, 0, 100, 100);

       Ctrl.Visible := True;

     except

     end;

   end;

 end;

end;

procedure TForm1.FreePack;

var

 I                 : Integer;

begin

 for i := Panel1.ControlCount - 1 downto 0 do

   Panel1.Controls[i].Free;

 FreeAndNil(FPack);

end;

end.

窗体文件如下:

object Form1: TForm1

 Left = 87

 Top = 120

 Width = 518

 Height = 375

 Caption = 'Form1'

 Color = clBtnFace

 Font.Charset = DEFAULT_CHARSET

 Font.Color = clWindowText

 Font.Height = -11

 Font.Name = 'MS Sans Serif'

 Font.Style = []

 OldCreateOrder = False

 OnClose = FormClose

 PixelsPerInch = 96

 TextHeight = 13

 object GroupBox1: TGroupBox

   Left = 270

   Top = 0

   Width = 240

   Height = 224

   Align = alRight

   Caption = '类'

   TabOrder = 0

   object ListBox1: TListBox

     Left = 2

     Top = 15

     Width = 236

     Height = 207

     Align = alClient

     ItemHeight = 13

     TabOrder = 0

   end

 end

 object Panel1: TPanel

   Left = 0

   Top = 224

   Width = 510

   Height = 124

   Align = alBottom

   Color = clCream

   TabOrder = 1

 end

 object Button1: TButton

   Left = 8

   Top = 8

   Width = 249

   Height = 25

   Caption = '载入包'

   TabOrder = 2

   OnClick = Button1Click

 end

 object Button2: TButton

   Left = 8

   Top = 40

   Width = 249

   Height = 25

   Caption = '创建所选中的类的实例在Panel上'

   TabOrder = 3

   OnClick = Button2Click

 end

 object Memo1: TMemo

   Left = 8

   Top = 72

   Width = 257

   Height = 145

   ReadOnly = True

   ScrollBars = ssBoth

   TabOrder = 4

 end

 object OpenDialog1: TOpenDialog

   Filter = '*.BPL|*.BPL'

   Left = 200

   Top = 16

 end

end

在这些基础上我们完全可以建立一个自己的Delphi的IDE,对象的属性的获得和设置用TYPInfo单元的RTTI类函数完全可以轻松搞定,我就不在这里多费口舌了。

记住了,编译时一定要用携带VCL.BPL 包的方式.