简介:这份资源面向Delphi开发者与报表设计初学者,聚焦如何将Excel作为数据源接入FastReport,从而快速生成结构化的报表。内容围绕数据连接、数据集定义、报表布局设计、字段映射以及导出打印等环节展开,帮助读者理解在Delphi环境中把Excel整理好的数据转化为可视化报表的完整思路。压缩包共10个文件,约835KB,包含dpr工程文件、pas与dfm单元、可执行exe、htm说明页、txt源码说明、xls模板以及若干url链接,覆盖从源码到模板的实践素材,便于直接运行对照学习。目前已有574人学习下载,适合希望掌握FastReport与Excel协同、提升报表开发效率的Delphi程序员参考。
1. 用Excel做FastReport报表:Delphi老兵的另类数据源方案
接手一个 Delphi 维护项目时,客户丢过来一堆 Excel 文件,说“报表格式我都调好了,你直接读就行”。打开 FastReport 设计器一看,数据源全是 ADOQuery 连数据库的套路,而这次数据根本不在库里。硬编码解析 xlsx 吧,客户隔三差五改列名;让客户导成 CSV 吧,人家嫌麻烦。折腾半天发现,FastReport 其实支持把 Excel 当数据源用,配合 Delphi 的 OLE 自动化或者第三方 Excel 读写库,能把整个报表链路跑通。这套方案适合手头有大量 Excel 模板、又不想动数据库结构的 Delphi 开发者,尤其是做内部管理系统、财务对账、生产日报这类场景的同行。下面把我踩过的路拆开讲,从环境准备到动态列映射,再到导出排错,争取让你少走弯路。
2. 环境准备与 Excel 数据源接入:从 OLE 到 TfrxUserDataSet
2.1 选型:为什么不用 ADO 直连 Excel
很多人第一反应是用 ADO 的 Microsoft.ACE.OLEDB 驱动直接连 xlsx,写个SELECT * FROM [Sheet1$]就完事。这招在 32 位程序里确实快,但有几个硬伤:一是目标机器必须装 Access Database Engine,版本还得和你的程序位数对上,64 位 Delphi 编译出来就得装 64 位驱动,客户环境往往只有 32 位 Office;二是列名带空格或中文时,SQL 里要加方括号,字段类型推断也经常翻车,比如身份证号被当成数字丢精度。我一般会绕开 ADO,改用 OLE 自动化直接操作 Excel.Application,或者用开源的TMS FlexCel、NativeExcel这类库读成内存表,再喂给 FastReport 的TfrxUserDataSet。这样不依赖外部驱动,列类型自己控制,缺点是速度比 ADO 慢一点,但报表场景数据量通常不大,几千行以内感知不明显。
选TfrxUserDataSet的另一个好处是它完全由代码驱动,你可以在OnGetValue、OnCheckEOF这些事件里自由决定每一行每一列返回什么,甚至能做运行时动态列——客户今天加一列“备注”,明天删一列“税率”,报表不用重新设计。
2.2 用 OLE 自动化读取 Excel 并填充 UserDataSet
先看一段最小可运行的代码,演示如何用 OLE 打开 Excel,把第一个工作表的数据灌进TfrxUserDataSet。注意这里用的是晚绑定,不需要在 uses 里加 Excel2000 单元,避免版本冲突。
uses ComObj, Variants, frxClass, frxDBSet; procedure LoadExcelToUserDataSet(const AFileName: string; ADataSet: TfrxUserDataSet); var ExcelApp, WorkBook, Sheet: OleVariant; RowCount, ColCount, i, j: Integer; FieldName: string; begin ExcelApp := CreateOleObject('Excel.Application'); try ExcelApp.Visible := False; ExcelApp.DisplayAlerts := False; WorkBook := ExcelApp.Workbooks.Open(AFileName); Sheet := WorkBook.Sheets[1]; // 假设第一行是标题行,从第二行开始是数据 RowCount := Sheet.UsedRange.Rows.Count; ColCount := Sheet.UsedRange.Columns.Count; ADataSet.Close; ADataSet.Fields.Clear; // 动态创建字段,列名取第一行 for j := 1 to ColCount do begin FieldName := 'F' + IntToStr(j); // 实际项目里可读第一行标题 ADataSet.Fields.Add(FieldName, ftString, 255); end; ADataSet.Open; for i := 2 to RowCount do begin ADataSet.Append; for j := 1 to ColCount do ADataSet.FieldByName('F' + IntToStr(j)).AsString := VarToStr(Sheet.Cells[i, j].Value); ADataSet.Post; end; finally WorkBook.Close(False); ExcelApp.Quit; ExcelApp := Unassigned; end; end;这段代码的逻辑很直白:创建 Excel 实例,打开文件,取第一个 Sheet 的已用区域行列数,然后按列数动态给TfrxUserDataSet加字段。字段名这里先用F1、F2占位,实际项目里应该读第一行的标题,做一次列名清洗(去空格、去特殊字符),再Fields.Add。参数方面,ftString和长度 255 是保守设置,如果确定某列是数字或日期,可以改成ftFloat、ftDateTime,这样 FastReport 里做合计、格式化会更方便。Append/Post是标准数据集操作,注意每行都要配对,否则数据会丢。
提示:OLE 自动化启动 Excel 进程比较重,如果报表要频繁刷新,建议把数据一次性读进内存的
TClientDataSet或泛型列表,再在OnGetValue里按需返回,而不是每次都开关 Excel。
2.3 在 FastReport 设计器里挂接 UserDataSet
代码里准备好数据源后,回到 FastReport 设计器。在报表的Data页上放一个TfrxUserDataSet组件,把它的UserName设成代码里对应的名字,然后在Report的DataSets里引用它。接着在OnGetValue事件里写取值逻辑:
procedure TForm1.frxReport1GetValue(const VarName: string; var Value: Variant); begin if VarName = 'F1' then Value := CurrentRowData[0] else if VarName = 'F2' then Value := CurrentRowData[1]; end;OnGetValue是 FastReport 向数据源要值的回调,VarName就是你在设计器里绑定的字段名。CurrentRowData是你自己维护的当前行数组,随OnCheckEOF推进。这种模式比直接绑TfrxUserDataSet更灵活,适合列不固定的场景。如果列固定,直接用TfrxUserDataSet的Fields绑定更省事,设计器里拖字段就行。
3. 动态列与格式映射:让报表跟着 Excel 模板走
3.1 运行时生成列:处理客户随时改表头
客户改 Excel 表头是家常便饭。如果报表设计器里写死了字段,每次都要重新打开设计器改,效率太低。我的做法是在报表OnBeforePrint事件里,根据实际读到的列数动态创建TfrxMemoView,并设置它们的DataSet和DataField。
procedure TForm1.frxReport1BeforePrint(Sender: TfrxReportComponent); var Page: TfrxPage; Memo: TfrxMemoView; i: Integer; ColWidth: Extended; begin Page := frxReport1.Pages[0]; ColWidth := Page.Width / FColumnCount; // 平均分配宽度 for i := 0 to FColumnCount - 1 do begin Memo := TfrxMemoView.Create(Page); Memo.Parent := Page; Memo.Left := i * ColWidth; Memo.Top := 0; Memo.Width := ColWidth; Memo.Height := 20; Memo.Font.Name := '宋体'; Memo.Font.Size := 9; Memo.DataSet := frxUserDataSet1; Memo.DataField := 'F' + IntToStr(i + 1); Memo.Text := '[' + Memo.DataField + ']'; end; end;这里的关键是Memo.Parent := Page,把动态创建的 Memo 挂到报表页上,否则它不会显示。DataField要和TfrxUserDataSet里的字段名一致。ColWidth按页宽平均分,如果某些列特别宽,可以按内容长度加权,但那是另一个话题了。注意这段代码放在OnBeforePrint里,每次打印都会执行,如果报表有多页,要判断是否已经创建过,避免重复添加。
3.2 格式映射:日期、金额、百分比的统一处理
Excel 里的日期是浮点数,金额可能带千分位,百分比存的是小数。直接VarToStr出来会很难看。我一般会在填充TfrxUserDataSet之前做一层格式化,或者在 FastReport 的OnGetValue里根据字段类型转换。
| Excel 原始值 | 目标显示格式 | 处理方式 |
|---|---|---|
| 45000 | 2023-03-15 | FormatDateTime('yyyy-mm-dd', 45000 + EncodeDate(1899,12,30)) |
| 1234567.89 | 1,234,567.89 | FormatFloat('#,##0.00', Value) |
| 0.15 | 15% | FormatFloat('0.0%', Value) |
| 文本带前导零 | 保留原样 | 读单元格时用Text属性而非Value |
日期转换那个EncodeDate(1899,12,30)是 Excel 的日期序列起点,网上有说 1900 的,但实际用 1899-12-30 更准,因为 Excel 有个 1900 年闰年的历史 bug。金额和百分比在 FastReport 的 Memo 里也可以直接设DisplayFormat,但动态列场景下用代码格式化更可控。文本前导零的问题,比如工号00123,用Sheet.Cells[i,j].Value会变成 123,必须用Sheet.Cells[i,j].Text才能拿到字符串。
3.3 用 TfrxUserDataSet 的 OnGetValue 做条件取数
有时候报表需要根据某列的值决定另一列显示什么,比如“状态”列是 1 显示“启用”,是 0 显示“停用”。这种逻辑放在OnGetValue里最合适:
procedure TForm1.frxUserDataSet1GetValue(const VarName: string; var Value: Variant); begin if VarName = 'StatusText' then begin if FCurrentStatus = 1 then Value := '启用' else Value := '停用'; end else if VarName = 'AmountWithTax' then Value := FCurrentAmount * 1.13; // 含税价 end;OnGetValue的触发时机是 FastReport 渲染每个 Memo 时,所以你可以在这里做任意计算,甚至查数据库。但注意不要在这里做耗时操作,否则报表会卡。FCurrentStatus、FCurrentAmount这些变量要在OnCheckEOF推进行号时同步更新。
4. 避坑与排查:Excel 数据源在 FastReport 里的五个翻车现场
4.1 现象:报表打开就报“未找到数据源” → 原因:UserDataSet 没 Open 或字段没加 → 解决:检查 Open 顺序
TfrxUserDataSet必须先Fields.Add再Open,顺序反了会报错。另外,如果报表设计器里绑定的字段名和代码里Fields.Add的名字不一致,FastReport 会提示找不到字段。我习惯在Open之后加一句Assert(ADataSet.FieldCount > 0),提前暴露问题。
4.2 现象:数字列合计为 0 → 原因:字段类型设成了 ftString → 解决:按实际类型建字段
TfrxUserDataSet的字段类型决定了 FastReport 能否做聚合。如果金额列用ftString,Sum函数会返回 0。建字段时根据 Excel 列的内容判断,或者统一用ftFloat,在OnGetValue里做StrToFloatDef转换。日期列用ftDateTime,否则排序会按字符串排。
4.3 现象:OLE 读取大文件时程序假死 → 原因:Excel 进程未释放或逐格读取太慢 → 解决:批量读入 Variant 数组
逐格Sheet.Cells[i,j].Value在几千行时就会明显变慢。优化方法是把整个UsedRange.Value一次性读进一个二维 Variant 数组,然后遍历数组。另外,ExcelApp.Quit之后要确保WorkBook和ExcelApp都Unassigned,否则 Excel 进程会残留在任务管理器里,下次打开文件可能被锁。
4.4 现象:中文列名在报表里显示为乱码 → 原因:OLE 返回的字符串编码问题 → 解决:用 WideString 或 UTF8Encode 转换
Delphi 的string在 2009 之后是 UnicodeString,但 OLE 返回的Variant有时是 ANSI 编码。如果发现乱码,试试UTF8ToString(VarToStr(...))或者直接用WideString接收。FastReport 的 Memo 默认支持 Unicode,问题多半出在读取环节。
4.5 现象:动态创建的 Memo 不显示 → 原因:Parent 没设或 Top/Left 超出页面 → 解决:先设 Parent 再设坐标
TfrxMemoView.Create(Page)只是创建了对象,必须Memo.Parent := Page才会挂到页面上。另外,如果Top设成了负数或者超过Page.Height,Memo 会跑到页面外。调试时可以先设一个固定坐标,确认能显示再改成动态计算。
5. 进阶技巧:把 Excel 模板直接当报表底稿用
前面讲的都是把 Excel 当数据源,FastReport 负责排版。但有些客户就是认 Excel 的样式,你排得再漂亮他也要导成 xlsx。这时候可以反过来:用 FastReport 导出 xlsx,但保留 Excel 模板的格式。FastReport 的TfrxXLSXExport支持设置TemplateFile,你可以把客户给的 Excel 模板路径传进去,导出的文件会基于模板生成,样式、公式、合并单元格都保留。
var XLSXExport: TfrxXLSXExport; begin XLSXExport := TfrxXLSXExport.Create(nil); try XLSXExport.TemplateFile := 'D:\Templates\ReportTemplate.xlsx'; XLSXExport.ShowProgress := False; XLSXExport.OpenAfterExport := True; frxReport1.PrepareReport; frxReport1.Export(XLSXExport); finally XLSXExport.Free; end; end;TemplateFile指向的模板里,用[FieldName]占位符标记数据要填的位置,FastReport 导出时会自动替换。这个技巧在财务报表场景特别管用,客户自己维护模板,你只管往里灌数。注意模板文件必须是 xlsx 格式,xls 不支持。另外,如果模板里有图表,导出后图表不会自动更新数据范围,需要客户手动刷新,这是 Excel 本身的限制,不是 FastReport 的锅。
验证导出结果时,我习惯写一个简单的检查脚本:用 OLE 打开导出的文件,读几个关键单元格的值,和预期对比。这样能在交付前发现字段错位、格式丢失的问题。从那以后我每次做 Excel 相关报表,都强制走一遍“模板导出→回读校验”的流程,宁可多花十分钟,也比客户反馈“数字对不上”强。希望帮到你。
本文还有配套的精品资源,点击获取