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 包的方式.