Delphi 開發者在構建使用者介面時,經常需要處理大量資料的顯示和更新,這對應用程式的效能提出了挑戰。 提升 UI 回應速度的關鍵在於減少不必要的 UI 更新操作和使用高效的演算法。 本文將探討進度條更新的最佳實務,分析 TMemo 和 TListBox 在效能上的差異,並介紹如何利用 Virtual TreeView 等虛擬化技術來最佳化 UI 效能。 此外,文章也將探討如何透過批次更新和延遲更新等策略,減少 Windows 訊息的數量,從而提升 UI 回應速度。 最後,我們將探討一些進階的最佳化技巧,例如按需初始化節點和使用第三方元件,以進一步提升應用程式的效能。

最佳化演算法

在前一章中,我們探討了效能的概念,並檢視了不同情境下希望程式執行得更快的情況。前一章主要著重於理論探討,而現在是時候以更實用的方式來檢視它了。

加速程式主要有兩種方法: • 用更好的演算法取代原有的演算法 • 對程式碼進行微調,使其執行得更快

在前一章中,我花了很多時間討論時間複雜度,只是為了讓大家清楚地認識到,兩種演算法之間的差異可以導致令人印象深刻的加速。這種加速可能遠遠超過一個簡單的常數因子(例如10倍的加速)。如果我們從一個時間複雜度較差的演算法(例如O(n^2))切換到一個行為更好的演算法(例如O(n log n)),那麼隨著資料規模的增加,速度差異將會變得越來越明顯。

編寫回應式使用者介面

使用者與任何程式的第一接觸始終是使用者介面。一個好的使用者介面可以決定一個程式的成敗。暫且不談使用者介面的設計(因為我並不具備相關的專業知識),我將專注於一個事實:使用者討厭反應遲鈍的使用者介面。

換句話說,每一個好的使用者介面都必須對使用者的輸入快速做出反應,無論是鍵盤、滑鼠、觸控板還是其他輸入裝置。

更新進度條

對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;
    // 讀取 'block' 位元組

內容解密:

這段程式碼模擬了讀取一個大檔案的操作,並且在讀取過程中更新進度條。TStopwatch用於測量操作的耗時。進度條的最大值被設定為檔案的大小,並且在每次讀取操作後更新進度條的位置。變數total用於跟蹤已經讀取的總位元組數,而block變數則代表每次讀取操作的位元組數。

此圖示呈現了更新進度條對效能的影響: 圖表翻譯: 此圖示描述了在讀取檔案過程中更新進度條的流程。首先,設定進度條的最大值,然後進入迴圈,在每次迭代中計算本次讀取的區塊大小,並更新進度條的位置,直到檔案讀取完畢。

快取機制

有時候,我們無法更換演算法,但引入快取機制仍然可以顯著提高程式碼的速度。實作一個快速的快取機制相當具有挑戰性,因此,我將介紹一個通用的快取類別,你可以在自己的程式碼中自由使用它。

通用快取類別

TCache<TKey, TValue> = class
private
  FCache: TDictionary<TKey, TValue>;
public
  constructor Create;
  destructor Destroy; override;
  function GetValue(const Key: TKey): TValue;
  procedure SetValue(const Key: TKey; const Value: TValue);
end;

constructor TCache<TKey, TValue>.Create;
begin
  FCache := TDictionary<TKey, TValue>.Create;
end;

destructor TCache<TKey, TValue>.Destroy;
begin
  FCache.Free;
  inherited;
end;

function TCache<TKey, TValue>.GetValue(const Key: TKey): TValue;
begin
  if not FCache.TryGetValue(Key, Result) then
    Result := Default(TValue);
end;

procedure TCache<TKey, TValue>.SetValue(const Key: TKey; const Value: TValue);
begin
  FCache.AddOrSetValue(Key, Value);
end;

內容解密:

這個通用的快取類別使用了TDictionary<TKey, TValue>來儲存鍵值對。GetValue方法嘗試從快取中檢索指定鍵的值,如果鍵不存在,則傳回預設值。SetValue方法則將指定的鍵值對加入到快取中,如果鍵已經存在,則更新其對應的值。

分析和最佳化未知演算法

在某些情況下,我們可能會遇到一個未知的演算法,並且需要對其進行分析和最佳化。這通常需要深入瞭解演算法的工作原理,並找出可能的最佳化點。

示例:最佳化Mr. Smith的SlowCode

function SlowCode(const Input: TArray<Integer>): Integer;
// 原始實作

內容解密:

首先,我們需要分析SlowCode函式的時間複雜度。透過檢視原始實作,我們發現它具有O(n^2)的時間複雜度。接下來,我們可以嘗試最佳化它,例如透過使用更高效的資料結構或演算法。

提升UI回應速度的關鍵技術:進度條最佳化與批次更新

在開發使用者介面(UI)時,回應速度是確保良好使用者經驗的關鍵因素。本文將探討兩種常見的UI效能問題:進度條更新與大量資料的批次更新,並提供具體的最佳化方案。

進度條更新的最佳化

進度條是常見的UI元素,用於顯示任務的進度。然而,不當的更新方式可能導致效能問題。以下是一個初始實作範例:

function TfrmProgressBar.Test0To100: 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;
    Inc(total, block);
    ProgressBar1.Position := total;
    ProgressBar1.Update;
  end;
  Result := sw.ElapsedMilliseconds;
end;

內容解密:

  1. 效能瓶頸ProgressBar1.Position := total;ProgressBar1.Update; 是主要效能瓶頸。前者傳送 PBM_SETPOS 訊息給 Windows,後者則強制重繪進度條。
  2. 最佳化方案:減少不必要的更新。將 Max 設定為 100,並計算百分比後再更新 Position

最佳化後的程式碼如下:

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;
    Inc(total, block);
    currPct := Round(total / CFileSize * 100);
    if currPct > lastPct then
    begin
      lastPct := currPct;
      ProgressBar1.Position := currPct;
      ProgressBar1.Update;
    end;
  end;
  Result := sw.ElapsedMilliseconds;
end;

內容解密:

  1. 減少更新次數:僅當百分比變化時才更新 Position,大幅減少傳送給 Windows 的訊息數量。
  2. 儲存上次位置:使用 lastPct 儲存上次的百分比,避免重複讀取 Position

處理 Windows Vista 之後的進度條動畫問題

從 Windows Vista 開始,進度條的更新變為動畫形式,這可能導致某些程式行為異常。解決方案是利用動畫觸發機制:

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

圖表翻譯:

此圖示顯示了進度條更新機制與動畫觸發的關係。

大量資料的批次更新

在處理大量資料時,直接更新 UI 元件(如 TListBoxTMemo)可能導致效能問題。示例如下:

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

內容解密:

  1. 效能問題:每次呼叫 Add 方法都會觸發 UI 更新,導致效能下降。
  2. 最佳化方案:使用 BeginUpdateEndUpdate 方法暫停 UI 更新。

最佳化後的程式碼如下:

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

圖表翻譯:

此圖示說明瞭 BeginUpdateEndUpdate 對 UI 更新的控制。

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Delphi高效能UI程式設計技巧

package "系統架構" {
    package "前端層" {
        component [使用者介面] as ui
        component [API 客戶端] as client
    }

    package "後端層" {
        component [API 服務] as api
        component [業務邏輯] as logic
        component [資料存取] as dao
    }

    package "資料層" {
        database [主資料庫] as db
        database [快取] as cache
    }
}

ui --> client : 使用者操作
client --> api : HTTP 請求
api --> logic : 處理邏輯
logic --> dao : 資料操作
dao --> db : 持久化
dao --> cache : 快取

note right of api
  RESTful API
  或 GraphQL
end note

@enduml

提升程式效能:最佳化演算法與虛擬化顯示控制

在開發使用者介面時,如何有效地處理大量資料的顯示是一個常見的挑戰。本篇文章將探討如何透過最佳化演算法和虛擬化顯示控制來提升程式的效能,以 TListBox 和 TMemo 為例,分析其在處理大量資料時的效能差異,並介紹如何使用虛擬化技術來改善效能。

TMemo 的效能問題

當我們嘗試向 TMemo 新增大量資料時,會發現其效能遠遠低於 TListBox。測試結果顯示,向 TMemo 新增 10,000 行資料需要約 1.2 秒,而 TListBox 只需要約 285 毫秒。這是因為 TMemo 在新增每一行資料時都會傳送多個 Windows 訊息,導致效能下降。

深入 VCL 程式碼

透過分析 VCL 程式碼,我們發現 TMemoStrings.Add 方法會呼叫 GetCount 和 Insert 方法。GetCount 方法會傳送兩個 Windows 訊息,即使控制項處於更新模式。Insert 方法則會傳送三個訊息來更新當前選擇。這導致了為每一行資料傳送五個 Windows 訊息,從而嚴重影響了效能。

最佳化 TMemo 的效能

為了提高 TMemo 的效能,我們可以將所有更新操作收集在一個 TStringList 中,然後一次性指定給 TMemo 的 Text 屬性。這樣可以將更新操作減少到一次,從而大大提高效能。測試結果顯示,這種方法只需要約 56 毫秒。

虛擬化顯示控制

大多數情況下,使用者並不需要檢視所有資料。因此,我們可以採用虛擬化技術,只生成和顯示可見的資料,從而提高程式的效能。

虛擬 TListBox

要將 TListBox 轉換為虛擬控制,我們需要:

  1. 將 Style 屬性設為 lbVirtual。
  2. 編寫 OnData 事件處理程式。
  3. 可選地編寫 OnDataFind 和 OnDataObject 事件處理程式。

透過設定 Count 屬性,我們可以快速地為 TListBox 新增大量資料。VCL 只會為可見的行呼叫 OnData 事件處理程式,從而大大提高效能。

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

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

虛擬 TreeView

Virtual TreeView 是一個功能強大的元件,可以用來顯示樹狀結構或作為快速的列表框。透過移除 toShowRoot 和 toShowTreeLines 選項,我們可以將其用作列表框。

**圖表翻譯:**
此圖示展示了 Virtual TreeView 的結構與應用範例

重點整理

  • TMemo 的效能問題源於其在新增每一行資料時傳送多個 Windows 訊息。
  • 可以透過收集更新操作並一次性指定給 Text 屬性來提高 TMemo 的效能。
  • 虛擬化技術可以幫助我們只生成和顯示可見的資料,從而提高程式的效能。
  • Virtual TreeView 是一個功能強大的元件,可以用來顯示樹狀結構或作為快速的列表框。

在未來的開發中,我們可以進一步探索其他虛擬化技術和最佳化方法,以不斷提高程式的效能和使用者經驗。同時,也可以考慮使用其他第三方元件來簡化開發流程和提高程式的效能。

提升虛擬樹狀檢視(Virtual TreeView)的效能:深入分析與最佳實踐

前言

在開發高效能的使用者介面時,選擇合適的元件對於確保應用程式的流暢運作至關重要。Delphi 開發者經常面臨的一個挑戰是,如何高效地處理大量資料並呈現在 UI 上。本文將探討 Virtual TreeView(虛擬樹狀檢視)元件的效能最佳化,特別是在處理大量資料時的效能改進。

虛擬樹狀檢視(Virtual TreeView)簡介

Virtual TreeView 是 Delphi 中一個強大的元件,它採用了檢視/模型(View/Model)原則。與其他元件不同,Virtual TreeView 本身不儲存資料,而是顯示資料。這意味著開發者需要維護一個資料儲存(模型),而 Virtual TreeView 只保留一個簡短的參考以存取這些資料。

設定 NodeDataSize

在使用 Virtual TreeView 之前,首先需要決定資料參考的大小,並相應地設定 NodeDataSize 屬性。通常,這個大小取決於所儲存的資料型別,例如整數索引(4 位元組)、物件或介面(在 Win32 上為 4 位元組,在 Win64 上為 8 位元組)。

使用 Virtual TreeView 的基本方法

新增節點

使用 AddChild 方法可以新增一個新的節點(顯示行)並傳遞使用者資料(對模型的參考)作為引數。以下是一個簡單的範例:

VirtualStringTree1.BeginUpdate;
for i := 1 to 10000 do begin
    idx := FModel1.Add('Line ' + IntToStr(i));
    VirtualStringTree1.AddChild(nil, pointer(idx));
end;
VirtualStringTree1.EndUpdate;

處理 OnGetText 事件

為了檢索模型中的資料以顯示在螢幕上,需要建立 OnGetText 事件處理常式。此事件會針對每個可見行的每個欄位呼叫一次。程式碼必須首先呼叫 Node.GetData 以取得與節點相關聯的使用者資料。

procedure TfrmVTV.VirtualStringTree1GetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
begin
    CellText := FModel1[PInteger(Node.GetData)^];
end;

效能比較與最佳化

實驗結果表明,當一次性新增大量資料時,Virtual TreeView 的效能優於標準的 ListBox。然而,當逐一新增資料時,ListBox 的效能卻遠遠優於 Virtual TreeView。

進一步的調查發現,預設情況下,Virtual TreeView 在每次呼叫 AddChild 時都會進行資料排序,除非事先呼叫了 BeginUpdate。這種行為會嚴重影響效能。

解決方案:停用 toAutoSort 選項

停用 TreeOptions.AutoOptions 中的 toAutoSort 選項可以解決這個問題。這樣做後,逐一新增資料的效能得到了顯著提升,與 ListBox 持平。

進一步最佳化:按需初始化節點

除了停用自動排序外,還可以透過設定 RootNodeCount 屬性來告訴 Virtual TreeView 需要顯示多少個節點,並在 OnInitNode 事件處理常式中初始化每個可見節點。這種按需初始化的方法進一步提高了效能。

for i := 1 to 10000 do
    FModel3.Add('Line ' + IntToStr(i));
VirtualStringTree3.RootNodeCount := VirtualStringTree3.RootNodeCount + 10000;

OnInitNode 事件處理

OnInitNode 事件處理常式中,可以使用 SetData 方法設定使用者資料。

procedure TfrmVTV.VirtualStringTree3InitNode(Sender: TBaseVirtualTree; ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
begin
    Node.SetData(pointer(Node.Index));
end;