简介:XLSReadWriteII v6.0.0.47 是一款面向 Delphi 开发者的 Excel 文件读写组件,支持 D7 至 D10.3 Rio 版本,无需安装 Excel 或其他软件即可原生访问 Excel 文件。组件内置针对单元格值优化的内存管理器,可处理较大文件,并提供 Direct Read/Write 模式直接在磁盘上处理数据;其 XLSX 引擎依据文件格式规范自动构建,确保兼容性与数据安全,且无运行时费用、附完整源码。资源包共 596 个文件,以 248 个 pas 源码、53 个 dfm 窗体、32 个 dpr 工程及 16 个 dproj 项目文件为主,另含 html 文档、obj 编译单元、png/gif 图示与 cfg 配置等,压缩包约 2.59MB,目录结构清晰,便于按示例工程检索学习。目前已有 299 人学习下载,适合需要稳定 Excel 读写能力的中高级 Delphi 开发者参考与集成。
1. 老 Delphi 项目里那个绕不开的 Excel 读写组件
接手一个 Delphi 7 时代的老系统,需求是导出带格式的 Excel 报表,还要能读回用户填好的数据。第一反应是 OLE 自动化调 Excel,但客户机器上不一定装了 Office,装了版本也不一样,服务器上更是别想。这时候老同事丢过来一个词:XLSReadWriteII。它是一套原生读写 Excel 文件的 Delphi 组件库,不依赖 Office,不依赖驱动,直接按 BIFF 和 OOXML 格式操作文件。v6.0.0.47 这个版本号意味着它同时覆盖了从 Delphi 7 到 Delphi 10.3 Rio 的编译目标,一套代码能在老项目和新 IDE 之间迁移。适合谁?维护遗留 Delphi 系统、需要批量生成报表、又不想被 Office 环境绑架的开发者。下面把我实际用下来的选型逻辑、编译配置、核心 API 和踩过的坑讲清楚。
2. 为什么不用 OLE 和第三方驱动:XLSReadWriteII 的定位与选型
2.1 三种 Excel 操作路线的真实成本对比
在 Delphi 里操作 Excel,常见做法无非三条路。第一条是 OLE 自动化,用CreateOleObject('Excel.Application')拉起 Excel 进程。写起来直观,但每个客户端都要装 Office,进程残留和权限问题在服务端几乎无解。第二条是 ODBC 或 OLEDB 驱动,把 xlsx 当数据库查。读还行,写格式基本残废,合并单元格和样式别想。第三条就是 XLSReadWriteII 这类原生库,自己解析文件格式,进程内完成,不依赖外部环境。
| 方案 | 依赖 Office | 支持格式 | 样式控制 | 服务端可用 | 大文件表现 |
|---|---|---|---|---|---|
| OLE 自动化 | 必须 | xls/xlsx | 强 | 差 | 差 |
| ODBC/OLEDB | 需驱动 | xls/xlsx | 无 | 中 | 中 |
| XLSReadWriteII | 无 | xls/xlsx | 强 | 好 | 好 |
选型结论很直接:只要部署环境不可控,或者要在服务端批量跑,原生库是唯一稳妥的选择。XLSReadWriteII 的另一个好处是它同时支持老的 BIFF8(.xls)和新的 OOXML(.xlsx),老系统导出的文件新 Excel 能打开,新系统生成的文件老机器也能读。
2.2 v6.0.0.47 对 D7 和 D10.3 Rio 的兼容意味着什么
这个版本号里最关键的信息是它横跨了两个差异极大的 IDE 时代。Delphi 7 用的是 ANSI 字符串,Delphi 10.3 Rio 默认 Unicode,且引入了新的包管理和编译目标。一套组件要同时支持,说明它在字符串处理和条件编译上做了大量适配。
实际配置时,D7 下直接安装设计期包,把组件面板里的TXLSReadWriteII拖到窗体上即可。D10.3 Rio 下要注意两点:一是确保包的编译目标平台选对(Win32 还是 Win64),二是如果项目从 D7 迁移过来,字符串赋值处要检查string和AnsiString的隐式转换。我一般会在 D10.3 下先建一个空工程,只放一个组件、写一个最小导出,确认编译和运行都通过,再往老代码里合。
// D10.3 Rio 下的最小验证:创建一个 xlsx 并写入一个单元格 uses XLSReadWriteII5; procedure TForm1.MinimalTest; var XLS: TXLSReadWriteII5; begin XLS := TXLSReadWriteII5.Create(nil); try XLS.Clear; XLS.Sheets.Add; // 新增一个工作表 XLS.Sheets[0].Name := 'TestSheet'; // 工作表命名 XLS.Sheets[0].AsString[0, 0] := 'Hello'; // 第0行第0列写入 XLS.Filename := 'C:\temp\minimal.xlsx'; XLS.Write; // 按扩展名自动选择格式 finally XLS.Free; end; end;这段代码的逻辑说明:Clear清空内部状态,Sheets.Add创建默认工作表,AsString[Col, Row]是单元格读写入口,注意它的参数顺序是列在前、行在后,和很多人直觉相反。Filename赋值后Write会根据扩展名决定写 BIFF 还是 OOXML。参数上,AsString的索引从 0 开始,越界不会报错但会静默失败,这是后面避坑章节要展开的点。
3. 从零跑通第一个导出:环境配置与核心 API
3.1 安装包与编译目标的对应关系
拿到组件包后,先看目录结构。通常会有Source、Packages、Demos几个文件夹。Source 里是全部单元文件,Packages 里按 IDE 版本分目录。D7 用Packages\D7下的包,D10.3 Rio 用Packages\Rio下的包。安装步骤是:打开对应的.dpk或.dproj,编译,然后 Install。如果编译报找不到单元,检查 Source 路径有没有加到 Library Path 里。
D10.3 Rio 下有个容易忽略的点:如果项目要编译成 Win64,组件包也必须先编译出 Win64 版本,否则链接阶段会报找不到目标文件。我一般会在 IDE 的包管理器里把 Win32 和 Win64 两个目标都编译一遍,省得后面切平台时翻车。
3.2 单元格读写、格式设置与多工作表操作
核心 API 集中在TXLSReadWriteII5这个类上。工作表通过Sheets[i]访问,单元格通过AsString、AsInteger、AsFloat、AsDateTime等属性读写。格式设置走Sheets[i].CellFormat[Col, Row],可以设字体、边框、背景色、对齐方式。
// 带格式的报表导出:表头加粗、数据列右对齐、设置列宽 procedure TForm1.ExportReport; var XLS: TXLSReadWriteII5; Sheet: TXLSWorksheet; i: Integer; begin XLS := TXLSReadWriteII5.Create(nil); try XLS.Clear; XLS.Sheets.Add; Sheet := XLS.Sheets[0]; Sheet.Name := '月度报表'; // 表头 Sheet.AsString[0, 0] := '编号'; Sheet.AsString[1, 0] := '金额'; Sheet.AsString[2, 0] := '日期'; for i := 0 to 2 do begin Sheet.CellFormat[i, 0].Font.Style := [fsBold]; Sheet.CellFormat[i, 0].Brush.BackgroundColor := clSilver; Sheet.CellFormat[i, 0].HorzAlign := haCenter; end; // 数据行 for i := 1 to 10 do begin Sheet.AsInteger[0, i] := i; Sheet.AsFloat[1, i] := i * 123.45; Sheet.AsDateTime[2, i] := Now + i; Sheet.CellFormat[1, i].HorzAlign := haRight; Sheet.CellFormat[2, i].NumberFormat := 'yyyy-mm-dd'; end; // 列宽 Sheet.ColWidth[0] := 8; Sheet.ColWidth[1] := 15; Sheet.ColWidth[2] := 14; XLS.Filename := 'C:\temp\report.xlsx'; XLS.Write; finally XLS.Free; end; end;逻辑说明:先写表头再统一设格式,避免每写一个单元格就设一次格式带来的性能损耗。CellFormat返回的是格式对象引用,直接改属性即可生效。NumberFormat用 Excel 的格式字符串,日期用yyyy-mm-dd,金额可以用#,##0.00。ColWidth的单位是字符宽度,不是像素。
参数上要注意AsDateTime写入的是 Delphi 的TDateTime,Excel 内部按序列号存储,读回来时用AsDateTime还原。如果读回来发现日期偏了,多半是 1900 和 1904 日期系统的差异,这个在避坑章节细说。
3.3 读取已有文件并遍历数据
读文件比写文件多一个Read调用,然后遍历有效区域。判断有效区域可以用LastRow和LastCol,但这两个属性在文件格式不规范时可能偏大,我一般会自己维护一个边界。
// 读取用户填好的 xlsx,遍历到最后一个有效行 procedure TForm1.ImportData; var XLS: TXLSReadWriteII5; Sheet: TXLSWorksheet; Row, LastRow: Integer; CellValue: string; begin XLS := TXLSReadWriteII5.Create(nil); try XLS.Filename := 'C:\temp\input.xlsx'; XLS.Read; Sheet := XLS.Sheets[0]; LastRow := Sheet.LastRow; // 注意:可能包含空行 for Row := 0 to LastRow do begin CellValue := Sheet.AsString[0, Row]; if CellValue = '' then Continue; // 跳过空行 // 处理 CellValue end; finally XLS.Free; end; end;逻辑说明:Read之后Sheets[0]就是第一个工作表。LastRow返回的是有格式或内容的最后一行,但用户如果误操作在很下面点过单元格,这个值会虚高。稳妥做法是加一个空行跳过逻辑,或者自己记录有效数据行数。AsString读数值单元格时会做隐式转换,但精度可能丢失,数值列建议用AsFloat读。
4. 避坑与排查:那些让我加班到凌晨的细节
4.1 日期读回来差一天或差四年
现象:写入2024-01-15,读回来变成2024-01-14或1900-01-15。原因:Excel 有两种日期系统,1900 和 1904,Mac 版 Excel 默认用 1904。另外 Excel 有个著名的 1900 年闰年 bug,它认为 1900 年是闰年,导致 1900 年 3 月之前的日期序列号偏移。解决:在读写日期前统一设置XLS.Options里的日期系统,或者干脆用字符串格式yyyy-mm-dd存储日期,读的时候自己解析。我现在的习惯是报表里日期一律用字符串写,省得跨平台出玄学问题。
4.2 大文件写入内存暴涨
现象:导出几万行数据时,进程内存从几十兆涨到几个 G,最后 OutOfMemory。原因:XLSReadWriteII 默认在内存里构建整个工作簿,所有单元格和格式对象都驻留内存。解决:分批写,每写够一定行数就Write一次然后Clear重新开始,或者用它的流式写入模式(如果版本支持)。另一个办法是减少格式对象数量,不要给每个单元格单独设格式,而是按行或按列批量设。我一般会在导出前估算行数,超过五万行就主动分批。
4.3 中文乱码:D7 和 D10.3 的字符串差异
现象:D7 下导出的中文在 D10.3 编译的程序里读出来是乱码,或者反过来。原因:D7 的string是 ANSI,D10.3 的string是 Unicode。组件内部如果按 ANSI 存储,跨版本就会错位。解决:在 D10.3 下确保所有传入组件的字符串都是UnicodeString,如果从老代码迁移,检查有没有AnsiString变量直接赋值给AsString。必要时用UTF8Encode或UTF8Decode做显式转换。我踩过一次坑,老代码里用ShortString存中文,迁移后全变问号,后来统一改成string才解决。
4.4 单元格索引越界不报错
现象:循环写数据时,行号算错,写到AsString[0, 100000],程序不崩,但文件打开后数据缺失。原因:组件的索引器对越界访问做了静默处理,不抛异常。解决:写之前自己校验行列范围,或者封装一个安全写入函数,越界时记录日志。我现在的做法是在导出循环里加Assert,Debug 模式下越界直接断下来,Release 下记日志。
4.5 编译目标平台不匹配导致链接失败
现象:D10.3 Rio 下 Win32 编译通过,切到 Win64 报Unsatisfied forward or external declaration。原因:组件包只编译了 Win32 目标,Win64 的 dcu 不存在。解决:在 IDE 里打开组件包项目,把 Win64 平台加进来,重新编译并 Install。如果用的是运行时包,还要确保运行时包的 Win64 版本也编译了。这个坑在第一次切平台时几乎必踩,记住「包和项目平台要一致」就行。
5. 进阶技巧:批量导出性能优化与格式复用
5.1 用格式池减少重复对象创建
前面提过,每个单元格单独设格式会拖慢速度。更高效的做法是预先创建几个格式模板,然后批量应用。XLSReadWriteII 的CellFormat返回的是引用,但每次访问可能触发内部查找。我一般会先把表头格式、数据格式、日期格式各设一次,然后通过循环批量赋值,而不是在循环里反复改属性。
// 格式复用:先定义好表头和数据行的格式,再批量写入 procedure TForm1.ExportWithFormatPool; var XLS: TXLSReadWriteII5; Sheet: TXLSWorksheet; i: Integer; begin XLS := TXLSReadWriteII5.Create(nil); try XLS.Clear; XLS.Sheets.Add; Sheet := XLS.Sheets[0]; // 一次性设置表头区域格式 Sheet.CellFormat[0, 0].Font.Style := [fsBold]; Sheet.CellFormat[1, 0].Font.Style := [fsBold]; Sheet.CellFormat[2, 0].Font.Style := [fsBold]; // 批量写数据,格式在写完后统一应用 for i := 1 to 5000 do begin Sheet.AsInteger[0, i] := i; Sheet.AsFloat[1, i] := i * 1.5; end; // 统一设置数据列格式(按列设置比按单元格快) Sheet.CellFormat[1, 1].NumberFormat := '#,##0.00'; // 注意:这里只设了第一个数据单元格,实际项目中 // 可以用循环按列批量设置,或使用组件的区域格式方法 XLS.Filename := 'C:\temp\large.xlsx'; XLS.Write; finally XLS.Free; end; end;逻辑说明:把格式设置从写入循环里拆出来,减少循环内的属性访问次数。对于列级格式,如果组件支持区域格式设置(部分版本有SetFormatRange之类的方法),优先用区域方法。参数上,NumberFormat只影响显示,不影响存储值,所以可以最后统一设。
5.2 验证导出结果的三个检查点
写完文件后别急着交付,我一般会做三个检查。第一,用 Excel 或 LibreOffice 打开,看有没有报「文件损坏」。第二,检查行数和列数是否和预期一致,特别是最后一行有没有丢。第三,抽查几个关键单元格的格式和值,日期和金额最容易出问题。如果文件打不开,多半是Write之前Filename没设对,或者扩展名和实际格式不匹配。如果行数少了,回去查循环边界和越界静默失败的问题。
5.3 从 D7 迁移到 D10.3 的检查清单
老项目升级时,按这个顺序过一遍能省很多时间。先确认组件包在 D10.3 下编译安装成功,再建一个最小导出工程验证基本功能,然后逐步把老代码里的导出模块移过来,每移一个就测一次。重点检查字符串类型、日期处理、文件路径(D10.3 下建议用TPath组合路径)。最后在 Win32 和 Win64 两个目标下各跑一遍完整流程。我自己的习惯是迁移前先写一个自动化测试脚本,用固定数据导出再读回,比对结果,这样能快速定位是组件问题还是业务代码问题。
希望帮到你。
本文还有配套的精品资源,点击获取