在視窗應用程式開發的實務中,處理多行文字內容是極為常見的需求。無論是開發文字編輯器、日誌檢視工具或是資料輸入介面,都需要能夠有效管理與操作多行文字的元件。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
@endumlTMemo 的關鍵屬性詳解
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 的所有功能。開發資料管理系統運用陣列與排序。參與開源專案學習專業程式碼組織。透過不斷實踐累積經驗,將理論知識轉化為實際能力,最終成為能夠獨立解決複雜問題的軟體工程師。