资讯动态

Delphi 12 FMX ListView跨平台列表开发:动态外观与性能优化

发布时间:2026/10/20 16:40:56 来源:尧图企业网站定制
简介Delphi 12 FMXUI 是一套聚焦 FireMonkey 跨平台开发的示例资源面向需要快速掌握 FMX 界面组件的中高级 Delphi 程序员尤其适合研究 ListView 在 Windows、macOS、iOS、Android 等多平台下的落地用法。资源共 224 个文件以 100 个 pas 源代码、54 个 fmx 界面文件及 33 个 png 图片为主辅以项目文件、文档和设计资源整体压缩包约 4.24MB。已有 1131 人学习下载。包内含 ModernList、Layout、BaseFrame 等多个示例项目覆盖数据绑定、视图定制、分组折叠、动画交互等典型场景通过阅读源码结构与 FMX 布局文件可以清晰理解 ListView 的适配机制并直接参考其中的自定义 Item 设计来构建自己的业务列表界面。对于正在从 VCL 转向 FMX 或需要完善多端列表交互的开发者这套资源提供了可运行、可改写的实践起点。1. 先把结论放在前面FMX ListView 是 Delphi 12 跨平台项目里被低估的组件拿到这份 Delphi 12 FMXUI 源码的时候我正被某跨平台系统项目里的列表页搞得头大。VCL 时代用习惯了 TListView 的直属列、双击编辑一换到 FMX 这边第一眼看 ListView 又没网格又没列头差点直接放弃换回 VCL。但等我把它的 ItemAppearance 和动态外观机制摸明白才发现 Delphi 12 的这个 Fmx UI 组件是真能打移动端底下走的是原生列表渲染桌面端又能用 DynamicAppearance 做高度自定义一套代码两个端都能交付。这篇就按我拆包时的顺序从数据填充、动态模板、图片加载讲到避坑和性能优化新手能照着抄熟手可以直接跳到第 4 章看翻车记录。2. TListView 的工作原理与三种数据填充方式先搞清楚它在各端渲染成什么再决定怎么填数据2.1 同样是 ListView它在 iOS、Android 和 Windows 上其实是三套渲染行为拆这份 Delphi 12 FMXUI 源码包之前我一直以为 FMX 的 ListView 是纯自绘控件后来翻着源码往下追才发现不是这么回事。TListView 在 iOS 端映射到 UITableView、Android 端映射到原生 ListView 适配层Windows 和 macOS 桌面端才走 FMX 自绘。这个差异直接决定了你在设计器里看到的效果和真机运行效果是两码事。理解了这个底子再回头看它的行为就说得通了。移动端滚动惯性、边缘回弹、触摸反馈这些体验不是 FMX 模拟出来的而是原生列表天然带进来的这也是为什么 TListView 在手机上的滚动流畅度明显优于同屏堆几十个 TRectangle 的做法。桌面端自绘带来的则是高自由度Item 内部放什么视觉元素、什么颜色、什么对齐方式完全由你控制。但也有代价。两端渲染机制不一样坑也不一样。最常见的情况是桌面端 Windows 上调试得好好的文字布局跑到 Android 上发现高度算错了、图片间距不对劲。这不是你的代码有问题而是自绘和原生渲染对尺寸的处理本来就不同。我建议在项目里做列表页时一周至少真机跑一次别等到最后联调才拿出来。2.2 三种填充数据的方式直接 Items.Add、LiveBindings、DynamicAppearance 该怎么选FMX ListView 填充数据有三条路先看这张对比表再按项目阶段选。方式上手难度适合场景维护成本性能边界直接操作 Items.Add低数据量小、原型验证、固定几条的菜单页低千条以内足够LiveBindings 数据绑定中与数据库、对象列表联动字段多且结构稳定中中量数据字段修改需重编译DynamicAppearance 动态外观中高图文混排、视觉定制度高的列表中高视觉对象由模板控制可做到数千条流畅我的习惯是如果只是展示一批文本直接 Items.Add 就行简单直接不引入额外的绑定层。如果数据来自数据库而且要实时联动LiveBindings 能省掉很多手工同步代码。但到了真正要交付的界面比如带图标、带状态、带副标题那种列表我会直接上 DynamicAppearance——只有它能让你完全控制每个 Item 长什么样。这份源码包给我的启发也在这里。它没有只演示一种写法而是把三种模式并列摆在同一个工程里同一个数据源分别用三种方式填充运行起来对比性能和代码量。拆包时强烈建议你按这个节奏走一遍比只看文档有用得多。2.3 先抄一段最简单的直接 Items.Add 的骨架代码先从最基础的方式入手。把 ListView 放到窗体上之后在代码里这样填充procedure TForm1.BuildSimpleList; var LI: TListViewItem; I: Integer; begin ListView1.BeginUpdate; try ListView1.Items.Clear; for I : 0 to 9 do begin LI : ListView1.Items.Add; LI.Text : 任务 I.ToString; LI.Detail : 这是列表第 I.ToString 行; end; finally ListView1.EndUpdate; end; end;这段代码的逻辑很简单先调用 BeginUpdate 告诉 ListView 我要连续改数据了你不要每条都刷新界面等我全部加完再一次刷新接着清空旧 Items然后用循环往里加 10 行最后 EndUpdate 收尾界面一次性更新。这里有两个参数要特别注意。一个是 LI.Text它对应列表项的主标题默认显示在左侧另一个是 LI.Detail 副标题它需要配合 ItemAppearance 里的 ListItemRightDetail 或 ListItemTopDetail 模式才会显示如果只在默认外观下赋了 Detail界面上是看不到任何变化的。这句话值得记住我刚开始做的时候就踩过字段赋了好几个界面上什么都不变以为控件坏了其实是外观模式没切。把 ItemAppearance.ItemAppearance 切到 ListItemRightDetailBuildSimpleList 里赋的 Detail 就会出现在主标题右侧。这是理解后面 DynamicAppearance 的起点。3. 动态 Appearance 实战设计器里搭模板代码里填数据跑起来才是图文列表3.1 先在设计器里把视觉骨架搭好模板命名是代码和视觉之间的契约DynamicAppearance 和前面两种方式的本质区别在于列表项长什么样不是控件预制的而是你在设计器里用一个模板定义出来的模板里放多少个 TListItemText、TListItemImage、TListItemSwitch实际运行时每个 Item 就会带多少个子视觉对象。在这个 Delphi 12 源码包里动态模板是在 ListView1 的属性编辑器里打开的。步骤是把 ItemAppearance.ItemAppearance 设为 DynamicAppearance然后点开 Appearance 集合编辑器在里面添加 TListItemText 和 TListItemImage。这里有一个很容易被忽略但至关重要的事给模板里的元素取名字。名字是你代码访问它的唯一凭据我建议按前缀区分类型和用途格式固定成下面这种const CTitleName txtTitle; // 标题文本 CDetailName txtDetail; // 副标题文本 CIconName imgIcon; // 左侧图标之所以用常量写死是因为后续在代码里要大量引用这三个名字直接写字符串容易拼错而且一旦模板改名你也只需要改这一处。我把命名格式定为 txt 前缀代表文本、img 前缀代表图片看代码立即能知道每个元素是干什么的。模板搭好后设计期看到的是一个空的样子真正的数据填充发生在代码里这就引出下面这个环节。3.2 通过 Data 字典和 OnUpdateObjects 事件批量填充图文数据动态外观下数据流的正确写法是把数据挂到 TListViewItem.Data 上再由 OnUpdateObjects 事件把 Data 里的值搬到模板元素上。先看这段核心代码procedure TForm1.LoadListData(ATaskList: TMyTaskList); var I: Integer; LItem: TListViewItem; begin ListView1.BeginUpdate; try ListView1.Items.Clear; for I : 0 to ATaskList.Count - 1 do begin LItem : ListView1.Items.Add; LItem.Data[title] : ATaskList[I].Title; LItem.Data[detail] : ATaskList[I].Detail; LItem.Data[icon] : ATaskList[I].IconIndex; end; finally ListView1.EndUpdate; end; // 数据准备好后不需要手动刷ListView 会按需触发 UpdateObjects end; procedure TForm1.ListView1UpdateObjects(const Sender: TObject; const AItem: TListViewItem); var LText: TListItemText; LImg: TListItemImage; begin LText : AItem.Objects.FindObjectByName(CTitleName) as TListItemText; if LText nil then LText.Text : AItem.Data[title].AsString; LText : AItem.Objects.FindObjectByName(CDetailName) as TListItemText; if LText nil then LText.Text : AItem.Data[detail].AsString; LImg : AItem.Objects.FindObjectByName(CIconName) as TListItemImage; if LImg nil then begin if AItem.Data[icon].AsInteger 0 then begin LImg.ImageList : ImageList1; LImg.ImageIndex : AItem.Data[icon].AsInteger; end else LImg.Visible : False; end; end;拆开讲这段的逻辑。LoadListData 负责造数据它把每条数据的标题、副标题、图标索引存进 LItem.Data 这个字典里。Data 是 TListViewItem 自带的附加数据容器键是字符串、值可以是字符串或者整数非常适合做这种数据和视图的临时中转站。OnUpdateObjects 是动态外观机制里的关键回调ListView 在某个 Item 即将显示时会调用它。你要做的就是从 AItem.Objects 里按名字找到模板元素再把 Data 的值赋上去。注意 FindObjectByName 返回的是父类 TListItemVisual所以要用 as 做强转到具体子类。万一样板里没这个名字返回值就是 nil所以每个查找都要做空判断不用省省一次崩一次。这个方案最舒服的地方是数据和视觉完全解耦。填充数据时你只关心数据本身完全不管显示细节显示细节全部集中在 OnUpdateObjects 里要调整视觉效果只改一个地方。批量加载几千条时也不会因为每条都重复查找模板而卡死因为 ListView 只对可见项触发这个回调滚到哪才更新到哪。3.3 点击事件里取出业务 ID别在 AItem 上长期存对象引用列表做好之后马上要面对点击交互。OnItemClick 事件给出的 AItem 参数就是用户点中的那一行但如果你的业务数据是一个对象千万不要把对象引用挂到 AItem 上再存着因为 ListView 的 Item 会被回收复用你持有的引用可能在某次滚动后被框架改掉了。推荐的做法是把业务 ID 存进 Data 字典里点击事件里取出来用procedure TForm1.ListView1ItemClick(const Sender: TObject; const AItem: TListViewItem); var LTaskId: Integer; begin LTaskId : AItem.Data[id].AsInteger; ShowTaskDetail(LTaskId); end;存 id 而不是存 Task 对象这是列表开发里很常见的设计习惯。Item 负责展示事数据存储归还给业务层。点击事件拿到 id 之后去全局数据源里查完整对象就好不会有引用失效带来的各种怪问题。4. 避坑记录四个让 ListView 翻车的细微问题每个都花了我一整天4.1 图片显示不出来Android 相册返回的 content URI 不是文件路径现象列表里的头像从系统相册选择后保存完刷新列表图片区域要么空白要么直接黑块Windows 上一切正常。原因TListItemImage.Bitmap.LoadFromFile 接收的是文件路径但 Android 相册返回的是一个 content:// 形式的 URI这个 URI 不是真实文件路径FMX 拿它去 LoadFromFile 必然失败。这个机制在 iOS 上表现还不一样iOS 返回的也是经过沙箱处理的路径两端行为不一致。解决在赋值给 TListItemImage 之前先把图片复制到应用沙箱目录再用真实文件路径去加载。示意做法如下function CopyToSandboxIfNeeded(const AValue: string): string; var LBytes: TBytes; begin Result : AValue; if AValue.StartsWith(content://) then begin LBytes : LoadBytesFromContent(AValue); // 平台相关实现走内容解析器 Result : TPath.Combine(TPath.GetDocumentsPath, TPath.GetFileNameWithoutExtension(AValue) .jpg); TFile.WriteAllBytes(Result, LBytes); end; end;LoadBytesFromContent 这个函数需要按 Android 平台实现FMX 没有统一封装所有 content URI 的读取。从这之后我就形成习惯了凡是列表里要展示图片加载前先过一道沙箱复制逻辑保证传入 TListItemImage 的路径一定是本地文件。4.2 滚动两屏之后显示错乱别长时间持有 TListViewItem 引用现象列表滚动到后面几屏前一屏的标题串到了后一屏的 Item 上甚至点击某一行的按钮触发的事件却跑到了另一个业务数据上。原因TListView 在滚动时会回收离开可视区的 Item把滚动进入可视区的行复用给新数据。如果你在外部变量里保存了一个 TListViewItem 指针滚动之后这个指针指向的对象已经被框架改写了它上面的 Text、Data 全部不是你当初赋值的内容。这不是随机 bug是 Item 复用的回收机制决定的。解决把需要长期用的数据从 Data 字典里拿出来存到业务层的变量或者字段里而不是保存 Item 指针。每次需要访问某一行的数据时用 ListView1.Items[Index] 重新取这属于短引用就没有问题。简单的规则是item 引用拿到后用用完即弃不在事件处理函数之外保存。4.3 数据一多全量刷新卡顿别把列表清空了再重建现象数据源发生变化时调用 Items.Clear 再全量循环 Add数据一旦超过两千条界面肉眼可见地卡一下Android 上更明显。原因Clear 会释放所有 Item 视觉对象全量 Add 又会重新创建一批这个重建过程还伴随 OnUpdateObjects 的集体触发频繁做完全是重复劳动。列表本来就有复用机制已有的 Item 完全可以原地更新数据。解决能用更新就别重建。如果只是部分行数据变化直接定位到对应 Items 索引找到模板元素改属性然后不用碰 Clear 和 BeginUpdateprocedure TForm1.UpdateOneRow(AIndex: Integer; const ATitle: string); var LText: TListItemText; begin LText : ListView1.Items[AIndex].Objects. FindObjectByName(CTitleName) as TListItemText; if LText nil then LText.Text : ATitle; end;只更新一个文本的速度是微秒级完全不会被感知到。如果确实是数据规模变化比较大用 BeginUpdate 包裹但仍然要避免 Clear 后全量 Add改成先调整 Items.Count 再逐条更新 Data。4.4 动态外观下 OnUpdateObjects 频繁执行查找模板多就别每次都 FindObjectByName现象单个 Item 模板里有三四个文本、两个图片列表滚动时 OnUpdateObjects 每次都做五六次 FindObjectByName数据量大了之后平滑动效掉帧。原因FindObjectByName 是线性查找子对象列表单看一次代价不高但滚动时屏幕上有十几条 Item 被触发更新每条找五六次合计就是上百次查找。这是能堆出真实开销的。解决一是尽量减少模板元素能用一层布局表达就不要再包一层。二是把查找结果缓存到局部变量同一个 Item 的多次属性赋值共用同一个对象引用。上面的代码已经体现了这个思路——同一个 LText 变量被复用了两次先查 title 再查 detail避免重复查找。三是在高频刷新场景下优先走 4.3 的局部更新方式不要依赖整页 OnUpdateObjects 重跑。5. 性能优化的四个关卡让几千条数据的列表滚动不卡在设计期就能决定5.1 数据模型先定稿加载列表时别在循环里做耗时解析列表卡顿的根子往往不在 ListView 本身而在进入 OnUpdateObjects 之前的数据准备阶段。常见的错误是循环里每条都去解析 JSON、查数据库或者格式化大字符串这些性能瓶颈会直接透传到列表刷新的节奏里。我一般会先把数据源处理成只读的内存列表比如 TList 里放业务对象对象里存的都是预格式化好的字符串和整数索引。循环填充 Items.Data 时只做赋值不做解析这样 ListView 的数据准备速度才追得上滚动速度。在这个源码包里数据层已经做成了与视图分离的结构填充函数只负责把内存列表翻译成 Data 字典这正好对应 5.2 要讲的事。5.2 视觉对象数量控制一层能表达的布局不要堆第二层DynamicAppearance 给你自由度但它不等于让你随便堆。模板里每多一个视觉元素每个 Item 就多一次创建和更新的开销。做图文列表最常见的过度设计是头像、标题、副标题、右侧图标、底部日期全部用独立的 TListItemText 和 TListItemImage 平铺在模板里。实际上做得好的列表模板往往只有三四个元素一个图标、一个标题、一个副标题、一个右侧状态。如果需要表达更多信息优先用缩进和对齐解决而不是继续加元素。减少视觉对象这里我用一个很直接的判断标准模板里每个元素必须能回答这行数据里哪个字段在用它答不上来的元素就是不必要元素删掉。5.3 图片加载是最大的性能变量能用 TImageList 就别反复 LoadFromFile列表里带图片时图片加载慢的优先级永远高于其他优化项。TImageList 管理的图标资源在内存里只有一份通过 ImageList 和 ImageIndex 引用滚动时不需要重新从磁盘读取这是首选方案。如果列表图片来自网络或者用户本地选择无法放进 TImageList那就必须在后台线程完成图片文件的读取和写入缓存主线程只负责把准备好后的文件路径赋给 TListItemImage。图片解码这个操作不能放在 OnUpdateObjects 里它会让列表滚动瞬间变成幻灯片。我习惯在数据加载阶段就预生成好图片的本地缓存文件OnUpdateObjects 里只传路径字符串。5.4 局部更新代替整表刷新EndUpdate 不是万能后悔药BeginUpdate 和 EndUpdate 适合批量填充但是它在 EndUpdate 时会对整个列表做一次完整的视觉同步如果只是改一两行数据这个同步成本就白付了。高频变化场景下比如任务进度实时刷新应该直接找到对应行做局部属性更新而不是重走全量流程。我把局部更新的代码封装成了一个通用方法传入行号和新的 Data 字典内部通过 Objects.FindObjectByName 精确修改目标元素。这样既保留了数据驱动的写法又避免了重复触发整页刷新。核心思路是区分清楚什么时候该全量同步、什么时候该局部修改性能和代码整洁度能同时保住。6. 收尾技巧给列表加上下拉刷新与滚动到底自动加载更多二十行代码搞定列表做到能展示、能点击之后紧接着就是两个最常见的交互需求下拉刷新数据和滚动到底部加载下一页。FMX TListView 本身内置了 PullRefresh 支持配合滚动位置监听可以做完整的加载链路整个实现不超过二十行关键代码。下拉刷新的入口是 OnPullRefresh 事件开启方式是在设计器里把 ListView 的 PullRefresh 属性设为 True。事件触发时调用数据层刷新完成后必须调用 PullRefreshFinished 收掉系统刷新指示器不然顶部转圈会一直挂着procedure TForm1.ListView1PullRefresh(Sender: TObject); begin FLoadingMore : False; LoadFromServer(True); // True 表示强制刷新 ListView1.PullRefreshFinished; end;触底加载通过 OnScrollViewChange 事件判断滚动位置。原理是拿内容总高度减去当前滚动位置和可视区高度差值小于某个阈值就触发加载procedure TForm1.ListView1ScrollViewChange(const Sender: TObject; const ANewScrollPos, AOldScrollPos: TPointF; const AScrolling: Boolean); begin if FLoadingMore then Exit; // 防止一次滚动事件触发多次加载 if AScrolling and (ANewScrollPos.Y AOldScrollPos.Y) then begin if ListView1.ScrollView.Content.Size.Height - (ANewScrollPos.Y ListView1.ScrollView.ViewportSize) 80 then begin FLoadingMore : True; LoadFromServer(False); // False 表示增量追加 end; end; end;FLoadingMore 是防重入的关键开关加载完成后在 LoadFromServer 的结束回调里复位。阈值 80 的意思是距离底部还有 80 个像素时就开始预加载这样用户滑到最底下时新数据差不多已经进来了体验上很顺。用这套组合拳列表就有了完整的刷新和翻页能力滚动性能也受到前面的优化策略保护。现在每次做列表我都会强制走一遍这个流程先定数据模型再评估用哪种填充方式模板元素控制到最少图片提前落沙箱最后才接手交互事件。这套流程写进我的习惯里是从第一次在真机上翻车之后开始的——那一次列表页面完全没法看标题错乱、图片全黑、滚动卡顿教训全写在上面这五章里了希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑