
简介本资源是面向Delphi中高级开发者的一套FMX跨平台UI实战源码包聚焦Delphi 12环境下FireMonkey框架中ListView组件的深度应用与定制开发。它系统展示了数据绑定、分组展示、自定义项模板含TImage/TLabel等控件嵌套、交互事件响应及动画效果实现等核心能力适用于Windows/macOS/iOS/Android多端桌面与移动应用界面开发。压缩包共224个文件以100个.pas单元文件和54个.fmx界面文件为主体辅以33张PNG资源图、6个.dproj工程配置及配套说明文档.md/.txt整体4.24MB结构完整、模块清晰含BaseFrame、ModernList、Layout等多个可复用UI模块。目前已有1129人学习下载开发者可直接导入IDE运行调试快速掌握FMX ListView在真实项目中的集成方式、性能优化技巧与扩展适配方法。1. Delphi 12 FMXUI 中的 TListView不是“很好用哦”那么简单而是跨平台 UI 渲染里最易翻车又最值得深挖的控件你刚在 Delphi 12 FireMonkey 项目里拖一个TListView进来双击加几行数据界面秒出、滚动流畅、图标自动适配——确实“很好用哦”。但等你真要上线iOS 上滑动卡顿、Android 上点击无反馈、Windows 上字体发虚、macOS 上分组标题错位……这时候才明白“很好用”是表象“很玄学”才是日常。这不是控件本身的问题而是 FMX 渲染管线、平台原生桥接、样式资源绑定、数据生命周期这四层黑匣子叠在一起的结果。这份 Delphi 12 FMXUI 源码包含完整 ListView 实战示例不是教你怎么拖控件而是帮你把这四层全拆开从TListViewItem的内存布局到TStyleBook的样式注入点从OnItemClick的事件触发时机到TListDataItem的异步加载边界。适合正在用 Delphi 12 做跨平台桌面/移动应用的工程师——尤其当你发现TListBox能跑通但TListView总在某个平台崩或者CustomView模式下OnPaint根本不调用时这份源码就是你的后悔药。2. TListView 架构解析为什么它比 TListBox 更重、更慢、也更可控FireMonkey 的TListView不是 Win32 下那个纯容器控件而是一个声明式命令式混合渲染引擎。它的核心不在Items属性而在Model数据模型、View视图模板、Style样式资源三者之间的实时绑定关系。理解这个三角关系才能避开 80% 的“莫名失效”。2.1 Model 层TListDataItem 与 TListDataGroup 的内存契约TListView的数据源本质是TListDataItem集合每个 item 是一个轻量级结构体不持有实际业务对象引用只存Text、Detail、Bitmap等展示字段的副本。这意味着修改原始业务对象如TProduct.Name不会自动刷新列表项TListView.Items.Add实际是创建TListDataItem并拷贝字段值不是插入指针分组TListDataGroup必须显式设置GroupName且GroupName必须与TListView.Grouping的GroupBy字段完全一致区分大小写。// ✅ 正确显式构造分组并绑定 var LGroup: TListDataGroup; LItem: TListDataItem; begin LGroup : TListDataGroup.Create; LGroup.GroupName : Electronics; // 注意大小写 ListView1.Model.Groups.Add(LGroup); LItem : TListDataItem.Create; LItem.Text : iPhone 15; LItem.Detail : 128GB, Black; LItem.GroupName : Electronics; // 必须与 GroupName 完全一致 ListView1.Model.Items.Add(LItem); end;提示TListDataItem.GroupName是字符串匹配不是对象引用。若拼错或大小写不一致item 就会“消失”——它没被删只是没归入任何已定义的 group而TListView.Grouping.Visible True时未分组项默认不显示。2.2 View 层CustomView 模式下的模板控制权移交TListView.ViewMode设为vmCustom后FMX 不再用内置模板而是交由你通过TListView.ItemTemplate或TListView.ItemAppearance控制渲染逻辑。关键点在于OnPaint不是每帧调用而是仅在 item 进入可视区且样式变更时触发。常见误用是把耗时操作如网络请求、图片解码塞进OnPaint——结果滑动时卡死。正确做法是预加载 缓存// ✅ 在 Form.OnCreate 中预加载 Bitmap非 OnPaint procedure TForm1.FormCreate(Sender: TObject); var LBitmap: TBitmap; begin // 预加载常用图标存入全局 TDictionarystring, TBitmap LBitmap : TBitmap.Create; LBitmap.LoadFromFile(assets/icon_phone.png); FIconCache.AddOrSetValue(phone, LBitmap); end; // ✅ OnPaint 中只做绘制不创建/加载 procedure TForm1.ListView1PaintItem(const ACanvas: TCanvas; const AItem: TListViewItem; const ARect: TRectF; const AState: TItemStates); var LBitmap: TBitmap; begin if FIconCache.TryGetValue(phone, LBitmap) then ACanvas.DrawBitmap(LBitmap, ARect, 0.2, 0.2, 0.8, 0.8, 1); ACanvas.FillText(ARect, AItem.Text, False, 1.0, [], TTextAlign.Center, TTextAlign.Center); end;2.3 Style 层TStyleBook 与平台原生样式的博弈TListView的视觉表现高度依赖TStyleBook。Delphi 12 自带iOS,Android,Windows,macOS四套默认样式但它们不是 CSS而是二进制.style文件无法直接编辑文本。修改样式必须用 Style DesignerTools Options Environment Options Style Designer导出.style→ 用记事本改 XML → 再导入。更关键的是TStyleBook绑定后TListView.StyleLookup必须指向该样式中定义的TListViewItem样式名如listviewitem否则回退到平台默认样式导致OnPaint失效。// ✅ 正确绑定样式假设 StyleBook1 已加载 custom.style ListView1.StyleBook : StyleBook1; ListView1.StyleLookup : customlistviewitem; // 必须与 .style 文件中 style namecustomlistviewitem 一致3. 数据绑定实战从静态列表到动态分页加载的完整链路TListView的Model支持双向绑定但 Delphi 12 的TBindingsList对 FMX ListView 支持有限。真正稳定的做法是手动管理TListDataItem生命周期并配合TThread实现后台加载。3.1 静态数据加载避免 OnUpdateItem 的陷阱很多教程教你在OnUpdateItem里赋值这是严重过时且危险的做法。OnUpdateItem在 Delphi 10.4 后已被标记为[Deprecated]且在vmCustom模式下根本不会触发。正确路径是先清空 Model.Items再批量 Add 新TListDataItem。// ✅ 安全清空并重载避免内存泄漏 procedure TForm1.LoadStaticData; var I: Integer; LItem: TListDataItem; begin // 1. 清空旧项注意不能用 Items.Clear()那是 UI 层不清理 Model ListView1.Model.Items.Clear; // 2. 批量添加新项 for I : 0 to High(FProducts) do begin LItem : TListDataItem.Create; LItem.Text : FProducts[I].Name; LItem.Detail : Format(%s - ¥%d, [FProducts[I].Category, FProducts[I].Price]); LItem.Bitmap : FIconCache[FProducts[I].IconKey]; ListView1.Model.Items.Add(LItem); end; end;3.2 动态分页加载滚动到底部触发防重复请求TListView没有OnScrollEnd事件需监听OnItemClick和OnKeyDown错。正确方式是订阅TListView的OnResize和OnPaint间接推算可视区域但更可靠的是用TTimer轮询ViewportPosition// ✅ 滚动到底部加载下一页防抖 防重 procedure TForm1.Timer1Timer(Sender: TObject); var LVisibleHeight, LTotalHeight, LScrollPos: Single; LIsAtBottom: Boolean; begin LVisibleHeight : ListView1.Height; LTotalHeight : ListView1.Model.GetHeight; // 注意此方法返回总高度含未渲染项 LScrollPos : ListView1.ViewportPosition.Y; LIsAtBottom : (LTotalHeight - LScrollPos - LVisibleHeight) 20.0; // 20px 容差 if LIsAtBottom and not FIsLoading then begin FIsLoading : True; TTask.Run( procedure begin // 后台加载数据模拟 API TThread.Sleep(800); // 模拟网络延迟 TThread.Synchronize(nil, procedure begin LoadNextPage; // 添加新项到 Model.Items FIsLoading : False; end); end); end; end;参数说明GetHeight返回整个列表逻辑高度像素ViewportPosition.Y是当前滚动偏移量负值表示上拉。容差值20.0需根据ItemHeight动态计算此处简化为固定值。3.3 分组与搜索联动实时过滤不重绘全部项TListView的Filter属性是字符串匹配但默认会重绘所有项。优化方案是维护两个TListTListDataItemFAllItems全量和FFilteredItems当前筛选结果每次 Filter 变化时只更新Model.Items// ✅ 高效过滤避免全量重绘 procedure TForm1.Edit1Change(Sender: TObject); var LFilterText: string; LFiltered: TListTListDataItem; I: Integer; begin LFilterText : Trim(Edit1.Text); LFiltered : TListTListDataItem.Create; try if LFilterText then LFiltered.AddRange(FAllItems.ToArray) else for I : 0 to FAllItems.Count - 1 do if Pos(UpperCase(LFilterText), UpperCase(FAllItems[I].Text)) 0 then LFiltered.Add(FAllItems[I]); // 关键只替换 Model.Items不触发动画 ListView1.Model.Items.Clear; ListView1.Model.Items.AddRange( LFiltered.ToArray ); finally LFiltered.Free; end; end;4. 跨平台避坑指南iOS/Android/Windows/macOS 四平台真实踩坑记录TListView在不同平台的行为差异不是 Bug而是 FMX 底层渲染策略的必然结果。以下是在 Delphi 12 实测的 5 条血泪经验每条都附带现象、根因和可立即生效的修复代码。4.1 iOS 上滑动卡顿GPU 渲染线程被阻塞现象iOS 设备滑动TListView时掉帧严重CPU 占用飙升但 Android/Windows 正常。原因iOS 的 Metal 渲染要求TCanvas.DrawBitmap必须在主线程执行而OnPaint默认在渲染线程调用。若OnPaint中调用TBitmap.LoadFromFile同步 IO会阻塞 GPU 线程。解决所有 Bitmap 加载必须提前完成OnPaint中只做绘制。禁用TBitmap.LoadFromFile在OnPaint中出现// ❌ 错误iOS 卡死 procedure TForm1.ListView1PaintItem(...); var LBitmap: TBitmap; begin LBitmap : TBitmap.Create; LBitmap.LoadFromFile(icon.png); // 同步 IO阻塞 Metal 线程 ACanvas.DrawBitmap(LBitmap, ...); LBitmap.Free; end; // ✅ 正确预加载 弱引用 // 在 Form.OnCreate 中 FIconBitmap : TBitmap.Create; FIconBitmap.LoadFromFile(icon.png); // 在 OnPaint 中 ACanvas.DrawBitmap(FIconBitmap, ...);4.2 Android 点击无反馈触摸事件被父容器吞掉现象Android 设备点击TListViewItem无OnClick响应但长按弹出菜单正常。原因TListView的HitTest在 Android 上对TListViewItem的HitTest区域计算不准常将点击判定为背景区域。解决强制为每个TListViewItem设置HitTest : True并在OnItemClick中手动校验坐标// ✅ 在 LoadStaticData 中为每个 item 设置 for I : 0 to High(FProducts) do begin LItem : TListDataItem.Create; // ... 设置 Text/Detail/Bitmap LItem.HitTest : True; // 关键 ListView1.Model.Items.Add(LItem); end; // ✅ OnItemClick 中增加坐标校验防误触 procedure TForm1.ListView1ItemClick(const AItem: TListViewItem); var LPoint: TPointF; begin LPoint : ListView1.ScreenToClient(Mouse.CursorPos); if PtInRect(ListView1.ClientRect, LPoint) then ShowMessage(Item clicked: AItem.Text); end;4.3 Windows 字体模糊DPI 缩放未适配现象Windows 125% DPI 缩放下TListView文字边缘发虚图标锯齿。原因TListViewItem.Text默认使用TFont的Size像素值未按 DPI 缩放因子换算。解决在OnPaint中动态计算缩放后的字体大小// ✅ DPI 感知字体绘制 procedure TForm1.ListView1PaintItem(...); var LFontSize: Single; LScale: Single; begin LScale : GetDpiForWindow(ListView1.Handle) / 96.0; // 标准 DPI 为 96 LFontSize : 14.0 * LScale; // 基础字号 14pt ACanvas.Font.Size : LFontSize; ACanvas.FillText(ARect, AItem.Text, False, 1.0, [], TTextAlign.Center, TTextAlign.Center); end;4.4 macOS 分组标题错位样式锚点偏移未校正现象macOS 上TListView分组标题TListDataGroup.Header文字右偏 10px图标不居中。原因macOS 样式中TListViewItem的Header模板默认Margin.Left 10但TListDataGroup的Header未继承该 Margin。解决在 Style Designer 中打开macOS.style找到listviewgroupheader样式将Margin.Left改为0保存后重新绑定TStyleBook。4.5 所有平台通用坑TListViewItem.Bitmap 内存泄漏现象长时间滚动后内存持续上涨最终 OOM。原因TListDataItem.Bitmap是弱引用TListView不负责释放需手动管理TBitmap生命周期。解决统一用TObjectListTBitmap管理Form.Destroy中释放// ✅ 在私有域声明 FBitmapPool: TObjectListTBitmap; // ✅ 在 Form.Create 中初始化 FBitmapPool : TObjectListTBitmap.Create(True); // OwnsObjects : True // ✅ 加载 Bitmap 时加入池 LBitmap : TBitmap.Create; LBitmap.LoadFromFile(icon.png); FBitmapPool.Add(LBitmap); LItem.Bitmap : LBitmap; // ✅ Form.Destroy 中释放 FBitmapPool.Free;5. 高级技巧用 TListView 实现「无限滚动骨架屏离线缓存」三合一方案真正的生产级TListView不止于展示数据而是要承载用户体验的关键路径。我最近在一个医疗设备管理 App 中落地了这套组合技用户下滑时显示骨架屏占位符后台静默加载数据同时将结果存入本地 SQLite 缓存断网时自动降级为缓存数据。整个流程不依赖第三方库纯 Delphi 12 FMX 实现。5.1 骨架屏实现用 TShape 模拟加载态TListView不支持原生骨架屏但可通过TListViewItem的CustomView模式注入TShape组件// ✅ 创建骨架屏 item灰色矩形模拟头像文字 function TForm1.CreateSkeletonItem: TListDataItem; var LItem: TListDataItem; LShape: TShape; begin LItem : TListDataItem.Create; LItem.Text : ; // 清空文本避免干扰 LItem.Detail : ; // 动态创建 TShape 作为骨架 LShape : TShape.Create(nil); LShape.Shape : TShapeType.Rectangle; LShape.Fill.Color : $FFCCCCCC; // 浅灰 LShape.Stroke.Color : TAlphaColorNull; LShape.Width : 40; LShape.Height : 40; LItem.CustomControl : LShape; Result : LItem; end; // ✅ 在 LoadNextPage 中先插入骨架项再替换为真实数据 procedure TForm1.LoadNextPage; var LSkeleton: TListDataItem; I: Integer; begin // 1. 插入 3 个骨架项视觉上立即响应 for I : 1 to 3 do begin LSkeleton : CreateSkeletonItem; ListView1.Model.Items.Add(LSkeleton); end; // 2. 后台加载真实数据 TTask.Run( procedure begin // 模拟 API 调用 TThread.Sleep(1200); TThread.Synchronize(nil, procedure begin // 3. 移除骨架项插入真实项 ListView1.Model.Items.Delete(ListView1.Model.Items.Count - 3, 3); for I : 1 to 3 do begin // ... 创建真实 TListDataItem ListView1.Model.Items.Add(LRealItem); end; end); end); end;5.2 离线缓存策略SQLite 存储 时间戳校验用LiteDACDelphi 自带连接本地 SQLite表结构极简FieldTypeNoteidINTEGER PRIMARY KEY自增json_dataTEXTJSON 序列化的 TListDataItem 数组updated_atINTEGERUnix timestamp// ✅ 缓存读取带过期检查 function TForm1.LoadFromCache: Boolean; var LSQL: string; LQuery: TFDQuery; LJSON: string; LItems: TJSONArray; I: Integer; LItem: TListDataItem; begin LSQL : SELECT json_data FROM cache WHERE updated_at ?; LQuery : TFDQuery.Create(nil); try LQuery.Connection : FDConnection1; LQuery.SQL.Text : LSQL; LQuery.ParamByName(updated_at).AsInteger : Trunc(Now * 86400) - 3600; // 1小时过期 LQuery.Open; if not LQuery.IsEmpty then begin LJSON : LQuery.FieldByName(json_data).AsString; LItems : TJSONObject.ParseJSONValue(LJSON) as TJSONArray; try ListView1.Model.Items.Clear; for I : 0 to LItems.Count - 1 do begin LItem : TListDataItem.Create; // 从 JSON 解析 Text/Detail/BitmapBitmap 用 base64 存 LItem.Text : LItems.Items[I].GetValuestring(text); ListView1.Model.Items.Add(LItem); end; Result : True; finally LItems.Free; end; end else Result : False; finally LQuery.Free; end; end;5.3 无缝降级网络状态监听 自动切换Delphi 12 没有原生网络监听用Winapi.WinINetWindows和iOSapi.CoreFoundationiOS太重。更轻量的方案是HTTP 请求超时后自动 fallback 到缓存// ✅ 带 fallback 的加载主流程 procedure TForm1.LoadDataWithFallback; begin if LoadFromCache then Exit; // 有缓存直接显示 // 尝试网络加载超时 5 秒 FNetworkTimer : TTimer.Create(Self); FNetworkTimer.Interval : 5000; FNetworkTimer.OnTimer : NetworkTimeoutHandler; FNetworkTimer.Enabled : True; // 发起 HTTP 请求用 TIdHTTP TTask.Run( procedure begin try FHTTP.Get(https://api.example.com/items); TThread.Synchronize(nil, LoadFromResponse); except // 异常时啥也不做等 Timer 触发 fallback end; end); end; procedure TForm1.NetworkTimeoutHandler(Sender: TObject); begin FNetworkTimer.Enabled : False; FNetworkTimer.Free; ShowMessage(网络不可用加载本地缓存...); LoadFromCache; // 此时必有缓存首次启动时应预置 demo 数据 end;从那以后我每次重构TListView相关功能都强制走一遍这三步先用TStyleBook导出当前平台.style确认TListViewItem的Margin和Padding所有TBitmap加载必须放在Form.OnCreate或TThread中OnPaint里只调DrawBitmap每次Model.Items.Add后立刻用TListView.Model.GetHeight打印日志验证高度计算是否符合预期。这三步做完TListView就从“很好用哦”的幻觉变成你手里一把可预测、可调试、可压测的工程级控件。希望帮到你。本文还有配套的精品资源点击获取