後一頁 前一頁 回目錄 回首頁 |
Delphi除了支援使用可視化部件所見即所得地建立應用程式外,還支援為開發應用而設計自己的部件。 在本章中將闡述如何為Delphi應用程式編寫部件。這一章將達到兩個目的: ● 教你如何自定義部件 ● 使你的部件成為Delphi環境的有機群群組合部分
19.1 Delphi部件原理
19.1.1 什麼是部件
部件是Delphi應用程式的程式構件。儘管大多數部件代表用戶介面的可見元素,但部件也可以是程式中的不可見元素,如資料庫部件。為弄清什麼是部件可以從三個方面來考察它:功能定義、技術定義和經驗定義。 1. 部件的功能定義 從最終用戶角度,部件是在Component Palette上選擇的,並在窗體設計視窗和代碼視窗中操作的元素。從部件編寫者角度,部件是代碼中的物件。在編寫部件之前,你應用相當熟悉已有的Delphi部件,這樣才能使你的部件適合用戶的需要。編寫部件的目標之一是使部件盡可能的類似其它部件。 2. 部件的技術定義 從最簡單的角度看,部件是任何從TComponent繼承的物件。TComponent定義了所有部件必須要的、最基本的行為。例如,出現在Component Palette上和在窗體設計視窗中編輯的功能。但是TComponent並不知如何處理你的部件的具體功能,因此,你必須自己描述它。 3. 部件編寫者自己的定義。 在實際程式設計中,部件是能插入Delphi開發環境的任何元素。它可能具有程式的各種複雜性。簡而言之,只要能融入部件框架,部件就是你用代碼編寫的一切。部件定義只是接口描述,本章將詳細闡述部件框架,敘述部件的有限性,正如敘述程式設計的有限性。本章不準備教你用所給語言編寫每一種部件,只能告訴編定代碼的方法和怎樣使部件融入Delphi環境。
19.1.2 編寫部件的不同之處
在Delphi環境中建立部件和在應用程式中使用部件有三個重要差別: ● 編寫部件的過程是非可視化的 ● 編寫部件需要更深入的關於物件的知識 ● 編寫部件需要遵循更多的規則
1. 編寫部件是非可視化的 編寫部件與建立Delphi應用最明顯的區別是部件編寫完全以代碼的形式進行,即非可視化的 。因為Delphi應用的可視化設計需要已完成的部件,而建立這些部件就需要用Object Pascal 代碼編寫。 雖然你無法使用可視化工具來建立部件,但你能運用 Delphi開發環境的所有程式設計特性如代碼編輯器、整合化偵錯和物件瀏覽。 2. 編寫部件需要更深的有關物件的知識 除了非可視化程式設計之外,建立部件和使用它們的最大區別是:當建立新部件時,需要從已存部件中繼承產生一個新物件型式,並增加新的屬性和方法。另一方面,部件使用者,在建立Delphi應用時,只是使用已有部件。在設計階段通過改變部件屬性和描述響應事件的方法來定制它們的行為。 當繼承產生一個新物件時,你有權存取祖先物件中對最終用戶不可見的部分。這些部分被稱為protected介面的。在很大部分的實現上,後代物件也需要呼叫他們的祖先物件的方法,因此,編寫部件者應相當熟悉面向物件程式設計特性。 3. 編寫部件要遵循更多的規則 編寫部件過程比可視化應用產生採用更傳統的程式設計方法,與使用已有部件相比,有更多的規則要遵循。在開始編寫自己的部件之前,最重要的事莫過於熟練應用Delphi自帶的部件,以得到對命名規則以及部件用戶所期望功能等的直觀認識。部件用戶期望部件做到的最重要的事情莫過於他們在任何時候能對部件做任何事。編寫滿足這些期望的部件並不難,只要預先想到和遵循規則。
19.1.3 建立部件過程概略
簡而言之,建立自定義部件的過程包含下列幾步: ● 建立包含新部件的庫單元 ● 從已有部件型式中繼承得到新的部件型式 ● 增加屬性、方法和事件 ● 用Delphi註冊部件 ● 為部件的屬性方法和事件建立Help文件
如果完成這些工作,完整的部件包含下列4個文件 ● 編譯的庫單元 ( .DCU文件) ● 選擇板點陣圖 (.DCR文件) ● Help文件 (.HLP文件) ● Help-keyword文件 (.KWF文件)
19.2 Delphi部件程式設計方法
19.2.1 Delphi部件程式設計概述
19.2.1.1 Delphi可視部件類庫
Delphi的部件都是可視部件類庫(VCL)的物件繼承樹的一部分,下面列出群群組成VCL的物件的關係。TComponent是VCL中每一個部件的共同祖先。TComponent提供了Delphi部件正常工作的最基本的屬性和事件。庫中的各條分支提供了其它的更專一的功能。 當建立部件時,通過從物件樹中已有的物件繼承獲得新物件,並將其加入VCL中。 19.2.1.2 建立部件的起點 部件是你在設計時想操作的任意程式元素。建立新部件意味著從已有型式中繼承得到新的部件物件類。 建立新部件的主要途徑如下: ● 修改已有的控制 ● 建立原始控制 ● 建立圖形控制 ● 建立Windows控制的子類 ● 建立非可視部件
下表列出了不同建立途徑的起始類
表19.1 定義部件的起始點 ───────────────────────────── 途 徑 起 始 類 ————————————————————————————— 修改已有部件 任何已有部件,如TButton、TListBox 或抽象部件物件如TCustomListBox 建立原始控制 TCustomControl 建立圖形控制 TGraphicControl 建立視窗控制的子類 TWinControl 建立非可視部件 TComponent ─────────────────────────────
也可以繼承非部件的其它物件,但無法在窗體設計視窗中操作它們。Delphi包括許多這種物件,如TINIFile、TFont等。 1. 修改已有控制 建立部件的最簡單的方法是繼承一個已有的、可用的部件並定制它。可以從Delphi提供的任何部件中繼承。例如,可以改變標準控制的缺省屬性值,如TButton。 有些控制,如Listbox和Grid等有許多相同變數,在這種情況下,Delphi提供了抽象控制型式,從該型式出發可定制出許多的型式。例如,你也許想建立TListBox的特殊型式,這種部件沒有標準TListBox的某些屬性,你不能將屬性從一個祖先型式中移去,因此你需要從比TListBox更高層次的部件繼承。例如TCustomListBox,該部件實現了TCustomListBox的所有屬性但沒有公佈(Publishing)它們。當從一個諸如TCustomListBox的抽象類中繼承時,你公佈那些你想使之可獲得的屬性而讓其它的保護起來(protected)。 2. 建立原始控制 標準控制是在執行時可見的。這些標準控制都從TWinControl,繼承來的,當你建立原始控制時,你使用TCustomControl作為起始點。標準控制的關鍵特征是它具有視窗句柄,句柄存檔在屬性Handle中,這種控制: ● 能接受輸入焦點 ● 能將句柄轉送給Windows API函數
如果控制不需要接受輸入焦點,你可把它做成圖形控制,這可能節省系統資源。 3. 建立圖形控制 圖形控制非常類似定制的控制,但它們沒有視窗句柄,因此不佔有系統資源。對圖形控制最大的限制是它們不能接收輸入焦點。你需要從TGraphicControl繼承,它提供了作圖的Canvas和能處理WM_PAINT訊息,你需要覆蓋Paint方法。 4. 繼承視窗控制 Windows中有一種稱之為視窗類的概念,類似於面向物件的物件和類的概念。視窗類是Windows中相同視窗或控制的不同實例之間共享的資訊集合。當你用傳統的Windows程式設計方法建立一種新的控制,你要定義一個新的視窗類,並在Windows中註冊。你也能基於已有的視窗類建立新的視窗類。這就稱為從視窗類繼承。在傳統的Windows程式設計中,如果你想建立用戶化的控制,你就必須將其做在動態鍊結庫裏,就象標準Windows控制,並且提供一個存取介面。使用Delphi,你能建立一個部件包裝在已有視窗類之上。如果你已有用戶化控制的庫,並想使其執行在你的Delphi應用中,那你就能建立一個使你能使用已有控制和獲得新的控制的部件。在庫單元StdCtrls中有許多這樣的例子。 5. 建立非可視化的部件 抽象物件型式TComponent是所有部件的基礎型式。從TComponent直接繼承所建立的部件就是非可視化部件。你編寫的大多數部件都是可視控制。TComponent定義了部件在FormDesigner中所需的基本的屬性和方法。因此,從TComponent繼承來的任何部件都具備設計能力。 非可視部件相當少,主要用它們作為非可視程式單元(如資料庫單元)和對話方塊的介面。
19.2.1.3 建立新部件的方法
建立新部件的方法有兩種: ● 手工建立部件 ● 使用Component Expert
一旦完成建立後,就得到所需的最小功能單位的部件,並可以裝設在Component Palette上。裝設完後,你就能將新部件放置在窗體視窗,並可在設計階段和執行階段進行測試。你還能為部件增加新的特征、更新選擇板、重新測試。 1. 手工建立部件 顯然建立部件最容易的方法是使用Component Expert。然而,你也能通過手工來完成相同步驟。 手工建立部件需要下列三步: ● 建立新的庫單元 ● 繼承一個部件物件 ● 註冊部件
⑴ 建立新的庫單元 庫單元是Object Pascal代碼的獨立編譯單位。每一個窗體有自己的庫單元。大多數部件(在邏輯上是一群群組)也有自己的庫單元。 當你建立部件時,你可以為部件建立一個庫單元,也可將新的部件加在已有的庫單元中。 ① 為部件建立庫單元,可選擇File/New... ,在New Items對話方塊中選擇Unit,Delphi將建立一個新文件,並在代碼編輯器中打開它 ② 在已有庫單元中增加部件,只須選擇File/OPen為已有庫單元選擇源代碼。在該庫單元中只能包含部件代碼,如果該庫單元中有一個窗體,將產生錯誤
⑵ 繼承一個部件物件 每個部件都是TComponent的後代物件。也可從TControl、TGraphicControl等繼承。 為繼承一個部件物件,要將物件型式聲明加在庫單元的interface部分。 例如,建立一個最簡單的從TComponent直接繼承非可視的部件,將下列的型式定義加在部件單元的interface部分。
type TNewComponent=class(TComponent) …… end;
現在你能註冊TNewComponent。但是新部件與TComponent沒什麼不同,你只建立了自己部件的框架。 ⑶ 註冊部件 註冊部件是為了告訴Delphi什麼部件被加入部件庫和加入Component Palette的哪一頁。 為了註冊一個部件: ① 在部件單元的interface部分增加一個Register過程。Register不帶任何參數,因此聲明很簡單:
procedure Register;
如果你在已有部件的庫單元中增加部件,因為已有Register 過程,因此不須要修改聲明。 ② 在庫單位的implementation部件編寫Register過程為每一個你想註冊的部件呼叫過程RegisterComponents,過程RegisterComponents帶兩個參數:Component Palette的頁名和部件型式集。例如,註冊名為TNewComponent的部件,並將其置於Component Palette的Samples頁,在程式中使用下列過程:
procedure Register; begin RegisterComponents('Samples', [TNewComponent]); end;
一旦註冊完畢,Delphi自動將部件圖示顯示在Component Palette上。 2. 使用Component Expert(部件專家) 你能使用Component Expert建立新部件。使用Component Expert簡化了建立新部件最初階段的工作,因為你只需描述三件事: ● 新部件的名字 ● 祖先型式 ● 新部件要加入的Component Palette頁名
Component Expert執行了手工方式的相同工作: ● 建立新的庫單元 ● 繼承得到新部件物件 ● 註冊部件
但Component Expert不能在已有單元中增加部件。 可選擇File/New... ,在New Items對話方塊中選擇Component,就打開Component Expert對話方塊。 填完Component Expert對話方塊的每一個欄位後,選擇OK。Delphi建立包括新部件和Register過程的庫單元,並自動增加uses語句。 你應該立刻存檔庫單元,並給予其有意義的名字。
19.2.1.4. 測試未裝設的部件
在將新部件裝設在Component Palette之前就能測試部件執行時的動作。這對於偵錯新部件特別有用,而且還能用同樣的技術測試任意部件,無論該部件是否出現在Component Palette上。 從本質上說,你通過模仿用戶將部件放置在窗體中的Delphi的動作來測試一個未裝設的部件。 可按下列步驟來測試未裝設的部件 1. 在窗體單元的uses語句中加入部件所在單元的名字 2. 在窗體中增加一個物件欄位來表示部件 這是自己增加部件和Delphi增加部件的方法的主要不同點。 你將物件欄位加在窗體型式聲明底部的public部分。Delphi則會將物件欄位加在底部聲明的上面。 你不能將欄位加在Delphi管理的窗體型式的聲明的上部。在這一部分聲明的物件欄位將相應在存儲在DFM文件中。增加不在窗體中存在的部件名將產生DFM文件無效的錯誤。 3. 附上窗體的OnCreate事件處理過程 4. 在窗體的OnCreate處理過程中構造該部件 當呼叫部件的構造過程時,必須傳遞Owner參數(由Owner負責析構該部件)一般說來總是將Self作為Owner的傳入參數。在OnCreate中,Self是指窗體。 5. 給Component的Parent屬性賦值 設定Parent屬性往往是構造部件後要做的第一件事時。Parent在形式上包含部件,一般來說Parent是窗體或者GoupBox、Panel。通常給Parent賦與Self,即窗體。在設定部件的其它屬性之前最好先給Parent賦值。 6. 按需要給部件的其它屬性賦值 假設你想測試名為TNewComponent型式的新部件,庫單元名為NewTest。窗體庫單元應該是這樣的;
unit Unitl;
interface
uses SysUtils, Windows, Messages, Classes, Grophics, Controls, Forms, Dialogs, Newtest; type Tforml = class(TForm) procedure FormCreate(Sender: TObject); private { private申 明 } public { public申 明 } NewComponent: TNewComponent; end;
var Forml: TForml;
implementation
{$R *.DFM }
procedure TForml.FormCreate ( Sender: TObject ) ; begin NewComponent := TNewComponent.Create ( Self ); NewCompanent.Parent := Self; NewCompanent.Left := 12; end;
end.
19.2.1.5 編寫部件的面向物件技術
部件使用者在Delphi環境中開發,將遇到在包含數據和方法的物件。他們將在設計階段和執行階段操作物件,而編寫部件將比他們需要更多的關於物件的知識,因此,你應當熟悉Delphi的面向物件的程式設計。 1. 建立部件 部件用戶和部件編寫者最基本的區別是用戶處理物件的實例,而編寫者建立新的物件型式。這個概念是面向物件程式設計的基礎。例如,用戶建立了一個包含兩個按鈕的窗體,一個標為OK,另一個標為Cancel,每個都是TButton的實例,通過給Text、default和Cancel等屬性賦不同的值,給OnClick事件賦予不同的處理過程,用戶產生了兩個不同的實例。 建立新部件一般有兩個理由 ● 改變型式的缺省情況,避免反復 ● 為部件增加新的功能
目的都是為了建立可重用物件。如果從將來重用的角度預先計劃和設計,能節省一大堆將來的工作。 在程式設計中,避免不必要的重複是很重要的。如果發現在代碼中一遍又一遍重寫相同的行,就應當考慮將代碼放在子過程或函數中,或乾脆建立一個函數庫。 設計部件也是這個道理,如果總是改變相同的屬性或相同的方法呼叫,那應建立新部件。 建立新部件的另一個原因是想給已有的部件增加新的功能。你可以從已有部件直接繼承(如ListBox)或從抽象物件型式繼承(如TComponent,TControl)。你雖然能為部件增加新功能,但不能將原有部件的屬性移走,如果要這樣做的話,就從該父物件的祖先物件繼承。 2. 控制部件的訪向 Object Pascal語言為物件的各部分提供了四個級別的存取控制。存取控制讓你定義什麼代碼能存取物件的哪一部分。通過描述存取級別,定義了部件的接口。如果合理安排接口,將提高部件的可用性和重用性。 除非特地描述,否則加在物件裏的欄位、方法和屬性的控制級別是published,這意味著任何代碼可以存取整個物件。 下表列出各保護級別:
表19.2 物件定義中的保護級別 ─────────────────── 保護級 用處 ——————————————————— private 隱藏實現細節 protected 定義開發者接口 public 定義執行時接口 published 定義設計時接口 ───────────────────
所有的保護級都在單元級起作用。如果物件的某一部分在庫單元中的一處可訪向,則在該庫單元任意處都可訪向。 ⑴ 隱藏實現細節 如果物件的某部分被聲明為private,將使其它庫單元的代碼無法存取該部分,但包含聲明的庫單元中的代碼可以存取,就好象存取public一樣,這是和C++不同的。 物件型式的private部分對於隱藏詳細實現是很重要的。既然物件的用戶不能存取,private部分,你就能改變物件的實現而不影響用戶代碼。 下面是一個演示防止用戶存取private欄位的例子:
unit HideInfo;
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs;
type TSecretForm = class(TForm) { 聲明新的窗體視窗 } procedure FormCreate(Sender: TObject); private { declare private part } FSecretCode: Integer; { 聲明private欄位 } end;
var SecretForm: TSecretForm;
implementation
procedure TSecretForm.FormCreate(Sender: TObject); begin FSecretCode := 42; end;
end.
unit TestHide; { 這是主窗體庫單元 }
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms, Dialogs, HideInfo; { 使用帶TSecretForm聲明的庫單元 } type TTestForm = class(TForm) procedure FormCreate(Sender: TObject); end;
var TestForm: TTestForm;
implementation
procedure TTestForm.FormCreate(Sender: TObject); begin SecretForm.FSecretCode := 13; {編譯過程將以"Field identifier expected"錯誤停止} end;
end.
⑵ 定義開發者接口 將物件某部分聲明為protected,可使在包含該部件聲明的庫單元之外的代碼無法存取,就象private部分。protected部分的不同之處是,某物件繼承該物件,則包含新物件的庫單元可以存取protected部分,你能使用protected聲明定義開發者的接口。也就是說。物件的用戶不能訪向protected部分,但開發者通過繼承就可能做到,這意味著你能通過protected部分的可存取性使部件編寫者改變物件工作方式,而又不使用戶見到這些細節。 ⑶ 定義執行時接口 將物件的某一部分定義為public可使任何代碼存取該部分。如果你沒有對欄位方法或屬性加以private、protected、public的存取控制描述。那麼該部分就是published。 因為物件的public部分可在執行時為任何代碼存取,因此物件的public部分被稱為執行接口。執行時接口對那些在設計時沒有意義的項目,如依靠執行時資訊的和唯讀的屬性,是很有用的。那些設計用來供用戶呼叫的方法也應放在執行時接口中。 下例是一個顯示兩個定義在執行時接口的唯讀屬性的例子:
type TSampleComponent = class(TComponent) private FTempCelsius: Integer; { 具體實現是private } function GetTempFahrenheit: Integer; public property TempCelsius: Integer read FTempCelsius; { 屬性是public } property TempFahrenheit: Integer read GetTempFahrenheit; end;
function GetTempFahrenheit: Integer; begin Result := FTempCelsius * 9 div 5 + 32; end;
既然用戶在設計時不能改變public部分的屬性的值,那麼該類屬性就不能出現在Object Inspector視窗中。 ⑷ 定義設計時接口 將物件的某部分聲明為published,該部分也即為public且產生執行時型式資訊。但只有published部分定義的屬性可顯示在Object Inspector視窗中。物件的published部分定義了物件的設計時接口。設計時接口包含了用戶想在設計時定制的一切特征。 下面是一個published屬性的例子,因為它是published,因此可以出現在Object Inspector視窗:
TSampleComponent = class(TComponent) private FTemperature: Integer; { 具體實現是 private } published property Temperature: Integer read FTemperature write FTemperature; { 可寫的 } end;
3. 派送方法 派送(Dispatch)這個概念是用來描述當呼叫方法時,你的應用程式怎樣決定執行什麼樣的代碼,當你編寫呼叫物件的代碼時,看上去與任何其它過程或函數呼叫沒什麼不同,但物件有三種不同的派送方法的方式。 這三種派送方法的型式是: ● 靜態的 ● 虛擬的 ● 動態的
虛方法和動態方法的工作方式相同,但實現不同。兩者都與靜態方法相當不同。理解各種不同的派送方法對建立部件是很有用的。 ⑴ 靜態方法: 如果沒有特殊聲明,所有的物件方法都是靜態的.。靜態方法的工作方式正如一般的過程和函數呼叫。在編譯時,編譯器決定方法位址,並與方法聯接。 靜態方法的基本好處是派送相當快。因為由編譯器決定方法的臨時位址,並直接與方法相聯。虛方法和動態方法則相反,用間接的方法在執行時找到方法的位址,這將花較長的時間。 靜態方法的另一個不同之處是當被另一型式繼承時不做任何改變,這就是說如果你聲明了一個包含靜態方法的物件,然後從該物件繼承新的物件,則該後代物件享有與祖先物件相同的方法位址,因此,不管實際物件是誰,靜態方法都完成相同的工作。 你不能覆蓋靜態方法,在後代物件中聲明相同標簽的靜態方法都將取代祖先物件方法。 在下列代碼中,第一個部件聲明了兩靜態方法,第二個部件,聲明了相同名字的方法取代第一個部件的方法。
type TFirstComponent = class(TComponent) procedure Move; procedure Flash; end;
TSecondComponent = class(TFirstComponent) procedure Move; { 儘管有相同的聲明,但與繼承的方法不同 } function Flash(HowOften: Integer): Integer; { 同Move方法一樣 } end;
⑵ 虛方法 呼叫虛方法與呼叫任何其它方法一樣,但派送機制有所不同。虛方法支援在後代物件中重定義方法,但呼叫方法完全相同,虛方法的位址不是在編譯時決定,而是在執行時才找到方法的位址。 為聲明一個新的方法,在方法聲明後增加virtual指令。方法聲明中的virtual指令在物件虛擬方法表(VMT)中建立一個入口,該虛擬方法表存檔物件類所有虛有擬方法的位址。 當你從已有物件獲得新的物件,新物件得到自己的VMT,它包含所有的祖先物件的VMT入口,再增加在新物件中聲明的虛擬方法。後代物件能覆蓋任何繼承的虛擬方法。 覆蓋一個方法是擴展它,而不是取代它。後代物件可以重定義和重實現在祖先物件中聲明的任何方法。但無法覆蓋一個靜態方法。覆蓋一個方法,要在方法聲明的結尾增加override指令,在下列情況,使用override將產生編譯錯誤: ● 祖先物件中不存在該方法 ● 祖先物件中相同方法是靜態的 ● 聲明與祖先物件的(如名字、參數)不匹配
下列代碼演示兩個簡單的部件。第一個部件聲明了三個方法,每一個使用不同的派送方式,第二個部件繼承第一個部件,取代了靜態方法,覆蓋了虛擬方法和動態方法。
type TFirstComponent = class(TCustomControl) procedure Move; { 靜態方法 } procedure Flash; virtual; { 虛 方 法 } procedure Beep; dynamic; { 動態虛擬方法 } end;
TSecondComponent = class(TFirstComponent) procedure Move; { 聲明了新的方法 } procedure Flash; override; { 覆蓋繼承的方法 } procedure Beep; override; { 覆蓋繼承的方法 } end;
⑶ 動態方法 動態方法是稍微不同於虛擬方法的派送機制。因為動態方法沒有物件VMT的入口,它們減少了物件消耗的記憶體數量。派送動態方法比派送一般的虛擬方法慢。因此,如果方法呼叫很頻繁,你最好將其定義為虛方法。 定義動態方法時,在方法聲明後面增加dynamic指令。 與物件虛擬方法建立入口不同的是dynamic給方法賦了一數字,並存儲相應代碼的位址,動態方法清單只包含新加的和覆蓋的方法入口,繼承的動態方法的派送是通過找到每一個祖先的動態方法清單(按與繼承“反轉的順序”),因此動態方法用於處理訊息(包括Windows訊息)。實際上,訊息處理過程的派送方式與動態方法相同,只是定義方法不同 ⑷ 物件與指標 在Object Pascal中,物件實際上是指標。編譯器自動地為程式建立物件指標,因此在大多數情況下,你不需要考慮物件是指標。但當你將物件作為參數傳遞時,這就很重要了。通常,傳遞物件是按值而非按引用,也就是說,將物件聲明為過程的參數時,你不能用var參數,理由是物件已經是指標引用了。 |
後一頁 前一頁 回目錄 回首頁 |