程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> 動態加載和動態注冊類技術的深入探索

動態加載和動態注冊類技術的深入探索

編輯:Delphi
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 包的方式.
  
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved