简介这份资料面向Delphi开发者聚焦FastReport报表工具与Excel数据源的结合应用适合已掌握基础窗体开发、希望把现成表格数据整理成规范报表的读者参考。压缩包共10个文件约835KB内含Delphi工程与窗体源码、可执行示例、Excel表格模板、网页说明和文本注释能够对照完整工程结构理解报表布局、字段绑定及运行时预览的实现思路。已有574人学习反映该主题在桌面报表开发中具有一定关注度。读者可从中获得一套可运行的示例程序与配套表格素材借助源码观察数据连接、数据集定义、字段映射和导出打印的组织方式并参考说明文档排查配置与数据显示问题再迁移到自身业务报表中减少从零搭建环境的摸索成本对于需要快速验证报表样式的开发者尤其实用。1. Excel 当数据源、FastReport 当排版器这条链路适合谁业务方发来一个 xlsx说下周要按这个格式出 300 份带抬头的 PDF数据库排期排到下个月。这种局面下最快的解法不是改库而是把 Excel 当数据源、把 FastReport 当排版和打印引擎Delphi 项目里放一个TfrxReport数据侧用 ADO 或内存表接上 xlsx版式在设计器里拖一遍PDF 和 xlsx 两套输出都能出。它适合三类人手里已经有 FastReport、数据还停留在表格阶段的维护型项目需要把用户上传的 Excel 直接变成正式单据的内部系统以及不想为几张报表单独搭一套数据管道的小团队。不适合的是百万行级、要事务和并发的场景那属于数据库的活。下面按“选通路、翻字段、复刻版式、排错、验收”五步拆。2. 三条接法的选型ADO 直连、内存表、版式参照各管什么先明确一件事标题里的“用 Excel 做报表”其实混着两个方向。一个是读——Excel 是数据源报表负责排版一个是写——报表跑完导出成 xlsx 给下游继续加工。这两个方向的组件、参数、坑完全不同混在一起谈容易在排错时找不到北。这一章先把读的三条接法摆开导出放在第 4 章末尾单独说。2.1 先看一张对比表三种接法的边界接法核心组件适合的场景主要代价ADO 直连TADOConnectionTADOQuery再挂TfrxDBDataSet表头规范、行数几千到几万、列结构稳定依赖 ACE 数据访问组件类型推断不可控脏列会一路传染到报表内存表接管自己解析后塞进TFDMemTable或TfrxUserDataSet列名带空格括号、要合并多张表、要按业务规则清洗代码量大游标、排序、空值都得自己维护版式参照数据仍走数据库Excel 只提供列宽、行高、分组规则数据已经在库里只是要“打成那张 Excel 的样子”版式要人工换算Excel 改一版就得跟一版判断标准很朴素如果 Excel 是业务方每天上传的、表头由人手工维护那基本只能走内存表因为 ADO 对脏表头的容忍度低到你会怀疑人生如果 Excel 是系统导出的、格式固定ADO 直连能省掉八成代码如果 Excel 只是“客户想要这个样式”那它跟数据源没关系别把版式问题当成数据问题。FastReport 自带的 frxADO 组件包里也有TfrxADODataBase、TfrxADOQuery可以少建一个 DataModule但连接串的表达能力比原生 ADO 弱涉及IMEX、区域限定这类参数时我一般还是用原生组件再挂TfrxDBDataSet。2.2 ADO 直连 xlsx连接串、工作表名和游标位置最小可用的一套是这样连接对象挂在窗体上查询对象在设计期放好运行时只改连接串和 SQL。// 打开指定工作簿的指定工作表结果集直接给 TfrxDBDataSet 用 procedure TfrmMain.OpenSheet(const AFile, ASheet: string); begin FreeAndNil(FConn); // 换文件时先断开避免句柄泄漏 FConn : TADOConnection.Create(Self); FConn.LoginPrompt : False; // 无人值守禁止弹登录框 FConn.KeepConnection : False; FConn.ConnectionString : ProviderMicrosoft.ACE.OLEDB.12.0; Data Source AFile ; Extended PropertiesExcel 12.0 Xml;HDRYES;IMEX1; FConn.Connected : True; qryOrders.Connection : FConn; qryOrders.CursorLocation : clUseClient; // 客户端游标报表遍历时不占服务端 qryOrders.SQL.Text : Format(SELECT * FROM [%s$], [ASheet]); qryOrders.Open; end;逻辑上就三步建连接、设游标、发查询。真正决定成败的是连接串里那段Extended Properties它的三个开关各自管一件事。Excel 12.0 Xml对应 xlsx老式 xls 要换成Excel 8.0二进制工作簿又是另一个取值选错了会直接报“外部表不是预期的格式”。HDRYES表示首行当列名设成 NO 的话所有列会变成F1、F2报表里就没法按名字引用。IMEX1是把混合类型的列按文本读它能救回“订单号 0001 变成 1”这类事故代价是整列失去数值语义后面做 SUM 会得到 0这一点在第 5 章还会展开。工作表名的写法是最容易翻车的地方FROM [Sheet1$]对FROM Sheet1$错FROM [Sheet1]也错美元符号必须在方括号里面。中文表名一样要方括号比如[订单明细$]。如果只想读某个区域可以写成[Sheet1$A1:F200]注意这个区域是含表头的配合HDRYES时第一行仍然当列名。取不到表名时别猜用FConn.GetTableNames(AList, False)把工作簿里所有可读的表名列出来看一眼隐藏工作表也会出现在这个列表里。注意TADOConnection用Create(Self)挂到窗体上别写成Create(nil)又不释放报表跑批几百次之后句柄会堆起来表现为跑着跑着报“未指定的错误”。2.3 内存表接管TfrxUserDataSet 的五个事件怎么填当表头脏到 SQL 没法处理时把数据先读进一个泛型列表再让TfrxUserDataSet逐行喂给报表。它不是一个真正的数据集而是靠事件回调模拟游标所以至少要实现五个事件少一个预览和翻页就会错位。type TOrderRow record No: string; Customer: string; Qty: Integer; Price: Currency; BookDate: TDateTime; end; // 报表每引用一个字段就回调一次VarName 是设计器里写的字段名 procedure TfrmMain.dsOrdersGetValue(const VarName: string; var Value: Variant); var R: TOrderRow; begin if (FIndex 0) or (FIndex FRows.Count) then Exit; R : FRows[FIndex]; if SameText(VarName, OrderNo) then Value : R.No else if SameText(VarName, Customer) then Value : R.Customer else if SameText(VarName, Qty) then Value : R.Qty else if SameText(VarName, Amount) then Value : R.Qty * R.Price else if SameText(VarName, BookDate) then Value : R.BookDate; end; procedure TfrmMain.dsOrdersCheckEOF(var Eof: Boolean); begin Eof : FIndex FRows.Count; // 引擎在 Next 之后调用这里只判断不移动 end; procedure TfrmMain.dsOrdersFirst(Sender: TObject); begin FIndex : 0; end; procedure TfrmMain.dsOrdersNext(Sender: TObject); begin Inc(FIndex); end; procedure TfrmMain.dsOrdersPrior(Sender: TObject); begin Dec(FIndex); end;OnGetValue是核心它把“报表字段名”和“内存里的记录”对上号。这里用SameText而不是因为设计器里手写的字段名大小写经常和代码不一致用大小写不敏感的比较能省掉一类“明明有值却显示空白”的问题。返回的Value是Variant金额建议直接给Currency或Double不要给格式化过的字符串——一旦给了字符串报表里的 SUM 就退化成字符串拼接合计必然是 0。OnCheckEOF只负责回答“到头了没有”不要在里面Inc(FIndex)。OnFirst、OnNext、OnPrior才是移动游标的地方OnPrior看着多余但报表预览的“上一页”和某些交叉表回扫会用到不实现的话翻页会跳行。如果你的数据需要排序别指望TfrxUserDataSet它没有索引排序要在FRows填完之后自己做一次或者在读取阶段就按 Excel 里的顺序固定下来。2.4 版式参照数据走库、Excel 只提供尺子第三种情况经常被误判。业务方给的 Excel 里数据是假的只有表头、列宽、分组位置是真的这时候不要为了“跟 Excel 一致”去读这个文件。正确做法是把 Excel 当尺子用量出每列宽度、行高、页边距在设计器里按第 4 章的换算表把对象摆好数据仍然从数据库来。这样做的好处是数据量不受 Excel 限制坏处是业务方每改一次版式你就得重新量一次。缓解办法是把列宽、行高这些数字抽到配置里或者干脆约定“版式变更走需求单”别让一张截图就触发一次改版。3. 把 Excel 的列翻译成 FastReport 的字段类型、别名与计算列数据能读出来只是第一步真正决定报表能不能跑起来的是字段层。Excel 没有 schema列的“类型”是靠前几行数据猜出来的猜错的后果会一路传到报表表达式里报错信息还很难懂。这一章处理三件事列名怎么清洗、别名在哪做、计算列放哪算。3.1 HDR、IMEX 与列名清洗数据字典的第一道关HDRYES之后首行就是列名但 Excel 对“列名”没有约束于是会出现这些情况表头为空驱动补成F1、F2两列同名第二列被命名成金额1列名里带空格、点号、括号、方括号。前两种在报表里还能引用第三种在 FastReport 表达式里必须写成[frxDBDataset1.金额(元)]这种形式方括号加双引号少一个就解析失败。手写这种表达式很容易漏引号所以我一般不让脏列名进到报表层。先在打开报表之前把 schema 打出来看一眼比在报错里猜快得多。// 打开工作簿列出所有可读表名和首行字段的类型 procedure TfrmMain.DumpSchema(const AFile: string); var Conn: TADOConnection; Rs: TADOQuery; Tabs: TStringList; i: Integer; begin Conn : TADOConnection.Create(nil); Tabs : TStringList.Create; try Conn.LoginPrompt : False; Conn.ConnectionString : ProviderMicrosoft.ACE.OLEDB.12.0;Data Source AFile ; Extended PropertiesExcel 12.0 Xml;HDRYes;IMEX1; Conn.GetTableNames(Tabs, False); // 隐藏工作表也会列出来 Memo1.Lines.Assign(Tabs); Rs : TADOQuery.Create(nil); try Rs.Connection : Conn; Rs.SQL.Text : SELECT TOP 1 * FROM [ Tabs[0] ]; Rs.Open; for i : 0 to Rs.FieldCount - 1 do Memo1.Lines.Add(Format(%s - %s, [Rs.Fields[i].FieldName, Rs.Fields[i].DataType.ToString])); finally Rs.Free; end; finally Tabs.Free; Conn.Free; end; end;GetTableNames拿到的名字已经带$可以直接拼进 SQL。DataType是驱动猜出来的类型ftWideString说明这一列被判成文本即使里面全是数字——这就是IMEX1的典型结果也是合计为 0 的根源。看到整列都是ftWideString而业务上应该是数值就该考虑去掉IMEX或者在 SQL 里显式转换。清洗的规则我固定成三条表头必须一行、不能有合并单元格、不能有空行夹在中间。这三条不满足时ADO 读出来的列会整体错位而且不报错只是数据全跑到隔壁列。与其在代码里做容错不如在导入入口卡一道校验把不合格的文件打回去。3.2 别名视图把“订单号”“金额(元)”挡在报表之外别名有两种做法。一种是在 SQL 里用AS直接改这是首选因为它把脏名字挡在数据层报表里永远只见得到干净的英文名。// 用 ACE SQL 做一次投影改列名、补空值、统一类型 qryOrders.SQL.Text : SELECT [订单号] AS OrderNo, [客户名称] AS CustomerName, IIF(ISNULL([数量]), 0, [数量]) AS Qty, IIF(ISNULL([单价]), 0, [单价]) AS Price, [下单日期] AS BookDate FROM [订单明细$];IIF和ISNULL在 ACE 引擎里是可用的作用是把空单元格变成 0避免NULL传染到后面的乘法。注意AS之后的名字不能带空格也别用 SQL 关键字Date、Value、Order这类词在某些驱动版本上会被解析成保留字。如果某一列在 Excel 里确实缺失SELECT会直接报错这反而是好事——比报表跑到一半发现字段是空的强。另一种做法是在TfrxDBDataSet上做映射。TfrxUserDataSet靠OnGetValue里的VarName分支天然就是一层别名想叫什么就叫什么TfrxDBDataSet有FieldAliases属性也能做类似的事但不同版本的行为有差异跨版本升级时容易出问题。我的习惯是别名统一在数据层做完报表层只认一套固定的字段名这样换数据源Excel 换数据库时报表模板不用动。3.3 计算列放在哪算SQL、内存、报表表达式的取舍“金额 数量 × 单价”这种列三个地方都能算选错了会在后期付出代价。位置写法适合风险SQL 里算[数量]*[单价] AS Amount行内四则运算、简单汇总驱动类型推断差空值会传染除零直接抛异常内存里算读入后统一用Currency计算需要精度控制、要多表合并、要写单元测试代码量增加得自己保证每行都算到报表表达式frxDBDataset1.Qty * frxDBDataset1.Price临时展示、调试期快速验证每页重复计算空值要显式判断出错时定位困难金额这类涉及钱的计算我一律放在内存里用Currency算完再给报表原因是浮点误差在合计行上会放大Excel 那边显示两位小数看不出来报表打出来差几分钱就要被财务问。报表表达式适合做展示层的加工比如“金额大于 1000 标红”判断条件写在对象的Highlight里而不是把计算结果塞回数据层。如果非要在报表里处理空值表达式得写成IIf(frxDBDataset1.Qty Null, 0, frxDBDataset1.Qty)这种形式注意 FastReport 的表达式用做比较、用做不等和 Pascal 的不是一个写法这一点在切过来的头几天特别容易写错。4. 版式复刻让 FastReport 打出来像那张 Excel数据和字段都通了剩下的就是“像不像”。这一章的四个点按影响从大到小排列宽行高决定整体骨架分组决定分段小计对不对交叉表决定透视结构能不能复刻导出参数决定交付物会不会被打回来。4.1 列宽行高换算从 Excel 字符宽到报表毫米Excel 的列宽单位是“默认字体下的字符数”FastReport 的对象坐标默认按毫米走中间要过一次像素。经验公式是像素 ≈ 列宽 × 7 57 是默认字体下最大数字的宽度字体换了要重新校准。行高更直接Excel 按磅给1 磅 96/72 像素。Excel 里的值含义折合像素报表侧设成列宽 8.43默认标准列宽约 64 px16.9 mm列宽 1212 个字符约 89 px23.5 mm列宽 2020 个字符约 145 px38.4 mm行高 15默认行高20 px5.3 mm页边距 1.9 cmExcel 默认左右边距—页面左右各 19 mm换算完还要过一道宽度校验A4 纸宽 210 mm减去左右边距各 20 mm可用宽度只有 170 mm。Excel 里八列各 25 mm 就是 200 mm直接搬到报表上必然横向溢出打印时会被截断或者挤到第二页。这时候只有三条路改横向纸张可用宽度变成 277 mm、压列宽、或者把不关键的列挪到第二页的续表里。我一般先试横向因为压列宽会让长文本换行行高跟着变反而更难对齐。页面边距还有个细节FastReport 的页面默认四边各 20 mmExcel 默认是 19 mm差的这 1 mm 在页脚位置会表现为半行偏移套打的时候能看出来。要么把报表页边距改成 19 mm要么把页脚对象整体上移 1 mm别两个都改会变成 2 mm。4.2 分组小计与主从表GroupHeader 替代 Excel 的分段Excel 里那种“每个客户一段、段尾一行小计”的结构在 FastReport 里对应GroupHeaderGroupFooter。做法是把数据集按客户排序放一个 GroupHeaderCondition设成客户字段的表达式Footer 里放SUM聚合。// 按客户分组组头打印客户名组尾打印该客户的小计 frxGroupHeader1.Condition : frxDBDataset1.CustomerName; frxMemoCust.Text : [frxDBDataset1.CustomerName]; frxMemoSubTotal.Text : [SUM(frxDBDataset1.Amount, MasterData1, 1)];Condition的表达式语法和普通字段引用一样但要写在里面。SUM的第二个参数指定统计范围所在的 band第三个参数1表示按组内统计0 是整页、2 是整报表这个参数写错会表现为“每组小计都是总数”排查时先看它。分组要求数据已经排好序ADO 那边可以在 SQL 里加ORDER BYTfrxUserDataSet就得自己在内存里排否则同一客户会被拆成好几段。主从表是另一个高频需求一个订单头带若干明细行。最稳的写法不是依赖TfrxDBDataSet.Master而是在主表的 band 打印前手动过滤从表。// 主表每打印一行前把从表定位到当前订单 procedure Tf p a hrefhttps://download.csdn.net/download/u013275409/10746739 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p