Delphi 內建的資料庫開發工具(如 Database Form Expert)功能強大,讓開發者無需撰寫任何程式碼,即可快速建立簡單的資料庫應用程式,甚至能建立基於多個資料表的主從式應用。本文將介紹使用 Delphi 開發簡單資料庫應用的一般方法與步驟,並以安裝目錄下的 MASTAPP 範例為基礎,帶領讀者逐步掌握從單表瀏覽到多表關聯、欄位物件操作、資料查詢與修改等核心技術。
啟動新專案,將 Form1 的 Caption 設為 CustomerForm1,Name 設為 CustomerForm1。從元件選擇版的 Data Access 頁面拖入一個 TDataSource 和一個 TTable 元件(它們是非可視元件,僅在設計時顯示圖示);再從 Data Controls 頁面拖入一個 TDBGrid 放在兩者下方。完成後表單如圖 14.1 所示。
為了讓 TDBGrid 顯示 Customer.db 表的內容,必須正確設定三個元件的關鍵屬性,如下表:
| 元件.屬性 | 屬性值 |
|---|---|
| DataSource1.AutoEdit | False |
| DataSource1.DataSet | Table1 |
| Table1.DatabaseName | DBDEMOS |
| Table1.TableName | CUSTOMER.DB |
| Table1.Active | True |
| DBGrid1.DataSource | DataSource1 |
注意事項:
DBDEMOS 是 Delphi 預設安裝時 C:\Delphi\Demos\DB\Mastapp 目錄的別名,請確認路徑正確。DataSource1.DataSet、Table1.TableName、DBGrid1.DataSource 皆提供下拉清單,方便選取,不易出錯。DataSource1.AutoEdit = False 可防止使用者直接修改資料;若需允許編輯,改為 True 即可。Table1.Active = True 時會開啟資料表;若資料表不存在或為空,會彈出錯誤並自動將 Active 設回 False。欲修改 DatabaseName 或 TableName,必須先將 Active 設為 False。儲存單元為 Cust.pas,專案為 CustPrj.dpr,按 F9 編譯執行。執行後可用捲軸或鍵盤方向鍵瀏覽記錄,但無法修改資料(因 AutoEdit 為 False)。
三個元件各司其職:
若需更友善的介面,可改用 TDBEdit、TDBNavigator 等單獨資料瀏覽元件。
新增專案,表單 Name 設為 CustomerForm2,Caption 同。放置 TDataSource、TTable 於右上角,TDBNavigator 於左上角。依圖 14.3 配置 TDBEdit 與 TLabel,分別命名為 EditCustNo、EditCompany、EditAddr1…EditContact。設定所有資料瀏覽元件的 DataSource 為 DataSource1,並將每個 TDBEdit 的 DataField 屬性對應至資料表對應欄位(如 EditCustNo 對應 CustNo)。儲存單元為 Cust2.pas,專案為 CustPrj2.dpr。
執行後顯示第一筆記錄。按 Tab 鍵可在欄位間移動,但無法編輯(Table1.AutoEdit = False)。若需編輯、新增、刪除或瀏覽其他記錄,需點擊 TDBNavigator 上對應按鈕。各按鈕功能如圖 14.4 所示,Navigator 會自動偵測是否位於首/尾記錄,並啟用/停用對應按鈕。
若要禁止任何修改,可設 DBGrid.ReadOnly = True 且 Options.dgEditing = False。
主表(如 Customer.db)存放綜合資訊,明細表(如 Orders.db)存放詳細資訊,兩者透過共同欄位(如 CustNo)關聯。建立兩個 TTable、兩個 TDataSource、兩個 TDBGrid、一個 TDBNavigator,屬性設定如表 14.2:
| 元件 | 屬性 | 值 | 備註 |
|---|---|---|---|
| Table1 (主表) | Active | True | |
| DatabaseName | DBDEMOS | ||
| TableName | CUSTOMER.DB | ||
| DataSource1 | DataSet | Table1 | |
| AutoEdit | False | ||
| Table2 (明細表) | Active | True | |
| DatabaseName | DBDEMOS | ||
| TableName | ORDERS.DB | ||
| IndexFieldNames | CUSTNO | 指定索引欄位 | |
| MasterFields | CUSTNO | 與主表關聯欄位 | |
| MasterSource | DataSource1 | 主表資料來源 | |
| DataSource2 | DataSet | Table2 | |
| AutoEdit | False | ||
| DBGrid1 | DataSource | DataSource1 | 對應主表 |
| DBGrid2 | DataSource | DataSource2 | 對應明細表 |
| DBNavigator1 | DataSource | DataSource1 | 控制主表導覽 |
操作 DBNavigator 移動主表記錄時,DBGrid2 會自動顯示該客戶的所有訂單明細。
擴展為三層:Customer.db → Orders.db → Items.db。Orders 同時扮演 Customer 的明細表與 Items 的主表。需三組 Table/DataSource/DBGrid,關鍵屬性如表 14.3:
IndexFieldNames=CUSTNO,MasterFields=CUSTNO,MasterSource=DataSource1IndexFieldNames=ORDERNO,MasterFields=ORDERNO,MasterSource=DataSource2TField 物件對應資料表各欄位,常見類型見表 14.4:
| 類型 | 對應資料型別 |
|---|---|
| TBooleanField | 布林 |
| TCurrencyField | 貨幣 |
| TStringField | 字串 |
| TIntegerField | 整數 |
| TBlobField | 大型二進位物件 |
程式碼中無須關心實際類型,主要差異在於內部儲存與傳遞的資料型別。
Table1.Active = True 顯示資料。選取 Table1 → 雙擊開啟 Fields Editor → 選取欄位 → 在 Object Inspector 修改屬性。常用屬性見表 14.5:
| 屬性 | 說明 |
|---|---|
| Alignment | 顯示對齊:左、右、置中 |
| Calculated | True 表示為計算欄位 |
| DisplayLabel | 網格顯示的標題(預設為欄位名) |
| DisplayWidth | 網格欄寬(字元數) |
| DisplayFormat / EditMask | 顯示格式與輸入遮罩 |
| FieldName | 資料表中對應欄位名 |
| Index | 欄位在資料集中的邏輯位置(從 0 起算) |
| Name | 欄位物件名稱(如 Table1CUSTNO),可用 Table1CUSTNO.Value 存取值 |
| ReadOnly | True 時不可修改 |
| Visible | True 時在網格中顯示 |
範例修改 DisplayLabel 為中文標題(表 14.6),執行結果如圖 14.10。
透過 Table1.Fields[索引] 或 Table1.FieldByName('欄位名') 存取。例如:
Table1.Fields[0].DisplayLabel := '客戶編號';
Table1.FieldByName('Company').DisplayLabel := '公司名稱';
在 FormCreate 事件中批次設定:
procedure TForm1.FormCreate(Sender: TObject);
begin
with Table1 do begin
Fields[0].DisplayLabel := '客戶編號';
FieldByName('Company').DisplayLabel := '公司名稱';
FieldByName('Phone').DisplayLabel := '電話號碼';
FieldByName('LastInvoiceDate').DisplayLabel := '購買日期';
end;
end;
直接以 Name 屬性存取,如 Table1CustNo.DisplayLabel := '客戶編號';。
透過 Value 屬性。範例:按鈕點擊時將 Company 欄位值讀入 Edit1:
procedure TForm1.Button1Click(Sender: TObject);
begin
Edit1.Text := Table1Company.Value; // Table1Company 為 TStringField,Edit1.Text 為 string,型別相符
end;
注意:讀取值時必須指派給相容型別的變數,否則需轉換。
為 Table1Phone 的 OnGetText 事件撰寫程式碼,將美式格式 808-555-0269 轉為中式 (808) 5550269:
procedure TForm1.Table1PhoneGetText(Sender: TField; var Text: string; DisplayText: Boolean);
begin
if DisplayText then begin
Text := Table1Phone.Value;
Delete(Text, 4, 1); // 移除第 4 字元 '-'
Delete(Text, 7, 1); // 移除第 7 字元 '-'
Insert('(', Text, 1);
Insert(')', Text, 5);
end;
end;
步驟:
Currency,名稱輸入 Balance。Calculated = True。OnCalcFields 事件撰寫計算邏輯:procedure TForm1.Table1CalcFields(DataSet: TDataSet);
begin
Table1Balance.Value := Table1ItemsTotal.Value - Table1AmountPaid.Value;
end;
步驟:確認查找欄位為鍵值或有輔助索引 → SetKey 進入查找模式 → 將查找值指派給欄位的 AsString → 呼叫 GotoKey 並檢查回傳值(True 成功,False 失敗)。
procedure TForm1.Button1Click(Sender: TObject);
begin
with Table1 do begin
Label1.Caption := ''; Label2.Caption := '';
IndexFieldNames := 'CustNo';
SetKey;
FieldByName('CustNo').AsString := Edit1.Text;
if GotoKey then begin
Label1.Caption := '查找成功';
Label2.Caption := FieldByName('Phone').AsString;
end else
Label1.Caption := '查找失敗';
end;
end;
更簡潔,直接將查找值陣列傳給 FindKey:
procedure TForm1.Button1Click(Sender: TObject);
var SeekValue: string;
begin
with Table1 do begin
Label1.Caption := ''; Label2.Caption := '';
IndexFieldNames := 'CustNo';
SeekValue := Edit1.Text;
if FindKey([SeekValue]) then begin
Label1.Caption := '查找成功';
Label2.Caption := FieldByName('Phone').AsString;
end else
Label1.Caption := '查找失敗';
end;
end;
差異:FindKey 參數為陣列,支援多欄位同時查找(需先設 IndexFieldNames 為多欄位,逗號分隔)。
總會傳回最接近的記錄,不會失敗。用法同 GotoKey/FindKey,但查找值可不完整(如 'Dun' 可找到 'Dunteman' 或 'Duncan')。範例:
procedure TForm1.Button1Click(Sender: TObject);
begin
with Table1 do begin
IndexFieldNames := 'Company';
SetKey;
FieldByName('Company').AsString := Edit1.Text;
GotoNearest;
Label3.Caption := FieldByName('Company').AsString;
end;
end;
修改流程:定位記錄 → Edit 進入編輯模式 → 修改欄位 → Post 寫入資料庫。批次修改範本:
with Table1 do begin
DisableControls; // 斷開資料瀏覽元件連結,提升效能
try
First;
while not EOF do begin
// 讀取欄位值、修改
Edit;
FieldByName('TargetField').AsString := NewValue;
Post;
Next;
end;
finally
EnableControls; // 即使發生例外也會執行,恢復元件連結
end;
end;
關鍵點:
DisableControls/EnableControls 配對使用,避免每次 Post 都重繪介面。First/Next 確保逐筆處理,避免無限迴圈。try...finally 保護 EnableControls 必定執行(異常保護),區別於 try...except(異常處理)。程式碼見原文,展示 ForceCase 程序搭配 try...finally 批次修改 Addr1、Addr2 欄位。
with Table1 do begin
Insert; // 建立空白記錄
// 填充各欄位
Post; // 寫入磁碟
end;
有索引表會自動依索引順序插入;無索引表則插在當前記錄之後。亦可用 Append 插至尾端(僅適用無索引表)。
InsertRecord(['2000', nil, nil, nil]); // 依欄位順序傳值,nil 表空值
完整範例(原文例 14.7)示範以 FindKey 檢查重複後 InsertRecord,刪除前以 MessageDlg 確認。
三種驗證層級:
OnValidate 事件,如驗證 SaleDate 不得晚於今日:procedure TForm1.Table1SaleDateValidate(Sender: TField);
begin
if Sender.Value > Now then
raise Exception.Create('不能輸入未來的日期');
end;Required = True 強制必填,配合 OnValidate 驗證。TTable.BeforePost 事件對整筆記錄進行驗證。TDBComboBox:Items 列出允許值(如 Prepaid、Net 30、COD)。TDBLookupComboBox:從另一資料表(如 Employee)取值,設 LookupSource、KeyField、LookupField。屬性設定詳見表 14.8。