简介:XLSReadWriteII v6.0.0.47 是面向 Delphi 开发者的 Excel 文件读写组件包,兼容 D7 至 D10.3 Rio,适合在桌面程序中直接生成、解析和修改工作簿。组件原生访问 Excel 文件,运行无需安装 Excel 或其他办公软件,内置针对单元格值优化的内存管理器,并提供 Direct Read 与 Direct Write 模式在磁盘上处理较大文件,强调不损坏文件、不篡改数据以及 XLSX 格式兼容,且无运行时费用。
压缩包共 596 个文件,约 2.59MB,以 248 个 pas 源码、164 个 html 文档、53 个 dfm 窗体和 32 个 dpr 工程文件为主,另有 obj、dpk、res、图片及 cfg 示例配置,便于阅读实现、编译示例和查阅说明。已有 302 人学习,完整源码与示例工程可用于研究 Excel 读写引擎、内存管理、直接读写模式及兼容性处理;示例配置覆盖图表、自动筛选、数据透视表和表格控件等场景,也能为旧版 Delphi 项目升级和导出功能开发提供参考,对中高级开发者有较高复用价值。
1. 为什么还在用 XLSReadWriteII:从 Delphi 7 到 Rio 的 Excel 读写选型
维护老系统的朋友发来一张截图:服务器上没装 Excel,业务却要按模板导出对账单,Delphi 7 的工程还不能随便换框架。这个场景里,XLSReadWriteII v6.0.0.47 这类组件就是冲着“不依赖 Excel 进程、直接在代码里读写 .xls”来的。它覆盖 D7 到 D10.3 Rio 的包,适合老项目补导出、内网工具做报表、以及不想引入 OpenXML 重依赖的桌面端。反直觉的是,版本跨度大不等于装完就能跑,包路径、字符集和公式重算才是后面真正花时间的地方。先把它当成一个“能编译进 IDE 的 Excel 文件读写库”,再谈怎么落地。
2. 把组件装进 IDE:包编译与搜索路径的实操顺序
拿到压缩包先别急着双击.dpk。包里通常按source、dcu、packages、demo分目录,D7 和 Rio 的包与 DCU 往往各放一套。老编译器对路径很敏感,解压到带空格或中文的目录,后面编译报“单元找不到”会很难查。我一般先把包解压到类似D:\dev\XLSRW\这种短路径,再打开 IDE 对照目录结构确认版本,最后才动 Library 路径。
2.1 先认清包里有什么:source、dcu、demo 的分工
source放的是.pas源文件,编译期如果选择从源码构建,IDE 会到这里找单元;dcu是预编译单元,适合不想每次重编源码的工程;packages下是设计期包和运行期包,设计期包负责把组件注册到工具面板,运行期包负责真正链接进程序;demo不是装饰品,里面通常能看到当前版本推荐的写入、读取、格式设置写法,遇到 API 名称对不上时,先翻 demo 比搜旧帖子可靠。把这几类目录混在一起加进 Library 路径,短期能编译,长期容易让 IDE 抓到旧版本 DCU,出现“明明改了代码但行为没变”的玄学问题。
| 路径类型 | 加什么 | 适用场景 | 注意点 |
|---|---|---|---|
| 源码路径 | <包根>\source | 需要从.pas编译 | 版本切换时要清 DCU |
| DCU 路径 | <包根>\dcu\D7或<包根>\dcu\Rio\Win32 | 直接用预编译单元 | 不要和源码路径混放 |
| 包路径 | <包根>\packages\D7或...\Rio | 打开.dpk安装 | 设计期与运行期分开 |
| 示例路径 | <包根>\demo | 对照 API 和参数 | 不要直接改 demo 当生产代码 |
2.2 D7 与 Rio 的包安装顺序
D7 的流程相对直白:打开对应版本的包文件,先 Build 运行期包,再 Install 设计期包。如果先装设计期包,IDE 可能提示找不到运行期依赖,组件面板也不会出现。Rio 多了一层平台选择,先确认当前 IDE 是 Win32 还是 Win64,只装当前要用的平台包;如果工程同时出 32 位和 64 位,运行期包要分别编译,Library 路径也要按平台分开加。设计期包只装在 IDE 侧,不要把设计期包链接进业务程序,否则发布时会把 IDE 依赖带出去。
操作顺序可以按下面走:
- 关闭所有打开的项目,避免旧包占用。
- 在 IDE 的 Library 路径里加入当前版本的
source和对应平台dcu。 - 打开
packages下对应版本的运行期包,Build,确认没有红色错误。 - 打开设计期包,Install,观察组件面板是否出现。
- 新建一个空 VCL 工程,放一个组件到窗体,编译一次。
- 再运行第 2.3 节的验证代码,确认写入文件成功。
2.3 编译失败时先看哪三处路径
第一处看 Library 路径是否指向当前版本,尤其是机器上同时装过旧版组件时,IDE 可能优先抓到旧 DCU。第二处看包依赖顺序,运行期包没编译成功,设计期包一定装不上。第三处看工程自身的搜索路径,有些项目在.dpr或.dproj里写死了相对路径,换机器后路径失效,表现是“包装好了但项目编译不过”。遇到这种问题,先清掉工程里的__history、__recovery和 DCU 输出目录,再重编译,比反复卸载包有效。
uses XLSReadWriteII; // 单元名以包内实际 .pas 文件为准 procedure TForm1.Button1Click(Sender: TObject); var XLS: TXLSReadWriteII; begin XLS := TXLSReadWriteII.Create(nil); try XLS.Clear; // 清掉默认工作表,避免重复 Sheet XLS.Sheets.Add; // 新增一个工作表 XLS.Sheets[0].Name := '验证'; // 工作表名尽量短,避免特殊字符 XLS.Sheets[0].AsString[0, 0] := 'Excel'; XLS.Write('verify.xls'); // 先用相对路径验证,确认当前目录可写 finally XLS.Free; end; end;这段验证代码只做三件事:创建组件、写一个单元格、保存文件。Create(nil)表示不依赖窗体所有者,适合放在工具类或数据模块里;Clear用来清空组件内部默认表,避免保存出多余 Sheet;AsString[0,0]的列、行都从 0 开始,和 Excel 界面上的 A1 不是同一套坐标;Write的参数是保存路径,第一次验证不要用中文目录和只读目录。编译时如果提示TXLSReadWriteII未定义,先检查uses单元名是否和包内文件一致,再检查设计期包是否真的安装成功,不要急着改代码。
3. 写一个能跑的 Excel 导出:单元格、格式与公式的落点
导出功能最容易翻车的地方不是“写不进去”,而是写进去后业务方打开发现列宽挤在一起、日期变成一串数字、合计列是 0。XLSReadWriteII 的写入接口围绕单元格展开,文本用AsString,数值用AsFloat,日期用AsDateTime,公式用AsFormula。先把这四类写对,再谈样式。.xls的行列上限是 65536 行、256 列,超过这个量级不要硬塞,按业务月份或组织维度拆 Sheet 更稳。
3.1 最小写入流程:从 TXLSReadWriteII 到保存
下面这段导出代码把表头、数据行和合计公式放在一个流程里,行列坐标都从 0 开始。业务数据在真实项目里通常来自数据集或列表,这里用循环模拟。保存前建议先写到一个临时文件名,成功后再替换目标文件,避免导出中途异常留下半截文件。
procedure ExportInvoice(const AFileName: string); var XLS: TXLSReadWriteII; Sheet: TXLSReadWriteIISheet; // 类型名以包内声明为准 Row: Integer; begin XLS := TXLSReadWriteII.Create(nil); try XLS.Clear; XLS.Sheets.Add; Sheet := XLS.Sheets[0]; Sheet.Name := '对账单'; // 表头:第 0 行 Sheet.AsString[0, 0] := '客户'; Sheet.AsString[1, 0] := '金额'; Sheet.AsString[2, 0] := '日期'; Sheet.AsString[3, 0] := '备注'; // 数据行:第 1 行开始,写 3 行示例 for Row := 1 to 3 do begin Sheet.AsString[0, Row] := '客户' + IntToStr(Row); Sheet.AsFloat[1, Row] := 100.5 * Row; // 数值列不要写字符串 Sheet.AsDateTime[2, Row] := Now + Row; // 日期写 TDateTime Sheet.AsString[3, Row] := 'Excel 导出'; end; // 合计公式:B2 到 B4,对应行列从 0 开始就是 [1,1] 到 [1,3] Sheet.AsFormula[1, 4] := '=SUM(B2:B4)'; // 列宽、字体、边框按包内 Cell 属性设置,属性名以 demo 为准 XLS.Write(AFileName); finally XLS.Free; end; end;逻辑上先建 Sheet,再按行写表头和数据,最后写公式并保存。AsFloat写入的是数值,Excel 打开后可以直接参与计算;如果金额写成AsString,表面看一样,求和公式可能算不到。AsDateTime写入的是日期序列值,显示成2024-01-01还是01/01/2024取决于单元格格式,不取决于写入字符串。AsFormula的参数是公式文本,等号不能省,区域引用按 Excel 的 A1 写法写,不要按 0 基坐标写。保存路径用绝对路径时,先确认目录存在;用相对路径时,确认程序当前目录不是只读。
| 写入方法 | 写入类型 | 典型用途 | 注意点 |
|---|---|---|---|
AsString | 文本 | 名称、备注、编码 | 长数字不要用文本,避免科学计数 |
AsFloat | 数值 | 金额、数量、比例 | 先做空值判断 |
AsDateTime | 日期序列 | 下单时间、账期 | 显示格式另设 |
AsFormula | 公式文本 | 合计、占比 | 等号开头,区域用 A1 写法 |
3.2 格式与列宽:别让业务方打开就骂
格式设置不是面子工程。金额列没有千分位、日期列宽度不够显示成####、备注列把内容截断,业务方第一眼就会认为导出功能坏了。常见做法是写完数据后统一设列宽,再对金额列设数字格式,对日期列设日期格式,表头加粗。XLSReadWriteII 的格式入口通常在 Sheet 或 Cell 对象上,不同小版本的属性名可能略有差异,最稳的方式是对照包内 demo 找到当前版本的Font、Fill、Borders、NumberFormat写法,再抄到自己的导出单元里。
列宽不要按字符数硬猜,先给一个业务能接受的初始值,比如客户列 20、金额列 14、日期列 16、备注列 30,然后拿真实最长内容试一遍。金额格式建议用#,##0.00这类带千分位的格式,日期格式用yyyy-mm-dd或业务习惯的格式。表头加粗和底色可以提升可读性,但不要给整张表加复杂边框,数据行一多,文件体积和打开速度都会受影响。格式化代码不要散落在每个单元格赋值后面,统一放在数据写完之后,维护时更容易改。
3.3 公式与日期:写入的是值还是显示串
公式列最常见的翻车是“导出时是公式,打开后是 0”。原因通常有两个:一是只写了公式文本,没有触发组件重算;二是公式引用的区域和实际数据行对不上。写完公式后,按包内 demo 提供的重算入口执行一次,再把文件保存。如果组件版本没有自动重算能力,可以在导出后读回文件,检查公式单元格的计算结果是否大于 0,不通过就记录日志,不要让有问题的文件直接发给业务方。
日期列则要区分“写入值”和“显示串”。用AsDateTime写入TDateTime,Excel 会把它当日期处理,排序和筛选都正常;用AsString写入FormatDateTime('yyyy-mm-dd', Now),看起来是日期,实际是文本,后续做日期区间筛选会出问题。如果业务只要求展示、不要求计算,文本日期也能接受,但要在导出说明里写清楚。我的习惯是:只要这一列未来可能被筛选、排序、做公式,就写日期序列;只有纯展示的备注性日期才写字符串。
4. 读回业务表:遍历、类型判断与空值处理
读取比写入更容易被低估。业务方发来的 Excel 往往有合并单元格、空行、表头前两行是说明、金额列混着空值和文本。直接按固定行号取值,第一版能跑,第二版换模板就崩。XLSReadWriteII 读取时先用Read打开文件,再定位工作表,然后按LastRow遍历。遍历过程中不要假设每一行都有值,也不要把AsString当成万能取值器,数值和日期列要单独判断。
4.1 打开与工作表定位
打开文件前先判断文件是否存在、是否被 Excel 占用。Read失败时不要只弹一个“读取失败”,把异常信息写进日志,至少能区分是文件格式不对、路径没权限,还是文件正在被别的进程打开。工作表定位不要只认第一个 Sheet,业务模板可能把说明页放在前面,数据页在第二个或第三个。常见做法是按 Sheet 名匹配,匹配不到再回退到第一个 Sheet,并在日志里记下实际使用的表名。
procedure ReadInvoice(const AFileName: string); var XLS: TXLSReadWriteII; Sheet: TXLSReadWriteIISheet; Row: Integer; S: string; V: Double; D: TDateTime; begin XLS := TXLSReadWriteII.Create(nil); try XLS.Read(AFileName); // 文件被占用时会抛异常,外层要捕获 Sheet := XLS.Sheets[0]; // 生产代码建议按 Sheet 名查找 for Row := 1 to Sheet.LastRow do // 第 0 行当表头,从 1 开始读 begin if Sheet.AsString[0, Row] = '' then Continue; // 空行直接跳过,避免把空行当业务数据 S := Sheet.AsString[0, Row]; // 文本列兜底读取 if Sheet.AsString[1, Row] <> '' then V := Sheet.AsFloat[1, Row] // 数值列先判空再转 else V := 0; if Sheet.AsString[2, Row] <> '' then D := Sheet.AsDateTime[2, Row] // 日期列读序列值,显示格式另处理 else D := 0; end; finally XLS.Free; end; end;这段代码的重点是“先判空再取值”。LastRow表示最后有内容的行,不一定等于业务数据行数,中间可能夹着空行;AsString对文本列安全,但数值列用AsString判断空值再转AsFloat,比直接转更稳;日期列用AsDateTime读出来的是序列值,要显示成业务格式时再格式化。外层调用一定要加try except,把异常信息写进日志,不要只给用户一个模糊提示。
4.2 单元格类型判断与安全取值
Excel 单元格的类型不是固定的,同一列里可能前几行是数值,后面被人改成文本。安全取值的顺序是:先判断空,再按业务预期类型取,取不到时回退到字符串并记录异常行号。金额列如果读到文本“1,200.00”,直接AsFloat可能得到 0 或抛异常,这种数据要在导入前做清洗,或者在读取时把千分位去掉再转。日期列如果被写成文本,AsDateTime也会不按预期工作,最好在模板说明里约定列格式,读取端再做一次兜底。
| 检查项 | 常见表现 | 处理方式 |
|---|---|---|
| 空单元格 | AsString为空 | 跳过或按业务默认值 |
| 数值列混文本 | AsFloat结果异常 | 去千分位后再转,记录行号 |
| 日期列是文本 | AsDateTime不对 | 按约定格式解析,失败记日志 |
| 合并单元格 | 只有左上角有值 | 读取时向下填充或按区域取 |
| 表头偏移 | 第 0 行不是表头 | 用配置项指定表头行 |
4.3 空行、合并单元格与表头偏移
合并单元格是读取环节的血泪点。Excel 界面上看到合并区域每一格都有值,底层只有左上角单元格存了值,其余是空。直接按行读,第二行开始就是空,业务方会以为导入漏数据。常见做法是读取前先找合并区域,把左上角的值填充到整个区域;或者导入模板时禁止合并数据列,只在表头使用合并。表头偏移则用配置解决,不要写死“第 1 行是表头”,把表头行号、数据起始行号做成参数,换模板时只改配置。
空行处理也要有策略。有些模板用空行分隔不同组织的数据,直接Continue会丢掉分段信息;有些模板末尾有一堆格式化过的空行,LastRow很大但实际没数据。我的做法是:连续空行超过 3 行就认为数据结束,单行空行跳过但记录行号;如果业务需要分段,就在空行处切分数据集,而不是简单丢弃。读取完成后做一次行数核对,导出多少行、读回多少行,对不上就停下来查,不要带着疑问继续跑批。
5. 避坑与排查:版本、编码、内存和公式的五条血泪记录
这套组件的坑集中在“环境”和“数据边界”,代码本身反而直白。下面五条记录按现象、原因、解决写,都是落地时容易反复遇到的。每一条都建议在交付前过一遍,尤其是同时维护 D7 和 Rio 的团队,版本差异会把小问题放大。
5.1 安装与版本类:三条记录
现象:D7 装完包,组件面板没有出现。原因:只编译了运行期包,没有安装设计期包,或者设计期包的依赖没有编译成功。解决:先打开运行期包 Build,确认没有错误;再打开设计期包 Install;装完后重启 IDE,新建工程放组件验证。如果面板仍没有,检查 Library 路径是否指向当前版本的包目录,卸载旧包后再装一次,不要在同一台机器上保留多个版本的设计期包。
现象:Rio 下编译提示单元找不到,D7 工程却正常。原因:Win32 和 Win64 的 DCU 路径混用,或者 Library 路径只加了 D7 目录。解决:按平台分开加路径,Win32 工程只加 Win32 的 DCU,Win64 工程加 Win64 的 DCU;切换平台后清理 DCU 输出目录再编译。如果工程同时出两个平台,建议在构建脚本里按平台切换搜索路径,不要依赖 IDE 的全局路径碰运气。
现象:打开包提示重复单元或版本冲突。原因:机器上装过旧版组件,旧 DCU 还在搜索路径里,IDE 抓到了旧版本。解决:先在 IDE 的包列表里卸载旧设计期包,删除旧 DCU 目录,清理工程输出,再重启 IDE。还不行就检查系统环境变量和 IDE 的全局 Library 路径,把旧路径删掉。这个问题表现为“代码没改但行为变了”,查起来很耗时间,装新版本前先卸旧版本能省很多事。
5.2 读写与格式类:两条记录
现象:导出的中文在 D7 下正常,在 Rio 下变问号或乱码。原因:D7 默认AnsiString,Rio 默认UnicodeString,写入端和读取端的字符串类型没有统一。解决:组件接口如果接收WideString或UnicodeString,就显式转换后再写;读取时按实际编码转回业务字符串。跨版本共用的导出单元里,字符串变量尽量用string并显式Trim,不要在 D7 里假设 Unicode,也不要在 Rio 里把AnsiString直接塞进 Excel 单元格。
现象:公式列导出后是 0,手动在 Excel 里回车一下才算出结果。原因:只写了公式文本,没有触发组件重算,或者公式区域引用了空行。解决:写完公式后调用包内 demo 提供的重算入口,再保存;检查公式区域是否覆盖实际数据行;读回文件时对关键公式单元格做一次结果校验,不通过就记录日志并阻断导出。不要指望业务方打开文件后手动重算,批量导出场景里没人会逐个单元格按回车。
6. 进阶验证:用批处理式自检把导出结果压住
导出功能交付前,我习惯加一道自检:文件写完后立刻用组件读回来,检查表头、行数和关键公式列。这个动作不替代业务验收,但能拦住“保存成功但内容错位”的低级问题。自检代码不要写在 UI 按钮里,抽成独立方法,批量导出时每生成一个文件就跑一次,失败的文件单独记日志,不要混在成功列表里。
procedure SelfCheck(const AFileName: string); var XLS: TXLSReadWriteII; Sheet: TXLSReadWriteIISheet; begin XLS := TXLSReadWriteII.Create(nil); try XLS.Read(AFileName); Sheet := XLS.Sheets[0]; // 表头校验:防止列顺序被模板改动 if Sheet.AsString[0, 0] <> '客户' then raise Exception.Create('表头错位:A1 不是客户'); // 行数校验:至少要有表头加 3 行数据 if Sheet.LastRow < 4 then raise Exception.Create('数据行不足'); // 公式结果校验:合计列不应为 0 if Sheet.AsFloat[1, 4] <= 0 then raise Exception.Create('合计公式未重算或区域错误'); finally XLS.Free; end; end;这段自检只查三件事:表头第一列是不是预期文本、最后一行是否达到最小行数、合计公式单元格是否大于 0。实际项目里可以按业务再加关键列非空、金额合计与源数据一致、日期列落在合理区间。raise Exception在工具内部用没问题,交付给业务时改成日志加错误码,避免弹窗打断批量任务。
| 检查项 | 失败表现 | 处理建议 |
|---|---|---|
| 表头文本 | A1 不是预期列名 | 检查模板版本和列顺序 |
| 数据行数 | LastRow小于预期 | 检查数据源是否为空 |
| 合计公式 | 结果为 0 或空 | 检查重算入口和公式区域 |
| 日期列 | 读回为 0 或文本 | 检查写入类型和单元格格式 |
从那以后我每次交付导出功能前,都先把生成的文件读回来跑一遍这个自检,确认表头、行数和合计列都对得上再交给业务方。希望帮到你。
本文还有配套的精品资源,点击获取