在視窗應用程式開發的實務中,處理多行文字內容是極為常見的需求。無論是開發文字編輯器、日誌檢視工具或是資料輸入介面,都需要能夠有效管理與操作多行文字的元件。Delphi 的 TMemo 元件正是為此需求而設計的強大工具,它提供遠超單行文字方塊的功能,能夠處理大量文字內容並支援豐富的操作方法。與 TEdit 元件相比,TMemo 不僅允許多行輸入與顯示,更提供完整的程式化控制介面,讓開發者能夠精確操作每一行文字。本文將深入探討 TMemo 的核心特性與實務應用,同時延伸到陣列資料結構與排序演算法的實作,提供完整的技術解決方案。

TMemo 元件的核心架構

TMemo 元件繼承 TEdit 的許多基礎特性,例如 Font 屬性控制文字的字型、大小與顏色,Readonly 屬性決定內容是否可編輯,這些屬性的運作方式與單行文字方塊完全一致。但 TMemo 的真正價值在於其處理多行文字的能力,這種能力透過一套精心設計的屬性與方法體系來實現。

Text 屬性提供存取 TMemo 全部文字內容的途徑,但這個屬性只在應用程式執行時可用,在設計階段無法透過 Text 屬性設定初始內容。這個限制並非缺陷,而是設計上的考量,因為多行文字的編輯在視覺化設計工具中需要特殊的介面支援。

Lines 屬性是 TMemo 的核心,它將文字內容組織為一個行的集合。這個集合使用從零開始的索引編號,第一行的索引為 0,第二行為 1,依此類推。這種索引機制與大多數程式語言的陣列慣例一致,讓習慣陣列操作的開發者能夠直覺地使用。透過 Lines 屬性,我們能夠精確讀取或修改特定行的內容,實現細粒度的文字控制。

procedure DemoLinesAccess;
var
  S: String;
begin
  S := memEx.Lines[2];
  ShowMessage('第三行內容: ' + S);
  
  memEx.Lines[3] := 'Hello Delphi';
  ShowMessage('已修改第四行內容');
end;

這個示範過程展示 Lines 屬性的基本用法。memEx.Lines[2] 讀取索引為 2 的行,實際上是第三行(因為索引從 0 開始)。讀取的內容儲存到字串變數 S 中,然後透過對話方塊顯示。接著使用賦值運算將字串 ‘Hello Delphi’ 寫入索引為 3 的行,即第四行。這種直接的索引存取方式非常直覺,但必須注意只能存取已存在的行,試圖存取超出範圍的索引會導致執行時期錯誤。

行的存在性是使用 TMemo 時必須注意的關鍵概念。行可以透過三種方式建立:在設計階段使用物件檢視器的 Lines 編輯器輸入,在執行時期由使用者透過鍵盤輸入,或是透過程式碼呼叫相關方法動態建立。嘗試存取不存在的行會立即引發例外,因此在操作前應該先確認行數。

@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

class TMemo {
  +Text: String
  +Lines: TStrings
  +Font: TFont
  +ReadOnly: Boolean
  +WordWrap: Boolean
  +MaxLength: Integer
  +ScrollBars: TScrollStyle
  +Alignment: TAlignment
  +Clear()
  +SelectAll()
  +CopyToClipboard()
}

class TStrings {
  +Count: Integer
  +Text: String
  +Add(S: String): Integer
  +Append(S: String)
  +Insert(Index: Integer, S: String)
  +Delete(Index: Integer)
  +Exchange(Index1, Index2: Integer)
  +Move(CurIndex, NewIndex: Integer)
  +Clear()
}

TMemo --> TStrings: Lines 屬性

note right of TMemo
  多行文字編輯元件
  支援完整的文字操作
end note

note right of TStrings
  字串集合類別
  提供豐富的行操作方法
end note

@enduml

TMemo 的關鍵屬性詳解

Count 屬性回傳 TMemo 中的總行數,這是一個唯讀屬性,無法直接修改。Count 的值隨著行的增加或刪除自動更新,反映當前實際的行數。這個屬性在需要遍歷所有行時特別有用,通常作為迴圈的終止條件。

procedure ProcessAllLines;
var
  I: Integer;
  LineCount: Integer;
begin
  LineCount := memEx.Lines.Count;
  
  for I := 0 to LineCount - 1 do
  begin
    ShowMessage('處理第 ' + IntToStr(I + 1) + ' 行: ' + memEx.Lines[I]);
  end;
end;

這個過程展示如何使用 Count 屬性遍歷 TMemo 的所有行。首先取得總行數並儲存在變數中,然後使用 for 迴圈從索引 0 遍歷到 Count - 1。在迴圈中顯示每行的內容,注意向使用者顯示時使用 I + 1 以符合人類習慣的行號(從 1 開始而非 0)。

WordWrap 屬性控制文字是否在達到右邊界時自動換行。當設定為 True 時,長文字會自動折行以適應元件寬度,避免出現水平捲動列。這個特性在顯示說明文字或閱讀長文件時提升使用者體驗。設定為 False 時,文字不會自動換行,超出可視範圍的部分需要透過水平捲動才能看到。

MaxLength 屬性定義可輸入的最大字元數。設定為 0 時表示沒有限制,TMemo 可以容納任意長度的文字。這個屬性在需要限制使用者輸入時很有用,例如限制評論或備註的長度。值得注意的是,當 ScrollBars 屬性啟用垂直捲動列時,MaxLength 屬性會被忽略,允許輸入更長的內容。

ScrollBars 屬性決定 TMemo 是否顯示捲動列。可選值包括 ssNone(無捲動列)、ssHorizontal(水平捲動列)、ssVertical(垂直捲動列)與 ssBoth(雙向捲動列)。捲動列的存在讓使用者能夠瀏覽超出可視範圍的內容。通常對於多行文字編輯器,至少應該啟用垂直捲動列。

Alignment 屬性控制文字在元件中的對齊方式,可設定為 taLeftJustify(左對齊)、taCenter(置中)或 taRightJustify(右對齊)。這個屬性影響所有行的對齊方式,無法針對個別行設定不同的對齊。

TMemo 的核心方法應用

Delete 方法從 TMemo 刪除指定索引的行,刪除後所有後續行會自動上移並重新編號。這個行為確保索引的連續性,但也意味著在迴圈中刪除行時需要特別注意索引變化。

procedure DeleteEvenLines;
var
  I: Integer;
  TotalLines: Integer;
begin
  TotalLines := memEx.Lines.Count;
  
  for I := TotalLines - 1 downto 0 do
  begin
    if (I mod 2) = 0 then
      memEx.Lines.Delete(I);
  end;
end;

這個過程刪除所有偶數行(索引為 0、2、4 等)。關鍵技巧是從後向前遍歷,從最後一行開始往前處理。這種方式確保刪除操作不會影響尚未處理行的索引。如果從前向後遍歷,刪除一行後所有後續行的索引都會改變,導致邏輯錯誤。mod 運算子計算除法餘數,索引除以 2 餘數為 0 的行就是偶數行。

Exchange 方法交換兩個指定索引行的位置,這是重新排列行順序的基本操作。兩行的內容會互換,其他行保持不變。

procedure SwapFirstAndLast;
var
  LastIndex: Integer;
begin
  if memEx.Lines.Count < 2 then
  begin
    ShowMessage('至少需要兩行才能交換');
    Exit;
  end;
  
  LastIndex := memEx.Lines.Count - 1;
  memEx.Lines.Exchange(0, LastIndex);
  ShowMessage('已交換第一行與最後一行');
end;

這個過程交換第一行與最後一行的位置。首先檢查行數是否至少為 2,避免在行數不足時執行無意義的操作。LastIndex 變數儲存最後一行的索引,等於 Count - 1。呼叫 Exchange 方法完成交換,然後顯示確認訊息。

Move 方法將指定行移動到新位置。原行被移除後,所有後續行上移,然後該行插入到目標位置,該位置之後的行下移。這個操作改變行的順序但不改變內容。

procedure MoveLineToTop(LineIndex: Integer);
begin
  if (LineIndex < 0) or (LineIndex >= memEx.Lines.Count) then
  begin
    ShowMessage('無效的行索引');
    Exit;
  end;
  
  if LineIndex = 0 then
  begin
    ShowMessage('該行已經在頂部');
    Exit;
  end;
  
  memEx.Lines.Move(LineIndex, 0);
  ShowMessage('已將行移至頂部');
end;

這個過程將指定行移至 TMemo 頂部。首先驗證索引的有效性,確保在合法範圍內。如果目標行已經是第一行,則無需移動。否則呼叫 Move 方法將行移至索引 0 的位置,即頂部。

Clear 方法完全清空 TMemo 的所有內容,Count 屬性會變為 0。這是重置 TMemo 狀態的最快方式,常用於準備填充新內容或清除使用者輸入。

procedure ResetMemo;
begin
  memEx.Lines.Clear;
  memEx.Lines.Add('TMemo 已清空');
  memEx.Lines.Add('準備輸入新內容');
end;

這個過程先清空 TMemo,然後加入兩行提示訊息。Clear 方法執行後 TMemo 變為空白,隨後的 Add 呼叫建立新的行。

Add 與 Append 方法都在 TMemo 末尾增加新行,它們的差異在於 Add 方法會回傳新行的索引,而 Append 不回傳任何值。當需要知道新增行的位置時,使用 Add 方法更方便。

procedure AddMultipleLines;
var
  NewIndex: Integer;
  I: Integer;
begin
  memEx.Lines.Clear;
  
  for I := 1 to 10 do
  begin
    NewIndex := memEx.Lines.Add('第 ' + IntToStr(I) + ' 行');
    
    if I = 5 then
      ShowMessage('第五行的索引是: ' + IntToStr(NewIndex));
  end;
end;

這個過程展示 Add 方法的回傳值應用。清空 TMemo 後,在迴圈中增加 10 行。每次呼叫 Add 都會回傳新行的索引,儲存在 NewIndex 變數中。當加入第五行時,顯示其索引值(應該是 4,因為索引從 0 開始)。

Insert 方法在指定位置插入新行,原本在該位置及之後的行都會下移。這讓我們能夠在文字中間插入內容而不影響其他行。

procedure InsertHeader;
begin
  memEx.Lines.Insert(0, '=== 文件標題 ===');
  memEx.Lines.Insert(1, '');
  memEx.Lines.Insert(2, '內容開始於下方:');
  memEx.Lines.Insert(3, '');
end;

這個過程在 TMemo 開頭插入標題區塊。連續四次呼叫 Insert,在索引 0、1、2、3 插入標題行、空行、說明行與分隔空行。原本的內容會被推到索引 4 之後的位置。

@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 "TMemo 行操作流程" as lineops {
  state "讀取行數 Count" as count
  state "存取特定行" as access
  state "增加新行" as add
  state "插入行" as insert
  state "刪除行" as delete
  state "移動行" as move
  state "交換行" as exchange
  
  [*] --> count
  count --> access: 確認索引有效
  access --> add: 需要新增
  access --> insert: 需要插入
  access --> delete: 需要刪除
  access --> move: 需要移動
  access --> exchange: 需要交換
  add --> [*]
  insert --> [*]
  delete --> [*]
  move --> [*]
  exchange --> [*]
}

note right of access
  必須確保索引
  在有效範圍內
end note

note right of delete
  從後向前遍歷
  避免索引錯亂
end note

@enduml

實作 TMemo 行排序演算法

對 TMemo 的行進行排序是常見需求,例如整理名單、排序日誌或組織資料。實作排序需要結合字串比較與行位置交換。

function FindMinLineIndex(StartIndex: Integer): Integer;
var
  I: Integer;
  MinIndex: Integer;
begin
  MinIndex := StartIndex;
  
  for I := StartIndex + 1 to memEx.Lines.Count - 1 do
  begin
    if memEx.Lines[I] < memEx.Lines[MinIndex] then
      MinIndex := I;
  end;
  
  Result := MinIndex;
end;

procedure SortMemoLines;
var
  I: Integer;
  MinIndex: Integer;
begin
  for I := 0 to memEx.Lines.Count - 2 do
  begin
    MinIndex := FindMinLineIndex(I);
    
    if MinIndex <> I then
      memEx.Lines.Exchange(I, MinIndex);
  end;
  
  ShowMessage('行已按字母順序排序');
end;

這個實作使用選擇排序演算法。FindMinLineIndex 函式從指定起始位置開始,尋找到末尾的最小行索引。它初始化 MinIndex 為起始位置,然後遍歷後續所有行,每當發現更小的行就更新 MinIndex。字串比較是按字母順序進行的,大小寫敏感。

SortMemoLines 過程是主排序邏輯。外層迴圈從第一行遍歷到倒數第二行(Count - 2),因為最後一行在其他所有行排序完成後自然處於正確位置。對每個位置,找出剩餘部分的最小行,如果不是當前位置的行,則交換它們。經過 Count - 1 次迭代,所有行按字母順序排列。

常數的定義與應用

常數是程式中值不變的實體,使用常數而非直接寫入數值能夠提升程式碼的可讀性與可維護性。Delphi 支援匿名常數與命名常數兩種形式。

匿名常數直接出現在程式碼中,沒有特定名稱。例如數字 100、字串 ‘Hello’ 或布林值 True 都是匿名常數。它們的缺點是意義不明確,且修改時需要找出所有出現位置。

命名常數在宣告區段定義,具有名稱與類型。語法為 const 關鍵字開始,後接常數名稱、等號與值。類型會自動從值推導,無需明確指定。

const
  MaxStudents = 100;
  AppTitle = 'Student Management System';
  DefaultGrade = 'A';
  Pi = 3.14159265358979;
  IsDebugMode = False;

procedure DemoConstants;
var
  Students: array[1..MaxStudents] of String;
begin
  Application.Title := AppTitle;
  
  if IsDebugMode then
    ShowMessage('除錯模式已啟用');
end;

這個範例定義多個不同類型的常數。MaxStudents 是整數常數,用於定義陣列大小。AppTitle 是字串常數,儲存應用程式標題。Pi 是實數常數,提供圓周率的精確值。IsDebugMode 是布林常數,控制除錯功能的開關。

使用命名常數的優勢明顯。程式碼更易讀,MaxStudents 比數字 100 更能表達意圖。修改更容易,只需在一處更改常數定義,所有使用該常數的地方自動更新。避免打字錯誤,重複輸入相同數值容易出錯,使用常數名稱由編譯器檢查拼寫。

自定義類型的設計原則

Delphi 允許開發者定義自己的類型,這是提升程式碼語意清晰度的重要技術。自定義類型基於現有類型建立,但賦予特定領域的意義。

type
  TKilogram = Integer;
  TCentimeter = Integer;
  TStudentID = String;
  TPercentage = Real;

var
  PersonWeight: TKilogram;
  PersonHeight: TCentimeter;
  StudentCode: TStudentID;
  ExamScore: TPercentage;

procedure ProcessStudent;
begin
  PersonWeight := 70;
  PersonHeight := 175;
  StudentCode := 'STU20240001';
  ExamScore := 85.5;
  
  ShowMessage('學生編號: ' + StudentCode);
  ShowMessage('身高: ' + IntToStr(PersonHeight) + ' 公分');
  ShowMessage('體重: ' + IntToStr(PersonWeight) + ' 公斤');
  ShowMessage('成績: ' + FloatToStr(ExamScore) + '%');
end;

這個範例定義四個自定義類型。TKilogram 與 TCentimeter 都基於 Integer,但語意上分別代表重量與長度單位。TStudentID 基於 String,專門儲存學生編號。TPercentage 基於 Real,表示百分比數值。

使用自定義類型的變數宣告立即傳達變數的用途。PersonWeight 的類型 TKilogram 明確表示這是公斤單位的重量。如果後續發現 Integer 精度不足,只需修改類型定義為 Real,所有使用該類型的變數自動更新,無需逐一修改宣告。

自定義類型也能定義結構化類型,如陣列類型。這在需要傳遞陣列作為參數時特別有用。

type
  TScoreArray = array[1..50] of Real;

procedure CalculateAverage(Scores: TScoreArray; Count: Integer; var Avg: Real);
var
  I: Integer;
  Sum: Real;
begin
  Sum := 0;
  
  for I := 1 to Count do
    Sum := Sum + Scores[I];
  
  if Count > 0 then
    Avg := Sum / Count
  else
    Avg := 0;
end;

定義 TScoreArray 類型後,可以將其用作過程參數類型。這比在參數列表中直接寫陣列定義更清晰簡潔。CalculateAverage 過程接受成績陣列、有效元素數量,計算平均值並透過變數參數回傳。

陣列資料結構基礎

陣列是最基本的資料結構之一,它將相同類型的多個元素組織為一個集合,每個元素透過唯一的索引存取。靜態陣列在編譯時確定大小,執行時期無法改變。

陣列宣告語法包含陣列名稱、array 關鍵字、索引範圍與元素類型。索引範圍定義第一個元素的索引與最後一個元素的索引,中間用兩個點分隔。

var
  Numbers: array[1..100] of Integer;
  Names: array[0..49] of String;
  Scores: array[1..30] of Real;
  Flags: array[1..10] of Boolean;

procedure InitializeArrays;
var
  I: Integer;
begin
  for I := 1 to 100 do
    Numbers[I] := I * 10;
  
  for I := 0 to 49 do
    Names[I] := 'Student' + IntToStr(I + 1);
  
  for I := 1 to 30 do
    Scores[I] := Random * 100;
  
  for I := 1 to 10 do
    Flags[I] := (I mod 2) = 0;
end;

這個範例宣告四種不同類型的陣列並初始化它們。Numbers 陣列儲存 100 個整數,索引從 1 到 100。Names 陣列儲存 50 個字串,索引從 0 到 49。Scores 陣列儲存 30 個實數。Flags 陣列儲存 10 個布林值。

InitializeArrays 過程使用迴圈填充這些陣列。Numbers 的每個元素設為索引值乘以 10。Names 的每個元素設為 ‘Student’ 加上流水號。Scores 使用 Random 函式生成 0 到 100 之間的隨機數。Flags 將偶數位置設為 True,奇數位置設為 False。

選擇排序演算法的完整實作

選擇排序是最直觀的排序演算法之一,其核心思想是重複從未排序部分選擇最小元素,將其放到已排序部分的末尾。雖然效能不是最優,但實作簡單且容易理解,適合教學與小規模資料排序。

type
  TIntArray = array[1..100] of Integer;

procedure Swap(var A, B: Integer);
var
  Temp: Integer;
begin
  Temp := A;
  A := B;
  B := Temp;
end;

function FindMinIndex(const Data: TIntArray; StartIndex, EndIndex: Integer): Integer;
var
  I: Integer;
  MinIndex: Integer;
begin
  MinIndex := StartIndex;
  
  for I := StartIndex + 1 to EndIndex do
  begin
    if Data[I] < Data[MinIndex] then
      MinIndex := I;
  end;
  
  Result := MinIndex;
end;

procedure SelectionSort(var Data: TIntArray; Count: Integer);
var
  I: Integer;
  MinIndex: Integer;
begin
  for I := 1 to Count - 1 do
  begin
    MinIndex := FindMinIndex(Data, I, Count);
    
    if MinIndex <> I then
      Swap(Data[I], Data[MinIndex]);
  end;
end;

這個實作包含三個核心元素。Swap 過程交換兩個整數變數的值,使用臨時變數儲存其中一個值,避免資料丟失。這是交換演算法的標準模式。

FindMinIndex 函式在指定範圍內尋找最小值的索引。它接受陣列、起始索引與結束索引作為參數,初始化 MinIndex 為起始位置,然後遍歷後續元素,每當發現更小的元素就更新 MinIndex,最後回傳最小元素的索引位置。

SelectionSort 過程實作主要排序邏輯。外層迴圈從第一個位置遍歷到倒數第二個位置,因為最後一個元素在其他所有元素排序完成後自然處於正確位置。對每個位置 I,呼叫 FindMinIndex 找出從 I 到陣列末尾的最小元素位置,如果最小元素不在當前位置,則交換它們。經過 Count - 1 次迭代,陣列完全排序。

時間複雜度分析顯示選擇排序在所有情況下都需要 O(n²) 次比較操作。無論輸入資料是否已排序,演算法都執行相同次數的比較。空間複雜度為 O(1),只需要常數個額外變數。選擇排序的優勢在於交換次數少,最多執行 n-1 次交換,這在交換操作代價高昂時是優勢。

@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 selectsort {
  state "初始化" as init
  state "尋找最小值索引" as findmin
  state "交換元素" as swap
  state "增加位置" as increment
  state "檢查是否完成" as check
  
  [*] --> init
  init --> findmin
  findmin --> swap: 最小值不在當前位置
  findmin --> increment: 最小值在當前位置
  swap --> increment
  increment --> check
  check --> findmin: 未完成
  check --> [*]: 完成
}

note right of findmin
  從當前位置到末尾
  尋找最小元素
end note

note right of swap
  將最小元素
  移至當前位置
end note

@enduml

持續學習與實務應用

透過 TMemo 元件與陣列資料結構的深入學習,我們掌握文字處理與基礎演算法的核心技術。但這只是程式設計旅程的一個階段,持續學習更進階的主題是成為專業開發者的必經之路。

動態陣列提供執行時期調整大小的能力,克服靜態陣列的限制。二維陣列支援表格資料的處理,適合矩陣運算與遊戲開發。泛型集合類別提供類型安全的資料結構,包括 List、Dictionary 與 Queue 等,大幅簡化複雜資料管理。

演算法學習應該繼續深入。快速排序與合併排序提供更高效的排序方案,時間複雜度達到 O(n log n)。搜尋演算法如二分搜尋在有序資料中快速定位目標。圖形演算法處理複雜的關係網路。理解演算法的時間與空間複雜度,能夠在不同場景選擇最適合的解決方案。

實務專案開發整合所有學到的技術。建立完整的文字編輯器應用 TMemo 的所有功能。開發資料管理系統運用陣列與排序。參與開源專案學習專業程式碼組織。透過不斷實踐累積經驗,將理論知識轉化為實際能力,最終成為能夠獨立解決複雜問題的軟體工程師。