|
|
动态加载和动态注册类技术的深入探索 |
|
|
作者:未知 来源:月光软件站 加入时间:2005-2-28 月光软件站 |
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 包的方式.

|
|
相关文章:相关软件: |
|