ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Delphi 12 中 FMX ListView 的高性能跨平台列表实现

Delphi 12 中 FMX ListView 的高性能跨平台列表实现 简介Delphi 12 FMXUI 源码示例包围绕 FireMonkey 跨平台界面开发展开重点演示 ListView 在数据绑定、ItemObjects 自定义视图、点击与焦点事件、分组折叠、滚动动画等方面的灵活用法。资源压缩包共 224 个文件约 4.24MB包含 100 个 pas 单元、54 个 fmx 窗体、33 张 png 图片素材以及 dproj、dpr 工程配置、说明文档和授权文件目录结构完整便于直接打开工程对照学习。已有 1122 人学习下载。通过 YxdUI、FMXUI、Layout、ModernList 等多个可运行示例开发者可以直观理解 TListView 的数据源接入、自定义列表项渲染和 Adapter 扩展逻辑掌握 FMX 界面框架的模块组织方式适合希望快速提升跨平台 UI 开发效率的 Delphi 入门与进阶开发者是一份兼顾原理与实操的 FMXUI 学习资料。1. 项目概述Delphi 12 里那个被低估的 FMX ListView看到这个标题估计不少老Delphi用户会心一笑。说实话以前提到 FireMonkey 的 ListView很多人的第一反应是“卡”“不好用”尤其是从 VCL 转过来的人总觉得这组件不如原生 ListView 顺手。但 Delphi 12 这代 FMXUI 的 ListView 确实改头换面了我最近在一个移动端项目中重度使用它实测下来滚动流畅度、内存占用都比预期好很多已经可以作为主力列表组件来用了。这个组件能解决什么问题简单说你需要在 Android、iOS、Windows、macOS 上做一套界面差不多的列表页要求滑动顺滑、数据动态加载、不同行有不同样式那么 FMX 的 TListView 就是官方给的最优解。它内置了适配器Adapter机制、动态外观DynamicAppearance、视觉状态管理、懒加载回调很多以前要自己造轮子的活儿现在拖拖控件、写几行代码就搞定了。这篇文章适合谁看正在做 Delphi 跨平台项目的同学尤其是想用 FMX 列表替换传统 ListBox 或者打算给老项目升级到 Delphi 12 的朋友。读完你能搞清楚 ListView 的运行机制、常用的开发套路、以及几个从文档里翻不出来的性能杀手。2. FMX ListView 的整体设计思路为什么它比旧方案强这么多2.1 核心优势解析从“控件”到“虚拟化容器”的转变老版本 FMX 里大家喜欢用 TListBox 来做列表因为它简单——加几个 TListBoxItem往里面塞控件就行。但问题也出在这里如果你在 TListBoxItem 里放了多个控件每条记录对应一个真实存在的组件实例1000 条数据就是 1000 个 TLable、1000 个 TImage、1000 个 TProgressBar这在移动端直接就卡成 PPT 了。而 FMX 的 TListView 不一样它在底层引入了一套类似于“视图回收”的机制。你看到的每一个列表项并不是一个完整的、始终存在的控件树而是通过适配器按需创建的。配合 DynamicAppearance 模式你可以在一个 TListItem 上同时显示多行文本、图片、进度条、自定义按钮但它内部其实是在几个预定义的“外观层”上做绘制而不是真的创建出几十上百个 FMX 控件实例。用大白话说旧方案是每个座位坐一个真人ListView 方案是靠“影子分身”来回切换。这套机制带来的最直接收益就是内存占用大幅下降。我实测过在 Android 模拟器上加载 5000 条联系人记录每个记录包含头像、两行文本和一个状态标签FMX TListView 的内存基本稳定在 50MB 以内同样的数据量放到 ListBox 上分分钟突破 150MB 并且滚动时掉帧严重。Delphi 12 在底层的渲染管线又做了进一步优化让这套机制的流畅度又往上走了一截。2.2 方案选型背后的取舍为什么不用自定义绘制或第三方组件我见过很多朋友为了解决列表卡顿问题第一反应就是“干脆写一个自绘列表吧”。这个思路确实可行FMX 的 TListView 本身就提供了非常底层的绘制接口——你可以完全接管每一项的绘制逻辑Unity 里叫“自绘 UI”在 FMX 里就是重写整个 ListView 的渲染。但这样做的代价是你需要自己处理触摸事件、点击热区、文本换行、图片裁剪、状态动画等一大堆细节工作量非常可观。第三方组件库如 TMS FMX 的 ListView 系列也很优秀但一方面需要额外付费另一方面增加了一层学习成本。我的建议是除非你的列表长得很离谱比如每一行都是完全不同的自定义布局否则优先用官方的 TListView DynamicAppearance 就够了。Delphi 12 对这套机制的支持已经非常完善包括 Visualizer可视化视觉、多状态外观切换甚至和 TMultiView 一起做侧滑导航菜单都毫无压力。而且 FMX TListView 的数据绑定能力在 Delphi 12 中也得到了增强。配合 TListView.Searchable、TListViewPullToRefresh 这类内置属性你可以很轻松地实现“下拉刷新、实时搜索过滤”这两大移动端高频需求不需要再额外引入一大堆数据操作逻辑。这就是为什么我在新项目中果断选择了 TListView 而不是反复纠结回退到旧方案。3. 核心细节解析与关键配置花半小时搞懂这几个概念省下三天的坑3.1 DynamicAppearance 模式列表性能的生死线FMX ListView 之所以快最大的功臣是 DynamicAppearance动态外观。这是一种特殊的列表项模式它把“一行列表项”拆分成若干个可独立控制的“视觉区块”。比如你想实现“头像 标题 副标题 右侧日期”这种最常见的布局在传统 ListBox 里你需要拖 4 个控件进去而在 DynamicAppearance 模式下你只需要在 ListView 的 ItemAppearance 属性里选择 DynamicAppearance然后定义几个 TListItemText、TListItemImage 即可。这里有一个非常关键的设计思想这些视觉区块和列表项本身并不是一对一的而是由同一个模板类TListItemDynamicAppearance动态绘制出来的。也就是说当你定义一个 TListItemText 用来显示用户昵称时所有行上的“昵称”都共用同一条绘制逻辑而不是各自独立持有文本控件。最终效果就是即便你有 10000 条数据内存里虚拟出来的真实“可视化对象”也就那么几十个滚动时不断复用。具体操作路径是这样在 Form 上放一个 TListView把 ListView1.ItemAppearance.ItemAppearance : TListItemAppearance.DynamicAppearance右键控件在“Edit DynamicAppearance”中点击“Add Item”添加 TListItemText命名比如“txtTitle”设置 PlaceOffset偏移和 PlaceSize尺寸继续添加 TListItemImage命名比如“imgAvatar”设置尺寸和位置。然后在代码中通过ListView1.Items.AddItem(用户1, 你好欢迎, , , 0, 0)这样一条语句就可以快速添加基础项再通过item.Data[txtTitle] : 用户1为各个视觉区块赋值。你看整个过程完全不需要实例化任何控件性能不爆炸才怪。3.2 常用 Attributes 与 Visualizer让行为细节从代码中解耦FMX ListView 还有一个很容易被忽略但非常实用的设计Attributes属性标签和 Visualizer视觉化器。Attributes 可以理解成“附加在列表项上的自定义数据包”它可以存放字符串、整数、图片等任意类型。比如你要做一个订单列表每个订单有“订单号”“金额”“状态”你可以把这些数据全部塞进 TListItem 的 Attributes 字段然后在绘制样式时再取出来用。这样做的好处是数据的存储和展示完全解耦你甚至可以在多个 ListView 之间共享一套数据源或者把同一份数据展示成完全不同的外观。Visualizer 则解决的是“列表背景上的图形元素”问题。如果你想让列表行之间显示一条细线作为分隔又不愿意在每个 Item 里加一个 TLine 控件那就在 ListView 的 Visualizer 中添加一条分隔线指定颜色和偏移即可。这套机制做出来的分隔线本质上只是“背景绘制”根本不会随着每行数据创建实例对于实现行高、间距和分隔效果非常优雅。有一个细节我特别想说在 DynamicAppearance 模式下你可以将一个 TListItemText 的 Truth 值即是否显示绑定到某个数据字段上从而实现“某些行显示日期某些行不显示日期”的按需显示效果。这一点在移动端界面里非常实用因为它避免了在每行数据里做 if-else 逻辑。很多 Delphi 新手会忽略这个设计把简单的事情复杂化白白浪费了 FMX 的强大能力。3.3 数据源与 Adapter别把数据组装写在控件的 OnUpdate 事件里我可以负责任地说见过的新手案例里十有八九就是这样干的在 TListView 的 OnUpdateObjects 事件中通过if AObject is TListItemText then ...来动态赋值又把图片从数据库里查出来赋给 TListItemImage。这种写法看起来没毛病实际上是在“绘制事件”里做数据 IO性能损失极其明显滚动时会出现明显的顿挫。更合理的方式是利用 FMX 的 DataSource 机制。你既可以用 LiveBindings实时绑定把 TListView 和一个数据集绑定起来也可以在内存里手动建立 TListView 的 Adapter。后者在复杂场景下会更灵活——你可以在加载数据时就安排好每个列表项需要展示的所有字段党组织成TListItemData或直接操作TListView.Items让列表控件只负责纯粹的展示。这样不仅处理逻辑清晰而且当用户触发“下拉刷新”时你只需要重新往 Items 里填充数据不需要反复触发绘制事件去改控件的值。说一句经验之谈如果你发现 ListView 的滚动时不时卡顿一下第一反应别去怀疑渲染性能先检查有没有在 OnUpdateObjects 事件里写耗时的逻辑。通常把数据组装提前到列表填充阶段卡顿问题就已经解决了一半。4. 完整实操从零到一搭建一个带搜索和下拉刷新的动态列表4.1 项目背景与目标设定直接上案例。以“任务管理中心”为例列表页需要展示任务名称、负责人、进度百分比、截止日期并且支持下拉刷新与关键词过滤。运行平台至少覆盖 Android 和 Windows。这是移动办公类 App 最常见的场景用 FMX TListView 来做非常典型。先搭好界面骨架一个 TLayout 作为顶部搜索栏内含 TEdit 和 TButton下面是 TListView设置 Align:Client。这个结构在手机和桌面上都能自适应。为了显示进度条要在 DynamicAppearance 中添加一个 TListItemProgressBar并将 PlaceOffset.Y 设置在副标题的下方。配置完成后的视觉布局大致是左上角头像右侧两行文字右侧下方一条进度条最右边显示截止日期。4.2 数据准备与填充先列表后 UI在 FormCreate 中模拟生成 2000 条任务数据。这里用到一个技巧用一个 TList 泛型列表暂存数据Record 里有 TaskName、OwnerName、DateText、Progress 四个字段。之后手动创建列表项var LItem: TListViewItem; LData: TTaskInfo; begin for LData in FTaskList do begin LItem : ListView1.Items.Add; LItem.Data[txtName] : LData.TaskName; LItem.Data[txtOwner] : LData.OwnerName; LItem.Data[pbProgress] : LData.Progress; LItem.Data[txtDate] : LData.DateText; end; end;关键点来了LItem.Data[...]里的键名必须和 DynamicAppearance 中定义的属性名完全一致。如果你的文本组件在编辑界面里命名为 txtName那这里的字符串就是 txtName。一个很反直觉但非常重要的事实是ListView 在绘制时并不会自动根据你设置的键名去寻找对应组件而是通过内部的字符串哈希表快速匹配并写入对应属性。所以这个名字一旦写错往往没有任何编译报错或运行异常就是界面上什么都不显示。我遇到过的这种“静默失败”场景不下十次排查起来非常折磨人。图片赋值稍微特殊一点。TListItemImage 的显示需要给 TListItemImage.Bitmap 赋值但通过 Data 机制你可以直接传一个 TBitmap 实例LItem.Data[imgAvatar] : FBitmapDict[LData.OwnerName];这里有个大坑就是TListItemImage在多次绘制时会反复取内存中的 Bitmap如果你用的是同一个窗体本地变量可能没事但如果图片是从异步网络加载的必须确保在事件回调中给 item 的 Data 赋值后调用ListView1.Invalidate来强制刷新该行。很多朋友说“网络图片加载不出来”排查到最后往往就是这一点没处理好。4.3 搜索与下拉刷新两种高频交互的实现搜索框的逻辑非常直观直接在 TEdit 的 OnChange 事件里过滤。为了性能考虑不建议每次敲一个字符就重新添加整个 ListView 的 Items而是用ListView1.BeginUpdate; ListView1.Items.Clear; ...; ListView1.EndUpdate;把整批刷新包起来。实测在 2000 条数据下这样批量刷新一次的耗时在 Windows 上约 25ms在 Android 真机上约 80ms完全可以接受。下拉刷新是通过ListView1.PullToRefresh : True以及OnPullToRefresh事件实现的。在这个事件中你只需要重新加载数据、填充列表然后给ListView1.PullToRefreshPending赋False指示器就会自动收起。这里提醒一句在这个事件处理函数里不要做耗时太长的同步操作比如从数据库读 5000 条记录。正确姿势是把耗时操作放到 TThread 或使用 Delphi 12 引入的并行编程库等数据准备完成后再回到 UI 线程去更新 ListView。我通常写一个LoadTasksFromDB方法内部用 TThread.CreateAnonymousThread 执行数据加载然后用TThread.Synchronize回到主线程更新 ListView。这样下拉刷新手势执行完UI 线程几乎零阻塞动画效果也很自然。Delphi 12 在 Android 上对主线程阻塞做了更严格的检测如果你在 UI 线程里跑超过 100ms 的操作系统可能直接弹 ANR应用无响应提示框。这是 Delphi 12 下比较容易踩到的新坑值得注意。4.4 滚动性能调优Delphi 12 的隐藏升级Delphi 12 的 TListView 在底层引出了几个新的性能特性。最直观的一个是TListView.ScrollViewport的惯性滚动算法改进。旧版本在 Windows 上用鼠标滚轮滚动 ListView手指一抖页面就飞出去老远体验非常诡异Delphi 12 对鼠标滚轮和触摸滚轮的响应做了区分现在鼠标滚轮一次滚动一个固定距离触摸滑动则按照手指速度做惯性动画更像原生 App 的手感。另一个隐藏升级是TListView.EnableScrollBounce。这属性在 iOS 上默认开启在 Android 上也不再是摆设。开启后当你把列表拖到底部时会出现一个有回弹效果的阻尼动画和真实移动端 App 的交互一致。不要小看这个小动画它对用户体验的提升是质变级别的。如果你想要更激进的性能表现还可以把TListView.ItemAppearance.ItemHeight设置为一个固定值而不是 DynamicAppearance 的 auto-height。这样排序、布局、索引计算全部变成 O(1) 级别的常量操作渲染时能省掉大量的浮点运算。代价是你得保证每一行内容都差不多高。如果内容会换两行就老老实实用 auto-height否则会看到内容被裁切。这个度需要根据业务形态来权衡。5. 常见问题与踩坑实录这些都是文档里查不到的5.1 图片资源释放为何运行一会儿内存就暴涨这个坑我印象太深了。当时做一个设备巡检应用设备列表每一项都有一张现场照片。我开始的设计是从网络下载图片后直接item.Data[imgThumb] : ABitmap然后就不再管。跑了几分钟后App 内存以肉眼可见的速度往上飙最后直接闪退。后来排查发现FMX 的 TListItemImage 在未设置OwnsBitmap : False时默认会接管 LoadFromStream 或通过 Data 赋值的 Bitmap 生命周期。当列表项被回收时那个 Bitmap 对象会被释放。但如果同一张图片被多个 item 同时引用其中一个 item 被回收整个 Bitmap 就变成野指针导致内存泄漏甚至访问冲突。解决办法有两条路。第一是在外部维护一个全局 Bitmap 缓存字典ListView 里的TListItemImage.Bitmap只持有同一个缓存实例的引用并把OwnsBitmap置为 False。第二是使用 FastMM4 的泄漏报告功能在退出前检查看是否有 TBitmap 对象始终没有释放。这两个方案在我实际项目中都用过方案一在“图片总量少、复用率高”的场景下更稳方案二适合“图片多且每个 item 图片不同”的富媒体场景。5.2 动态外观改属性不生效先检查 Name 再考虑缓存问题在 FMX ListView 中调试“为什么某行文本没显示”是一件容易让人头秃的事。有次调试一个实时股票行情页面某个字段的行情涨跌幅总是显示不出来图文代码逻辑看起来都没问题后来发现问题出在 DynamicAppearance 里那个属性的名字带了前导空格。这是一个极其隐蔽的坑在动态外观编辑器里你输入属性名时如果手滑在名字前打了一个空格它在设计期看着完全正常但在代码里通过Data[字段名]赋值时却永远匹配不上因此那部分 UI 就永远是默认状态。遇到这种问题先用代码强制给那个 Data 键赋一次值然后在 OnUpdateObjects 里打断点看AObject的名称是否匹配。如果名称不匹配十有八九就是名称有不可见字符。还有一种情况是当你修改完 DynamicAppearance 里的属性布局后界面上没有任何变化。这不是 FMX 的 bug而是因为列表项的视觉属性是在TListViewItem第一次被创建时根据模板生成并缓存的。如果你在运行过程中改变了某个属性的PlaceOffset.X旧 item 不会自动更新视觉效果需要先清空列表重建或者调用ListView1.ItemAppearance.ResetAppearance。5.3 列表项点击区域太小手滑误触的优化方案移动端列表的另一种高频需求是点击整行触发跳转但点击空白处或点击右侧图片时不希望触发。FMX TListView 在 DynamicAppearance 下默认只在“文本区域”收到点击时才触发 OnClick 事件。当你在“头像 文本”布局中想让头像也成为点击区域时要给头像这个 TListItemImage 设置HitTest : True属性。这个概念和 VCL 里的 HitTest 含义类似。但是这里有一个副作用如果你把多个子区域的 HitTest 都设为 True那么点击行为会变得“只命中子区域”而不是整行响应。所以实际开发中我习惯把“透明背景”的 TListItemText 铺满整行作为唯一 HitTest 区域这样既能保证整行可点击又不会干扰单独的图片点击事件。当你把点击事件逻辑拆到子区域之后务必记得把主文本区域的 HitTest 设为 False否则点击文本时会同时触发两个逻辑导致跳转两次。5.4 多线程更新 ListView别忘掉 Synchronize 的本质限制在 Delphi 12 中开发跨平台应用很多人会用 TTask 或匿名线程去处理业务逻辑比如网络请求、数据解析。这本身没问题但如果你直接在子线程里调用ListView1.Items.Add或修改 Data 字段轻则界面不刷新重则直接崩在 Windows 的 RTL 层。FMX 不是线程安全的所有 UI 操作都必须回到主线程执行。常见的正确写法是TTask.Run( procedure var LDataList: TListTTaskInfo; begin LDataList : LoadDataFromService; TThread.Queue(nil, procedure begin MergeDataToListView(LDataList); end); end);用TThread.Queue而不是TThread.Synchronize的理由是Queue 不会阻塞子线程而是把操作放进主线程消息队列中排队异步执行Synchronize 则会等待主线程执行完毕才返回如果主线程在处理耗时操作子线程会白白卡住。在数据加载量比较小的场景两者区别不明显但如果是高频分页加载Queue 的优势非常明显。这一点同样适用于拉动刷新后加载大量数据的情况。5.5 纯字符串字典键引发的性能黑洞这个话题正好呼应热词里的“delphi 字符串作字典key”。如果你用 TStringDictionary 或 TDictionarystring, ... 存储列表数据推荐使用 Delphi 12 默认的字符串哈希算法它在大多数情况下已经足够好。但有个容易忽略的问题当列表数据量大超过大几千条并且你不小心走了大小写敏感的重载每查一次 key 都会做一次完整的大写转换内存开销会明显放大。因此在给 ListView 的 Data 字段赋值时应尽量保证字符串键的大小写稳定不要一会儿用 txtName一会儿用 TXTNAME否则在内部哈希匹配时会产生额外的比较开销。6. 最后的经验分享从会用组件到用顺手的距离我不是第一次跟别人说FMX TListView 是一个值得投入时间研究的组件。Delphi 12 对 FMX 的调度和底层渲染管线的优化让 TListView 的性能已经追平甚至超越了大多数原生跨平台方案。但真正决定它上限的还是你如何组织数据和绘制逻辑。在我目前实际项目中最常用的一个性能组合术是外部用“轻量数据记录”缓存原始数据ListView 的 Data 字段永远只保留展示所需的字段副本。这样不管是做搜索过滤、分组、排序还是切换视图都只需要对新产生的临时数据集合做一次列表重建而不需要在 ListView 的 internals 里反复修改。习惯这种思路之后你会发现列表即使做到几万条数据依然顺滑得像丝绸一样。还有一个很容易被忽略的实用技巧利用TListView.ItemIndex的返回值实现“定位到某一行”。当用户通过搜索找到目标后需要滚动到那一行醒目显示可以直接ListView1.ItemIndex : idx; ListView1.ScrollToItem(idx)这在旧版 FMX 中很不稳定但在 Delphi 12 中表现很可靠。如果你想做“回到顶部”这类的操作同样可以用这个方法。最后想提的一点是建议多去 Delphi 官方社区和 Embarcadero 的文档站翻一翻 TListView 的 Release Notes。Delphi 12 中修复了不少旧版本中“点击区域偏移”“图片闪烁”这类顽固问题而且某些性能优化只在新版本中生效。如果你正在从旧项目升级把 TListView 相关代码的测试用例准备好升级后的收益会比想象中大得多。本文还有配套的精品资源点击获取
返回列表