Delphi 12 FMX ListView 复用机制与性能优化避坑指南

发布时间:2026/11/2 12:41:47

Delphi 12 FMX ListView 复用机制与性能优化避坑指南 简介Delphi 12 FMXUI 跨平台界面组件库的预览版源代码包面向已具备 Delphi 与 FireMonkey 基础、希望深入掌握 ListView 的开发者可用于搭建 Windows、macOS、iOS、Android 及 Linux 多端界面并解决列表展示、数据绑定、交互定制与视觉适配等实际问题。压缩包共 224 个文件约 4.24MB以 100 个 pas 单元、54 个 fmx 窗体及 33 个 png 界面素材为主另含 dproj 与 dpr 工程文件、dpk 包文件、说明文档和少量资源文件目录组织便于直接编译研究适合对照源码学习组件封装、列表项复用及跨平台渲染差异。已有 1134 人学习。读者可从中获得 ListView 的示例实现与组件源码线索围绕数据绑定、ItemObjects 定制视图、OnItemClick 等交互事件、分组折叠、平滑动画、排序过滤及 TListViewBaseAdapter 扩展展开研读理解多平台列表的界面组织与适配思路并据此改进实际项目中的可操作性与视觉体验积累可复用的界面搭建经验。1. 从一次滑动卡顿说起Delphi 12 FMX 的 ListView 到底强在哪把 Delphi 12 的 FMX UI 从头搭一遍最让我改观的控件不是那些带动画的面板而是 ListView。模拟项目X 里有一屏订单列表最初我用 TScrollBox 手堆三十张卡片安卓千元机一滑就掉帧换成 ListView 的 DynamicAppearance 之后同样的视觉、同样的数据滑动直接跟手。后来我把这套写法整理成一份可复用的源代码这里把判断依据、接线细节和出问题的地方一次讲清楚。这篇写给正在纠结 FMX 列表到底该怎么搭、已经上手但被复用机制坑过的人新手能照着抄结构熟手能看到我为什么这么取舍。2. 三种数据组织方式怎么挑内置外观、DynamicAppearance 与 LiveBindingFMX 的 ListView 看起来只有一个控件实际上底层有三条完全不同的路径选错了后面全是返工。我一般先问一句这条列表要显示几个字段、要不要自定义布局、数据是从对象列表来还是从数据集来。三个问题的答案基本就把路定死了。2.1 内置外观能撑到哪一步ItemAppearance 在设计期是一组枚举值常用的就这么几个ListItem 是标题加一行说明ListItemRightDetail 把详情挪到右侧ImageListItem 是左图右文ImageListItemBottomDetail 是图片下面压两行字。选完之后你只要Items.Add再往Text、Detail、Bitmap上赋值就能出效果高度自动算连 ItemHeight 都不用管。这套东西的优点是零代码、稳、不吃性能。缺点是布局写死图片多大、文字离图片多远、行间距多少全部由框架决定你能调的只有 ItemHeight 和字体。我见过有人为了在内置外观上把金额挪到右边去写 OnPainting 自己画画完发现高 DPI 下全歪。判断线其实很朴素如果设计稿上只有一个圆角矩形加一行字加一行灰字内置外观够用只要出现「金额右对齐、标签在标题下方、右上角一个状态点」这种组合直接上 DynamicAppearance别硬撑。2.2 DynamicAppearance 加 OnUpdateObjects 的正确落点DynamicAppearance 的完整流程是结构面板里选中 ListView把 ItemAppearance 设成 DynamicAppearance然后在 Item Designer 里拖 TListItemText、TListItemImage、TListItemAccessory 这些对象逐个改 Name。之后所有赋值都在 OnUpdateObjects 里做。这里有个必须刻进肌肉记忆的点OnUpdateObjects 不是「添加时调一次」而是「这条要被显示时」都会调滚动、重绘、改尺寸、切前后台全都会触发。所以里面必须幂等——每个对象的所有属性都写一遍包括这一条不该显示图片的时候把图片清空。procedure TFormMain.ListView1UpdateObjects(Sender: TObject; const AItem: TListViewItem); var LOrder: TOrderItem; LText: TListItemText; LImg: TListItemImage; begin // Tag 里存数据主键项被复用时 Tag 一定跟着换 if not FOrderMap.TryGetValue(AItem.Tag, LOrder) then Exit; // 每次全量赋值不做「有值才写」的优化复用残留就是这么来的 LText : AItem.Objects.FindObjectTTListItemText(txtTitle); if LText nil then LText.Text : LOrder.Title; LText : AItem.Objects.FindObjectTTListItemText(txtAmount); if LText nil then LText.Text : FormatCurr(0.00, LOrder.Amount); LImg : AItem.Objects.FindObjectTTListItemImage(imgThumb); if LImg nil then LImg.Bitmap : FImageCache.Placeholder; // 先占位异步结果回来再替换 end;AItem.Tag是 NativeInt存主键最省事FindObjectTT按 Name 反查对象查不到返回 nil所以每个都要判空——Item Designer 里删了一个对象忘了改代码运行时就靠这个判空活着。2.3 什么时候该换 TGrid 而不是硬凹 ListViewListView 的每一项是独立布局列与列之间不会互相对齐。只要需求里出现「表头、列宽固定、行与行必须对齐」ListView 就开始难受了你靠手写 Width 对齐换个 DPI、用户改一下系统字体大小立刻错位。我一般在三种情况下直接换 TGrid需要严格列对齐的报表、单元格内可编辑的表单、需要横向滚动的宽表。还有一种情况是条目高度差异极大——这条三行、那条十行——ListView 的 ItemHeight 是全局值虽然可以在 OnUpdateObjects 里改AItem.Height单条设高但重算时机很微妙滚动时容易跳反而不如 TScrollBox 直接塞。LiveBinding 那条路单独说一句DynamicAppearance 配合Item.Data[字段名]和数据集绑定能省掉一批赋值代码适合表单型 CRUD。代价是调试链路变长绑错一个字段名界面上什么都不显示也不报错我一般只在字段多且稳定的场景才用。3. 接线全流程从 TObjectList 到 ItemAppearance 的落地写法选定 DynamicAppearance 之后真正的活是把数据层、控件层和异步资源接起来。这段最容易写成能跑但不敢改的代码我的做法是把三件事分开数据谁管、对象谁取、图片谁加载。3.1 数据层先用 TObjectList 管住生命周期数据对象别直接用字符串数组或 TStringList 存字段一多就散架。我一般定义一条记录类再交给 TObjectList 托管OwnsObjects 保持默认的 TrueFree 的时候一并回收。type TOrderItem class private FId: Integer; FTitle: string; FAmount: Currency; FThumbUrl: string; public property Id: Integer read FId write FId; property Title: string read FTitle write FTitle; property Amount: Currency read FAmount write FAmount; property ThumbUrl: string read FThumbUrl write FThumbUrl; end; // 主键索引Tag 反查用比线性 Find 稳 FOrders : TObjectListTOrderItem.Create(True); FOrderMap : TDictionaryInteger, TOrderItem.Create;关键在于ListView 里不存数据对象只存主键。原因很简单项是复用的你没法保证某个 TListViewItem 永远对应某条数据把对象塞进Item.Data之类的地方滚动几屏之后一定错位。用 Tag 存 Id、用字典回查是最不容易翻车的写法。点击事件里也是同一套拿ItemIndex找到项拿项的 Tag 找数据。填充时记得成对使用 BeginUpdate 与 EndUpdate一次加几百条的时候差别非常明显。ListView1.BeginUpdate; try ListView1.Items.Clear; for LOrder in FOrders do begin LItem : ListView1.Items.Add; LItem.Tag : LOrder.Id; // 只挂主键OnUpdateObjects 里再取值 end; finally ListView1.EndUpdate; end;BeginUpdate期间不做重排EndUpdate之后一次性布局这是 ListView 上性价比最高的一个改动没有之一。添加一千条时不包和包着的差距不是百分之几十是几秒和几十毫秒的差距。3.2 Item Designer 里的对象命名与运行时取值Item Designer 里拖出来的每个对象都要改 Name别用默认的ListItemText1这种名字。我习惯用类型前缀txtTitle、txtAmount、txtTag、imgThumb、accStatus。这样代码里搜txt就知道哪些是文本一目了然。取值统一走一个封装别在业务逻辑里到处Objects[2]按下标取——下标一旦因为改布局变动全篇代码一起崩还不报错。// 统一取文本对象找不到就返回 nil调用方判空 function TFormMain.ItemText(const AItem: TListViewItem; const AName: string): TListItemText; begin Result : AItem.Objects.FindObjectTTListItemText(AName); end;还有一个细节如果 Item Designer 里把默认的 Text 对象删掉了再写Item.Text : ...是不会有任何效果的界面上什么都不变也不报错第一次遇到会怀疑人生。Detail 同理。所以要么保留默认对象要么统一走 FindObjectT别两套混着来。提示改布局之后先改代码再编译靠运行时判空兜底只能保证不崩不能保证显示对。3.3 缩略图异步加载与缓存复用列表里最拖后腿的几乎永远是图片。同步从网络或本地大图加载滚动必然顿。我一般的写法是先给占位图异步取回到主线程前再校验一次这条项是不是还对应同一份数据。procedure TFormMain.LoadThumb(const AItem: TListViewItem; const AUrl: string); var LImg: TListItemImage; LToken: Int64; begin LImg : AItem.Objects.FindObjectTTListItemImage(imgThumb); if LImg nil then Exit; LImg.Bitmap : FImageCache.Placeholder; LToken : AItem.Tag; // 用主键当令牌 TTask.Run( procedure var LBmp: TBitmap; begin LBmp : FImageCache.GetOrLoad(AUrl); // 内部 LRU 磁盘缓存 TThread.Queue(nil, procedure begin // 项可能已被复用给了另一条数据令牌不一致就丢弃结果 if (AItem.Tag LToken) and (LImg nil) then LImg.Bitmap : LBmp; end); end); end;缓存对象我一般设一个上限比如 120 张按最近使用淘汰。位图统一在加载时缩放到目标尺寸再入缓存别指望控件去缩——TListItemImage每帧缩一张 4K 图的代价滚动时不掉帧才怪。缓存里的 TBitmap 由缓存统一持有和释放列表项只是引用这样 Clear 重新填充时不会出现谁释放谁的争议。4. 一万条数据实测几个开关的收益排序与卡顿定位性能这件事不看文档看数据。我在模拟项目X 上灌了一万条订单在千元安卓机上做了一轮粗略对照结论比较反直觉真正决定流畅度的不是 ListView 本身的属性而是你往里塞了多少对象、以及 OnUpdateObjects 里干了什么。4.1 影响最大的四个开关实测对照调整点默认做法建议做法主观收益代价批量填充逐条 Add每次都重排BeginUpdate / EndUpdate 包住极大无Item Designer 对象数一条项拖 12 个对象压到 56 个能合就合大布局要重排图片原图直接赋值靠控件缩放预缩放到目标尺寸再入缓存大多一份缓存管理OnUpdateObjects 里的查找每个字段 FindObjectT一次取出多个字段复用中代码略啰嗦ItemHeight留空让框架算固定一个值中不能自适应阴影/动效项上加 Effect去掉改用静态底图大视觉要妥协必须说清楚这些数字只是我在一台测试机上的相对感受不是基准测试换设备换布局结论可能变。但排序大体稳定——批量填充和对象数量是最值得先动的两刀先砍这两个八成卡顿就没了剩下的再谈细节。还有个容易被忽略的点ListView 是虚拟化的它只创建可视区域附近的项并循环复用。所以「数据一万条」本身不直接等于慢慢的是每一条被显示时你做了多少事。反过来说如果 OnUpdateObjects 里做了 IO、做了字符串拼接、做了数据库查询数据量再小也会卡。4.2 卡顿定位在 OnUpdateObjects 里埋计时点卡顿最难的是定位。我的土办法很有效在 OnUpdateObjects 里挂一个秒表单次超过阈值就打日志跑一遍滚动看慢在哪几条、慢多少。procedure TFormMain.ListView1UpdateObjects(Sender: TObject; const AItem: TListViewItem); var LSw: TStopwatch; begin LSw : TStopwatch.StartNew; try BindItem(AItem); // 真正干活的地方单独抽出去 finally LSw.Stop; FTotalMs : FTotalMs LSw.Elapsed.TotalMilliseconds; Inc(FUpdateCount); // 8ms 大约是一帧的预算单条超了基本就能感觉到 if LSw.ElapsedMilliseconds 8 then OutputDebugString(PChar(Format(slow item id%d cost%dms, [AItem.Tag, LSw.ElapsedMilliseconds]))); end; end;BindItem单独抽出来是为了让计时点干净。跑完看日志如果慢的条目集中在带图的那几条就是图片的锅如果全都慢就是对象太多或者字段赋值里有隐藏开销比如每次都 Format 一遍日期、每次都查字典。这套埋点在 Windows 上跑一遍安卓上跑一遍两边的分布经常不一样别只测一端。注意OutputDebugString 在大量调用时自己也会拖慢定位完记得把阈值调高或者直接去掉。4.3 内存回收项复用与图片缓存上限ListView 的项是复用的但复用的项上挂的位图引用不会自动换成新的。如果图片缓存不设上限滚一遍长列表就等于把所有缩略图堆在内存里安卓上很容易在中低端机被系统请出去。我的做法是缓存设硬上限淘汰时只丢没有被当前可见项引用的Items.Clear之后主动调一次缓存的 Trim。另外一点不要在 OnUpdateObjects 里 new 任何对象包括临时 TBitmap、临时 TStringList。这个方法一秒能被调几百次里面 new 一次就是几百个对象GC 和分配器都扛不住。5. 避坑FMX ListView 最容易出问题的五处下面这五条都是我自己或同事真实遇到过的每条按现象、原因、解决写清楚遇到同类问题可以直接对照。5.1 复用项显示上一条的数据现象滚动几屏之后某些条目的副标题或图片是上一屏某条的内容往回滚又好了。原因OnUpdateObjects 里写了「有值才赋值」的分支比如图片为空就跳过不赋值。项是复用的上一轮挂上去的位图还在于是显示成别人的。解决所有对象的所有属性每轮全量赋值空值也要显式赋成空或占位图。判断标准很简单把这个方法单独拿出来看能不能在任意初始状态下调用都得到正确结果。5.2 运行时换 ItemAppearance自定义对象凭空消失现象代码里动态改ListView1.ItemAppearance做两套视觉切换切完自定义的 txtTitle 全没了取值全是 nil。原因ItemAppearance 变更会重建项的设计结构设计期挂在 DynamicAppearance 上的对象不会跟着搬过去切回来也不一定还原。解决别在运行时切外观。要做多套视觉就在 Item Designer 里放两套对象按状态控制 Visible 和位置确实需要两套差异极大的布局就用两个 ListView 交替显示代价是内存收益是可控。5.3 OnUpdateObjects 里增删项导致卡死现象在 OnUpdateObjects 里根据数据补一条「加载更多」的项运行起来直接卡死或者抛访问异常。原因这个方法是在遍历和布局过程中被回调的你在里面改 Items 集合等于在迭代中修改容器。解决这个方法里只做赋值不碰集合。加载更多放到 OnScrollViewChange 里判断滚动位置接近底部再追加并且用布尔量挡住重入避免用户连续快滑触发多次追加。5.4 点击取 ItemIndex 取到了错的行现象过滤之后再点击打开的是过滤前的那条数据。原因代码里用 ItemIndex 去原始数据列表按下标取。过滤改变了 ListView 的顺序和数量但原始列表没变下标自然对不上。解决一律用Item.Tag存主键回查不按下标取数据。OnItemClick 和 OnItemClickEx 两条路都统一成同一套反查别一个用一个不用。5.5 安卓首次滚动白块与图片闪烁现象列表刚出来时先是一片空白快速滑动时图片一会儿有一会儿没有。原因图片是在项显示之后才异步取的第一轮必然是占位图同时缓存命中率低同一个 URL 被并发请求了多次回填顺序错乱。解决占位图给一个和最终图同尺寸的纯色位图视觉上不会跳缓存里对同一个 key 做请求合并已经有请求在路上就不要重复发回填时用 5.1 里的令牌校验。6. 收口技巧把列表初始化收成一个可复用单元最后落到一个我每次新建列表都会先写的方法。它把那些「忘了就出问题」的设置一次性钉死后面写业务只管 OnUpdateObjects。procedure TFormMain.SetupOrderList; begin ListView1.ItemAppearance : TListViewItemAppearance.DynamicAppearance; ListView1.ItemHeight : 76; // 固定高度别让框架逐条算 ListView1.ShowScrollBar : True; ListView1.AllowSelection : True; ListView1.OnUpdateObjects : ListView1UpdateObjects; ListView1.OnItemClickEx : ListView1ItemClickEx; // 统一走 Tag 反查 FImageCache.Capacity : 120; // 缓存上限超出按 LRU 淘汰 end;这六行里ItemHeight 固定和 OnUpdateObjects 必须挂上是最关键的两条。前者省掉逐条测量后者保证复用不会串数据。剩下的按项目需要加比如搜索框、下拉刷新都是锦上添花。从那以后我每加一个新列表都强制走一遍这套流程先定 ItemAppearance再改对象名再写全量赋值的 OnUpdateObjects再给 Tag 存主键最后才填数据。宁可多写二十行也不让复用残留有机会冒出来。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/11/2 12:41:47

HBase分布式数据库安装与核心操作指南

1. HBase简介与核心特性HBase是一个开源的、分布式的、面向列的NoSQL数据库,它构建在Hadoop文件系统(HDFS)之上,专为处理海量结构化数据而设计。作为Google BigTable的开源实现,HBase在实时读写访问大规模数据集方面表…

2026/11/2 12:41:47

合成复用原则:组合优于继承的软件设计实践

1. 合成复用原则的本质解析在软件工程领域,合成复用原则(Composite Reuse Principle)是面向对象设计的七大原则之一,它直指代码复用这一核心命题。我第一次真正理解这个原则的价值,是在维护一个使用深度继承体系的遗留…

2026/11/2 12:36:47

PSCAD启动编译运行故障排查:配置、DLL、端口与权限

简介:《PSCAD-解决启动、编译和运行方面的问题》是一份面向电力系统仿真工程师、科研人员及高校师生的故障排查文档,聚焦PSCAD X4与V5使用中常见的启动失败、编译中断和运行异常。内容按问题类型组织,涵盖“根元素缺失”、启动时间过长、脚本…

2026/11/2 13:42:04

无人船与无人车编队MPC协同控制实战解析

1. 无人船与无人车编队协同控制的核心挑战在海洋和陆地环境中实现多智能体协同作业一直是自动化领域的前沿课题。我最近完成了一个结合无人船(USV)和无人车编队的MPC控制项目,这里分享一些实战经验。这类系统最核心的难点在于如何处理动态环境下的协同一致性——当1…

2026/11/2 13:42:04

专科生论文写作利器:千笔工具全解析

1. 专科生论文写作痛点与解决方案作为一名在高校工作多年的教育从业者,我深知专科生在毕业论文写作过程中面临的困境。与本科生相比,专科生往往缺乏系统的学术训练,在选题、文献检索、论文结构等方面存在明显短板。很多同学直到毕业前夕才匆忙…

2026/11/2 13:42:04

Neuralink脑机接口技术解析与应用前景

1. Neuralink芯片:脑机交互的技术革命当马斯克在社交平台宣布Neuralink即将量产时,整个科技圈为之震动。这枚硬币大小的芯片,正在重新定义人类与数字世界的交互方式。与传统脑机接口设备不同,Neuralink的创新之处在于它实现了&…

2026/11/2 13:42:03

从机器学习到大模型:一条能走通的学习路线与实战指南

大模型这个词这两年几乎成了技术圈的硬通货,不管你是做后端、做前端、做数据还是做算法,面试里不聊两句Transformer都不好意思开口。但问题也恰恰出在这里——网上关于大模型的资料多到爆炸,从三行代码调用API的爽文,到几百页的论…

2026/11/2 13:37:03

三维数字孪生与多源感知融合的智能仓储解决方案

1. 项目背景与核心需求 在大型仓储管理领域,军储库区作为特殊场景具有三个典型特征:空间结构复杂(多层货架、立体存储)、作业设备多样(叉车/AGV/人工协同)、安全要求严苛(防碰撞/防误操作&#…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑