简介:XLSReadWriteII 6.00.47 For Delphi10.3 RIO Full Source 是一套面向 Delphi 开发者的 Excel 读写组件完整源码包,解决在未安装 Microsoft Excel 环境下直接操作 XLS/XLSX 文件的问题,适用于服务器端、瘦客户端以及需要定制报表生成的 Delphi 10.3 Rio 项目。资源共 596 个文件,压缩包 2.55MB,包含 248 个 Pascal 源文件、53 个窗体定义文件、32 个工程文件,以及大量 HTML 帮助文档、示例工程和图标资源,便于从源码到界面理解组件机制。已有 929 人学习下载。除了核心读写能力,还覆盖工作表、单元格、公式、样式、图表、图片等对象操作;Full Source 意味着可深入定制与性能优化,附带的 Changes、Install、License 文档和 Samples 示例为快速集成提供指引,是内网环境或专业报表场景下的实用备选。 写这篇文之前先说句实话:我拿到“XLSReadWriteII 6.00.47 For Delphi10.3 RIO Full Source”这套东西时,第一反应不是兴奋,而是疑惑。一个已经不怎么更新、国内资料零散到几乎要靠考古才能找到的第三方 Excel 读写组件,为什么我还要专门写一篇长文来讲?答案其实很简单——如果你做过 Delphi 下的批量 Excel 报表生成、模板填充这类活,你就会明白:官方没有原生支持、OLE 自动化慢到怀疑人生、其他商业控件又贵得肉疼,XLSReadWriteII 反而成了很多老项目里最稳的那根救命稻草。而且这次是 Full Source 版,意味着你可以直接看源码、改源码、编进项目里,不再有试用版那种“能跑但处处限制”的憋屈感。这篇文章就围绕它在 Delphi 10.3 RIO 下的安装、核心用法、踩坑记录和源码调试四个方面,把能说的实操经验和排查链路一次讲清楚。
1. 为什么必须选 Full Source:试用版和源码版差的不是一星半点
1.1 试用版那些“看不见”的限制有多难受
XLSReadWriteII 的试用版和 Full Source 版,表面上看功能没差别,读 xls、xlsx、写公式、设样式都能跑。但试用版最恶心的地方在于:它会随机在生成的表格里插入水印行,而且文档里写得很清楚“不可用于商业分发”。以前我在一个老项目里图省事用了试用版做原型验证,结果给客户演示时表格里突然冒出一行奇怪的文本,场面一度非常尴尬。更重要的是,试用版的某些高级功能(比如 xlsx 的严格 OOXML 模式、图表导出)会被静默忽略,不是报错,而是“你写了代码但它不生效”,这种问题排查起来非常耗时间。
Full Source 版最大的价值不是“没有水印”,而是你能确确实实看到它内部怎么解析 BIFF8、怎么处理 Shared String Table、怎么计算行高列宽。对于做企业级报表系统的团队来说,这种透明度意味着:当你遇到文档规范之外的非标准 Excel 文件时,可以直接在源码层修,不用等官方更新。另外,Full Source 版不会像试用版那样限制并发或运行时长,你编译出来的 DLL 或 EXE 想怎么分发都行,没有授权黑盒。
1.2 二进制包调试时的无力感,源码版直接看本质
我印象最深的一件事:有一次读一个客户发来的 xls 文件,里面有一列日期始终解析出来是错误值,用二进制版组件只能拿到一个#VALUE!的单元格结果,完全不知道是格式识别问题还是内部函数计算问题。后来换了 Full Source 版,直接用 Delphi 的调试器进入TExcelWorksheet.ReadCell相关代码,一路跟踪才发现,客户那个文件里日期不是常规的 Date 类型,而是用了1904 date system(Mac Excel 的老底子),组件内部按照 1900 系统解析,偏移量差了 4 年。
这种问题,没有源码根本不可能靠“猜”解决。所以我一直建议团队:如果项目里要重度依赖某个第三方库,至少要选带源码的版本,否则一旦遇到文档没覆盖的边界情况,你只能在外面包一层又一层 workaround,治标不治本。XLSReadWriteII 的源码组织结构也很清晰,XLSReadWriteII5.pas是主控,XLSSheet5.pas是工作表,XLSCell5.pas是单元格,结构不算复杂,认真看两三天就能理清调用链,后面再改东西就顺手很多。
2. 在 Delphi 10.3 RIO 上把源码包跑起来的完整过程
2.1 先确认版本对齐:这套组件不是拿来就能用的
XLSReadWriteII 有多个版本分支,不同版本对应不同 Delphi 编译器的包文件。标题里写的是For Delphi10.3 RIO,实际解压后你会看到目录结构里有一堆以 Delphi 版本号命名的子目录,比如D10_3、DXE、D5之类。千万别贪方便去打开别的版本目录下的.dpk文件,因为不同编译器对泛型、接口、字符串类型的处理有差异,强行编译会报一堆低级错误,比如E2010 Incompatible types: 'UnicodeString' and 'AnsiString'。
我这边的环境是 Windows 10 + Delphi 10.3.3 RIO(Community Edition 也行,因为 XLSReadWriteII 的源码包本身不依赖付费库)。打开源码包根目录后,建议先看Source文件夹里的XLSReadWriteII10.dpk(具体文件名以你下载的包为准),然后确认你的 Delphi 的 Library 路径里已经添加了Source目录,否则编译时会提示找不到.dcu文件。版本确认这一步看似简单,但很容易被忽略,我见过有人直接打开DXE的包在 10.3 里编译,报错后还以为是组件有问题,其实是版本选错了。
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 包文件版本 | 选择D10_3或命名含 10.3 的.dpk | 打开其他版本目录下的包 |
| Delphi Library 路径 | 添加Source和Lib目录 | 只添加了 Source,缺少运行时包路径 |
| 编译器选项 | 检查 Unicode 编码、启用运行时主题 | 沿用默认,导致资源文件加载异常 |
2.2 编译安装 DPK 时最容易出错的三个细节
第一步:打开.dpk文件后,先在项目管理器里确认包类型。Runtime Package(运行时包)和Design Package(设计时包)要分清楚:设计时包会注册组件到 IDE 的工具面板,运行时包则只是提供单元。XLSReadWriteII 通常有两个包,一个带Design字样、一个不带,两个都要编译,顺序是先运行时包再设计时包。如果你只装设计时包,编译时会有找不到运行时单元的错误。
第二步:编译之前,建议先把项目选项里的Output directory设置到一个单独的目录(比如源码包下的Lib\Win32\Release),别让生成的.dcu和.bpl散落得到处都是。这一步不是可选项,因为你后续还要给其他 Delphi 工程引用,把输出集中起来,工程搜索路径才容易配。我第一次装的时候没注意,结果.dcu全跑到源码目录里,后来升级组件版本时清理起来特别费劲。
第三步:编译时如果遇到F2046 File not found: 'XLSReadWriteII5.dcu'这类错误,多半是编译顺序不对或者搜索路径漏了。建议先把所有包含单元文件的目录(Source、Lib)都加入全局 Library 路径,再编译包,基本上一次能过。还有个细节:如果系统装了多个 Delphi 版本,确认你在 10.3 的 IDE 里执行的是Build而不是Compile,因为Build会强制重新生成所有.dcu,避免用到旧版本残留的二进制文件。
2.3 安装完组件面板和搜索路径的正确状态
编译安装成功后,打开 Delphi 工具面板,在样例会多出一页叫TMS或Axolot Data的组件页(具体名称取决于包注册信息),里面有TXLSReadWriteII、TXLSWriteII等组件。如果面板上没出现,别急着怀疑安装失败,先检查设计时包是否Install成功。在项目管理器里,设计时包节点上右键,选择Install,等提示成功再看面板。
除了 IDE 面板,我更关心的是工程编译时的搜索路径。新建一个测试工程,把Source目录加入 Project Options 里的Search path,然后写一行uses XLSSheet5;,编译一下,能通过说明环境基本没问题。这一步一定要实测,别只看 IDE 里组件面板有没有。因为有些项目是纯命令行编译(比如 CI 环境),不依赖 IDE 面板,只要搜索路径配置正确就能用。我最开始就吃过亏:面板上组件拖拖拽拽没问题,但脱离 IDE 用 MSBuild 编译时直接报找不到单元,就是因为忘了把Source目录加进搜索路径。
3. 核心读写 API 的使用逻辑:和 Excel 对象模型完全不同
3.1 先搞懂 Sheet 与 Cell 的索引模型
XLSReadWriteII 的编程模型和 Excel VBA 的Range概念差得很远,它更接近底层 BIFF 格式的直接映射。顶层对象是TXLSReadWriteII5,它内部有Sheet数组,每个Sheet是一个TExcelWorksheet5,再往下每个单元格通过Sheet.Cell[X, Y]或AsString[X, Y]这种二维下标访问。注意它的行和列都是 0 基索引,也就是说 Excel 里的 A1 单元格对应的是Cell[0, 0],B2 对应Cell[1, 1]。这个和很多 Delphi 开发者的直觉不太一样——如果你用惯了 VBA 的Range("A1"),第一次写Sheet[0].AsString[0,0]时很容易把行列搞反。
单元格的访问属性有好几个:AsString、AsFloat、AsDateTime、AsBoolean。它们内部会做类型判断和转换,但有个问题:AsString遇到数字类型时会根据单元格的 Number Format 来转换,如果你的脚本里大量使用AsString去读数字,可能会拿到带千分位分隔符或科学计数法的字符串。更稳妥的做法是:先判断CellValueType,再按类型选择对应的属性。否则你从 Excel 里读到的“1000”可能变成“1,000”,这会导致后续字符串拼接的逻辑出错。
3.2 读取场景:模板解析、数据提取的典型写法
我项目里最常见的一个读取需求是:读取一个模板文件,然后根据模板里某个标记单元格的内容来决定报表标题和查询条件。用 XLSReadWriteII 写起来很简单:
var XLS: TXLSReadWriteII5; i, j: Integer; val: string; begin XLS := TXLSReadWriteII5.Create(nil); try XLS.LoadFromFile('D:\template.xlsx'); // 读取第一个工作表的 A1 单元格 val := XLS.Sheet[0].AsString[0, 0]; // 遍历前 20 行前 10 列,打印非空单元格 for i := 0 to 19 do for j := 0 to 9 do begin if XLS.Sheet[0].CellValueType[i, j] <> xlvtNone then WriteLn(Format('(%d,%d): %s', [i, j, XLS.Sheet[0].AsString[i, j]])); end; finally XLS.Free; end; end;这里有个关键点:CellValueType枚举里有xlvtNone表示空白单元格,但要注意,它和“值为空字符串”是两个概念。有些模板会用空格填满“空白”单元格,这种单元格类型不是xlvtNone,而是xlvtString且内容为空格字符串,如果你用AsString读取后直接用Trim处理,很容易漏掉这种“假空白”。我踩过这个坑,后来统一写了个函数:先判断CellValueType,再判断字符串是否全为空白,两个条件都满足才按真空处理。
3.3 写入场景:从零生成一份格式完整的报表
写入报表比读取更常用,也更能体现这个组件的价值。下面是一个生成带表头、带列宽、带数据区域的例子:
var XLS: TXLSWriteII5; s: TExcelWorksheet5; begin XLS := TXLSWriteII5.Create(nil); try s := XLS.Sheet[0]; s.Name := '销售报表'; // 设置列宽(单位是字符宽度) s.ColumnWidth[0] := 12; s.ColumnWidth[1] := 30; s.ColumnWidth[2] := 15; // 写表头 s.AsString[0, 0] := '序号'; s.AsString[0, 1] := '产品名称'; s.AsString[0, 2] := '销售额'; // 给表头加粗和背景色 s.Cell[0, 0].FontStyle := [xfsBold]; s.Cell[0, 0].FillPatternForeColor := $00CCFFCC; // 淡绿色 s.Cell[0, 0].FillPattern := xfpSolid; // 写 10 行数据 for i := 1 to 10 do begin s.AsInteger[i, 0] := i; s.AsString[i, 1] := Format('产品%d', [i]); s.AsFloat[i, 2] := i * 123.45; end; XLS.SaveToFile('D:\report.xlsx'); finally XLS.Free; end; end;这里要说一个新手非常容易踩的点:TXLSWriteII5和TXLSReadWriteII5这两个类的区别。TXLSWriteII5是专门用于快速写入的类,它内部做了大量优化,比如批量提交样式、延迟排序等,写入性能比用TXLSReadWriteII5逐格写快得多。但是它的读取能力很弱,基本只是“能保存”而已。所以如果你要改一个已有的 Excel 文件,用TXLSReadWriteII5;如果是从零生成一张新表且数据量很大,用TXLSWriteII5。用反了的话,几百行的数据体感不明显,几万行数据差距就很惊人了。
单元格样式是另一个要重点注意的地方:FillPatternForeColor和FillPattern必须一起设置才会生效,只设置颜色不设置填充模式的话,背景色会被 Excel 忽略。另外颜色值用的是 BGR 格式,不是常规的 RGB。比如 RGB 红色是$000000FF,但在 XLSReadWriteII 里要写成$00FF0000,很多从 TMS 等其他控件转过来的开发者会在这里栽跟头。我当时为了一个表头颜色问题排查了半天,最后才发现是颜色字节序弄反了。
4. 实测踩坑记录:这些坑不实测很难发现
4.1 坑一:日期格式被静默改写
这个坑我在前面已经提过一部分背景,但完整排查链路值得单独拿出来写。现象是:读取一个 xls 文件里的日期列,某些单元格读出来的值和 Excel 里看到的完全对不上,差几年甚至差一个月。
排查过程是这样:我先把LoadFromFile之后的数据直接导出成文本,发现日期被读成了浮点数,比如 Excel 里显示2023-06-15,读出来是45092.0。这个数字其实是 Excel 内部存储的日序数,按 1900 日期系统计算,45092 对应的就是 2023-06-15,说明组件解析数值本身没问题。那问题出在哪?继续跟踪到单元格的NumberFormat,发现客户文件的单元格格式是yyyy/m/d,而组件的日期解析依赖于格式串里的yyyy这种标记,如果格式串写的是[$-409]yyyy\-m\-d这种带 Locale 前缀的格式,组件在部分版本里会识别不了,导致返回vtFloat而不是vtDateTime。
解决方式有两种:一种是在读取前设置XLS.Options := XLS.Options - [xloDateMode1904],强制按 1900 系统解释,但这只解决日期系统偏移,不解决格式串识别问题;另一种是通过源码修改,在ParseNumberFormat里增加对[$-xxx]前缀的兼容。既然我们拿到的是 Full Source,我果断选择了第二种,改完重新编译,问题彻底解决。用二进制版的朋友就只能在外面自己写日期解释逻辑,绕一大圈还容易出 bug。
4.2 坑二:大数据量写入慢得离谱,问题出在 Style 上
一个测试场景:向新生成的 xlsx 写入 10 万行、每行 10 列的数据,用TXLSWriteII5逐格写入,结果跑了将近两分钟,完全没办法接受。一开始我以为是组件本身性能不行,后来看源码才发现,问题出在样式处理上:每次写入一个单元格时,如果显式或隐式地设置了单元格样式,组件内部都要去样式表里查找是否已有相同样式,没有的话就新建一个。这个查找是线性遍历,随着样式数量增加,写入速度急剧下降。
解决办法有两个方向:一是复用同一个样式对象。在大量写入的循环里,不要每格都设置FontStyle、FillPattern这些属性,而是先定义好样式,赋给一个变量,循环里只改值。二是利用BeginUpdate/LockUpdate之类的方法暂停界面和样式索引更新(具体方法名看版本),等批量写入完成后再统一刷新。实测下来,复用样式后 10 万行数据从两分钟降到了 10 秒左右,差别非常大。如果你数据量超过万行,必须关注这个优化点。
4.3 坑三:64 位编译时报“模块计算机类型与目标计算机类型冲突”
这个问题的场景是:我在 32 位工程里用得好好的,把同一个工程切到 Win64 平台后,编译时提示找不到.dcu或者直接报错。根源在于:XLSReadWriteII6 的源码包里,默认编译输出是针对 32 位的.dcu,64 位环境下需要重新编译生成.dcu。如果你的 Library 路径里同时包含 32 位输出目录和 64 位输出目录,Delphi 可能抓到错误位数的.dcu,于是出现莫名其妙的冲突。
排查链路很简单:清空源码包下所有的.dcu和.bpl,确保当前编译平台选的是 Win64,重新编译并安装所有包,再把工程切到 Win64 重新编译。注意不要图方便把 32 位和 64 位的输出目录配置成同一个路径,否则 Delphi 会随机选择,时好时坏。另外,有些第三方依赖单元(比如TMS的通用库)也有 32/64 位区分的.dcu,确认它们也都重新编译过。
4.4 坑四:公式写入后不重算,打开 Excel 才刷新
我把带公式的 xlsx 生成后,用 Excel 打开时发现所有公式列都是 0,要手动按一下 F9 重算才能显示正确结果。这个问题的原因是:Excel 打开文件时会根据文档里的fullCalcOnLoad标记决定是否全量重算;如果生成方没有设置这个属性,Excel 会优先使用缓存的计算结果,而 XLSReadWriteII 写入公式时只写了公式,没有写入缓存值,于是显示为空或 0。
解决方式是在保存前设置:
XLS.Workbook.Globals.CalcMode := xcmAuto; // 或类似属性 XLS.Workbook.Globals.FullCalcOnLoad := True;设置完后重新保存,Excel 打开就会自动重算。这个属性在不同版本里名字可能略有差异,但关键字就是FullCalcOnLoad,在源码里搜一下能找到。还有个小细节:如果公式里用了自定义函数,Excel 打开时可能会提示“无法更新某些内容”,这个其实是 Excel 的安全机制,不是组件的问题,处理方式是在代码里同时写入公式的缓存结果,或者引导用户启用宏和自动计算。
5. 拿到 Full Source 后如何利用源码定位问题:一场真实调试
5.1 准备断点:从 OnLoad 到单元格解析的调用链
Full Source 的价值绝不只是一个心理安慰,实际排查问题时它能帮你省下大量时间。我拿到的这套代码里,核心文件是XLSReadWriteII5.pas,它定义了TXLSReadWriteII5.LoadFromFile的实现,内部会调用ReadWorkbook,再逐 Sheet 处理。如果你想定位某个单元格解析问题,建议先在TExcelWorksheet5.ReadCellData或者类似的内部方法上下断点。
一个比较实用的调试技巧:如果你要查某个具体单元格(比如第 5 行第 3 列),可以在断点条件里写(Row = 4) and (Col = 2)(因为行号和列号是 0 基)。这样不会在解析每个单元格时都中断,只会在目标单元格处停下来,然后单步跟进看它的类型推断逻辑、格式化字符串解析流程,非常有针对性地定位问题。
另外,源码里有很多类似{$IFDEF XLSREADWRITEII_DEBUG}的调试开关,打开这些宏定义后,组件会在解析关键路径上输出调试信息(通常通过OutputDebugString)。用 Delphi IDE 的 Event Log 或者 Debug Inspector 可以看到这些日志,实测对于排查“为什么这个文件加载失败”或“这个单元格为什么变成空白”这类问题很有效。
5.2 定位问题:一个看源码解决问题的实例
今年年初,我遇到一个非常刁钻的问题:用组件生成的 xlsx 文件,在 WPS 里打开正常,在微软 Excel 里打开提示“文件已损坏,是否尝试恢复”。这可是大问题,客户直接拒绝验收。我第一时间想到的是 Full Source,直接打开XLSWriteII5.pas看保存逻辑,重点排查和文件结构有关的代码。
看源码的过程中,我注意到组件在写入 xlsx 时,需要维护一组关系文件(_rels目录)和Content_Types.xml。如果某张工作表的名称包含特殊字符,比如/或者?,XML 里对应的r:id会生成错误,导致文件结构不合法。但问题是我预检过所有 Sheet 名称,都是中文和数字,按理说不会触发这个问题。继续跟踪,才发现问题出在单元格里的某些字符串包含首尾空格,组件在写入sharedStrings.xml时没有做xml:space="preserve"标记,当 Excel 严格解析时就会报错,而 WPS 容错性更强所以能正常打开。
解决方式很简单:在写字符串前统一调Trim,或者修改源码里的WriteSharedString方法,把空白字符串的处理标记加上。如果当时拿的是二进制版,这个问题基本只能靠外部工具反复试错才能修复。有源码在手,定位到方法级别,改完重编译就解决了,前后不到两个小时。
5.3 修改和扩展源码的注意事项
既然拿到了源码,很多人肯定会想着按自己的需求去改。改源码没问题,但要注意几个坑。
第一,版本升级时修改会被覆盖。如果你改的是源码包里的.pas文件,下次从官网下新版或者从 SVN 拉新代码时,改动会丢失。更合理的做法是把改动尽量收拢在子类里,通过继承或接口扩展,不要直接改原类。如果必须改原类,建议用版本管理工具记录差异,升级时逐个 diff 手动合入。我自己习惯用 Beyond Compare 做版本对比,每次都把XLSReadWriteII5.pas这类文件的改动单独汇总,升级时优先检查这些文件。
第二,修改后一定要跑通全部测试场景。很多改动看似局部,实际会影响整个文件解析流程。比如我曾在ParseNumberFormat里增加了对[$-409]前缀的兼容处理,结果影响了IsDateTimeFormat的判断,间接导致某些日期格式被识别成字符串,后来用了一组囊括多种日期格式和 Locale 前缀的测试文件才把边界情况补齐。不要只测你客户的那一个文件,多收集一些真实场景下的文件做回归测试,这个成本绝对不能省。
第三,注意第三方依赖。XLSReadWriteII 的某些功能使用到了Abbrevia或者TMS的通用单元(具体看源码uses子句),如果你修改的代码碰巧涉及这些依赖,要确保你的许可证允许修改和分发。虽然这是源码版,但冷门的依赖也要提前排查,否则发布给客户时可能会遇到授权问题。
最后再分享一个我个人的小技巧:用 Full Source 前,先花一个下午把源码目录结构和核心对象的生命周期理清楚——哪些对象由外部创建、哪些由内部管理、释放顺序是什么。因为一旦你开始改代码,释放顺序就是最容易出内存泄漏和崩溃的地方。我在Sheet的释放逻辑上栽过一次,所以后来每次修改单元生命周期相关代码时,都会仔细检查Create和Free的配对情况,用 FastMM 的泄漏报告验证。这套代码本身老,但结构不复杂,花点时间摸清楚,后面用起来会顺手很多。
本文还有配套的精品资源,点击获取