在《一步一步學習使用LiveBindings(12)》中,介紹瞭如何通過設計面板來定製TListView中Item的顯示,雖然方便,但是重用性確也是一個問題;此外,當列表項的內容不固定時,如何能顯示完整的內容,就涉及到動態列表項的問題。
本課將介紹如何創建自適應高度的列表項,不但列表項的高度自適應,還演示瞭如何在列表項中進行圖形繪製。
1. 根據內容尺寸確定列表項的高度
Delphi自帶的Demo中有一個VariableHeightItems的項目,提供了絕佳的定製尺寸高度的示例。

1.1 UI構成
這個項目的主窗體上放了一個TListView控件,它具有DynamicAppearance的Appearance,Design Mode時的效果如下所示:

圖中的txtMain是一個寬度為0(即佔據整個容器寬度)的TextObjectAppearance對象。右側放了一個TImageObjectAppearance對象,它的Width為30,Align為Trailing(即右對齊)。ScalingMode為Original,表示不進行任何縮放。
在這個項目中,添加了一個名為Blabla.txt的資源文件,項目將使用此嵌入的文本文件來顯示隨機性的文本。

窗體右上角有一個“添加一項”的按鈕,單擊該按鈕,會顯示一些隨機長度和字體大小的文本,高度會自動調整以適應文本的顯示,並且在會顯示一個線條圖片,圖片中間顯示當前的高度,效果如下圖所示:

通過這個例子,可以學到如何測量文本的高度,以及如何繪製位圖。
1.2 從資源文件中加載列表項文本
在這個項目中包含一個名為TChain的馬爾可夫鏈文本生成器,在單擊“添加一項”後,按鈕事件處理程序執行了如下的代碼來生成隨機字體大小和隨機長度的文本:
procedure TVariableHeight.Button1Click(Sender: TObject);
begin
// 第一種方式: 從資源文件中讀取文本內容
//ReadText;
// 向ListView添加新項,並設置其txtMain字段為隨機選擇的文本
//stView1.Items.Add.Data['txtMain'] := FText[Random(Length(FText))];
// 如果不是使用DynamicAppearance樣式,可以使用以下代碼設置文本
// ListView1.Items.Add.Text := FChain.Generate(Random(100) + 5);
// 第二種方式:使用馬爾可夫鏈構建文本
//從資源文件中加載馬爾可夫鏈文本生成器。
if FChain=nil then
FChain:=TChain.FromResource('Blabla');
//使用馬爾可夫鏈生成隨機文本
ListView1.Items.Add.Data['txtMain'] := FChain.Generate(Random(100) + 5);
end;
這裏只是簡單的給txtMain文本對象進行了賦值,但是顯示出來的卻是字體和長度隨機顯示的文本,這是發生在TListView的OnUpdateObject事件中完成的。
OnUpdateObjects在列表視圖組件更新後立即發生。
編寫OnUpdateObjects事件處理程序以便在更新列表視圖組件後提供附加功能。
OnUpdateObjects是一個TItemEvent類型的事件。
OnUpdateObjects一般用於如下的場合:
- 當列表項需要根據內容(如多行文本、圖片等)動態計算高度時。
- 根據數據狀態動態修改列表項樣式(如顏色/字體/可見性)
- 避免在GetItem中頻繁創建/釋放對象,改用事件觸發時加載
- 當列表寬度變化時重新計算子控件佈局
- 配合CustomDraw實現更復雜的視覺效果
這個事件確實是非常實用,示例中的代碼如下所示:
1.3 調用OnUpdateObjects事件更新列表項的寬度和高度,以及繪製標尺位圖。
// 更新ListView項的對象屬性
procedure TVariableHeight.ListView1UpdateObjects(const Sender: TObject;
const AItem: TListViewItem);
var
Drawable: TListItemText; // 文本繪製對象
SizeImg: TListItemImage; // 尺寸指示器圖像
Text: string; // 文本內容
AvailableWidth: Single; // 可用寬度
begin
// 獲取尺寸指示器圖像對象
SizeImg := TListItemImage(AItem.View.FindDrawable('imgSize'));
// 計算可用寬度(總寬度減去邊距和指示器寬度)
AvailableWidth := TListView(Sender).Width - TListView(Sender).ItemSpaces.Left
- TListView(Sender).ItemSpaces.Right - SizeImg.Width;
// 查找用於計算項大小的文本繪製對象
// 對於動態外觀,使用項名稱
// 對於經典外觀,使用 TListViewItem.TObjectNames.Text
// Drawable := TListItemText(AItem.View.FindDrawable(TListViewItem.TObjectNames.Text));
Drawable := TListItemText(AItem.View.FindDrawable('txtMain'));
Text := Drawable.Text;
// 首次更新時隨機設置字體
if Drawable.TagFloat = 0 then
begin
Drawable.Font.Size := 1; // 確保默認字體大小不影響我們隨機設置字體大小
Drawable.Font.Size := 10 + Random(4) * 4; // 隨機設置字體大小(10,14,18或22)
Drawable.TagFloat := Drawable.Font.Size; // 保存字體大小
if Text.Length < 100 then
Drawable.Font.Style := [TFontStyle.fsBold]; // 短文本加粗顯示
end;
// 根據文本內容計算項高度
AItem.Height := GetTextHeight(Drawable, AvailableWidth, Text);
// 設置繪製對象的高度和寬度
Drawable.Height := AItem.Height;
Drawable.Width := AvailableWidth;
// 設置尺寸指示器圖像
SizeImg.OwnsBitmap := False; // 不自動釋放位圖
SizeImg.Bitmap := GetDimensionBitmap(SizeImg.Width, AItem.Height); // 獲取尺寸指示器位圖
end;
代碼裏邊通過判斷Drawable.TagFloat是否為0確認是否是首次更新,如果是則隨機設置字體,並將字體大小保存到TagFloat中。
AvailableWidth 變量是用來計算列表項的可用寬度,在這個事件中動態為文本指定寬度和高度,這也是OnUpdateObjects的一般應用場合。
-
GetTextHeight是自定義的用來計算文本真正高度的函數,它返回的結果將用來設置列表項的高度。
-
GetDimensionBitmap將用來繪製標尺位圖,並賦給sizeImg.Bitmap屬性。
1.4 GetTextHeight測量文本高度
GetTextHeight主要是通過TTextLayoutManager.DefaultTextLayout.Create創建了一個TTextLayout對象,這個對象包含了文本尺寸信息,代碼如下所示:
// 計算文本繪製所需的高度
function TVariableHeight.GetTextHeight(const D: TListItemText; const Width: single; const Text: string): Integer;
var
Layout: TTextLayout; // 文本佈局對象
begin
// 創建文本佈局對象用於測量文本尺寸
Layout := TTextLayoutManager.DefaultTextLayout.Create;
try
Layout.BeginUpdate;
try
// 使用繪製對象的參數初始化佈局
Layout.Font.Assign(D.Font); // 設置字體
Layout.VerticalAlign := D.TextVertAlign; // 垂直對齊方式
Layout.HorizontalAlign := D.TextAlign; // 水平對齊方式
Layout.WordWrap := D.WordWrap; // 是否自動換行
Layout.Trimming := D.Trimming; // 文本截斷方式
Layout.MaxSize := TPointF.Create(Width, TTextLayout.MaxLayoutSize.Y); // 最大尺寸
Layout.Text := Text; // 設置要測量的文本
finally
Layout.EndUpdate;
end;
// 獲取佈局高度
Result := Round(Layout.Height);
// 增加一個字符m的高度作為額外間距
Layout.Text := 'm';
Result := Result + Round(Layout.Height);
finally
Layout.Free; // 釋放文本佈局對象
end;
end;
這裏通過為TTextLayout對象賦予TListItemText的相關信息,再通過Layout.Height來獲取文本高度,這裏還添加了一個'm'的高度來作為額外的間距。
1.4 GetDimensionBitmap繪製尺寸指示器
每一項的最右側包含一個上下箭頭的指示器,指示器的中間是列表項的高度數字。
GetDimensionBitmap傳入的Width是右側位圖的寬度,因此要繪製的豎線應該是右側位圖寬度的中間,而高度則是整個列表項的高度。並且需要在兩側繪製了箭頭。
繪製了線條和箭頭後,還需要在線條中間顯示高度文本,這是通過繪製一個位圖來實現的,請看下面的代碼:
// 獲取指定寬高的位圖,用於顯示尺寸指示器
function TVariableHeight.GetDimensionBitmap(const Width, Height: Single): TBitmap;
// 繪製箭頭圖形的內部過程
procedure Arrow(C: TCanvas; P: array of TPointF);
begin
C.DrawLine(P[0], P[1], 1.0); // 繪製箭頭主幹
C.DrawLine(P[0], P[2], 1.0); // 繪製箭頭左側分支
C.DrawLine(P[0], P[3], 1.0); // 繪製箭頭右側分支
end;
var
EndP1, EndP2: TPointF; // 箭頭的起點和終點
TextBitmap: TBitmap; // 用於繪製文本的臨時位圖
IntHeight: Integer; // 整數形式的高度值
begin
IntHeight := Trunc(Height); // 將高度轉換為整數
// 初始化位圖緩存字典
if FBitmaps = nil then
FBitmaps := TDictionary<Integer, TBitmap>.Create;
// 嘗試從緩存中獲取位圖
if not FBitmaps.TryGetValue(IntHeight, Result) then
begin
// 創建新位圖
Result := TBitmap.Create(Trunc(Width), IntHeight);
FBitmaps.Add(IntHeight, Result);
// 開始繪製場景
if Result.Canvas.BeginScene then
begin
Result.Canvas.Clear(TAlphaColorRec.Null); // 清空畫布
Result.Canvas.Stroke.Color := TAlphaColorRec.Darkgray; // 設置線條顏色
// 繪製上下箭頭
EndP1 := TPointF.Create(Width/2, 0); // 上箭頭起點
EndP2 := TPointF.Create(Width/2, Height); // 下箭頭起點
Arrow(Result.Canvas,
[EndP1, TPointF.Create(Width/2, Height/2 - Width/2),
EndP1 + TPointF.Create(-2, 5), EndP1 + TPointF.Create(2, 5)]);
Arrow(Result.Canvas,
[EndP2, TPointF.Create(Width/2, Height/2 + Width/2),
EndP2 + TPointF.Create(-2, -5), EndP2 + TPointF.Create(2, -5)]);
// 創建臨時位圖用於繪製文本
TextBitmap := TBitmap.Create(Trunc(Width), Trunc(Width));
try
if TextBitmap.Canvas.BeginScene then
with TextBitmap.Canvas do
begin
Clear(TAlphaColorRec.Null); // 清空畫布
Fill.Color := TAlphaColorRec.Darkgray; // 設置文本顏色
// 繪製高度數值文本
FillText(TextBitmap.BoundsF, ''.Format('%d', [IntHeight]), False, 1,
[], TTextAlign.Center, TTextAlign.Center);
EndScene;
end;
TextBitmap.Rotate(90); // 旋轉文本位圖90度
// 將文本位圖繪製到結果位圖上
Result.Canvas.DrawBitmap(TextBitmap, TextBitmap.BoundsF,
TextBitmap.BoundsF.CenterAt(Result.BoundsF), 1);
finally
TextBitmap.Free; // 釋放臨時位圖
end;
Result.Canvas.EndScene; // 結束繪製場景
end;
end;
end;
在這裏構建了一個與列表項高度同樣高的位圖,寬度是列表項內部內嵌的位圖的寬度,然後繪製了一根直線和兩個箭頭。
接下來構建了一個名為TextBitmap的位置,在裏邊繪製了當前項高度的數字,並且旋轉90度,再繪製到前一步驟創建的具有箭頭的位圖中,並顯示在中間。