在《一步一步学习使用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的马尔可夫链文本生成器,在单击“添加一项”后,按钮事件处理程序执行了如下的代码来生成随机字体大小和随机长度的文本:

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度,再绘制到前一步骤创建的具有箭头的位图中,并显示在中间。

一步一步学习使用LiveBindings(13) TListView的进阶使用(1)的更多相关文章

  1. 12.Linux软件安装 (一步一步学习大数据系列之 Linux)

    1.如何上传安装包到服务器 有三种方式: 1.1使用图形化工具,如: filezilla 如何使用FileZilla上传和下载文件 1.2使用 sftp 工具: 在 windows下使用CRT 软件 ...

  2. (转) 一步一步学习ASP.NET 5 (四)- ASP.NET MVC 6四大特性

    转发:微软MVP 卢建晖 的文章,希望对大家有帮助.原文:http://blog.csdn.net/kinfey/article/details/44459625 编者语 : 昨晚写好的文章居然csd ...

  3. (转) 一步一步学习ASP.NET 5 (二)- 通过命令行和sublime创建项目

    转发:微软MVP 卢建晖 的文章,希望对大家有帮助. 注:昨天转发之后很多朋友指出了vNext的命名问题,原文作者已经做出了修改,后面的标题都适用 asp.net 5这个名称. 编者语 : 昨天发了第 ...

  4. 一步一步学习SignalR进行实时通信_1_简单介绍

    一步一步学习SignalR进行实时通信\_1_简单介绍 SignalR 一步一步学习SignalR进行实时通信_1_简单介绍 前言 SignalR介绍 支持的平台 相关说明 OWIN 结束语 参考文献 ...

  5. 一步一步学习SignalR进行实时通信_8_案例2

    原文:一步一步学习SignalR进行实时通信_8_案例2 一步一步学习SignalR进行实时通信\_8_案例2 SignalR 一步一步学习SignalR进行实时通信_8_案例2 前言 配置Hub 建 ...

  6. 一步一步学习SignalR进行实时通信_9_托管在非Web应用程序

    原文:一步一步学习SignalR进行实时通信_9_托管在非Web应用程序 一步一步学习SignalR进行实时通信\_9_托管在非Web应用程序 一步一步学习SignalR进行实时通信_9_托管在非We ...

  7. 一步一步学习SignalR进行实时通信_7_非代理

    原文:一步一步学习SignalR进行实时通信_7_非代理 一步一步学习SignalR进行实时通信\_7_非代理 SignalR 一步一步学习SignalR进行实时通信_7_非代理 前言 代理与非代理 ...

  8. 一步一步学习SignalR进行实时通信_5_Hub

    原文:一步一步学习SignalR进行实时通信_5_Hub 一步一步学习SignalR进行实时通信\_5_Hub SignalR 一步一步学习SignalR进行实时通信_5_Hub 前言 Hub命名规则 ...

  9. 一步一步学习SignalR进行实时通信_6_案例

    原文:一步一步学习SignalR进行实时通信_6_案例 一步一步学习SignalR进行实时通信\_6_案例1 一步一步学习SignalR进行实时通信_6_案例1 前言 类的定义 各块功能 后台 上线 ...

  10. 一步一步学习SignalR进行实时通信_4_Hub

    原文:一步一步学习SignalR进行实时通信_4_Hub 一步一步学习SignalR进行实时通信\_4_Hub SignalR 一步一步学习SignalR进行实时通信_4_Hub 前言 创建Hub 配 ...

随机推荐

  1. 入库出库查询软件——qt

    miniMes系统操作说明 一:功能说明 主界面有扫描--查询两个界面,扫描界面如下 1:默认开启自动入库出库功能 2:右上角限制位数可根据需求设定二维码字符串的位数,设置完成后点击设定,弹窗设定成功 ...

  2. k8s servicemonitor 采集超时配置

    背景说明 我们有时候在编写exporter时,其中某个采集的metrics接口获取数据很慢,可能需要达到10-20S,基于此种情况,如果我们按照ServiceMonitor默认的配置进行,这里默认sc ...

  3. Java源码分析系列笔记-1.JMM模型之先谈硬件

    目录 1. 冯诺依曼体系结构 2. 高速缓存 2.1. 工作原理 2.2. 存储器层次结构 2.3. 局部性原理 3. 缓存一致性/可见性问题 3.1. 如何解决 3.1.1. 总线加锁 3.1.2. ...

  4. 理解PHP array_reduce函数

    http://blog.tanteng.me/2015/07/php-array-reduce/

  5. Kafka入门实战教程(2)基于Docker搭建Kafka环境

    1 准备工作 这里我们使用一台Linux CentOS系统的服务器来模拟三个Kafka Broker的伪集群(即一台server上开三个不同端口)环境用于学习测试,大概的准备工作有两个: 安装Dock ...

  6. C# 选择文件保存路径

    public static string SetSaveFilePath(string filterType= "所有文件|*.*",string fileName="我 ...

  7. nodejs起一个http2

    静态页面 其实就是复制官网的代码 其中证书 是我自己申请的可以用证书 dingshaohua.com import fs from "fs"; import http2 from ...

  8. ngxinx基本使用

    前言 ngxin的所有功能,一般都是在nginx的配置文件中完成的. 所以这同样也是一篇nginx配置文件的熟练过程. 虚拟主机 nginx使用虚拟主机来配置站点:每个虚拟主机使用server { } ...

  9. jz2440 环境搭建

    2.搭建三者互通 1.搭建TFTP服务 这两点搞定基本可以飞奔了 记录一下 配置 板子的ip ifconfig eth3 IP地址 不用重启network服务因为也没有这个服务 当然虚拟机里面的一样 ...

  10. Living-Food-自制 养殖:鸡鸭鹅/牛羊鱼 + 种植:蔬菜/蘑菇 + 主食: 米线/米粉

    Living-Food-自制 主食: 米线/米粉 养殖:鸡鸭鹅/牛羊鱼 公母混养. 温度.湿度.自然环境(自然土壤.通风透气.采光).野生环境(种子/阳光/水/土壤/空气) 食物:稻谷饲料.红薯.苔藓 ...