在軟體開發的實務中,檔案處理是構建實用應用程式不可或缺的基礎技能。無論是建立設定檔來儲存使用者偏好設定、記錄系統日誌追蹤程式運作狀況、匯出資料供其他系統使用,或是讀取外部資料源進行處理,文字檔案操作都扮演著關鍵角色。Delphi Pascal 提供完善且直覺的文字檔案處理機制,其設計理念清晰明確,讓開發者能夠快速掌握檔案操作的核心概念。相較於其他程式語言複雜的檔案串流處理模式,Pascal 的 TextFile 類型與相關函式提供更直接的操作方式,特別適合初學者建立穩固的檔案處理基礎。本文將從基本概念開始,逐步深入到進階技巧,涵蓋文字檔案的讀取、寫入、數值處理以及標準對話方塊的整合應用,幫助您全面掌握 Delphi 的檔案處理能力。

TextFile 的基本架構與操作流程

在 Pascal 中操作文字檔案遵循明確的流程,每個步驟都有其特定目的與重要性。這種結構化的方式確保檔案操作的安全性與可靠性,避免資源洩漏或資料損毀。

第一步是宣告檔案變數。Pascal 使用 TextFile 類型來代表文字檔案,這是一個特殊的資料類型,專門用於處理以文字形式儲存的檔案內容。與一般變數不同,TextFile 變數本身不儲存檔案內容,而是作為程式與實際檔案之間的連接橋樑。

var
  InputFile: TextFile;
  OutputFile: TextFile;
  DataFile: TextFile;

這段程式碼宣告三個 TextFile 變數,分別用於不同用途。InputFile 可能用於讀取輸入資料,OutputFile 用於寫入結果,DataFile 用於處理中間資料。變數名稱應該清楚表達其用途,這是良好程式設計習慣的體現。

第二步是關聯檔案名稱。使用 AssignFile 程式將 TextFile 變數與實際的檔案路徑建立關聯。這個步驟不會開啟或建立檔案,僅是告訴程式這個變數對應到哪個檔案。

begin
  AssignFile(InputFile, 'data.txt');
  AssignFile(OutputFile, 'C:\Reports\result.txt');
  AssignFile(DataFile, ExtractFilePath(Application.ExeName) + 'temp.dat');
end;

這個範例展示三種不同的檔案路徑指定方式。第一種使用相對路徑,檔案位於程式執行目錄。第二種使用絕對路徑,明確指定完整位置。第三種動態組合路徑,使用 ExtractFilePath 函式取得程式所在目錄,然後附加檔案名稱。這種動態方式在部署應用程式時特別有用,因為程式可能安裝在不同位置。

第三步是開啟檔案。根據操作需求選擇適當的開啟模式。Pascal 提供三種基本模式,每種都有特定用途與行為。

Rewrite 模式用於寫入新檔案。呼叫 Rewrite 後會建立全新的空檔案,如果同名檔案已存在,其內容將被完全清除。這是破壞性操作,使用時必須謹慎。

AssignFile(OutputFile, 'output.txt');
Rewrite(OutputFile);
WriteLn(OutputFile, '這是新檔案的第一行');
CloseFile(OutputFile);

這段程式碼建立新檔案並寫入一行文字。如果 output.txt 已存在,原有內容會被覆寫。這種行為適合產生報表或匯出資料的場景,因為每次執行都應該產生全新結果。

Reset 模式用於讀取現有檔案。呼叫 Reset 會開啟檔案並將讀取位置設在開頭。檔案必須已存在,否則會引發執行時期錯誤。

AssignFile(InputFile, 'config.ini');
Reset(InputFile);
ReadLn(InputFile, ConfigLine);
CloseFile(InputFile);

這段程式碼開啟設定檔並讀取第一行內容。Reset 將讀取游標定位在檔案開頭,ReadLn 讀取一整行並將游標移至下一行。這種模式適合讀取設定檔、日誌檔或資料檔。

Append 模式用於在現有檔案末尾增加內容。呼叫 Append 會開啟檔案並將寫入位置設在檔案結尾,新內容會附加在原有內容之後。檔案必須已存在,若不存在會引發錯誤。

AssignFile(LogFile, 'application.log');
Append(LogFile);
WriteLn(LogFile, FormatDateTime('yyyy-mm-dd hh:nn:ss', Now) + ' - 使用者登入');
CloseFile(LogFile);

這段程式碼開啟日誌檔並在末尾增加一筆記錄。Append 保留原有內容,新記錄附加在檔案結尾。這是記錄日誌的標準方式,每次執行都累積新記錄而不會覆蓋歷史資訊。

最後一步是關閉檔案。CloseFile 程式完成檔案操作並釋放系統資源。這個步驟極其重要,忽略它可能導致資料遺失或檔案損毀。

CloseFile(InputFile);
CloseFile(OutputFile);
CloseFile(DataFile);

關閉檔案確保所有緩衝資料寫入磁碟,釋放檔案鎖定讓其他程式能夠存取,並釋放系統資源避免洩漏。在任何情況下都應該確保檔案被正確關閉,即使發生錯誤也應該關閉。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

state "文字檔案操作流程" as fileops {
  state "宣告變數" as declare
  state "關聯檔案" as assign
  state "選擇模式" as mode
  state "Rewrite 寫入" as rewrite
  state "Reset 讀取" as reset
  state "Append 附加" as append
  state "執行讀寫" as rw
  state "關閉檔案" as close
  
  [*] --> declare
  declare --> assign: AssignFile
  assign --> mode
  mode --> rewrite: 建立新檔案
  mode --> reset: 讀取現有檔案
  mode --> append: 附加到檔案
  rewrite --> rw
  reset --> rw
  append --> rw
  rw --> close: CloseFile
  close --> [*]
}

note right of assign
  建立變數與
  檔案的關聯
end note

note right of close
  必須正確關閉
  避免資料遺失
end note

@enduml

Write 與 WriteLn 的精確控制

寫入文字檔案使用 Write 與 WriteLn 程式,它們的行為與控制台輸出函式類似,但第一個參數必須是檔案變數。理解這兩個函式的差異是掌握檔案寫入的關鍵。

Write 函式將資料寫入檔案但不換行,寫入游標停留在同一行末尾。這讓後續 Write 或 WriteLn 能夠在同一行繼續寫入內容。

procedure GenerateReport;
var
  ReportFile: TextFile;
  Total: Integer;
begin
  AssignFile(ReportFile, 'sales_report.txt');
  Rewrite(ReportFile);
  
  Write(ReportFile, '銷售總額: ');
  Total := 150000;
  Write(ReportFile, Total);
  Write(ReportFile, ' 元');
  WriteLn(ReportFile);
  
  Write(ReportFile, '成長率: ');
  Write(ReportFile, 15.5:5:1);
  WriteLn(ReportFile, '%');
  
  CloseFile(ReportFile);
end;

這個過程產生銷售報表。第一行由多個 Write 組成,分別寫入文字、數值與單位,最後用 WriteLn 換行。第二行同樣組合多個元素,其中數值使用格式化輸出,15.5:5:1 表示總寬度 5 字元,小數點後 1 位。這種組合方式讓輸出格式更靈活。

WriteLn 函式寫入資料後自動換行,寫入游標移至下一行開頭。這是最常用的寫入方式,因為大多數文字檔案是以行為單位組織的。

procedure WriteConfiguration;
var
  ConfigFile: TextFile;
begin
  AssignFile(ConfigFile, 'settings.ini');
  Rewrite(ConfigFile);
  
  WriteLn(ConfigFile, '[General]');
  WriteLn(ConfigFile, 'Language=zh-TW');
  WriteLn(ConfigFile, 'Theme=Dark');
  WriteLn(ConfigFile, '');
  WriteLn(ConfigFile, '[Database]');
  WriteLn(ConfigFile, 'Server=localhost');
  WriteLn(ConfigFile, 'Port=5432');
  
  CloseFile(ConfigFile);
end;

這個過程產生 INI 格式的設定檔。每個 WriteLn 寫入一行完整的設定項目,自動換行確保格式正確。空字串的 WriteLn 產生空行,用於分隔不同的設定區段,提升可讀性。

Write 與 WriteLn 支援多個參數,依序寫入檔案。參數可以是不同資料類型,包括字串、整數、實數、字元與布林值。

procedure WriteDataLog;
var
  LogFile: TextFile;
  Temperature: Real;
  Humidity: Integer;
  IsNormal: Boolean;
begin
  AssignFile(LogFile, 'sensor_log.txt');
  Append(LogFile);
  
  Temperature := 25.6;
  Humidity := 68;
  IsNormal := True;
  
  WriteLn(LogFile, Now, ' | ', Temperature:5:1, '°C | ', Humidity, '% | ', IsNormal);
  
  CloseFile(LogFile);
end;

這個過程記錄感測器資料。WriteLn 的參數包含日期時間、實數、整數與布林值,用分隔符號組合成結構化的日誌記錄。多參數寫入簡化程式碼,避免多次呼叫函式。

格式化輸出讓數值對齊更整齊。對整數使用 value:width 格式,對實數使用 value:width:decimals 格式。

procedure WriteTable;
var
  TableFile: TextFile;
  I: Integer;
begin
  AssignFile(TableFile, 'multiplication_table.txt');
  Rewrite(TableFile);
  
  for I := 1 to 9 do
  begin
    WriteLn(TableFile, I:3, ' x 7 = ', I * 7:3);
  end;
  
  CloseFile(TableFile);
end;

這個過程產生乘法表。數值使用 :3 格式化,確保佔用 3 個字元寬度,數字右對齊。這樣輸出的表格整齊劃一,容易閱讀。格式化對產生報表特別重要。

Read 與 ReadLn 的讀取機制

讀取文字檔案使用 Read 與 ReadLn 程式,它們的行為與控制台輸入函式相似,但第一個參數是檔案變數。理解讀取機制對正確處理檔案內容至關重要。

ReadLn 讀取一整行文字,包含從當前游標位置到行尾的所有字元。讀取後游標自動移至下一行開頭。

procedure ReadConfiguration;
var
  ConfigFile: TextFile;
  Line: String;
  Setting: String;
begin
  AssignFile(ConfigFile, 'app.config');
  Reset(ConfigFile);
  
  while not Eof(ConfigFile) do
  begin
    ReadLn(ConfigFile, Line);
    
    if Length(Line) > 0 then
    begin
      if Line[1] <> '#' then
      begin
        Setting := Trim(Line);
        ProcessSetting(Setting);
      end;
    end;
  end;
  
  CloseFile(ConfigFile);
end;

這個過程讀取設定檔的所有行。while 迴圈使用 Eof 函式檢查是否到達檔案末尾,ReadLn 逐行讀取內容。程式跳過空行與註解行(以 # 開頭),只處理有效的設定項目。這是讀取文字檔案的標準模式。

Read 讀取資料但不自動換行,游標停在讀取結束位置。對於字串變數,Read 行為與 ReadLn 相同,都是讀取整行。但對於數值變數,Read 的行為完全不同。

procedure ReadNumbers;
var
  DataFile: TextFile;
  Value: Integer;
  Sum: Integer;
begin
  AssignFile(DataFile, 'numbers.txt');
  Reset(DataFile);
  
  Sum := 0;
  while not Eof(DataFile) do
  begin
    Read(DataFile, Value);
    Sum := Sum + Value;
  end;
  
  ShowMessage('總和: ' + IntToStr(Sum));
  CloseFile(DataFile);
end;

這個過程從檔案讀取數值並計算總和。假設檔案內容為 “10 25 8 42 15”,Read 會依序讀取每個數字。Read 對數值的處理有特殊機制,它會自動跳過數字前的空白字元,讀取數值後停在數值後的第一個非數字字元。

讀取數值時 Read 與 ReadLn 的差異非常重要。Read 讀取數值後游標停在同一行,下次 Read 會繼續讀取同一行的下一個數值。ReadLn 讀取數值後游標移至下一行,即使當前行還有其他數值也會被跳過。

procedure CompareReadMethods;
var
  DataFile: TextFile;
  Value: Integer;
  Total1, Total2: Integer;
begin
  AssignFile(DataFile, 'data.txt');
  Reset(DataFile);
  
  Total1 := 0;
  while not Eof(DataFile) do
  begin
    Read(DataFile, Value);
    Total1 := Total1 + Value;
  end;
  
  CloseFile(DataFile);
  
  AssignFile(DataFile, 'data.txt');
  Reset(DataFile);
  
  Total2 := 0;
  while not Eof(DataFile) do
  begin
    ReadLn(DataFile, Value);
    Total2 := Total2 + Value;
  end;
  
  CloseFile(DataFile);
  
  ShowMessage('Read 總和: ' + IntToStr(Total1));
  ShowMessage('ReadLn 總和: ' + IntToStr(Total2));
end;

這個過程比較 Read 與 ReadLn 的差異。假設檔案有三行,每行包含多個數字。第一個迴圈使用 Read,會讀取檔案中的所有數字。第二個迴圈使用 ReadLn,只會讀取每行的第一個數字,因為讀取後立即換行。這個差異在處理數值檔案時必須特別注意。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

state "數值讀取機制" as numread {
  state "呼叫 Read" as read
  state "跳過空白字元" as skip
  state "讀取數值" as getnum
  state "游標停在數值後" as cursor
  state "檢查下一個字元" as check
  
  [*] --> read
  read --> skip
  skip --> getnum
  getnum --> cursor
  cursor --> check
  check --> skip: 還有數值
  check --> [*]: 行尾或檔案尾
}

note right of skip
  自動跳過
  空格與換行
end note

note right of cursor
  停在非數字字元
  不自動換行
end note

@enduml

EOF 與 EOLN 的檔案狀態檢測

處理檔案時需要知道何時到達檔案末尾或行末尾。Pascal 提供 EOF 與 EOLN 函式來檢測檔案狀態,它們是循序讀取檔案的基礎。

EOF 函式檢查檔案讀取游標是否已到達檔案末尾。當游標在最後一個字元之後時,EOF 回傳 True,否則回傳 False。

procedure CountLines;
var
  TextFile: TextFile;
  Line: String;
  Count: Integer;
begin
  AssignFile(TextFile, 'document.txt');
  Reset(TextFile);
  
  Count := 0;
  while not Eof(TextFile) do
  begin
    ReadLn(TextFile, Line);
    Inc(Count);
  end;
  
  CloseFile(TextFile);
  ShowMessage('檔案共有 ' + IntToStr(Count) + ' 行');
end;

這個過程計算檔案的總行數。while 迴圈使用 not Eof 作為繼續條件,只要還沒到達檔案末尾就繼續執行。每次 ReadLn 讀取一行並增加計數器。這是處理未知長度檔案的標準方式。

EOLN 函式檢查檔案讀取游標是否位於行末尾。當游標在行末的換行符號上時,EOLN 回傳 True,否則回傳 False。

procedure ReadLineCharacters;
var
  TextFile: TextFile;
  Ch: Char;
  Line: String;
begin
  AssignFile(TextFile, 'text.txt');
  Reset(TextFile);
  
  Line := '';
  while not Eoln(TextFile) do
  begin
    Read(TextFile, Ch);
    Line := Line + Ch;
  end;
  
  ReadLn(TextFile);
  
  CloseFile(TextFile);
  ShowMessage('第一行內容: ' + Line);
end;

這個過程逐字元讀取第一行內容。while 迴圈使用 not Eoln 檢查是否到達行尾,Read 讀取單一字元並附加到字串。迴圈結束後,ReadLn 移過換行符號讓游標移至下一行。這種逐字元讀取在需要精確控制時很有用。

EOF 與 EOLN 在處理數值檔案時有個重要陷阱。當行末有空格時,EOLN 會回傳 False,因為游標在空格上而非換行符號。這會導致 Read 跳過換行繼續讀取下一行。

procedure ProblematicRead;
var
  DataFile: TextFile;
  Value: Integer;
  Sum: Integer;
begin
  AssignFile(DataFile, 'numbers.txt');
  Reset(DataFile);
  
  Sum := 0;
  while not Eoln(DataFile) do
  begin
    Read(DataFile, Value);
    Sum := Sum + Value;
  end;
  
  CloseFile(DataFile);
  ShowMessage('第一行總和: ' + IntToStr(Sum));
end;

這段程式碼意圖只讀取第一行的數值,但如果行末有空格,Eoln 會回傳 False,Read 會跳過換行符號繼續讀取下一行的數值。這是使用 EOLN 檢測數值讀取時最常見的錯誤。

SeekEOF 與 SeekEOLN 的進階檢測

為解決 EOF 與 EOLN 在處理空白字元時的問題,Pascal 提供 SeekEOF 與 SeekEOLN 函式。這兩個函式在檢查檔案狀態前會先跳過所有空白字元。

SeekEOLN 函式先跳過空格與定位字元,然後檢查是否到達行尾。如果跳過空白後游標在換行符號上,回傳 True,否則回傳 False。

procedure ReadLineNumbers;
var
  DataFile: TextFile;
  Value: Integer;
  Sum: Integer;
begin
  AssignFile(DataFile, 'numbers.txt');
  Reset(DataFile);
  
  Sum := 0;
  while not SeekEoln(DataFile) do
  begin
    Read(DataFile, Value);
    Sum := Sum + Value;
  end;
  
  CloseFile(DataFile);
  ShowMessage('第一行數值總和: ' + IntToStr(Sum));
end;

這個過程正確讀取第一行的所有數值。SeekEoln 會先跳過尾端空格,然後檢查行尾,確保只讀取當前行的數值。即使行末有多個空格,SeekEoln 都能正確判斷,Read 不會跨行讀取。這是處理數值檔案的正確方式。

SeekEOF 函式先跳過所有空白字元(包括空格、定位符號與換行符號),然後檢查是否到達檔案末尾。如果跳過所有空白後已無內容,回傳 True,否則回傳 False。

procedure ReadAllNumbers;
var
  DataFile: TextFile;
  Value: Integer;
  Sum: Integer;
begin
  AssignFile(DataFile, 'numbers.txt');
  Reset(DataFile);
  
  Sum := 0;
  while not SeekEof(DataFile) do
  begin
    Read(DataFile, Value);
    Sum := Sum + Value;
  end;
  
  CloseFile(DataFile);
  ShowMessage('所有數值總和: ' + IntToStr(Sum));
end;

這個過程讀取檔案中的所有數值並計算總和。SeekEof 跳過所有分隔符號(空格與換行),確保 Read 能讀取檔案中的每個數值。不論數值如何分布在各行,也不論分隔符號有多少,這段程式碼都能正確讀取所有數值。

選擇正確的檔案狀態檢測函式是寫出可靠程式的關鍵。原則很簡單:以字串為單位讀取時使用 EOF 與 EOLN,以數值為單位讀取時使用 SeekEOF 與 SeekEOLN。

procedure ReadMixedData;
var
  DataFile: TextFile;
  Name: String;
  Score: Integer;
begin
  AssignFile(DataFile, 'students.txt');
  Reset(DataFile);
  
  while not Eof(DataFile) do
  begin
    ReadLn(DataFile, Name);
    
    while not SeekEoln(DataFile) do
    begin
      Read(DataFile, Score);
      ProcessScore(Name, Score);
    end;
    
    ReadLn(DataFile);
  end;
  
  CloseFile(DataFile);
end;

這個過程處理混合格式的檔案。每位學生佔兩行,第一行是姓名,第二行包含多個分數。外層迴圈使用 Eof 檢查檔案末尾,因為讀取的是字串。內層迴圈使用 SeekEoln 檢查行尾,因為讀取的是數值。這種組合確保正確處理不同類型的資料。

@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

state "檔案狀態檢測函式" as check {
  state "字串讀取模式" as strmode
  state "數值讀取模式" as nummode
  state "使用 EOF" as eof
  state "使用 EOLN" as eoln
  state "使用 SeekEOF" as seekeof
  state "使用 SeekEOLN" as seekeoln
  
  [*] --> strmode: ReadLn 字串
  [*] --> nummode: Read 數值
  strmode --> eof: 檢查檔案尾
  strmode --> eoln: 檢查行尾
  nummode --> seekeof: 檢查檔案尾
  nummode --> seekeoln: 檢查行尾
  eof --> [*]
  eoln --> [*]
  seekeof --> [*]
  seekeoln --> [*]
}

note right of strmode
  直接檢查位置
  不跳過空白
end note

note right of nummode
  先跳過空白
  再檢查位置
end note

@enduml

OpenDialog 與 SaveDialog 標準對話方塊

在開發桌面應用程式時,提供友善的檔案選擇介面能大幅提升使用者經驗。硬編碼檔案路徑或要求使用者手動輸入路徑都不符合現代應用程式的標準。Delphi 提供 OpenDialog 與 SaveDialog 元件,讓使用者透過熟悉的視窗標準對話方塊選擇檔案。

OpenDialog 用於選擇要開啟的現有檔案。使用者可以瀏覽檔案系統,選擇目標檔案,程式取得完整路徑後進行讀取操作。

procedure TMainForm.btnOpenClick(Sender: TObject);
var
  InputFile: TextFile;
  Line: String;
begin
  if OpenDialog1.Execute then
  begin
    AssignFile(InputFile, OpenDialog1.FileName);
    Reset(InputFile);
    
    Memo1.Lines.Clear;
    while not Eof(InputFile) do
    begin
      ReadLn(InputFile, Line);
      Memo1.Lines.Add(Line);
    end;
    
    CloseFile(InputFile);
    StatusBar1.SimpleText := '已開啟: ' + OpenDialog1.FileName;
  end;
end;

這個按鈕點選事件處理程式使用 OpenDialog 讓使用者選擇檔案。Execute 方法顯示對話方塊並等待使用者操作,如果使用者選擇檔案並按下開啟按鈕,Execute 回傳 True。FileName 屬性包含使用者選擇的完整路徑。程式開啟檔案,讀取所有內容並顯示在 Memo 元件中,最後在狀態列顯示檔案路徑。

SaveDialog 用於選擇儲存位置。使用者可以選擇現有檔案覆寫,或輸入新檔案名稱。對話方塊會自動詢問是否覆寫現有檔案,提供額外的安全確認。

procedure TMainForm.btnSaveClick(Sender: TObject);
var
  OutputFile: TextFile;
  I: Integer;
begin
  if SaveDialog1.Execute then
  begin
    AssignFile(OutputFile, SaveDialog1.FileName);
    Rewrite(OutputFile);
    
    for I := 0 to Memo1.Lines.Count - 1 do
    begin
      WriteLn(OutputFile, Memo1.Lines[I]);
    end;
    
    CloseFile(OutputFile);
    StatusBar1.SimpleText := '已儲存: ' + SaveDialog1.FileName;
    ShowMessage('檔案儲存成功');
  end;
end;

這個按鈕點選事件處理程式使用 SaveDialog 讓使用者選擇儲存位置。Execute 顯示儲存對話方塊,使用者選擇位置後 FileName 包含完整路徑。程式建立新檔案,將 Memo 的所有行寫入檔案,然後顯示確認訊息。

設定對話方塊的屬性能提供更好的使用者經驗。Filter 屬性限制顯示的檔案類型,InitialDir 設定初始目錄,DefaultExt 自動加入副檔名。

procedure TMainForm.FormCreate(Sender: TObject);
begin
  OpenDialog1.Filter := '文字檔案 (*.txt)|*.txt|所有檔案 (*.*)|*.*';
  OpenDialog1.FilterIndex := 1;
  OpenDialog1.InitialDir := ExtractFilePath(Application.ExeName);
  OpenDialog1.Title := '開啟文字檔案';
  
  SaveDialog1.Filter := '文字檔案 (*.txt)|*.txt|所有檔案 (*.*)|*.*';
  SaveDialog1.FilterIndex := 1;
  SaveDialog1.DefaultExt := 'txt';
  SaveDialog1.InitialDir := ExtractFilePath(Application.ExeName);
  SaveDialog1.Title := '儲存文字檔案';
  SaveDialog1.Options := SaveDialog1.Options + [ofOverwritePrompt];
end;

這個表單建立事件設定兩個對話方塊的屬性。Filter 定義檔案類型過濾器,使用管線符號分隔描述與萬用字元模式。FilterIndex 指定預設選擇的過濾器。InitialDir 設定對話方塊開啟時的目錄。Title 設定對話方塊標題列文字。SaveDialog 的 DefaultExt 自動在檔名沒有副檔名時加入 .txt。ofOverwritePrompt 選項確保覆寫現有檔案前會詢問使用者。

整合對話方塊與檔案處理建立完整的檔案操作功能。使用者透過對話方塊選擇檔案,程式執行實際的讀寫操作,這種模式符合使用者習慣且易於實作。

procedure TMainForm.btnProcessClick(Sender: TObject);
var
  InputFile, OutputFile: TextFile;
  Line: String;
  ProcessedLine: String;
begin
  if not OpenDialog1.Execute then Exit;
  
  SaveDialog1.FileName := ChangeFileExt(OpenDialog1.FileName, '.processed.txt');
  if not SaveDialog1.Execute then Exit;
  
  AssignFile(InputFile, OpenDialog1.FileName);
  AssignFile(OutputFile, SaveDialog1.FileName);
  Reset(InputFile);
  Rewrite(OutputFile);
  
  try
    while not Eof(InputFile) do
    begin
      ReadLn(InputFile, Line);
      ProcessedLine := UpperCase(Line);
      WriteLn(OutputFile, ProcessedLine);
    end;
    
    ShowMessage('處理完成');
  finally
    CloseFile(InputFile);
    CloseFile(OutputFile);
  end;
end;

這個過程整合 OpenDialog 與 SaveDialog 實現檔案處理。使用者先選擇輸入檔案,程式建議輸出檔案名稱(在原檔名加入 .processed),使用者確認或修改輸出位置。程式讀取輸入檔案的每一行,轉換為大寫,寫入輸出檔案。try-finally 確保無論是否發生錯誤都會關閉檔案,這是重要的資源管理實踐。

錯誤處理與資源管理

檔案操作容易發生各種錯誤,包括檔案不存在、權限不足、磁碟空間不足或檔案被其他程式鎖定。健全的錯誤處理機制確保程式能優雅地處理這些情況。

procedure SafeFileRead;
var
  DataFile: TextFile;
  Line: String;
begin
  try
    AssignFile(DataFile, 'data.txt');
    Reset(DataFile);
    
    try
      while not Eof(DataFile) do
      begin
        ReadLn(DataFile, Line);
        ProcessLine(Line);
      end;
    finally
      CloseFile(DataFile);
    end;
  except
    on E: EInOutError do
      ShowMessage('檔案讀取錯誤: ' + E.Message);
    on E: Exception do
      ShowMessage('未預期的錯誤: ' + E.Message);
  end;
end;

這個過程展示完整的錯誤處理結構。外層 try-except 捕捉所有可能的例外。內層 try-finally 確保檔案被正確關閉。EInOutError 是檔案輸入輸出錯誤的特定例外類型,應該優先處理。一般 Exception 捕捉其他未預期的錯誤。這種結構確保無論發生什麼錯誤,檔案都會被關閉,避免資源洩漏。

檢查檔案是否存在能避免不必要的錯誤。FileExists 函式檢查檔案是否存在,DirectoryExists 檢查目錄是否存在。

procedure ConditionalFileOperation;
var
  InputPath: String;
  OutputPath: String;
begin
  InputPath := 'input.txt';
  OutputPath := 'output.txt';
  
  if not FileExists(InputPath) then
  begin
    ShowMessage('找不到輸入檔案: ' + InputPath);
    Exit;
  end;
  
  if FileExists(OutputPath) then
  begin
    if MessageDlg('輸出檔案已存在,是否覆寫?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then
      Exit;
  end;
  
  PerformFileOperation(InputPath, OutputPath);
end;

這個過程在執行檔案操作前檢查條件。首先確認輸入檔案存在,不存在則顯示錯誤訊息並返回。如果輸出檔案已存在,詢問使用者是否覆寫,使用者拒絕則取消操作。這些檢查提供更友善的錯誤處理,避免程式崩潰或資料遺失。

持續精進與實務應用

透過對 Delphi Pascal 文字檔案處理機制的深入學習,我們掌握從基礎讀寫到進階技巧的完整知識體系。TextFile 操作的清晰流程、Write 與 WriteLn 的精確控制、Read 與 ReadLn 的微妙差異、EOF 與 SeekEOF 的適用場景,以及標準對話方塊的整合應用,這些技術構成檔案處理的堅實基礎。

但學習不應止步於此。現代應用程式面對更複雜的檔案處理需求,包括處理不同編碼的文字檔案(UTF-8、UTF-16、Big5 等)、解析結構化資料格式(CSV、JSON、XML)、實作非同步檔案操作避免介面凍結、處理大型檔案時的記憶體管理,以及實作檔案監視偵測外部修改。這些進階主題值得進一步探索。

實務專案開發整合所有學到的技術。建立文字編輯器應用完整的檔案操作功能。開發日誌分析工具處理大量日誌檔案。實作設定管理系統讀寫 INI 或 XML 設定檔。建立資料匯入匯出功能處理 CSV 格式。這些專案將理論知識轉化為實際能力,累積解決問題的經驗,最終成為能夠開發健全應用程式的專業開發者。