在《一步一步学习使用LiveBindings(12)》中,介绍了如何通过设计面板来定制TListView中Item的显示,虽然方便,但是重用性确也是一个问题;此外,当列表项的内容不固定时,如何能显示完整的内容,就涉及到动态列表项的问题。
本课将介绍如何创建自适应高度的列表项,不但列表项的高度自适应,还演示了如何在列表项中进行图形绘制。
1. 根据内容尺寸确定列表项的高度
Delphi自带的Demo中有一个VariableHeightItems的项目,提供了绝佳的定制尺寸高度的示例。
1.1 UI构成
这个项目的主窗体上放了一个TListView控件,它具有DynamicAppearance的Appearance,Design Mode时的效果如下所示:
textMain是一个宽度为0(也即占据整个容器宽度)的TextObjectAppearance对象。右侧放了一个TImageObjectAppearance对象,它的Width为30,Align为Trailing(即右对齐)。ScalingMode为Original,表示不进行任何缩放。
在这个项目中,添加了一个名为Blabla.txt的资源文件,项目将使用此嵌入的文本文件来显示随机性的文本。
窗体右上角有一个"添加一项"的按钮,单击该按钮,会显示一些随机长度和字体大小的文本,高度会自动调整以适应文本的显示,并且在会显示一个线条图片,图片中间显示当前的高度,效果如下图所示:
通过这个例子,可以学到如何测量文本的高度,以及如何绘制位图。
1.2 从资源文件中加载列表项文本
在这个项目中包含一个名为TChain的马尔可夫链文本生成器,在单击"添加一项"后,按钮事件处理程序执行了如下的代码来生成随机字体大小和随机长度的文本:
Pascal
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事件更新列表项的宽度和高度,以及绘制标尺位图。
Pascal
// 更新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对象,这个对象包含了文本尺寸信息,代码如下所示:
Pascal
// 计算文本绘制所需的高度
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是右侧位图的宽度,因此要绘制的竖线应该是右侧位图宽度的中间,而高度则是整个列表项的高度。并且需要在两侧绘制了箭头。
绘制了线条和箭头后,还需要在线条中间显示高度文本,这是通过绘制一个位图来实现的,请看下面的代码:
Pascal
// 获取指定宽高的位图,用于显示尺寸指示器
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度,再绘制到前一步骤创建的具有箭头的位图中,并显示在中间。