在軟體開發過程中,將複雜問題分解成較小、較易管理的部分是一項重要技能。Delphi語言提供了強大的模組化程式設計工具,特別是過程(Procedure)和函式(Function),讓我們能夠建立結構良好、易於維護的應用程式。今天,我將透過開發一個二次方程式求解器來展示這些概念的實際應用。

二次方程式求解器的介面設計

在開始撰寫程式碼前,我們需要規劃應用程式的使用者介面。對於二次方程式求解器,需要以下元素:

  1. 三個輸入欄位(edtA, edtB, edtC)用於輸入二次方程式的係數A、B、C
  2. 兩個標籤(lblX1, lblX2)用於顯示方程式的根
  3. 一個標籤(lblNo)顯示「No real roots are found」訊息
  4. 一個按鈕(btnFind)執行求解過程

初始時,所有顯示結果的標籤都應設為不可見(Visible = False),僅在有相應結果時才顯示。

解題思路與程式結構

在實作之前,讓我先用註解勾勒出解決方案的輪廓:

// 輸入二次方程式係數A、B和C
// 計算判別式D
// 如果D >= 0,則
//   計算方程式的根X1和X2
//   顯示根
// 否則
//   通知使用者無實數解

這個概述幫助我確定了所需的變數、過程和函式。若解決方案中的單一邏輯步驟需要多個Delphi陳述式來表達,我會將其封裝為一個過程;否則,單一陳述式就足夠了。

過程(Procedure)的實作

在Delphi中,過程是一組執行特定任務的程式碼塊。當程式遇到過程呼叫時,控制權會轉移到該過程,執行其中的陳述式,然後回傳呼叫點繼續執行後續程式碼。

按鈕點選事件處理程式

首先,讓我們實作按鈕點選事件處理程式:

procedure TfrmQuadEq.btnFindClick(Sender: TObject);
var
  A, B, C, D, X1, X2: real;
begin
  coefInput(A, B, C);     // 讀取係數ABC
  D := sqr(B) - 4*A*C;    // 計算判別式D
  if D >= 0 then          // 如果判別式 >= 0
  begin
    calc(A, B, D, X1, X2); // AB和D計算根x1x2
    prn(X1, X2);           // 顯示根x1x2
  end
  else                     // 否則
    lblNo.Visible := true; // 通知使用者無解
end;

這個過程是應用程式的核心,它定義了當使用者點選「Find」按鈕時執行的操作。程式流程非常清晰:

  1. 首先呼叫coefInput過程讀取使用者輸入的係數
  2. 然後計算判別式D = B² - 4AC
  3. 根據判別式的值決定後續動作:
    • 如果D≥0,則方程有實數解,呼叫calc計算根,並呼叫prn顯示結果
    • 如果D<0,則顯示「無實數解」的訊息

注意,在這個過程中我們使用了三個自定義過程:INLINE_CODE_0INLINE_CODE_2__和__INLINE_CODE_3,它們各自負責特定任務,體現了程式的模組化設計。

過程的定義與呼叫語法

過程呼叫語法: CODE_BLOCK_2

過程定義語法: CODE_BLOCK_3

重要的是,過程必須在__INLINE_CODE_7__部分中定義,與必須在呼叫之前就已定義。呼叫引數和形式引數之間存在一對應關係,它們的數量、順序和類別必須相同。

顯示結果的過程

讓我們定義一個用於顯示方程式根的過程:

CODE_BLOCK_4

INLINE_CODE_8__過程負責將計算出的根顯示在介面上。它接受兩個實數引數__INLINE_CODE_9__和__INLINE_CODE_10(對應呼叫時的__INLINE_CODE_11__和__INLINE_CODE_12__)。這些是值引數(value parameters),意味著它們只從呼叫者傳遞到過程,而不會回傳給呼叫者。

程式碼中,我們首先將顯示根值的標籤設為可見,然後使用__INLINE_CODE_13__函式將實數轉換為字串,並設定為標籤的Caption屬性。注意,在自定義過程中,我們需要指定元件所屬的表單(例如__INLINE_CODE_14__),因為一個應用程式中可能有多個表單。

輸入係數的過程

接下來,我們定義一個用於從文字框取得係數的過程:

CODE_BLOCK_5

__INLINE_CODE_0__過程從文字框中讀取係數值並儲存到變數中。這裡的關鍵是引數前面的__INLINE_CODE_16__關鍵字,它表示這些是變數引數(variable parameters)。變數引數不僅將資料傳遞給過程,還將過程中的修改回傳給主程式中的變數。

使用__INLINE_CODE_17__函式將文字框的字串值轉換為實數。這個轉換是必要的,因為文字框的Text屬性是字串類別,而我們的變數是實數類別。

計算根的過程

現在我們來定義計算方程式根的過程:

CODE_BLOCK_6

INLINE_CODE_18__過程實作了二次方程式求根公式。它接受係數和判別式作為輸入,然後計算兩個根。引數__INLINE_CODE_19、__INLINE_CODE_20__和__INLINE_CODE_21__是值引數,而__INLINE_CODE_9__和__INLINE_CODE_10__是變數引數,因為我們需要將計算結果回傳給呼叫程式。

計算使用標準的二次方程式求根公式:x = (-b ± √D) / (2a),其中D是判別式。

初始化過程

為了改善程式的使用體驗,我們還需要一個不帶引數的過程,用於重置顯示狀態:

CODE_BLOCK_7

__INLINE_CODE_24__過程將所有結果標籤設為不可見,確保應用程式處於乾淨的初始狀態。這在使用者輸入新的係數集並希望清除先前結果時特別有用。

函式(Function)的應用

函式與過程類別似,但有一個重要區別:函式會回傳一個值。Delphi有許多內建函式,但有時我們需要編寫自己的函式來滿足特定需求。

函式的定義與使用

讓我們看一個簡單的例子,計算直角三角形的邊長:

CODE_BLOCK_8

這個過程計算直角三角形中,已知一個角度(以度為單位)和鄰邊長度,求另一條邊的長度。它使用了一個名為__INLINE_CODE_25__的自定義函式運算正切值。

函式的宣告格式

CODE_BLOCK_9

函式可以同時具有值引數和變數引數,遵循與過程相同的規則。函式必須至少有一個指定運算元,將值賦給函式名稱,這就是函式的回傳值。

正切函式的實作

由於Delphi沒有內建的正切函式,我們可以自己實作一個:

CODE_BLOCK_10

這個函式運算給定角度(以度為單位)的正切值。首先,它將角度從度轉換為弧度(因為Delphi的三角函式使用弧度),然後使用正弦和餘弦的比值計算正切。

函式中的區域性變數__INLINE_CODE_26__只在函式內部可見,用於儲存中間計算結果。最關鍵的是__INLINE_CODE_27__這行,它將計算結果指定給函式名,這個值將作為函式的回傳值。

實作練習:鞏固學習

為了加深對過程和函式的理解,讓我們看幾個實作練習:

練習1:變數值交換

建立一個程式,交換變數A和B的值,以及交換變數C和D的值。定義一個過程來交換兩個變數的值。

CODE_BLOCK_11

練習2:三角形計算

已知兩個三角形各邊的長度,計算它們周長之和麵積之和。定義一個過程,根據三角形三邊長度計算其周長和麵積。

CODE_BLOCK_12 6/2 + 6/2 + 13/2 + 13/2 + 21/2 + 21/2 __CODE_BLOCK_13__pascal function y(x: Real): Real; begin Result := x + x; end; __CODE_BLOCK_14__pascal var result: Real; begin result := y(6/2) + y(13/2) + y(21/2); ShowMessage(‘計算結果: ’ + FloatToStr(result)); end; CODE_BLOCK_15 15/8 + 8/15 + 6/12 + 12/6 + 7/21 + 21/7 __CODE_BLOCK_16__pascal function y(a, b: Real): Real; begin Result := a + b; end;

procedure TForm1.CalculateExpression; var result: Real; begin result := y(15, 8) + y(6, 12) + y(7, 21); ShowMessage(‘計算結果: ’ + FloatToStr(result)); end; __CODE_BLOCK_17__pascal function Distance(x1, y1, x2, y2: Real): Real; begin Result := Sqrt(Sqr(x2 - x1) + Sqr(y2 - y1)); end;

procedure TForm1.CalculateTrianglePerimeter; var x1, y1, x2, y2, x3, y3: Real; perimeter: Real; begin // 假設我們已經從使用者那裡取得了三個頂點的座標 x1 := StrToFloat(Edit1.Text); y1 := StrToFloat(Edit2.Text); x2 := StrToFloat(Edit3.Text); y2 := StrToFloat(Edit4.Text); x3 := StrToFloat(Edit5.Text); y3 := StrToFloat(Edit6.Text);

// 計算周長 perimeter := Distance(x1, y1, x2, y2) + Distance(x2, y2, x3, y3) + Distance(x3, y3, x1, y1);

ShowMessage(‘三角形周長: ’ + FloatToStr(perimeter)); end; CODE_BLOCK_18 t = [max(a,b,c) - min(a,b,c)] / [2 * max(a,b,c) * min(a,b,c)] __CODE_BLOCK_19__pascal function Max3(a, b, c: Real): Real; begin Result := a; if b > Result then Result := b; if c > Result then Result := c; end;

function Min3(a, b, c: Real): Real; begin Result := a; if b < Result then Result := b; if c < Result then Result := c; end;

function CalculateT(a, b, c: Real): Real; var maxVal, minVal: Real; begin maxVal := Max3(a, b, c); minVal := Min3(a, b, c); Result := (maxVal - minVal) / (2 * maxVal * minVal); end;



在這個例子中,我們定義了兩個輔助函式 `Max3`  `Min3`,分別用於找出三個數中的最大值和最小值。然後,我們使用這些函式在 `CalculateT` 中計算給定表示式的值。這種方法使程式碼更加清晰與易於維護。

如果我們想要進一步最佳化,我們可以使用 Pascal 內建的 `Math` 單元中的 `Max`  `Min` 函式,但在某些情況下,自定義函式可能會提供更多的靈活性和控制。

## 圖形繪製基礎

Delphi 提供了強大的圖形繪製功能。在這一節中,我們將探討如何使用 TPaintBox 元件來繪製各種圖形。

### 設定繪圖環境

首先,我們需要在表單上放置一個 TPaintBox 元件,並設定其屬性:

```pascal
procedure TForm1.FormCreate(Sender: TObject);
begin
  //  PaintBox 命名為 pbxEx
  pbxEx.Name := 'pbxEx';
end;

TPaintBox 的座標系統原點在左上角,X 軸向右,Y 軸向下。我們可以透過 Width 和 Height 屬性取得 PaintBox 的尺寸,並計算其中心點:

procedure TForm1.CalculateCenter;
var
  x0, y0: Integer;
begin
  x0 := pbxEx.Width div 2;
  y0 := pbxEx.Height div 2;
  // 使用中心點座標進行繪圖...
end;

這段程式碼計算 PaintBox 的中心點座標。div 運算元執行整數除法,確保結果是整數。中心點座標 (x0, y0) 可以用於以 PaintBox 中心為基準的繪圖操作。

基本繪圖工具

Delphi 提供了各種繪圖工具,包括筆(Pen)、刷子(Brush)和字型(Font)。

設定繪圖筆的屬性

procedure TForm1.SetupPen;
begin
  pbxEx.Canvas.Pen.Color := clRed;    // 設定筆的顏色為紅色
  pbxEx.Canvas.Pen.Width := 3;        // 設定筆的寬度為 3 畫素
end;

設定填充刷子的屬性

procedure TForm1.SetupBrush;
begin
  pbxEx.Canvas.Brush.Color := clGreen;  // 設定刷子的顏色為綠色
end;

清空畫布

procedure TForm1.ClearCanvas;
begin
  pbxEx.Canvas.Brush.Color := clWhite;
  pbxEx.Canvas.FillRect(ClientRect);
end;

上述程式碼分別設定了繪圖筆和填充刷子的屬性。Pen.ColorPen.Width 控制繪製線條的顏色和寬度,而 Brush.Color 控制閉合圖形內部的填充顏色。FillRect 方法用白色填充整個畫布,相當於清空畫布。

繪製基本圖形

Delphi 提供了多種基本圖形繪製方法。以下是一些常用的方法:

繪製點

procedure TForm1.DrawPoint(x, y: Integer; color: TColor);
begin
  pbxEx.Canvas.Pixels[x, y] := color;
end;

Pixels 屬性允許我們直接設定畫布上特定座標的畫素顏色。這是繪製單個畫素點的最直接方法。

繪製線段

procedure TForm1.DrawLine(x1, y1, x2, y2: Integer);
begin
  pbxEx.Canvas.MoveTo(x1, y1);  // 移動到起點
  pbxEx.Canvas.LineTo(x2, y2);  // 繪製到終點
end;

MoveTo 方法將繪圖筆移動到指定位置,而不繪製任何內容。LineTo 方法從當前位置繪製一條線到指定位置,並將繪圖筆移動到終點。這兩個方法通常一起使用,用於繪製線段。

繪製矩形

procedure TForm1.DrawRectangle(x1, y1, x2, y2: Integer);
begin
  pbxEx.Canvas.Rectangle(x1, y1, x2, y2);
end;

Rectangle 方法繪製一個矩形,其對角線的端點為 (x1, y1)(x2, y2)。矩形的邊與座標軸平行。繪製的矩形會使用當前的筆繪製邊框,並使用當前的刷子填充內部。

繪製橢圓

procedure TForm1.DrawEllipse(x1, y1, x2, y2: Integer);
begin
  pbxEx.Canvas.Ellipse(x1, y1, x2, y2);
end;

Ellipse 方法繪製一個橢圓,該橢圓被包含在由 (x1, y1)(x2, y2) 定義的矩形中。橢圓的主軸與座標軸平行。如同矩形,橢圓也會使用當前的筆繪製邊框,並使用當前的刷子填充內部。

迴圈與圖形繪製例項

在程式設計中,迴圈是執行重複任務的強大工具。Delphi 提供了三種主要的迴圈結構:While…do、Repeat…Until 和 For 迴圈。

While…do 迴圈

While…do 迴圈是前測試迴圈,它在執行迴圈體之前先測試條件。如果條件為 True,則執行迴圈體;否則,跳過迴圈體。

procedure TForm1.WhileLoopExample;
var
  i: Integer;
begin
  i := 1;
  while i <= 10 do
  begin
    // 執行某些操作
    DrawPoint(i * 10, 100, clRed);
    i := i + 1;
  end;
end;

這個例子使用 While…do 迴圈在水平線上繪製 10 個紅點。迴圈初始化 i = 1,然後在每次迴圈中檢查 i <= 10。如果條件為真,則在位置

模擬時鐘與字串基礎

在進入字串處理的核心內容前,先來看一個實用的練習案例:建立一個模擬時鐘應用程式。這個練習要求我們透過文字方塊輸入時間,然後按下按鈕後,時鐘指標會顯示該時間。這個簡單的案例將會用到字串處理和圖形繪製技術。

字串的本質與定義

在 Delphi 中,許多元件屬性如 Caption 或 Text 只能接受字串值。但究竟什麼是字串?它們在程式中如何運作?

字串是一系列包含在單引號內的符號序列。若要宣告字串變數,需使用 String 型別:

Var
  s: String;

這個宣告允許程式使用不受長度限制的字串變數。在早期的 Delphi 版本中,字串長度有限制,但現代 Delphi 中的 String 型別已經能夠處理幾乎無限長度的字串。

字串的基本操作

字串連線

字串可以進行連線操作,連線符號為加號(+)。例如:

Var
  s, st: String;
Begin
  s := 'We learn'; // 將 "We learn" 存入第一個變數
  st := ' Delphi'; // 將 " Delphi" 存入第二個變數
  s := s + st;     // 連線這兩個字串
End;

這段程式碼展示了字串連線的基本操作。首先宣告了兩個字串變數 s 和 st,然後分別賦予初始值。關鍵在於第三行使用 + 運算元將兩個字串連線,結果會將 st 的內容附加在 s 之後,最終 s 的值變為 “We learn Delphi”。字串連線是處理文字資料時最常用的操作之一,特別是在需要動態組合訊息或建構複雜文字時。

字串比較

字串可以進行比較操作。比較是逐字元進行的,使用符號的內部表示進行比較。拉丁字母按字母順序排列,數字按自然順序排列,其中 0 < 1 < … < 9。

比較範例:

  • ‘AB’ > ‘AA’(第二個字元 B 大於 A)
  • ‘A’ < ‘AB’(第一個字串較短)
  • ‘DC’ > ‘ABCDE’(第一個字元 D 大於 A)
  • ‘ABCE’ > ‘ABCD’(第四個字元 E 大於 D)

字串比較在排序、搜尋和資料驗證等應用中非常重要。瞭解字串比較的規則有助於正確實作這些功能。

標準字串函式與程式

Delphi 提供了豐富的標準函式和程式來處理字串。以下是一些最常用的字串處理工具:

字串程式

名稱與引數引數型別功能說明
Delete(St, Pos, N)St: string;
Pos, N: integer;
從字串 St 的 Pos 位置開始刪除 N 個符號
Insert(St1, St2, Pos)St1, St2: string;
Pos: integer;
將字串 St1 插入到字串 St2 的 Pos 位置

字串函式

名稱與引數引數與回傳型別功能說明
Copy(St, Pos, N)回傳型別: string;
St: string;
Pos, N: integer;
從字串 St 的 Pos 位置複製 N 個符號並回傳
Length(St)回傳型別: integer;
St: string;
計算字串 St 的長度(符號數量)
Pos(St1, St2)回傳型別: integer;
St1, St2: string;
在字串 St2 中搜尋子字串 St1 的第一次出現。結果是子字串第一個符號的位置。若未找到則回傳 0

實際應用範例

Delete 程式範例:

字串 St 的值          | 執行的陳述式       | 執行完畢後 St 的值
--------------------|-------------------|------------------
'abcdef'            | Delete(St,4,2)    | 'abcf'
'Turbo-Pascal'      | Delete(St,1,6)    | 'Pascal'

Insert 程式範例:

字串 St1 的值  | 字串 St2 的值  | 執行的陳述式         | 執行完畢後 St2 的值
--------------|--------------|---------------------|------------------
'Turbo'       | '-Pascal'    | Insert(St1, St2,1)  | 'Turbo-Pascal'
'-Pascal'     | 'Turbo'      | Insert(St1, St2,6)  | 'Turbo-Pascal'

Copy 函式範例:

字串 St 的值    | 執行的陳述式           | 執行完畢後 Str 的值
--------------|-----------------------|------------------
'abcdefg'     | Str:=Copy(St,2,3);    | 'bcd'
'abcdefg'     | Str:=Copy(St,4,4);    | 'defg'

Copy 函式是字串操作中的利器!第一個引數指定來源字串,第二個引數指定起始位置(注意 Delphi 中位置計數從 1 開始),第三個引數指定要複製的字元數量。這個函式在需要擷取字串特定部分時特別有用,比如從完整路徑中提取檔名,或從全名中分離姓和名。

Length 函式範例:

字串 St 的值    | 執行的陳述式       | 執行完畢後 N 的值
--------------|--------------------|------------------
'abcdefg'     | N:=Length(St);     | 7
'Turbo-Pascal'| N:=Length(St);     | 12

Pos 函式範例:

字串 St2 的值  | 執行的陳述式        | 執行完畢後 N 的值
-------------|---------------------|------------------
'abcdef'     | N:=Pos('de', St2);  | 4
'abcdef'     | N:=Pos('r', St2);   | 0

Pos 函式在字串中搜尋特定子字串的位置,這是開發中常需要的功能。如果找到子字串,則回傳其第一次出現的起始位置;如果找不到,則回傳 0。這個特性使得 Pos 函式不僅能用於取得位置訊息,還能用於判斷某個子字串是否存在於目標字串中。

進階範例:字詞順序交換

以下程式接收一個包含兩個字詞(以單一空格分隔)的字串,並交換這兩個字詞的順序:

Procedure Change(var s: String);
var
  s1: string;
begin
  s1 := copy(s, 1, pos(' ', s)-1); // 複製第一個詞(空格之前的所有內容)
  delete(s, 1, pos(' ', s));      // 刪除第一個詞和空格(s 現在只包含第二個詞)
  s := s + ' ' + s1;              // 加上空格和第一個詞到結尾
end;

這個程式展示瞭如何靈活運用字串處理函式來處理實際問題。程式首先使用 copy 函式擷取原字串中第一個空格之前的內容(第一個詞)。然後使用 delete 函式從原字串中刪除第一個詞和空格,使原字串只剩下第二個詞。最後,將第二個詞、一個空格和第一個詞拼接起來,達到交換詞序的目的。

這種技術在處理自然語言文字、解析指令或格式化輸出時非常實用。例如,可以用類別似的方法將「姓 名」格式轉換為「名 姓」格式。

實用練習:字串處理技巧

以下是一系列練習,幫助鞏固對字串處理的理解:

練習 1:交換文字中的第二個和第三個詞

編寫一個程式,按下按鈕後,交換文字方塊中的第二個和第三個詞。文字方塊的內容應該只包含三個詞,以空格分隔。

實作思路:

  1. 找到第一個空格的位置
  2. 從該位置後找到第二個空格的位置
  3. 據此分割出三個詞
  4. 重新組合為「第一個詞 + 第三個詞 + 第二個詞」的順序
procedure TForm1.Button1Click(Sender: TObject);
var
  s, word1, word2, word3: string;
  pos1, pos2: Integer;
begin
  s := Edit1.Text;
  pos1 := Pos(' ', s);
  pos2 := Pos(' ', Copy(s, pos1+1, Length(s)));
  pos2 := pos2 + pos1;
  
  word1 := Copy(s, 1, pos1);
  word2 := Copy(s, pos1+1, pos2-pos1);
  word3 := Copy(s, pos2+1, Length(s));
  
  Edit1.Text := word1 + word3 + word2;
end;

這段程式碼首先取得文字方塊的內容,然後找到第一個空格的位置(pos1)。接著找到第二個空格的位置:先從第一個空格後的字串中找到空格,再加上 pos1 得到在原字串中的絕對位置(pos2)。然後分別提取三個詞,最後重新組合並更新文字方塊。這個技術在文書處理中非常實用,尤其是需要重新排列句子成分時。

練習 2:將空格替換為驚嘆號

按下按鈕時,將文字方塊中的所有空格替換為驚嘆號。

這個練習可以使用迴圈遍歷字串的每個字元,或者使用更高效的方法:

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
  i: Integer;
begin
  s := Edit1.Text;
  for i := 1 to Length(s) do
    if s[i] = ' ' then
      s[i] := '!';
  Edit1.Text := s;
end;

這段程式碼使用 for 迴圈遍歷字串中的每個字元。當遇到空格時,將它替換為驚嘆號。在 Delphi 中,字串可以像陣列一樣被存取,s[i] 表示字串 s 中第 i 個字元。這種字串操作方法對於需要逐字元處理的場景非常有用,比如字元替換、統計特定字元出現次數等。

練習 3:計算句點數量

計算文字方塊字串中句點的數量。

procedure TForm1.Button1Click(Sender: TObject);
var
  s: string;
  count, i: Integer;
begin
  s := Edit1.Text;
  count := 0;
  for i := 1 to Length(s) do
    if s[i] = '.' then
      Inc(count);
  Label1.Caption := '句點數量: ' + IntToStr(count);
end;

這個程式碼示範瞭如何計算特定字元在字串中出現的次數。它初始化一個計數器變數 count 為 0,然後遍歷字串中的每個字元。每當遇到句點時,就增加計數器的值。最後將結果顯示在標籤上。這種計數技術可以擴充套件到計算任何字元或模式的出現次數,例如計算文字中的單詞數量或特定關鍵字的出現頻率。