Delphi 元件設計初步(一)

作者:Danny Tzu
日期:May-2-2002

前言

其實這篇文章很早以前就想寫了, 但礙於個人能力及時間的問題, 就一直拖著沒動作, 本篇並不是要講很多 VCL 的大道理, 而是教您如何簡單的 Step By Step 作出您自己的元件.

當然有人從不用3-Party元件, 理由是可以直接在另一台電腦上撰寫程式不用安裝元件, 但是Delphi本身就是用元件堆出來的, 不使用自訂元件我認為並不是一個聰明的作法.

撰寫本篇是因為大多數使用 Delphi 的人只會使用元件, 也許這是當初採用 Delphi 的原因, 但是在設計軟體時, 有很多時候只要簡單的設計或修改元件就可以解決問題, 我知道也有人寫一些共用的 procedure 或 function 但使用起來會有些限制或不方便, 但一想到要寫(改)元件要知道很多技術及原理, 尤其完整的撰寫元件文章書籍都是片片斷斷, 可能就馬上打了退堂鼓, 其實寫元件也可以很簡單, 當然您懂的東西越多可以寫的元件功能及型態越多.

本篇不談 Interface 的使用(以下範例一個 Interface 都沒用到), 但並不表示 Interface 不重要, 相反的它非常重要, 主要是我認為這不適合在這理說明, 如果各位有興趣的話可以參考 BrianChang 的Interface的基礎 code6421 的淺談Interface」兩篇文章.

這裡我不會講很多的大道理, 但這並不表示您就可以不需要瞭解一些像: 物件導向, Windows 訊息機制, Stream I/O 機制 ... 等相關知識, 如果您懂得越多當然寫出來的元件越好; 雖然 Delphi 以視覺化設計著稱, 但寫元件一點也不視覺化, 您必須要像在 DOS 時代一樣純手工打造, 也許對有點年紀的人來說會反而更熟悉吧 !

使用環境

Delphi 5, 6 All Version
建議使用 Enterprise 版本可以有很多 VCL Source code 可以參考.

開始吧

元件大致上分為二大類:

  1. 不可視元件:
    不可視元件一般和系統有關, 他的特色是在 Run Time 時在軟體上是看不到他的, 但確實會提供服務, 像 TActionList, TMainMenu, TPopupMenu, TDataBase, TQuery.... 等. 
  2. 可視元件:
    可視元件和不可視元件剛好相反, 在 Run Time 時他是看得到的, 當然 Design Time 也是可以看到, 不然您如何設定特性(property)及 Event, 像 TImage, TDBGrid, TEdit .... 都是.

其實您已經在寫元件了可能您自己都不知道, 您每天寫程式都是寫在哪裡 ? TForm 對吧 ! 您的 Form 都是繼承自 TForm 這個元件, New Form Unit 會產生如下的框架程式碼:

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

end.

而以 New Component (Component -> New Component) Unit 所產生的框架大概像下面這樣:

unit CustomControl1;

interface

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

type
  TCustomControl1 = class(TCustomControl)
  private
    { Private declarations }
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TCustomControl1]);
end;

end.

注意到了嗎? 這二個 Unit 只差在 TForm 沒有宣告 protected, published 這二個節區, 因為大部份在寫 Form 時都用不到這二個節區, 所以 Delphi 會省略掉, 但是您要用的話也可以, 就自己手動加上吧!
另外因為要安裝到元件盤的關係, 所以 CustomControl1 會多出 procedure Register, 但反過來想 TForm 是不能安裝到元件盤的, 這部份您可以試試看有沒有辦法安裝到元件盤, 可以的話請告訴我一聲.
另外 TForm 不用安裝就能使用, 這點我想是為了方便的原因, 不過如果您是用 Delphi Package 技術的話還是需要安裝這一個步驟, 這就和元件一樣了, 如果您有興趣可以參考在下另一拙作 "Delphi Package 無痛使用"

第一個自製元件

首先我們作一個 "不可視元件" , 為何要先選 "不可視元件" 作呢 ? 原因是 "簡單" 因為不需要很多的技術就能作了.

開始當然要先選繼承的父親是誰 ? 因為我們是作不可視元件所以選 TComponent 為父親, 另外順便一提 Delphi 有定義繼承自 TComponent (含)以下的叫 "元件" , 其他的叫 "物件", 因此 "物件" 包含 "元件" (白馬非馬也, 聽過吧 !), "元件" 和 "物件" 差在可不可以安裝在 "元件盤" 上(不過TForm是例外,雖然它也是繼承自TComponent).
首先要決定繼承自那一個元件, 因為我們要撰寫 "不可視元件",當然是 TComponent 了,如果您高興也可以繼承別的元件, 不過相信一定要額外處理比較多的事情, 我們就不找麻煩了.

1. TAutoClose 定時關閉元件所在TForm:

在如下畫面中選擇 New Component 新增元件, 此步驟以後不在重覆提到.

Ancestor type 中選擇要繼承的父階元件, 我們選 TComponent, Class Name 內輸入我們要建立的元件名稱, 這裡順便一提, 元件的撰寫慣例是用 T 開頭, 後接 2-3Byte的元件組名(自定), 其後才是此元件的名稱(詳細定義請參考煥麟的「請依照標準寫碼風格撰寫程式 Delphi 5 寫碼標準和「Delphi 5 元件型態字首」), 這麼做的原因是 Delphi 不允許 Class Name 重覆, 如果大家都亂編的話, 重覆的機率就很高了; 但我們只是示範如何寫元件就省略元件組名了.
但新增元件和 Class 有何關係, 我這樣說好了, 元件(物件)是類別(Class)的實作這樣說比較容易理解了吧!

設好了以後可以在 Unit file name 中選擇存檔的路徑及名稱, 按 OK 後出現如下內容就可以開始寫元件了, 但我先說明各區段的意義一下:

unit AutoClose;

interface

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

type
  TAutoClose = class(TComponent)
  private
    { Private declarations }
    私有區段.
    本段只有本元件可看見, 包含變數, function, procedure, property 
    所使用的function, procedure系統自動放於此元件的欄位定義在此如 Text
  protected
    { Protected declarations }
    保護區段.
    只有繼承此元件的[子元件]方可看見及使用, 若不要將某property開放出來, 只要不在子元件的 
    published 區段不宣告即可隱藏, 這有點像是散裝模型套件, 零件, 顏料, 膠水都有了, 
    只等子元件組裝加工, 一般像 TCustomXXX 此類元件都是(可以繼承, 但在元件盤上是看不到的)
    在此區段宣告可以讓繼承此元件的子元件決定那些要公開那些要隱藏, 因為VCL如果已經公開 就無法
    再隱藏, 例如: TEdit繼承自TCustomEdit 幾乎只是宣告那些要公開而已.
  public
    { Public declarations }
    公開區段.
    只有在 Run Time 時才可以存取本區段, 不會在 Object Inspector (就是 Design Time 
    時左邊那個視窗)出現, 本區段一般作物件的方法(method). 虛擬方法(virtual method)
    可分為虛擬(virtual)及動態(dynamic)二種, 虛擬的效率快但佔用較多資源, 我想可能用在
    和資料庫有關的地方比較多, 動態和虛擬相反, 效率慢但佔用較少資源, 但現在的電腦速度都很
    快的時候, 應該 VCL 大部份都採用動態方法, 虛擬方法可以達到 [多形]的效果, 可讓後代繼承
    者用 Override 改寫虛 擬(cirtual)及動態(dynamic)二種的效率差異主要是在VMT(Virtual
    Method Table) 的不 同, 使用虛擬方法其後代不論是否有用override改寫, 其父代各個虛擬
    方法的進入點(佔4Byte)會在子代的VMT中出現(這也是為何會比較佔用資源的原因, 如果有繼承
    到五代, 第五代就記錄了五個進點), 而動態方法如果沒有用override改寫, VMT不會出現此方
    法的進入點, 系統必須向上往父階查詢有沒有此動態方法(當然要一些時間) 
  published
    { Published declarations }
    公開設定區段(開放區段)
    本區段會出現在 Object Inspector 中, 可讓 User 在 Design Time 時設定, 大概長得如
    下這樣:
    property Text: string read FText write SetText default '' stored True;
    如果只有 read 沒有 write 則不會出現在Object Inspector中, 只能在Run Time使用(如同
    是在 public 區段中一樣)且此 property 是 read only, default Boolean 是如果設定
    值剛好是此值則不會存於.DFM(是Windows Resource File 格式)中可以減少執行檔的大小, 
    default 後可以放傳回   Boolean 值且不傳值的 function, 例如: IsStoreColor
    stored 設為 True (系統內定) 會將此property 值存於 .DFM中, False 不會存檔, 
    default 及 stored 可不用此區段會提供RTTI資訊, 讓系統使用(有關RTTI用另一章節討論)
    read (讀取資料) 及 write (設定資料) 後可接 Field 變數, 如: FText 或 不帶參數 
    procedure 或 function 為了解決 procedure 及 function不能傳參數的問題, Delphi 
    提供一個叫 Index 的指令可以達到 procedure 及 function 共用的目的, 宣告方式如下(
    程式碼在implementation部份): 
    property Text3: String Index 3 read GetText write SetText3;
    property Text2: String Index 2 read GetText write SetText2;
    property Text: String Index 1 read GetText write SetText;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TAutoClose]);
end;

end.

如果您看不懂以上的說明那也很正常, 這並不會影響後面的進度, 好啦 ! 開始寫作了.

因為我們要設定時間自動可以Close Form, 第一個想到的是用 TTimer 來作, 那就試試看吧!

unit AutoClose;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, extctrls;

type
  TAutoClose = class(TComponent)
  private
    { Private declarations }
    FTimer: TTimer;  
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TAutoClose]);
end;

end.

首先在 private 中宣告一個Field(欄位)叫 FTimer: Timer, 但 Delphi 竟然不認識 TTimer, 在 TTimer 上按 F1 查到是放在 extctrls 單元中, 我們就手動加入吧 ! 

再來宣告產生及毀滅本元件的程式, 在 public 中宣告:

constructor Create(AOwner: TComponent); override;
destructor Destroy; override;

以上名稱當然可以改, 但是我不建議改它, 因為這會讓 C++ (就是BCB) 無法使用本元件, 因為 C++ 的記憶體管理和Delphi不同.
好啦! 現在按 [Ctrl] + [Shift] + [C] 讓Delphi幫我們產生相關程序, 再手動加工成為如下程式:

private
... 省略 ...
procedure NewTimer(Sender: TObject);
... 省略 ...

constructor TAutoClose.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  FTimer := TTimer.Create(Self); 
  FTimer.Enabled := False;
  FTimer.OnTimer := NewTimer;
end;

destructor TAutoClose.Destroy;
begin
  FreeAndNil(FTimer);
  inherited Destroy;
end;

由於 Timer 是我們自己要用的物件, 所以要自己產生自己消滅, 加上產生 Timer 程式碼及時間到了以後要做什麼事的procedure (內容先不急得寫),

我們可以宣告二個 property Enabled(啟動AutoClose) 及 Interval (時間) 讓User 自己設定處理, 在 published 中宣告如下:

property Enabled: Boolean read GetEnabled write SetEnabled;
property Interval: Cardinal read GetInterval write SetInterval;

一樣按 [Ctrl] + [Shift] + [C] 讓Delphi幫我們產生相關程序, 再改成如下這樣:

function TAutoClose.GetEnabled: Boolean;
begin
  Result := FTimer.Enabled;
end;

function TAutoClose.GetInterval: Cardinal;
begin
  Result := FTimer.Interval;
end;

procedure TAutoClose.SetEnabled(const Value: Boolean);
begin
  FTimer.Enabled := Value;
end;

procedure TAutoClose.SetInterval(const Value: Cardinal);
begin
  FTimer.Interval := Value;
end;

procedure TAutoClose.NewTimer(Sender: TObject);
begin
  Enabled := False;
  ShowMessage('Hello My First Component');       
end;

這樣基本上元件就寫好了, 因為 Delphi VCL 是以 Package(.BPL)型式存在的, 所以需有一個 Package 容納我們寫的元件。接著 File ->New,如圖3:

選 Package 再選 OK:

出現如下視窗 按 Mouse 右鍵選 Save:

選擇要存 Package 的檔名及位置, 建議存在 Projects\BPL 下 , 決定好後選存檔:

回到(圖 5) 畫面, 按一下 Add,出現如圖 7 的視窗:

選[Browse] 將 AutoClose.pas 加入, 再選 OK。

然後選 Install 安裝元件到元件盤中。

好啦! 到元件盤的 Samples 頁中選 AutoClose 到 Form 中測試看看有沒有正常.

可以放到 Form 而且 Object Inspector 內也有這二個自訂property了, 應該沒問題了吧 ! 將 Enabled 設成 True 測試看看

可以出現 (圖 10) 對話框沒問題了.

但是這元件如果在 Design Time 那不是也有問題嗎 ? 沒錯, 所以要稍微加工改一下, 再將 TAutoClose 真正要做的事寫到 NewTimer 中, 完整程式碼如下:

unit AutoClose;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls;

type
  TAutoClose = class(TComponent)
  private
    { Private declarations }
    FEnabled: Boolean;
    FTimer: TTimer;
    FOnTimer: TNotifyEvent;

    procedure NewTimer(Sender: TObject);
    function GetInterval: Cardinal;
    procedure SetEnabled(const Value: Boolean);
    procedure SetInterval(const Value: Cardinal);
  protected
    { Protected declarations }
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

  published
    { Published declarations }
    property Enabled: Boolean read FEnabled write SetEnabled;
    property Interval: Cardinal read GetInterval write SetInterval;
    property OnTimer: TNotifyEvent read FOnTimer write FOnTimer;
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents('Samples', [TAutoClose]);
end;

{ TAutoClose }

constructor TAutoClose.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FTimer := TTimer.Create(Self);
  FTimer.Enabled := False;
  FTimer.OnTimer := NewTimer;
end;

destructor TAutoClose.Destroy;
begin
  FreeAndNil(FTimer);
  inherited Destroy;
end;

function TAutoClose.GetInterval: Cardinal;
begin
  Result := FTimer.Interval;
end;

procedure TAutoClose.SetEnabled(const Value: Boolean);
begin
  FEnabled := Value;
  // 確定在 Run Time 時才有作用
  if not (csDesigning in ComponentState) then
    FTimer.Enabled := Value; 
end;

procedure TAutoClose.SetInterval(const Value: Cardinal);
begin
  FTimer.Interval := Value;
end;

procedure TAutoClose.NewTimer(Sender: TObject);
begin
  Enabled := False;  // 先將 FTimer 關掉避免重覆觸發.
  // 如果 OnTimer 內有程式則執行.
  if Assigned(FOnTimer) then
    FOnTimer(Sender);
    
  (Owner as TForm).Close;
end;

end.

以上程式已經將 procedure GetEnabled 移除, 並宣告一個 FEnabled 欄位, 這樣作的目的是可以讓我們在 Design 中設 Enabled := True , 但不會真正的觸發 FTimer , 當然 procedure SetEnabled 也要改寫才行, 另外再增加一個 FOnTimer , 可以讓User 在 Form Close 前處理一些事情; 完成後再寫一個程式測試一下此元件是否可以正常運作, 例如: 在 Design Time 設 Enabled := True 看在Design Time 會不會動作, Run Time 是否會在設定時間時 Close Form, 如果沒問題那此元件就測試完成了; 但是您有沒有覺得內定的元件Icon有點醜, 而且如果每一個元件 ICON 都長得一樣 User也會弄錯, 接下來做元件 ICON 吧!
選 Image Editor , File -> New -> Component Resource File (.dcr)

按 Mouse 右鍵, New -> Bitmap

圖型長寬設成 24 x 24 這是 Delphi 元件盤可以接受的ICON大小

按 Mouse 右鍵, Rename 輸入 Class Name TAutoClose

在 Class Name 上, 快按 Mouse 左鍵二次, 開始畫 ICON 內容, 您也可以按
[Ctrl] + [I] 放大
[Ctrl] + [U] 縮小

存檔檔名設 AutoClose.dcr 並且必需和 AutoClose.pas 同目錄才可以重新Install TAutoClose 的 Package (MyPackage.dpk), 如果 AutoClose.dcr 沒有在 Package 中, 您要手動加進來, 完成後看 Samples 的頁面:

已經有我們自己設計的 ICON 在上面了…..

前面講解的不只是如何寫一個元件, 還包含程式的debug方法及製造流程, 雖然有一點繁複但是卻是問題最少, 比較不會發生程式寫完了, 卻發生程式也完蛋了的情形, 必竟良好的程式設計習慣也是很重要的.

附錄A: 訊息的應用

訊息(Message)的應用很廣, 精確的說,如果沒有訊息, Borland 的 VCL 應該架構不出來吧!

訊息的使用時機大部份是補您在元件設計上的不足, 或者是為了程式的精簡而使用, 像有時候需要對很多的元件作處理(例如:重畫元件), 如果一個一個去處理, 是乎不是個好方法, 尤其元件種類繁多, 這時用訊息就是很好的選擇.

訊息可以只對某元件傳送或是用廣播的方式送出, 但是接受的元件必需有相對應的處理程序, 而且必須是元件的 Month, 聽起來好像和網路的處理方式一樣, 沒錯! 是一樣的. 

VCL其實已經將訊息處理機制包含在裡面了, 在所有元件(物件)的始祖 TObject 中已經包含訊息處理機制, 也就是說所有的元件都有處理訊息的能力, 不過 VCL 將訊息包裝的比較簡單了, 所以使用起來就很簡單了, 接下來我就來示範如何使用訊息.

元件內的訊息廣播:

要能夠做訊息的廣播必須是 TWinControl 的後代子孫, 廣播使用的是 BroadCast 方法, 要做廣播大慨像以下這樣寫法

var 
  Msg: TMessage;
begin
  Msg.Msg := CM_REPAINT_BY_SELF;
  Msg.WParam := 0;
  Msg.LParam := 0;
  Msg.Result := 0;
  (Self as TWinControl).Broadcast(Msg);
end;

其中 CM_REPAINT_BY_SELF; 是我自己定的訊息, 當然您也可以使用 Windows 的訊息, 他們定義在Windows.pas 中, 如果要定義自定訊息要像以下這樣

const
  CM_REPAINT_BY_SELF = WM_USER + 100;

自定訊息必須以 WM_USER + 100 開始一直到 $7FFF 結束, 我想 Windows 提供的應該夠用了, 這個區域以外請不要使用, 因為 Windows 已經定義了這區域, 如果重覆定義可能會有想不到的問題發生

接受訊息部份程式像以下這樣:

procedure WMREPAINT_BY_SELF(var Message: TMessage); message CM_REPAINT_BY_SELF;

… 省略 …

procedure TahDBEdit.WMREPAINT_BY_SELF(var Message: TMessage);
begin
  Invalidate;
end;

以上例子只是收到訊息重畫元件而已, 當然您可以做很複雜的事. 不過以上範例最多只有侷限在一個 TForm 中而已.


待續...

2. TDoHotKey 簡單設定 HotKey
3. 有底圖的TLabel
4. TStatusProcess 在 TStatusBar 上加上 TProcessBar 功能
5. 資料感知元件:
6. 元件協同處理:
7. 自製密碼詢問元件:
8. 不同程序(Processe)程式(EXE),視窗(Form)的訊息廣播:

1