☰
Delphi 12 FMX TListView跨平台列表控件实战指南
2026/10/16 3:56:28 网站建设 项目流程

简介:Delphi 12 FMXUI的ListView实战源码包,面向Windows、macOS、iOS、Android及Linux等多平台开发者,系统展示FMX框架中ListView组件的核心用法。资源共224个文件、压缩包仅4.24MB,以100个Pascal源代码文件、54个FMX界面文件为主体,搭配PNG图像素材、工程文件与说明文档,覆盖数据绑定、自定义列表项、分组折叠、动画效果、排序过滤等关键场景。包内包含ModernList、Layout、BaseFrame等多个可直接编译运行的示例工程,方便对照学习ListView的适配器扩展与交互事件处理。已有1131人浏览学习,适合希望提升FireMonkey界面开发效率、需要参考真实组件用法的中高级Delphi开发者。

1. 先聊 Delphi 12 的 FMXUI:为什么说 ListView 比想象中更能打

做过跨平台桌面的开发者大概都有这种经历:用 VCL 写习惯了,一换到 Delphi 12 的 FMXUI,第一反应是控件怎么都“不听话”了。尤其列表这块,VCL 里 TListView 那套 Columns 思路在 FMX 里完全失效,很多人直接转头去用 TListBox 凑合。但实际上,FMX 的 TListView 在 Delphi 12 里已经是一个相当完整的原生控件,动态模板、分组、多选、滚动分页这些都能在几十行代码内落地。这篇文章适合那些想用 FMX 做 Windows、macOS 或移动端界面,却还没摸透 ListView 用法的开发者。我会从选型原理讲起,把配置、绑定、搜索、多选、踩坑一次说清,最后给一个可抄的滚动分页技巧。

2. FMX 与 VCL 的界面栈差异:ListView 选型就该这么定

2.1 渲染管线与控件层级:FMX 的 GPU 绘制与 ListView 的定位

FMX 和 VCL 最大的差别在渲染底层。VCL 走的是 Windows 原生 GDI/GDI+,每个控件都是一个窗口句柄,布局受系统主题影响很大;FMX 则是一套自绘的 GPU 渲染管线,控件树由场景图管理,同一份代码在 Windows、macOS、Android、iOS 上保持一致的绘制结果。TListView 在 FMX 里不是简单封装系统原生列表,而是基于平台接口做了一层抽象:Android 上会走原生 RecyclerView 的实践路径,Windows 上则由 FMX 自己绘制。这意味着它的行为不能照搬 VCL 经验,但也换来了跨平台一致的模板体系。

就我的使用体验,Delphi 12 里的 FMX TListView 和 TListBox 虽然都能显示列表,但定位完全不同。TListBox 更接近早期 VCL 的 TListBox 升级版,适合简单的文本行、多选、拖拽排序,模板扩展能力有限;TListView 则是一个带完整项状态管理、动态外观、分组、编辑模式的复杂容器,适合做“卡片流”“信息流”这类需要频繁刷新局部数据的界面。凡是列表项里要放多行文本、图片加状态图标、甚至内嵌按钮的场景,都应该优先考虑 TListView,而不是在 TListBox 里做嵌套布局。

delphi // 演示 FMX 控件的创建方式:先定对齐,再定数据模型 ListView1 := TListView.Create(Self); ListView1.Parent := Self; ListView1.Align := TAlignLayout.Client; ListView1.ItemAppearance := TListItemAppearance.DynamicAppearance;

这段代码里,Align 决定控件在父容器里的贴边方式,FMX 里用 TAlignLayout.Client 表示填满整个客户区;ItemAppearance 则是 ListView 的灵魂属性,DynamicAppearance 表示每一项的外观不再由控件内置样式决定,而是由开发者用代码动态拼装,后面章节会专门展开。创建 TListView 之后,建议先把它放到界面上再配模板,否则部分设计期属性不会立即生效。

2.2 三种 ListView 方案:原生 FMX TListView、TListBox、第三方控件怎么选

这里给一个选型表。很多初学者上来就问“哪个控件最好”,实际是需求没拆开。表格里的核心差异在模板灵活度和数据刷新方式上,这两点决定了后续代码量和维护成本。

方案模板能力大数据量性能适用场景不适用场景
FMX TListView强,DynamicAppearance 可组合文本/图片/进度条好,项复用和增量刷新成熟信息流、分组列表、跨平台卡片表格型数据、多列报表
FMX TListBox弱,只能用标准 ListBoxItem 样式一般,大量项时滚动卡顿简单菜单、设置页复杂模板、高频刷新
第三方控件取决于库,部分支持流式布局参差不齐,需要压测特殊平台特性维护成本和升级风险高

我一般会建议:如果在 Windows 上只是做一个本地工具的内部列表,TListBox 完全够用;但只要涉及 Android、iOS 或者多端一致体验,直接上 TListView,省得后面从 TListBox 迁移到 TListView 时把事件绑定、外观代码全部重写一遍。第三方控件的坑主要在升级兼容上,尤其 FMX 版本更新频繁,外购控件跟不上框架版本会卡住整个项目,除非有明确需求,否则不值得冒这个险。

听众里如果有人正在犹豫“要不要为了列表去做一个原生 Activity 或 ViewController”,我的答案是:FMX 的 TListView 已经覆盖了绝大多数业务列表需求。真正需要跳出去的场景很明确——你需要在极低端 Android 设备上流畅展示上千条带图片的复杂列表,且需要利用原生分页机制,这时才值得考虑混合开发。否则,同一个 TListView 跨平台跑,维护成本会低很多。

3. 在 Delphi 12 里搭一个可用的 ListView:从 TListView 到动态模板

3.1 设计时配置:ItemAppearance=DynamicAppearance 与 ItemHeight 设置

动态外观是 FMX TListView 最核心的能力,它让你像拼积木一样定义每一项长什么样。打开 Delphi 12 窗体设计器,拖一个 TListView 到窗体上,先把 ItemAppearance 从默认的 ListItem 改成 DynamicAppearance,再设置 ItemHeight。ItemHeight 默认是 50,这个值直接影响滚动性能和视觉密度,设太小文本会被裁剪,设太大列表会显得稀疏。我的习惯是标题加副标题结构设 56,纯文本行设 40。

配置模板可以用对象检查器,但代码方式更清晰、可复用。在 FormCreate 里通过 TListViewDynamicAppearance 实例化不同的 ObjectAppearance,比如添加一个 TTextObjectAppearance 作为标题、一个 TImageObjectAppearance 作为左侧图标,再添加一个 TTextObjectAppearance 作为副标题。每个外观对象用 Name 属性做标识,运行时通过 Name 往对应的 Data 槽里塞值,这就是动态模板的绑定机制。

delphi procedure TfrmMain.FormCreate(Sender: TObject); var LTextTitle: TTextObjectAppearance; LTextSub: TTextObjectAppearance; begin ListView1.Align := TAlignLayout.Client; ListView1.ItemAppearance := TListItemAppearance.DynamicAppearance; ListView1.ItemHeight := 56;

// 标题对象:Name 作为运行时取值的 Key LTextTitle := TTextObjectAppearance.Create(ListView1.DynamicAppearance.ItemAppearance); LTextTitle.Name := 'txtTitle'; LTextTitle.DataName := 'Title'; LTextTitle.Align := TListItemAlign.Left; LTextTitle.PlaceOffset.X := 64; LTextTitle.PlaceOffset.Y := 8; LTextTitle.Width := 220; LTextTitle.Height := 24; with LTextTitle.Font do begin Size := 16; Color := TAlphaColorRec.Black; end;

// 副标题对象:PlaceOffset 决定了它和标题的间距 LTextSub := TTextObjectAppearance.Create(ListView1.DynamicAppearance.ItemAppearance); LTextSub.Name := 'txtSubTitle'; LTextSub.DataName := 'SubTitle'; LTextSub.Align := TListItemAlign.Left; LTextSub.PlaceOffset.X := 64; LTextSub.PlaceOffset.Y := 34; LTextSub.Width := 220; LTextSub.Height := 18; with LTextSub.Font do begin Size := 12; Color := TAlphaColorRec.Gray; end; end;

解释三个关键参数。Name 是外观对象在模板内的唯一标识,运行时 ListView1.Items[x].Data['txtTitle'] 里那个 Key 就对应这里的 Name;DataName 则是当数据源来自 TFDMemTable 或 TDataSet 时,控件尝试自动从字段里读取值的字段名,不写 DataName 时光靠 Name 也能手动赋值;PlaceOffset 是外观对象相对 item 左上角的偏移量,X 方向留出 64 像素是因为左侧要放图标或头像。

3.2 运行时绑定:构造 TListViewItem 的子项,填充标题与副标题

模板配好,接下来就是怎么把数据塞进去。FMX TListView 的项不是直接赋值给一个 Record,而是通过 TListViewItem.Data 这个类字典结构来写。很多人第一次用会犯一个错——直接访问 ListView1.Items[0].Text,这在 DynamicAppearance 模式下是没有意义的,因为外观对象的数量、布局完全由动态模板决定,文本都挂在 Data 字典里。

delphi procedure TfrmMain.LoadDemoList; var LItem: TListViewItem; I: Integer; begin ListView1.BeginUpdate; try ListView1.Items.Clear; for I := 0 to 99 do begin LItem := ListView1.Items.Add; // 通过模板里定义的 Name 来填值 LItem.Data['txtTitle'] := Format('第 %d 条记录', [I]); LItem.Data['txtSubTitle'] := '副标题:这里是列表项详情'; end; finally ListView1.EndUpdate; end; end;

BeginUpdate 和 EndUpdate 这对组合非常关键。BeginUpdate 会挂起 ListView 的重绘和布局计算,Items.Clear 加循环 Add 如果是几百条,不开 BeginUpdate 就会看到界面逐条闪烁;EndUpdate 后一次性重算布局,性能差别在小数据量时感觉不明显,一旦到千条级别就是云泥之别。如果代码在循环中途抛异常,EndUpdate 不会被调用,所以必须用 try/finally 包起来,这是我吃过亏的地方。

赋值时注意一个细节:Data['txtTitle'] 的 Key 必须和模板里 TTextObjectAppearance 的 Name 完全一致,大小写敏感。Delphi 的 Data 字典底层是 TDictionary<string, TValue>,填错一个字母不会编译报错,只会在运行时发现数据没显示出来。排查这类问题最快的方式是运行时 F9 中断点看 Data 的 Count 和 Keys。

3.3 数据源接入:把 TFDMemTable 数据灌进 ListView

如果数据来自数据库或接口返回,常见做法是用 FireDAC 的 TFDMemTable 做内存数据集,再手动把它同步到 ListView。为什么不直接用 LiveBindings?LiveBindings 绑定向导适合简单字段映射,性能也很好,但一旦需要动态模板里做条件判断(比如根据状态字段显示不同颜色、图标),绑定表达式会变得复杂且难调试。我更倾向于写一个同步函数,把数据集字段循环赋给 Data 字典,逻辑直白、便于后期加格式化逻辑。

delphi procedure TfrmMain.SyncFromMemTable(AMemTable: TFDMemTable); var LItem: TListViewItem; begin ListView1.BeginUpdate; try ListView1.Items.Clear; AMemTable.First; while not AMemTable.Eof do begin LItem := ListView1.Items.Add; LItem.Data['txtTitle'] := AMemTable.FieldByName('Title').AsString; LItem.Data['txtSubTitle'] := AMemTable.FieldByName('SubTitle').AsString; // 状态字段只作数据载体,不在模板里直接显示 LItem.Tag := AMemTable.FieldByName('Status').AsInteger; AMemTable.Next; end; finally ListView1.EndUpdate; end; end;

这个函数里我用了 LItem.Tag 来存一个不直接展示的状态值。TListViewItem 继承自 TComponent,带一个公开的 Tag 属性,不怕类型转换比 TValue 快得多。后续在点击事件里通过 Tag 判断这条记录到底是正常、异常还是待审核,就不用再反查数据集或字典了。FieldByName 在循环里每行执行两次,字段少无所谓,字段多时可以先把 TField 对象取到循环外做引用,能省一点时间,但在几千条数据级别感知不强。

4. 让 ListView 真正“好用”:搜索、分组、多选与滑动删除

4.1 搜索过滤:先存快照再重建列表,别用 Visible 硬顶

FMX TListView 自带一个 SearchVisible 属性,开启后顶部会出现搜索框,能在简单场景下按首字段过滤。但它的匹配逻辑是黑匣子,不支持模糊规则、不区分大小写、不能自定义高亮,稍微复杂点的需求就失效了。我的做法是在内存里维护一份 TArray 快照,过滤时把 ListView 清空后按条件重新 Add。这种方式牺牲了一点性能,换来了过滤逻辑完全可控。

delphi type TItemRec = record Title: string; SubTitle: string; end;

var FSnapshot: TArray ;

procedure TfrmMain.BuildSnapshot; var I: Integer; LItem: TListViewItem; begin SetLength(FSnapshot, ListView1.ItemCount); for I := 0 to ListView1.ItemCount - 1 do begin LItem := ListView1.Items[I]; FSnapshot[I].Title := LItem.Data['txtTitle'].AsString; FSnapshot[I].SubTitle := LItem.Data['txtSubTitle'].AsString; end; end;

procedure TfrmMain.ApplyFilter(const AFilter: string); var I: Integer; LItem: TListViewItem; begin ListView1.BeginUpdate; try ListView1.Items.Clear; for I := 0 to High(FSnapshot) do begin if (AFilter = '') or FSnapshot[I].Title.ToLower.Contains(AFilter.ToLower) then begin LItem := ListView1.Items.Add; LItem.Data['txtTitle'] := FSnapshot[I].Title; LItem.Data['txtSubTitle'] := FSnapshot[I].SubTitle; end; end; finally ListView1.EndUpdate; end; end;

BuildSnapshot 通常在数据首次加载完成后调用一次,ApplyFilter 则绑定在搜索框的 OnChange 事件里。注意 ApplyFilter 每次触发都清空重填,如果 ListView 有选中状态或编辑状态会被重置,因此在界面上搜索框和列表最好分属两个区域,搜索时允许列表短暂失去选中高亮。快照里只存了显示需要的字段,如果每行还有隐藏的业务 ID,也要一并存进 record,否则点选事件里拿不到对应主键。

4.2 多选与长按:EditMode 与 Checked 的配合套路

FMX TListView 的编辑模式算是它比 TListBox 好用的重要理由。把 ListView1.EditMode 设为 True,再打开 OptionsEdit 里的 MultiCheck,列表左侧会出现一排复选框,用户点按即可选中。关键是删除逻辑写在循环里要倒序遍历,否则一边删一边索引前移会漏行。

delphi procedure TfrmMain.btnEnterEditModeClick(Sender: TObject); begin ListView1.EditMode := True; ListView1.OptionsEdit.MultiCheck := True; btnDeleteSelected.Visible := True; btnEnterEditModeClick.Visible := False; end;

procedure TfrmMain.btnDeleteSelectedClick(Sender: TObject); var I: Integer; begin ListView1.BeginUpdate; try // 必须倒着删,正着删会跳过下一条 for I := ListView1.Items.Count - 1 downto 0 do begin if ListView1.Items[I].Checked then ListView1.Items.Delete(I); end; finally ListView1.EndUpdate; end; end;

倒序删除这个细节在 Delphi 里很多容器类都适用。另一个和长按相关的常见需求是“长按 item 弹出操作菜单”,FMX TListView 默认没有 OnLongTap 事件,但可以用 TListViewItem 的 OnClick 配合系统计时器模拟:点击按下时启动一个 500ms 的 TTimer,如果在计时器触发前发生 MouseUp 就取消,否则执行长按逻辑。这个方案足够应付大多数场景,缺点是触屏和鼠标混合使用时需要额外判断设备类型,Android 上推荐直接监听 OnGesture 里的 LongTap。

4.3 分组显示:GroupView 与自定义分组头

分组列表在通讯录、订单中心这类界面是刚需。FMX TListView 的 GroupView 属性开关位于 ListView1.GroupView,设为 Enabled 后,列表项会按 Groups 集合分组。分组的实现逻辑是每个 TListViewGroup 带一个 Header 字符串,显示在分组的顶部。把数据按首字母或日期分组的时候,注意先 Groups.Clear,再遍历快照逐条判断分组边界。

delphi procedure TfrmMain.ApplyGroupByFirstLetter; var LGroup: TListViewGroup; LItem: TListViewItem; I: Integer; LKey, LLastKey: string; begin ListView1.GroupView := TListViewGroupView.Enabled; ListView1.BeginUpdate; try ListView1.Items.Clear; ListView1.Groups.Clear; LLastKey := ''; for I := 0 to High(FSnapshot) do begin LKey := Copy(FSnapshot[I].Title, 1, 1); if LKey <> LLastKey then begin LGroup := ListView1.Groups.Add; LGroup.Header := LKey + ' 分组'; LLastKey := LKey; end; LItem := ListView1.Items.Add; LItem.Data['txtTitle'] := FSnapshot[I].Title; LItem.Data['txtSubTitle'] := FSnapshot[I].SubTitle; end; finally ListView1.EndUpdate; end; end;

这段代码只按首字母做粗分组,实际项目中如果要支持拼音分组,需要先对快照排序,再按拼音首字母提取 LKey。FMX 分组有一个已知特性:分组头默认高度是写死的,想改样式要在 DynamicAppearance 里添加对 GroupAppearance 的自定义对象,否则只能改 Header 文字颜色,不能换背景或加图标。另外,如果每一组数据量很大,可以考虑在组之间插入一个假的 TListViewItem 作为间隔,但这样会破坏 GroupView 的折叠能力,二选一即可。

5. 避坑与排查:ListView 卡顿、白屏与内存泄漏的 4 个真实案例

5.1 现象:滚动时图片错位,显示的是上一行的头像

这是 FMX ListView 里最经典的问题,我一开始遇到时还以为是“玄学”。现象是快速滚动列表时,某些 item 的图片和文字对不上,滚动变慢之后又恢复正常。原因在于 DynamicAppearance 下的项是复用机制,移出可视区域的 item 会被回收到滚动窗口后方,如果图片加载是异步完成的,旧的回调结果就可能落到新位置的 item 上,导致错位。

解决思路是让每次图片赋值前都校验一次目标 item 是否仍然持有期望的数据标识。常见做法是在 OnUpdateObjects 事件里做检查,或者给 TListViewItem 的 Tag 存一个行序号,异步回调回来时对比当前 item 的 Tag 与请求时的行号,不一致就不赋值。

delphi procedure TfrmMain.ListView1UpdateObjects(const Sender: TObject; const AItem: TListViewItem); var LImage: TListItemImageObject; LRow: Integer; begin // AItem.Tag 里存的是名字首字母或主键,这里做位运算校验 LRow := AItem.Tag; // 假设异步加载图片的类维护了一个 Row -> RequestId 的字典 if (FImageLoader.RequestRow = LRow) then begin LImage := AItem.Objects.FindObject('imgAvatar') as TListItemImageObject; if LImage <> nil then LImage.Bitmap := FImageLoader.GetBitmap; end; end;

简而言之,不要异步加载完成后再去遍历 ListView1.Items 找对象赋值,一定要通过 AItem 参数做局部更新,同时用 Tag 或 Data 里的业务主键做一致性校验。

5.2 现象:切换模板后列表整屏白屏

我在做某跨平台系统的设置页时,想根据用户权限动态给 ListView 换一套模板,结果运行起来列表直接白屏,连滚动条都没了。原因是模板切换不是一个简单赋值操作,TListView 的外观对象集合在运行时已经被 Items 引用,直接把 ItemAppearance 从 DynamicAppearance 改成另一种模式,外观集合没有正确重建,渲染就中断了。

解决方法是把模板变更放到清空列表之前,并且强制调用 Rebuild 相关方法。具体做法是先 ListView1.Items.Clear,再修改 ItemAppearance 和 DynamicAppearance,最后重新加载数据。顺序不能反,否则数据加载到旧的渲染链路里会访问到已经不存在的 ObjectAppearance。

delphi // 正确的模板切换顺序 procedure TfrmMain.ChangeAppearanceStyle(ANewStyle: TListItemAppearance); begin ListView1.Items.Clear; // 先清数据 ListView1.ItemAppearance := ANewStyle; // 再换模板 if ANewStyle = TListItemAppearance.DynamicAppearance then begin ListView1.ItemHeight := 56; // 重新添加外观对象、设置 DataName SetupDynamicTemplate; end; LoadDemoList; // 最后加载数据 end;

这个顺序也适用于修改 ItemHeight 的场景。不要在 BeginUpdate 和 EndUpdate 之间改模板,EndUpdate 触发重绘时模板集合不一致就会白屏。如果不确定当前模板状态,可以在切换前先输出 ListView1.DynamicAppearance.ItemAppearance.Count 到日志,帮助定位。

5.3 现象:Android 上中文文字被裁剪成一半

Windows 上显示正常,打包到 Android 模拟器里,ListView 的标题下半截不见了。排查过程发现是 TTextObjectAppearance 的 PlaceOffset.Y 和 ItemHeight 加起来超出了可视区,加上默认的 Trimming 行为会把超出部分直接裁掉。Android 的字体行高和 Windows 不一样,同一字号在 Android 上渲染偏高,16 号字配 24 的 Height 会溢出。

解决方法是给文本对象的高度留余量,或者调整 Trimming 属性。我一般会设置 WordWrap := False,Trimming := TTextTrimming.Character,然后 Height 比字体 Size 多 6 到 8 个像素,这样既不会出现省略号截断,也不会被裁剪。

delphi // 中文安全的文本外观参数:把 Height 放宽到 30 with LTextTitle.Font do begin Size := 16; Color := TAlphaColorRec.Black; end; LTextTitle.Height := 30; // 原来写 24,Android 上不够 LTextTitle.WordWrap := False; LTextTitle.Trimming := TTextTrimming.Character; LTextTitle.VerticalAlignment := TListItemAlign.Center;

这算是我血泪经验里印象最深的一条,跨平台 UI 开发中所有固定像素的设置都要留出至少 20% 余量,尤其是字体相关的高度。iOS 和 Android 的系统字体差异会让同一份界面观感完全不同。

5.4 现象:重复打开关闭页面,内存持续上涨

一个多页签应用,每切一次页签都重建 ListView 并重新加载数据,内存在几十MB范围内持续增长,几十分钟后应用卡死。定位后发现是 TListViewItem.Data 里存了对象引用,我在 Data 字典里塞了一个 TObject 子类实例做数据载体,但退出页面时只释放了 ListView 本身,没有遍历 Item 释放 Data 中的对象。

解决方式分两种情况。如果数据是基本类型,Data['key'] 里存的字符串、整数会自动管理,不用担心;如果存了对象,必须在释放 ListView 之前手动遍历 Items 并释放对象引用。FMX 不会替你调用 Free 方法,这是一个典型的无主引用泄漏。

delphi procedure TfrmMain.ReleaseListViewData; var I: Integer; LObj: TObject; begin for I := 0 to ListView1.Items.Count - 1 do begin // Data 里取出来的值先判断类型,再决定是否需要 Free if ListView1.Items[I].Data['payload'].IsObject then begin LObj := ListView1.Items[I].Data['payload'].AsObject; LObj.Free; end; end; ListView1.Items.Clear; end;

我的习惯是从根本上避免在 Data 里存对象,凡是业务数据都存主键 ID 和显示字段,需要关联对象时通过 ID 到内存表里找。这样做一举两得,既躲开了释放问题,又让快照和过滤逻辑都变得简单。

6. ListView 滚动的最后一道优化:分页加载与 ScrollView 联动

列表数据量一旦过千,一次性灌给 ListView 就会出现首屏渲染慢、内存占用激增、快速滑动时卡顿。FMX TListView 虽然做了原生回收,但绑定的数据模型如果有大量项,这些项占用的对象本身不会消失。更合理的方案是滚动到接近底部时再加载下一批,这也符合移动端交互习惯。

先定义一个分页状态。FMX TListView 提供了 ContentHeight 和 ScrollViewPos 两个属性,ContentHeight 表示整个列表内容的总高度,ScrollViewPos 是当前滚动偏移量。滚动到底部的判断逻辑很简单:当 ScrollViewPos 接近 ContentHeight 减去视口高度、且差值小于阈值时,触发加载下一页。

delphi const LOAD_MORE_THRESHOLD = 120;

procedure TfrmMain.ListView1ScrollViewChange(Sender: TObject); var LRemain: Single; begin // 剩余距离 = 内容总高度 - 已经滚过的距离 - 可视区高度 LRemain := ListView1.ContentHeight - ListView1.ScrollViewPos - ListView1.Height; if LRemain < LOAD_MORE_THRESHOLD then LoadNextPage; end;

procedure TfrmMain.LoadNextPage; var I: Integer; LItem: TListViewItem; begin if FIsLoading then Exit; // 防止重复触发 FIsLoading := True; try ListView1.BeginUpdate; try // FLastIndex 是快照里已加载的位置 for I := FLastIndex to FLastIndex + 19 do begin if I > High(FSnapshot) then Break; LItem := ListView1.Items.Add; LItem.Data['txtTitle'] := FSnapshot[I].Title; LItem.Data['txtSubTitle'] := FSnapshot[I].SubTitle; end; FLastIndex := FLastIndex + 20; finally ListView1.EndUpdate; end; finally FIsLoading := False; end; end;

阈值 120 像素的含义是:当用户还在滚动,但距离底部只剩一个多条目高度时,提前加载下一批。设置太大(比如 500)会导致数据还没看到就全加载完,失去分页的意义;设置太小(比如 20)则手指滑动很快时来不及加载,用户会看到底部闪烁等待。

LoadNextPage 里用 FIsLoading 布尔值做了重入保护,这是分页最容易忽视的坑——ScrollViewChange 事件在惯性滚动时会被高频触发,如果不加保护,一次滚动就能触发十几次数据加载,列表越长越灾难。我习惯把 FIsLoading 放在类字段里,加载完再置 False,加载过程中 ScrollViewChange 事件依然会触发,但 LoadNextPage 会被拦在第一行。

这是 Delphi 12 的 FMX ListView 带给我最大的启发:它比 VCL 的 TListView 更像一个完整的“列表容器”,而不是单纯的数据展示控件。从那以后,我做任何 FMX 界面的列表需求,都强制走一遍“先定模板、再灌数据、后加分页”的顺序,不为追求省事去走捷径。这个节奏不能说让每个列表都完美,但确实帮我少翻了很多车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询