現代應用程式介面設計中,回應速度是影響使用者經驗的關鍵因素。本文將探討如何設計回應式使用者介面,並以 Delphi 的 VCL 和 FireMonkey 框架為例,示範如何最佳化各種控制項的更新效率。同時,我們也將探討虛擬清單控制和動態快取等技術,以提升程式整體效能。

在 Delphi/VCL 中,頻繁更新控制項屬性,例如進度條的 Position 屬性,會導致大量的 Windows 訊息傳遞,進而影響效能。透過減少更新頻率,例如以百分比更新進度條,可以有效提升效率。對於清單盒和文字框等需要批次更新的控制項,使用 BeginUpdate 和 EndUpdate 方法可以避免不必要的螢幕重繪,從而提升效能。此外,針對 TMemo 控制項的特性,使用 TStringList 收集更新內容,最後一次性更新 Text 屬性,可以大幅減少 Windows 訊息的傳遞次數,進一步提升效能。FireMonkey 框架雖然與 VCL 不同,但 BeginUpdate/EndUpdate 方法仍然有效。虛擬清單控制技術,例如虛擬清單盒和虛擬樹狀圖,可以避免載入所有資料到記憶體,僅顯示可見區域的資料,從而提升效能,特別是在處理大量資料時。最後,以費波那契數列計算為例,說明如何利用快取技術避免重複計算,提升程式執行效率。動態快取設計可以根據實際需求,更彈性地管理快取內容,進一步提升效能。

寫作回應式使用者介面

一個很好的多執行緒方法的候選者是檔案讀取範例。的確,Windows 提供了檔案讀取的非同步 API,但它相當複雜且難以正確實作。將讀取操作推入執行緒更簡單,如果需要跨平臺相容,也更容易實作。

第二類(更新使用者介面需要很長時間)通常可以用一個簡單的解決方案來解決:減少更新!Delphi/Windows 控制項通常非常快,只有當我們想要在短時間內執行太多操作時才會失敗。

現在,我們將關注這一類別。我將展示一些過度更新的範例,並提供更快程式碼的解決方案。

更新進度條

幾乎任何對 VCL 控制項的更改都可能導致一個或多個 Windows 訊息被傳送到作業系統。這需要時間,尤其是當程式等待作業系統處理訊息並傳回答案時。即使使用者介面沒有改變,也可能發生這種情況。

接下來的示範將展示過多的訊息如何減慢執行速度。我必須承認,ProgressBar 演示中的程式碼有一點牽強。然而,我向你保證,我在生產環境中看到過類似的程式碼。

這個示範模擬讀取一個大檔案區塊(由玄貓執行)。對於每個區塊,進度條都會更新。為了進行速度比較,進度條更新是以兩種方式完成的。在第一種,較慢的方法中,進度條的 Max 屬性被設定為要讀取的檔案大小。每個區塊後,進度條的 Position 屬性都會設定為到目前為止已讀取的位元組數:

function TfrmProgressBar.Test0To2G: Integer;
var
  total: Integer;
  block: Integer;
  sw: TStopwatch;
begin
  sw := TStopwatch.StartNew;
  ProgressBar1.Max := CFileSize;
  ProgressBar1.Position := 0;
  total := 0;

  while total < CFileSize do
  begin
    block := CFileSize - total;

    if block > 1024 then
      block := 1024;
    // reading 'block' bytes

    Inc(total, block);
    ProgressBar1.Position := total;
    ProgressBar1.Update;
  end;
  Result := sw.ElapsedMilliseconds;
end;

這段程式碼執行緩慢有兩個原因。首先,設定 Position 屬性會向 Windows 傳送 PBM_SETPOS 訊息,這相對於非圖形程式程式碼是一個相對慢的操作。第二,當我們呼叫 Update 時,UpdateWindow Windows API 函式會被呼叫。這個函式會重繪進度條,即使其位置沒有改變,這也需要更多時間。由於這一切都被呼叫了 1,953,125 次,因此這會增加可觀的 overhead。

第二種,較快的方法,設定 Max 為 100。每個區塊後,進度會以百分比計算,currPct := Round(total / CFileSize * 100)。Position 屬性只會在百分比與進度條目前位置不同時更新。由於讀取 Position 屬性也會向系統傳送一條訊息,因此目前位置會被儲存在一個區域性變數 lastPct 中,並且新的值會與它進行比較:

function TfrmProgressBar.Test0To100: Integer;
var
  total: Integer;
  block: Integer;
  sw: TStopwatch;
  lastPct: Integer;
  currPct: Integer;
begin
  sw := TStopwatch.StartNew;
  ProgressBar1.Max := 100;
  ProgressBar1.Position := 0;
  lastPct := 0;
  total := 0;

  while total < CFileSize do
  begin
    block := CFileSize - total;

    if block > 1024 then
      block := 1024;
    // reading 'block' bytes
    Inc(total, block);
    currPct := Round(total / CFileSize \* 100);

    if currPct > lastPct then

圖表翻譯:

此圖示進度條更新過程,展示了兩種不同的更新方法。第一種方法是直接更新進度條的 Position 屬性,第二種方法是根據進度百分比更新 Position 屬性。第二種方法可以減少更新次數,從而提高效率。

  flowchart TD
    A[開始] --> B[讀取檔案區塊]
    B --> C[計算進度百分比]
    C --> D[更新進度條 Position]
    D --> E[檢查是否完成]
    E -->|是| F[結束]
    E -->|否| B

內容解密:

此範例展示瞭如何最佳化進度條更新過程。透過計算進度百分比並根據百分比更新進度條 Position 屬性,可以減少更新次數,從而提高效率。此方法適用於任何需要更新進度條的情況。

寫作回應式使用者介面

在設計使用者介面時,回應速度是非常重要的。一個快速回應的介面可以大大提高使用者經驗。然而,當處理大量資料或複雜計算時,介面的回應速度可能會變慢。在本節中,我們將探討如何最佳化介面的回應速度。

進度條示範

首先,我們來看一個進度條的示範。進度條是用於顯示任務進度的控制元件。在 Windows XP 之前,進度條的更新是即時的。但是在 Vista 之後,進度條的更新變成了動畫。這意味著,如果你設定進度條的位置為 50,進度條將會從左側動畫到中間。

ProgressBar1.Position := currPct;
ProgressBar1.Update;

然而,這種動畫效果有時會導致奇怪的行為。例如,如果你設定進度條的位置為 100,然後立即設定為 50,進度條將會動畫從右側到中間。但是,如果你想立即更新進度條的位置,可以使用以下技巧:

ProgressBar1.Position := currPct + 1;
ProgressBar1.Position := currPct;

這樣可以立即更新進度條的位置。

批次更新

另一個與過多訊息相關的問題是當你想要新增或修改多行文字時。例如,你可以使用 TListBoxTMemo 控制元件。

for i := 1 to CNumLines do
  ListBox1.Items.Add('Line ' + IntToStr(i));

然而,這種方法可能會很慢。幸運的是,TStrings 類別提供了 BeginUpdateEndUpdate 方法,可以暫停和還原視覺更新。

ListBox1.Items.BeginUpdate;
for i := 1 to CNumLines do
  ListBox1.Items.Add('Line ' + IntToStr(i));
ListBox1.Items.EndUpdate;

這樣可以大大提高效率。

最佳化演算法

在 demo 程式中,點選第二個按鈕後,程式的反應速度明顯加快。在我的電腦上,執行時間分別為 TListBox 的 285 毫秒和 TMemo 的 1,189 毫秒。後者看起來有些可疑,為什麼 TMemo 需要 1.2 秒來新增 10,000 行,如果不更新螢幕呢?

為了找到答案,我們需要深入 VCL 程式碼,檢視 TStrings.Add 方法:

function TStrings.Add(const S: string): Integer;
begin
  Result := GetCount;
  Insert(Result, S);
end;

首先,這個方法呼叫 GetCount 以傳回追加元素的正確索引。 TMemoStrings.GetCount 的具體實作即使在更新模式下也會發送兩個 Windows 訊息:

Result := SendMessage(Memo.Handle, EM_GETLINECOUNT, 0, 0);
if SendMessage(Memo.Handle, EM_LINELENGTH, SendMessage(Memo.Handle, EM_LINEINDEX, Result - 1, 0), 0) = 0 then Dec(Result);

之後,TMemoStrings.Insert 傳送三個訊息來更新當前選擇:

if Index >= 0 then
begin
  SelStart := SendMessage(Memo.Handle, EM_LINEINDEX, Index, 0);
  // ...
  SendMessage(Memo.Handle, EM_SETSEL, SelStart, SelStart);
  SendTextMessage(Memo.Handle, EM_REPLACESEL, 0, Line);
end;

這導致每追加一行都發送五個 Windows 訊息,導致程式變慢。我們可以做得更好嗎?當然!

為了加速 TMemo,我們需要收集所有更新到某種二級儲存中——例如,TStringList。最後,只需將新的 memo 狀態分配給其 Text 屬性,它就會在一次大規模操作中更新。

demo 程式中的第三個按鈕就是這樣做的:

sl := TStringList.Create;
for i := 1 to CNumLines do
  sl.Add('Line ' + IntToStr(i));
Memo1.Text := sl.Text;
FreeAndNil(sl);

這種改變使執行速度接近列表框。在我的電腦上,只需 1,178 毫秒即可在 memo 中顯示 10,000 行。

一個有趣的比較可以由玄貓進行。 FireMonkey 中的圖形控制元件不直接根據 Windows 控制元件,因此 BeginUpdate/EndUpdate 的效果可能不同。 BeginUpdateFMX 程式在程式碼存檔中就是這樣做的。我不會再次介紹整個過程,只呈現測量結果。所有時間均以毫秒為單位:

框架更新方法TListBoxTMemo
VCL直接更新3,27217,694
VCLBeginUpdate2851,178
VCL文字N/A56
FireMonkey

內容解密:

上述程式碼和討論解釋瞭如何最佳化 TMemo 的效能。透過使用 TStringList 來收集更新並一次性更新 memo 的 Text 屬性,可以顯著提高效能。這種方法可以應用於其他需要頻繁更新的控制元件,以提高使用者介面的回應速度。

虛擬顯示控制的最佳化

在上一節中,我們討論瞭如何使用 BeginUpdateEndUpdate 方法來最佳化 TListBoxTMemo 的更新過程。現在,我們將探討另一種最佳化方法:虛擬顯示控制。

虛擬顯示控制的概念

虛擬顯示控制是一種技術,允許您只生成可視區域的資料,而不是生成所有資料。這種方法可以大大提高程式的效率,特別是在處理大量資料的情況下。

虛擬 TListBox 的實作

要將標準 VCL TListBox 轉換為虛擬控制,您需要:

  1. 將其 Style 屬性設為 lbVirtual
  2. 寫一個 OnData 事件處理程式。
  3. 選擇性地寫 OnDataFindOnDataObject 事件處理程式。

新增 10,000 行到列表方塊變得非常簡單,只需設定其 Count 屬性即可:

procedure TfrmVirtualListbox.Button1Click(Sender: TObject);
var
  stopwatch: TStopwatch;
begin
  stopwatch := TStopwatch.Create;
  stopwatch.Stop;
  StatusBar1.SimpleText := Format('ListBox: %d ms', [stopwatch.ElapsedMilliseconds]);
end;

這個過程非常快速,我的測試電腦只需要幾毫秒就可以完成。

OnData 事件處理程式

要實際顯示資料,VCL 會為每個可視行呼叫 OnData 事件處理程式。輸入引數是行號(從 0 開始),輸出是該行的資料。以下是範例程式碼:

procedure TfrmVirtualListbox.ListBox1Data(
  Control: TWinControl; Index: Integer; var Data: string);
begin
  Data := 'Line ' + IntToStr(Index + 1);
end;

在這個事件處理程式中,您可以生成資料或使用 Index 引數作為查詢鍵來查詢其他結構中的資料。

OnDataFind 事件處理程式

如果您想要在虛擬列表方塊中找到特定的字串,您需要寫一個 OnDataFind 事件處理程式。這個處理程式可以在外部結構中查詢資料,或像下面的範例一樣解析行並根據其內容確定其位置:

function TfrmVirtualListbox.ListBox1DataFind(
  Control: TWinControl; FindString: string): Integer;
begin
  if Copy(FindString, 1, Length('Line ')) <> 'Line ' then
    Exit(-1);
  // ...
end;

寫作回應式使用者介面

在設計使用者介面時,回應式是指介面的能力,可以根據使用者的操作和環境進行適應和調整。這包括了對使用者輸入的反應、對螢幕大小和解析度的適應,以及對不同平臺和裝置的相容性。

虛擬清單盒

虛擬清單盒(Virtual Listbox)是一種特殊的清單盒控制元件,它不儲存所有清單項的內容,而是隻儲存目前可見的清單項。這樣可以大大減少記憶體的使用量,特別是在處理大量資料時。

虛擬樹檢視

虛擬樹檢視(Virtual TreeView)是一種根據虛擬清單盒的樹狀結構控制元件。它可以用來顯示樹狀結構的資料,並提供了許多自訂化和擴充的功能。

虛擬樹檢視的使用

要使用虛擬樹檢視,首先需要設定其節點資料大小(NodeDataSize)。這個屬性決定了節點的使用者資料的大小。然後,可以使用AddChild方法新增新的節點,並傳遞使用者資料作為引數。

虛擬樹檢視的優點

虛擬樹檢視的優點在於它可以處理大量資料,並提供了高效的顯示和操作。它還可以根據使用者的操作進行適應和調整,例如自動排序和過濾。

虛擬樹檢視的缺點

虛擬樹檢視的缺點在於它需要更多的程式碼和設定。它還需要使用者資料的管理和維護,這可能會增加開發的複雜度。

虛擬樹檢視的應用

虛擬樹檢視可以用於許多應用中,例如檔案管理器、資料函式倉管理器和網頁瀏覽器。它可以用來顯示樹狀結構的資料,並提供了許多自訂化和擴充的功能。

圖表翻譯
  graph LR
    A[使用者操作] --> B[虛擬樹檢視]
    B --> C[節點資料大小設定]
    C --> D[AddChild方法新增節點]
    D --> E[使用者資料傳遞]
    E --> F[虛擬樹檢視顯示]

內容解密

虛擬樹檢視的使用需要設定節點資料大小和新增節點。使用者資料需要傳遞給節點,並且需要管理和維護。虛擬樹檢視可以根據使用者的操作進行適應和調整,例如自動排序和過濾。

最佳化演算法

在前面的章節中,我們討論瞭如何最佳化程式的效能。現在,我們要探討另一種最佳化方法:快取(Caching)。快取是一種儲存暫時資料的技術,讓程式可以快速存取資料,而不需要每次都重新計算。

快取的重要性

快取可以大大提高程式的效能,特別是在需要反覆計算的場合。例如,計算費波那契數列的第 n 個元素。費波那契數列是一個典型的遞迴數列,每個元素都是前兩個元素的和。

function TfrmFibonacci.FibonacciRecursive(element: int64): int64;
begin
  if element < 3 then
    Result := 1
  else
    Result := FibonacciRecursive(element - 1) + FibonacciRecursive(element - 2);
end;

這個遞迴函式看起來很簡單,但它有個大問題:它需要反覆計算同一個元素多次。例如,計算第 10 個元素需要計算第 9 個和第 8 個元素,但計算第 9 個元素又需要計算第 8 個和第 7 個元素,以此類推。這樣就會導致很多重複的計算,浪費了很多時間。

快取的解決方案

快取可以解決這個問題。我們可以建立一個快取表格,儲存已經計算過的元素。當需要計算一個元素時,先查詢快取表格,如果已經計算過,就直接傳回快取的結果;如果沒有計算過,就計算並儲存到快取表格中。

var
  Cache: array of int64;

function TfrmFibonacci.FibonacciCached(element: int64): int64;
begin
  if element < 3 then
    Result := 1
  else if element <= Length(Cache) then
    Result := Cache[element - 1]
  else
  begin
    Result := FibonacciCached(element - 1) + FibonacciCached(element - 2);
    SetLength(Cache, element);
    Cache[element - 1] := Result;
  end;
end;

這個快取版本的函式可以大大提高效能,特別是在需要反覆計算的場合。

瞭解費波那契數列的計算問題

費波那契數列是一個由0和1開始的數列,後面的每個數字都是前面兩個數字的總和。然而,在計算這個數列的過程中,會遇到一個問題:計算時間的增加。

費波那契數列的計算方法

費波那契數列可以使用遞迴函式來計算,但是這種方法會導致計算時間的增加。例如,計算第40個費波那契數字需要約1秒鐘的時間,而計算第50個費波那契數字需要約96秒鐘的時間。

問題的根源

費波那契數列的計算問題在於其遞迴函式的複雜度。每次計算都會產生兩個新的遞迴呼叫,導致計算時間的增加。這種複雜度被稱為O(2^n)。

解決方案

為瞭解決這個問題,我們需要找到一個更有效的計算方法。其中一個方法是使用動態程式設計來儲存已經計算過的費波那契數字,從而避免重複計算。

動態程式設計的應用

動態程式設計是一種計算方法,透過儲存已經計算過的結果來避免重複計算。在費波那契數列的計算中,我們可以使用動態程式設計來儲存已經計算過的費波那契數字,從而減少計算時間。

def fibonacci(n):
    # 建立一個列表來儲存已經計算過的費波那契數字
    fib = [0] * (n + 1)
    fib[1] = 1
    for i in range(2, n + 1):
        # 使用動態程式設計來計算費波那契數字
        fib[i] = fib[i - 1] + fib[i - 2]
    return fib[n]

內容解密:

上述程式碼使用動態程式設計來計算費波那契數字。首先,建立一個列表來儲存已經計算過的費波那契數字。然後,使用迴圈來計算費波那契數字,並儲存結果在列表中。最後,傳回計算結果。

圖表翻譯:

  flowchart TD
    A[開始] --> B[建立列表]
    B --> C[計算費波那契數字]
    C --> D[儲存結果]
    D --> E[傳回結果]

上述圖表展示了動態程式設計的過程。首先,建立一個列表來儲存已經計算過的費波那契數字。然後,使用迴圈來計算費波那契數字,並儲存結果在列表中。最後,傳回計算結果。

快取技術在費波那契數列中的應用

費波那契數列是一個經典的數學問題,描述了一種數列,其中每個數字都是前兩個數字的和。然而,傳統的遞迴演算法會導致大量的重覆計算,從而降低執行速度。為瞭解決這個問題,我們可以使用快取技術,也就是所謂的備忘錄法(memoization)。

備忘錄法的基本思想

備忘錄法是一種簡單而有效的技術,當我們計算出一個結果後,就將其儲存在一個快取中。當下次需要計算相同的結果時,我們就可以直接從快取中取出,而不需要重新計算。這樣可以大大減少重覆計算,從而提高執行速度。

快取的實作

在費波那契數列的例子中,我們可以使用一個簡單的陣列來實作快取。陣列的索引對應於費波那契數列的元素索引,值對應於計算出的結果。當我們需要計算一個元素的值時,我們先檢查快取中是否已經有這個元素的值,如果有,就直接取出。如果沒有,就計算出這個元素的值,並將其儲存在快取中。

var
  FFibonacciTable: TArray<int64>;

function TfrmFibonacci.FibonacciMemoized(element: int64): int64;
var
  i: Integer;
begin
  SetLength(FFibonacciTable, element + 1);
  for i := Low(FFibonacciTable) to High(FFibonacciTable) do
    FFibonacciTable[i] := -1;
  Result := FibonacciRecursiveMemoized(element);
end;

備忘錄法的實作

備忘錄法的實作相對於傳統的遞迴演算法而言,增加了快取的管理。當我們需要計算一個元素的值時,我們先檢查快取中是否已經有這個元素的值,如果有,就直接取出。如果沒有,就計算出這個元素的值,並將其儲存在快取中。

function TfrmFibonacci.FibonacciRecursiveMemoized(element: int64): int64;
begin
  if FFibonacciTable[element] <> -1 then
    Result := FFibonacciTable[element]
  else
  begin
    if element = 0 then
      Result := 0
    else if element = 1 then
      Result := 1
    else
      Result := FibonacciRecursiveMemoized(element - 1) + FibonacciRecursiveMemoized(element - 2);
    FFibonacciTable[element] := Result;
  end;
end;

最佳化斐波那契數列演算法

斐波那契數列是一個經典的數學問題,指的是每個數字都是前兩個數字的和,從1和1開始。傳統的遞迴解法會導致效率低下,因為它會重複計算相同的數字多次。為瞭解決這個問題,我們可以使用記憶化(memoization)技術來 最佳化演算法。

記憶化技術

記憶化是一種技術,用於儲存昂貴的函式呼叫結果,以便在未來的呼叫中重用。這樣可以避免重複計算相同的結果,從而提高效率。以下是使用記憶化技術最佳化的斐波那契數列演算法:

def fibonacci_memoized(n, memo={}):
    if n in memo:
        return memo[n]
    if n < 3:
        return 1
    else:
        result = fibonacci_memoized(n-1, memo) + fibonacci_memoized(n-2, memo)
        memo[n] = result
        return result

最佳化結果

使用記憶化技術最佳化的斐波那契數列演算法可以顯著提高效率。例如,計算斐波那契數列的第92個數字,使用記憶化技術的演算法只需要少於1毫秒的時間。

限制

雖然記憶化技術可以最佳化斐波那契數列演算法,但是它仍然有一個限制。當計算斐波那契數列的第93個數字時,結果會超出int64的最大值,從而導致結果變為負數。這是因為第93個斐波那契數字超出了int64的最大值。

更好的解法

雖然記憶化技術可以最佳化斐波那契數列演算法,但是還有一個更好的解法。從第一個和第二個數字(1和1)開始,然後將它們相加得到第三個數字(1+1=2),然後將第二個和第三個數字相加得到第四個數字,依此類推。這樣可以避免重複計算相同的結果,從而提高效率。

def fibonacci_iterative(n):
    if n < 3:
        return 1
    a, b = 1, 1
    for _ in range(3, n+1):
        a, b = b, a + b
    return b

動態快取設計

在前面的章節中,我們討論了靜態快取的實作和應用。然而,靜態快取有其侷限性,尤其是在需要更動態和彈性的快取機制時。因此,接下來我們將探討動態快取的設計和實作。

動態快取設計的優勢與挑戰

從靜態快取的侷限性出發,本文深入探討了動態快取設計的優勢與挑戰。動態快取機制能夠根據實際情況調整快取策略,例如根據資料的存取頻率、資料的大小、系統資源的使用情況等,更有效地利用快取空間,提升系統效能。

分析了兩種動態快取的典型策略:LRU (Least Recently Used) 和 LFU (Least Frequently Used)。LRU 演算法淘汰最近最少使用的資料,而 LFU 演算法淘汰使用頻率最低的資料。兩種演算法各有優劣,LRU 適用於資料存取模式相對穩定的場景,而 LFU 更適合資料存取模式頻繁變化的場景。實務上,更複雜的動態快取系統會結合多種淘汰策略,並根據系統執行時的監控資料動態調整策略引數,以達到最佳的快取效果。

然而,動態快取的設計和實作也面臨諸多挑戰。首先,動態快取演算法的複雜度通常較高,需要更精細的設計和調校。其次,動態快取需要實時監控系統的執行狀態,這會帶來一定的效能開銷。此外,動態快取的策略引數需要根據具體應用場景進行調整,這需要開發者具備豐富的經驗和專業知識。

展望未來,隨著雲端計算和大資料技術的發展,動態快取技術將扮演越來越重要的角色。預計未來會出現更多根據機器學習和人工智慧的動態快取演算法,能夠自動學習資料的存取模式,並自適應地調整快取策略,進一步提升系統效能和資源利用率。對於開發者而言,深入理解動態快取的原理和實作技巧,將有助於構建更高效、更可靠的軟體系統。 玄貓認為,掌握動態快取技術對於提升軟體系統效能至關重要,開發者應積極探索並應用相關技術。