程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> Delphi控件的“拿來主義”

Delphi控件的“拿來主義”

編輯:Delphi

"一個優秀的Delphi程序員,不僅要會寫控件,還要會使用控件。"

我還是一個半瓢水的程序員,因此目前為止我所能努力達到的境界是:

一個半瓢水的程序員,管他會不會寫控件,只要能拿來改就可以了。

使用過Delphi的朋友都知道,我們在設計Delphi應用程序的某一功能時總是希望能夠有現成的第三方免費控件可以拿來使用。但實際情況往往是,網上的控件確實種類繁多,優秀作品也數不勝數,但真正完全能夠符合設計者具體要求的控件卻很少。究其原因,不外乎以下幾條

1、國人開發的應用程序大多具有中國特色,而優秀的第三方控件往往是國外程序開發人員開發的(大名鼎鼎的Rxlib,知道吧,俄羅斯人的作品)。功能定義上的差別導致了控件的不可用性,看看QuickReport就知道了(當然,QuickReport編的也很爛)。

2、真正優秀的第三方控件總是從大局上把握控件所能達到的功能,細節問題考慮的不多。而我們設計的程序通常要考慮到很多操作細節上的問題(一般是客戶的要求,有時會有些變態)

3、對於許多很有特色的控件,我們所需要的只是其中一部分功能,但是它被整合到了某一個大的方法中,無法為我們所用。

4、優秀和免費總是兩個矛盾,天下沒有免費的午餐。

種種原因,限制了開發者對控件資源的充分利用。其實,很多控件的代碼都提供了開放的接口。只要大家對控件的原理有一定的了解,然後對別人的控件代碼作一捏捏的修改,就可以無縫的添加自己需要的功能,或剔除冗余的功能。既達到了開發的目的,又避免了重寫代碼的麻煩,一舉兩得(好像很不負責任啊,沒辦法了,半瓢水嘛)。關於控件的原理,應該有很多文章介紹過了,在這裡我想通過一個具體的實例來介紹一下怎樣將別人的控件"拿來"為我所用。

這個例子是關於如何將一個文本數據導入導出控件作一些簡單的修改之後拿來使用的。

◆程序功能

將圖書館的ISO文件中的部分數據轉到Oracle8數據庫中。

◆設計要求

1.顯示導入進度條。

2.在導入過程中,如果某條紀錄導入失敗,不顯示異常,而將導入失敗的紀錄記入日志。

◆設計思路

程序的關鍵在文本數據字段的分離。通常的做法,都是先將字符串進行處理(RegulateString),然後把串中每個字符同分割符(可以是空格,逗號等)比較,將不是分割符的字符追加到一個串中(GetRecordItem),得到一個字段的內容。通過一個循環(循環次數由GetItemNum來定),就可以將一個字符串分成幾個字段。最後的工作就是將分離出來的數據對號入座加入數據庫。

按照上面的思路,利用Delphi提供的已有函數和過程,實現起來應該不難,但問題是,我可不想每次編文本導入程序的時候,都把什麼這啊那的函數過程重新定義一遍,哎,最煩的就是重復性的工作了.那麼有沒有現成的控件將上述過程都封裝起來呢?PS:又不用我編呢?

答案是肯定的!前幾天剛剛下了一個免費控件TPgCSV,據說可以實現文本的導入和導出.翻出來一看,正是我想要的。

在深入到下面的內容之前,有必要對該控件的類聲明部分作一定了解

//中文部分為筆者所作的注釋
//注意:
//在該控件中,Export代表將文本數據導入到數據庫,Import代表從數據庫導出到文本。??? 怎麼和我理解的
//導入導出概念剛好是反的 :)
type
//在處理數據產生異常時,可選擇繼續還是中止
TPgCSVErrorResponse = (pgcsvAbort, pgcsvIgnore);
//進程監控事件聲明,可以將導入/導出的進度作為參數傳出
TPgCSVProgressEvent = procedure (Sender : TObject; AProgress: LongInt; var StopIt: Boolean) of object;
//發生異常時的事件處理聲明,異常信息通過該接口傳給程序員。
TPgCSVExportErrorEvent = procedure (Sender : TObject; Mess: string; RecNo: LongInt; var Response:TPgCSVErrorResponse) of object;
TPgCSV = class(TComponent)
private
FDataset : TDataset;
FCSVMap,
FCSVFile,
FDateFormat,
FIgnoreStr : string;
FSeprator,
FDelimiter,
FFieldIndicator : Char;
FAutoOpen,
FUseDelimiter,
FSilentExport,
FTrimData,
FStop,
FEmptyTable : Boolean;
FBeforeOpenTable,
FAfterOpenTable,
FBeforeCloseTable,
FAfterCloseTable,
FBeforeEmptyTable,
FAfterEmptyTable,
FBeforeExport,
FAfterExport,
FBeforeImport,
FAfterImport,
FOnAddRecord : TNotifyEvent;
FExportProgress,
FImportProgress : TPgCSVProgressEvent;
FExportError : TPgCSVExportErrorEvent;
FMapItems,
FDefaultInt : Integer;
FBufferSize : LongInt;
FFieldCache : TList;
protected
FFile : TextFile;
//以下就是我所說的希望封裝的部分
function CountMapItems:Integer;//計算映射字符串的字段個數
function GetMapItem(ItemIndex:Integer;var AField:Boolean):string;//提取映射字符串的字段
function GetCSVRecordItem(ItemIndex:Integer;CSVRecord:string):string;//提取CSV文件字符串中的某一字段
function BuildMap:string;//自動創建映射,如果CSVMap一欄為空的話,會由它來產生映射字符串
function ExtractWord(Item: Integer;S, WordDelim: string): string;//提取文本數據字符串/映射字符串中的某一字段
function WordCount(const S ,WordDelim: string): Integer;//計算文本數據字符串/映射字符串中的字段數目
function WordPosition(Item: Integer; const S, SubStr: string): Integer;//計算子字符串在字符串中的位置
public
constructor Create(AOwner: TComponent); override;
published
//properties
property Dataset : TDataset read FDataset write FDataset;
//設置要導入或導出的目標數據集.
property CSVMap : string read FCSVMap write FCSVMap;
//CSV 文本數據文件到數據庫字段值的映射字符串.控件通過該映射決定文本中的哪些數據要導入及要導入哪個字段.
property CSVFile : string read FCSVFile write FCSVFile;
//CSV 文件格式,其實就是文本數據文件。CSV代表什麼意思?呵呵,我也不知道
property Seprator : Char read FSeprator write FSeprator;
//分隔符,可以是空格,也可以是,、;、#等符號
property FieldIndicator : Char read FFieldIndicator write FFieldIndicator;
//字段標識符.
property AutoOpen : Boolean read FAutoOpen write FAutoOpen;
//將AutoOpen設為True可以在處理數據前自動打開要導入的數據表並在操作完畢後自動關掉它。
property IgnoreString : string read FIgnoreStr write FIgnoreStr;
//忽略紀錄的標識串.
//舉例來說
//IgnoreString:='(ignore)';
//CSVMap:='$Name,(ignore),$Age';
//在這種情況下,CSVToDataSet方法,即導入數據方法將忽略文本文件中的第二列的字段。
property Delimiter : Char read FDelimiter write FDelimiter;
//在某些CSV文檔中標識字符串紀錄的標識符,比如,"john","boy",12中的",在這種情況下,TPgCSV
//會忽略這些標識符。
property EmptyTable : Boolean read FEmptyTable write FEmptyTable;
//只在從數據庫導出(DataSetToCSV)方法中有效,作用是創建一個新的CSV文件。
property UseDelimiter : Boolean read FUseDelimiter write FUseDelimiter;
//是否有Delimiter。
property SilentExport : Boolean read FSilentExport write FSilentExport;
//若該屬性為True,應用程序將不顯示數據操作時的異常,而將異常信息通過一個接口傳給程序員處理.
property DateFormat : string read FDateFormat write FDateFormat;
//設定CSV文件中日期數據的格式。
property TrimData : Boolean read FTrimData write FTrimData;
//是否去掉數據頭尾的空格.
property DefaultInt : Integer read FDefaultInt write FDefaultInt;
//整/實形數據轉換出錯後的默認值
property BufferSize : LongInt read FBufferSize write FBufferSize;
//CSV 文件的緩沖值,以字節為單位,可以加快導入和導出數據的速度。
//events
property BeforeOpenTable : TNotifyEvent read FBeforeOpenTable write FBeforeOpenTable;
property AfterOpenTable : TNotifyEvent read FAfterOpenTable write FAfterOpenTable;
property BeforeCloseTable : TNotifyEvent read FBeforeCloseTable write FBeforeCloseTable;
property AfterCloseTable : TNotifyEvent read FAfterCloseTable write FAfterCloseTable;
property BeforeEmptyTable : TNotifyEvent read FBeforeEmptyTable write FBeforeEmptyTable;
property AfterEmptyTable : TNotifyEvent read FAfterEmptyTable write FAfterEmptyTable;
property BeforeImport : TNotifyEvent read FBeforeImport write FBeforeImport;
property AfterImport : TNotifyEvent read FAfterImport write FAfterImport;
property BeforeExport : TNotifyEvent read FBeforeExport write FBeforeExport;
property AfterExport : TNotifyEvent read FAfterExport write FAfterExport;
property ExportProgress : TPgCSVProgressEvent read FExportProgress write FExportProgress;
//進程監控事件。每完成一條文本數據的導入就觸發該事件。
property ImportProgress : TPgCSVProgressEvent read FImportProgress write FImportProgress;
property OnAddRecord : TNotifyEvent read FOnAddRecord write FOnAddRecord;
property ExportError : TPgCSVExportErrorEvent read FExportError write FExportError;
//發生異常時交由該事件處理,異常信息通過該接口傳給程序員。
//methodes
//整個控件的核心內容
procedure CSVToDataset;//將文本導入到數據集的方法
procedure DatasetToCSV;//將數據集的數據導入到文本的方法
end;
procedure Register;
{略}
implementation
{略}
end.

從聲明部分中我們可以看到,TPgCSV將文本數據的導入導出全部封裝到了CSVToDataSet(文本數據導入)和DataSetToCSV(文本數據導出)兩個方法中.開發者可以在設計階段直接將文本文件同要導入/出的數據庫相連,然後在程序運行當中調用這兩個方法就可以了,根本不用理會那些函數什麼的,相當的方便.

那我是不是可以直接拿來使用呢?這裡就出現問題了.

----問題一及解決方案

在該控件自帶的Demo中,所使用的文本數據文件,格式如下

test.csv
"11","12","13","14"
"21","22","23","24"
"31","32","33","34"
...

而ISO文件中的數據格式(部分)為

sm01632.ISO

... a7507310175-b特精裝-d¥1893"?1 -a毛澤東珍品典藏(上、下冊)-f中共中央文獻研究室編著?c中獻-d011200"...

...-a7119029193-b平裝- d¥20"?1 -a中國:加入WTO與經濟改革-f王 夢 奎編著? -c外文-d011200"? ... -

我們看到,同test.csv相比,sm01632.ISO文件中的紀錄很不規則。

我們所需要的是這樣的紀錄

sm01632.ISO

... 7507310175,特精裝,1893,1,毛澤東珍品典藏(上、下冊),中共中央文獻研究室編著,中獻,011200...

...-7119029193,平裝,20,1,中國:加入WTO與經濟改革,王 夢 奎編著,外文,011200? ... -

TPgCSV能否對這種情況進行自動的處理呢?哇噻,這種萬能的控件好像不大可能有吧!(事實上也不需要)既然直接用TPgCSV處理無法實現正確導入的,我們就需要在每一條從文件中讀取出來的紀錄導入數據庫之前,用程序對它們進行處理,轉換之後再交由TPgCSV進行操作。怎麼樣處理這裡就不贅述了。我們所關心的是TPgCSV有沒有開放出這樣一個處理的接口呢?

我們來看DataSetToCSV方法的實現代碼(主要是注釋部分):

procedure TPgCSV.CSVToDataSet;
var
 RecordString,
 Temp : string;
 i : Integer;
 C : LongInt;
 D : Boolean;
 F : Real;
 ErrorResponse : TPgCSVErrorResponse;
 Buffer : Pointer;
begin
 //create field cache
 FFieldCache:=TList.Create;
 //initiate map items
 FMapItems:=0;
 //allocate buffer size
 GetMem(Buffer,FBufferSize);
 //assign and open CSV file
 AssignFile(FFile,FCSVFile);
 SetTextBuf(FFile,Buffer^,FBufferSize);
 Reset(FFile);
 //open table if nessecary
 if FAutoOpen then
 begin
  if Assigned(FBeforeOpenTable) then
   FBeforeOpenTable(Self);
   FDataset.Open;
   if Assigned(FAfterOpenTable) then
    FAfterOpenTable(Self);
   end;
   //export to table from CSV file
   if Assigned(FBeforeExport) then
    FBeforeExport(Self);
    //set the counter to zero
    C:=0;
    Temp:=ShortDateFormat;
    ShortDateFormat:=FDateFormat;
    {**********以下是文本數據導入的核心代碼部分,也是我要關心的部分********}
    FDataset.DisableControls;
    while (not Eof(FFile)) and (not FStop) do
    begin
     //read from CSV
     Readln(FFile,RecordString);
     //注意,這裡好像差了一點什麼東西
     //add new record
     try
      FDataset.Append;
      for i:=1 to CountMapItems do
       if Uppercase(GetMapItem(i,D)) <> Uppercase(FIgnoreStr) then
       case FDataset.FieldByName(GetMapItem(i,D)).DataType of
       ftInteger:
      FDataset.FieldByName(GetMapItem(i,D)).AsInteger:=
StrToIntDef(Trim(GetCSVRecordItem(i,RecordString)),FDefaultInt);
       ftFloat:
         begin
try
F:=StrToFloat(Trim(GetCSVRecordItem(i,RecordString)));
except
F:=FDefaultInt;
end;
FDataset.FieldByName(GetMapItem(i,D)).AsFloat:=F;
end;
else
if FTrimData then
FDataset.FieldByName(GetMapItem(i,D)).AsString:=
 Trim(GetCSVRecordItem(i,RecordString))
else
FDataset.FieldByName(GetMapItem(i,D)).AsString:=
 GetCSVRecordItem(i,RecordString);
end;
//post record
FDataset.Post;
except
on E:Exception do
if not FSilentExport then
raise
else
if Assigned(FExportError) then
begin
FExportError(Self,E.Message,C,ErrorResponse);
if ErrorResponse = pgcsvAbort then
Break;
end;
end;
if Assigned(FOnAddRecord) then
FOnAddRecord(Self);
if Assigned(FExportProgress) then
FExportProgress(Self, C, FStop);
Inc(C);
end;
FDataset.EnableControls;
{************以上是文本數據導入的核心代碼部分**************}
if Assigned(FAfterExport) then
FAfterExport(Self);
//close table if nessecary
if FAutoOpen then
begin
if Assigned(FBeforeCloseTable) then
FBeforeCloseTable(Self);
FDataset.Close;
if Assigned(FAfterCloseTable) then
FAfterCloseTable(Self);
end;
//close CSV file
CloseFile(FFile);
//disallocate buffer
FreeMem(Buffer);
ShortDateFormat:=Temp;
//free cache
for i:=FFieldCache.Count - 1 downto 0 do
Dispose(FFieldCache.Items[i]);
FFieldCache.Free;
end;

(這麼長!看得我眼都花了.好在找到了我所關心的核心代碼,其他的?別管了吧,我這個懶惰的人.)差的是什麼呢?很明顯,我們希望把這個時候的RecordString開放出去,經過處理之後再回來進行導入的操作.所以,這裡差的只是一個事件處理過程,在這個過程中,我們需要通過Delphi把RecordString的值傳出去讓用戶處理。

既然沒有提供這個接口,那就自己動手添加了。由於需要傳參數,這個事件不能用標准的TNotifyEvent來定義,而需要重新聲明。

新的事件聲明和事件屬性如下:

type
...
TPgCSVRegulateStrEvent = procedure (Sender : TObject; var ARecordString: string) of object;
...
TPgCSV = class(TComponent)
Published
property RegulateString : TPgCSVRegulateStrEvent read FRegulateString write FRegulateString;
//寫完後別忘了按一下ctrl+shift+c
...
End;

好了,現在可以在我剛才注釋的地方寫事件調用方法的程序了.

...
while (not Eof(FFile)) and (not FStop) do
begin
//read from CSV
Readln(FFile,RecordString);
//xm4014's modification
if Assigned(FRegulateString) then
FRegulateString(self,RecordString);
//add new record
try
FDataset.Append;
...

重新編譯包文件通過後,你就會在TPgCSV控件的事件頁面中發現RegulateString這個事件,雙擊它就可以添加你的處理代碼了。這樣一來,無論文本數據中有什麼樣怪異的字符或格式,我們都通過這個事件處理預先過濾一編,讓控件放心的處理導入的操作。簡單吧(也太簡單了點,居然還寫了這麼長!汗)

但是,這樣就行了嗎?

----問題二及解決方案

每一條導入表中的紀錄除了ISO文件中的內容外,還需要有書的類型,期號,以及ID號,而TPgCSV在插入新紀錄時只處理了同文本文件中的數據相關的字段,因此,這些字段的內容需要我們自己來加入。不用說,很自然的就會想到Table中的AfterInsert事件.但問題是,我的Table在DataModule單元中,我必須傳參數到AfterInsert事件中,麻煩!同時,這樣的處理會帶來維護上的混亂,一個單元裡面發生的事件應該盡量由這個單元裡面的函數或過程來處理。於是,我又想到了在當前單元中寫一個處理過程,然後在程序運行時把這個過程指派給Table的AfterInsert事件,導入結束後再禁止掉。可行,但還是麻煩!既然大多數情況下都會遇到這樣的問題,那何不一了百了,把這個事件封裝起來呢.

同問題一一樣,這裡我們還是需要一個事件處理過程AfterInsert.這一次應該加在哪兒呢?大家肯定一眼就看出來了:

procedure TPgCSV.CSVToDataSet;
 begin
  …
  FDataset.DisableControls;
  while (not Eof(FFile)) and (not FStop) do
   begin
    //read from CSV
    Readln(FFile,RecordString);
    //xm4014's modification
    if Assigned(FRegulateString) then
     FRegulateString(self,RecordString);
     //add new record
     try
      FDataset.Append;
      //應該加在這兒!
      //xm4014's modification
      if Assigned(FAfterInsert) then
       AfterInsert(self,FDataset);
       …
      for i:=1 to CountMapItems do
       …
    end;

同樣,這裡需要定義新的事件聲明和事件屬性,因為需要將FDataSet的參數傳遞出去,聲明代碼可參看Delphi控件的拿來主義(二)(http://www.csdn.net/develop/read_article.asp?id=11855).

重新編譯控件,就可在AfterInsert事件中添加代碼對書的類型,期號,以及ID號進行賦值了.

----問題三及解決方案

涉及到數據庫的問題現在都已經解決了.但是還有一個顯示問題.程序要求用ProgressBar顯示導入進度.為了設置ProgressBar.Max的值,我需要在導入之前知道ISO文件中一共有多少條紀錄,即一個類似於RecordCount的屬性.但是TPgCSV中沒有這樣的一個屬性.

那麼我們就來添加一個這樣的屬性

//xm4014's modification
property CSVRecordCount : integer read FCSVRecordCount write FCSVRecordCount default 0;

怎麼樣給它賦值呢? 很簡單,可以用ReadLn(F)對ISO文件進行掃描,然後將掃描的次數累加。

關鍵是在哪兒進行這個處理.很顯然,對於同一個文件,這樣的工作只需要做一次就可以了。既然對於不同的文件才需要重新統計,那麼我們可以在每次設置文件名屬性的時候,對文本文件進行掃描。

好,找到TPgCSV的文件名屬性

property CSVFile : string read FCSVFile write FCSVFile;

做一下小小的修改

新的屬性聲明

property CSVFile : string read FCSVFile write SetCSVFile;

按Ctrl+shift+c編寫SetCSVFile的方法代碼如下

procedure TPgCSV.SetFCSVFile(const Value: string);
var
 F1:TextFile;
 iCount:integer;
begin
 if FCSVFile<>Value then
 begin
  FCSVFile := Value;
  //文件名一換,就重新掃描,改變FCSVRecordCount的值
  if FileExists(Value) then
  begin
   AssignFile(F1, Value);
   Reset(F1);
   iCount:=0;
   while not Eof(F1) do
   begin
    ReadLn(F1);
    Inc(iCount);
   end;
   FCSVRecordCount:=iCount;
  end;
 end;
end;

編譯之後我們就可以在導入操作進行之前放心的調用CSVRecordCount屬性獲取紀錄個數值了:

ProgressBar1.Min :=0;
ProgressBar1.Max:=PgCSV1.CSVRecordCount;

以上程序在Delphi 6.0/Win98下調試通過

寫到這裡基本上算是大功告成了.其實前前後後做的修改掰著指頭都可以數出來.可就是這麼點小小的升級,讓我真正覺得用起來更加得心應手,我想,下次我再用的時候,會越發體會到它的價值.而且經過這麼一番分析修改,我也長進不少啊,比半瓢多了幾滴,呵呵!

需要指出的是,TPgCSV是一個簡單的控件,它既沒有很復雜的關系,也不涉及到對VCL核心內容的調用,所以在修改的時候是不用有太多的顧慮的.一旦源碼中涉及到很復雜的層次關系時,就需要對你添加或修改的每一行代碼深思熟慮了,不然,可能會牽一發而動全身,最後想回頭都難了.因此,我所說的拿來用的原則實際上還是依托於控件本身的功能,而沒有實質內容上的變動。要達到隨心所欲的境界,各位(我也在內)還要繼續努力啊!

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved