简介:XLSReadWriteII 6.00.16是面向Delphi 10.2 Tokyo环境的Excel文件读写组件包,专注解决开发者在Windows平台上快速生成、修改.xls/.xlsx文件的需求,无需安装Office即可实现单元格数据、公式、样式、图表及透视表等操作,适合报表系统、数据迁移和Excel自动化等场景。压缩包共816个文件,约13.47MB。其中164个HTML帮助页面、150个HPP头文件、149个DCU编译单元、149个OBJ目标文件以及大量PAS源码、DFM窗体文件,方便开发者直接复用;同时包含DPR、DPROJ工程文件与示例配置,可快速打开并运行。已有312人学习下载。从内容预览可见,包内提供了多个示例配置和样例数据,可演示图表创建、数据透视表交互、Excel网格显示以及文件监听等典型功能,帮助使用者快速掌握从基本读写到进阶特性的实际用法。配合HTML帮助文档可快速查阅API,各项配置样例也有助于理解不同功能模块的参数设置,能显著降低上手门槛。
1. XLSReadWriteII 6.00.16 for Tokyo 10.2:一台不装 Office 的机器上,把三张表揉成一张周报
XLSReadWriteII 6.00.16 for Tokyo 10.2 这个版本,是我在 RAD Studio 10.2 Tokyo 维护项目里最后敲定的 Excel 读写组件。项目现场不允许装 Office,数据库里三张表要合并成一张带表头、边框、列宽的周报;先用 ADO 去读 xls,在这个环境里翻车过两次。换了这套组件以后,Excel 的读写被封装成“工作簿、工作表、单元格”三层对象,公式、样式、列宽都能从代码里直接控制,不再依赖 Excel 进程和 COM 组件。对手里还压着一个 10.2 工程项目、又需要稳定导出报表的团队来说,这份资源可以先按下面的顺序完整复现一遍。
2. 装前先拆包:目录结构长什么样,版本为什么死磕 Tokyo
2.1 解压后先认目录:源码版和编译版怎么分辨
把下载包解压之后,先不要急着双击任何 Install 程序。我一般会直接看第一层目录。一份完整的组件资源,通常能看到 Install、Packages、Source、Demos、Docs 这几类目录。其中 Source 目录才是判断“源码版还是编译版”的关键:里面有没有大量.pas文件,决定了你能不能自己改组件内部逻辑,也决定了后面是走“编译安装”还是“拷贝 BPL 直接装”两条完全不同的路线。
如果 Source 目录是满的,并且 Packages 下按 IDE 版本分好了子目录,那说明这是一个可编译的完整包。如果 Source 目录几乎是空的,只有几个.dcu和预编译好的.bpl,那它就是一个编译版,安装方式很简单:把对应版本的 BPL 复制到 IDE 的包目录,再从 IDE 的 Packages 面板里加载。两种方式我都见过,判断标准只有一个:Packages 目录里有没有.dpk或.dproj项目文件,有就按源码版走,没有就按二进制版走。
另外,Demos 是个好东西。组件能不能在你自己的机器上跑通,不要看说明书,先看它的示例工程能不能编译。如果 Demos 里给了十个工程但全编译失败,说明这个资源包的版本匹配有问题,趁早换一套,别在这方面浪费时间。我见过不少人跳过 Demos 直接往自己工程里引单元,结果装了半天发现包版本对不上,整个 IDE 启动都报错。
2.2 版本匹配的硬规则:Tokyo 10.2 与前几代包不能混装
这套组件在文件名里写死了 “for Tokyo 10.2”,这不是随便挂着好看的。RAD Studio 的运行时包和编译器版本是强耦合的,包在编译时会带上运行时库的版本签名,10.2 Tokyo 的 DCC32 生成的包,和 10.1 Berlin、10.3 Rio 之间不能通用。很多人踩坑的第一站就是图省事,把一个 6.00.x 的 Berlin 版 BPL 直接拿到 10.2 里装,结果 IDE 起来之后要么找不到单元,要么编译时提示“Package not found”,甚至 IDE 直接崩掉。
为什么死磕 Tokyo?因为在 10.2 里,Win32、Win64 两套平台需要分别编译一次。你光装了 Win32 的设计期包,后面切换 Win64 目标平台时,运行时 BPL 可能还是旧版本,链接阶段就会报一堆找不到符号的错误。6.00.16 这个版本号本身是组件的迭代版本,它对应 10.2 的适配包通常是同一份源码里给出的多个 IDE 版本目录之一。所以拿到资源包后,第一件事不是装,而是确认 Packages 目录下是否有明确的 Tokyo 或 10.2 子目录。
版本匹配问题还牵扯到另一个细节:如果你同时装了多个 RAD Studio 版本,系统里会存在同名但不同版本的 BPL。Windows 的 DLL 搜索顺序在这种场景下很坑,IDE 有时会优先加载到旧版本的包,让你误以为新装的是坏的。我现在的做法是,每台开发机只保留一个主用 IDE 版本,其他版本全卸载干净,专机专用。这样可以少掉一大批“装完即废”的玄学问题。
2.3 命令行编译运行时包和设计期包
确认目录无误之后,我推荐用命令行方式编译,而不是在 IDE 里手动点鼠标。原因很简单:命令行能看清每一个编译步骤的输入输出,失败时能直接定位到是哪个包的问题。打开 RAD Studio 自带的命令行环境,切到包目录,先编译运行时包:
# 请先 cd 到 Packages\Delphi\Tokyo 目录下,用 dir 看一下实际的 dproj 文件名 cd D:\libs\xlsrw6\Packages\Delphi\Tokyo dir /b *.dproj # 清理旧产物,再以 Release 配置编译 Win32 平台运行时包 msbuild XLSReadWriteII6.dproj /t:Clean /p:Config=Release /p:Platform=Win32 msbuild XLSReadWriteII6.dproj /t:Build /p:Config=Release /p:Platform=Win32第一段命令是列出目录下的项目文件,实际文件名可能带版本后缀。有人直接复制别人博客里的完整包名来编译,结果找不到文件,第一反应是资源坏了,其实只是没看自己目录里的真实文件名。我用的是占位符包名,你要把它替换成自己资源里实际存在的.dproj名称。
/t:Clean是把之前的中间文件清掉,防止旧对象文件污染本次编译。有些包在多次切换 IDE 版本后,残留的.dcu会导致莫名其妙的 “Unit not found” 错误,Clean 一下能解决大半。/p:Config=Release和/p:Platform=Win32是 MSBuild 的标准属性传递,对应 IDE 里的 Release 配置和 Win32 目标平台。编译完运行时包后,再编译设计期包,设计期包文件名通常带dcl前缀:
# 编译并安装设计期包,安装后 IDE 组件面板才会出现新页签 msbuild dclXLSReadWriteII6.dproj /t:Build /p:Config=Release /p:Platform=Win32设计期包安装完成后,再切换 Win64 目标平台重新执行一次。这一步很容易被忽略:很多人只在 Win32 下装了设计期包,等到 Win64 工程里一引用,编译直接炸。顺序上有一个原则:必须先编译运行时包,再编译设计期包。设计期包依赖运行时包,编译顺序反了会报找不到 BPL 的错误。
2.4 在 IDE 里注册组件并设置搜索路径
包编译完之后,打开 RAD Studio,在 Component > Install Packages 里查找新加入的设计期包。安装成功后,组件面板上会出现一个标注为 XLS ReadWrite 的新页面,里面有主控组件和数据读写相关的几个组件。如果没出现,大概率是设计期包没装上,回到命令行检查刚才那步的输出日志。
接下来要设置库搜索路径,否则新建工程里引用不到单元。在 Tools > Options > Delphi Options > Library 里,把 Source 目录加到 Library path。这一步我吃过亏:只把 Packages 目录加进去,没加 Source,结果 IDE 提示找不到 XLSReadWriteII6.pas,原因是包项目能编译,但你的应用工程引的是源文件,不是包文件。
提示:路径里不要带空格和中文,我见过有人把资源放在“C:\Program Files...”里,q 编译时宏展开出错,折腾半个下午。
安装完做个五步检查,确认这套包真的能用:
| 检查点 | 预期结果 | 失败时看哪里 |
|---|---|---|
| 组件面板 | 出现 XLS ReadWrite 页签 | 设计期包是否安装成功 |
| 新建工程引用单元 | 编译能找到 XLSReadWriteII6 | Library path 是否包含 Source |
| 拖一个主控件到窗体 | 能创建无异常 | 设计期包版本与 IDE 是否匹配 |
| Win32 编译空工程 | 链接通过 | 运行时包是否已编译 |
| Win64 编译空工程 | 链接通过 | Win64 平台是否单独编译 |
这套检查流程花不了十分钟,但能帮你把“组件装好了但工程用不了”的问题挡在门外。我自己现在新装任何第三方包都强制走一遍这个清单,不再凭感觉往下写代码。
3. 从零跑通一份 xlsx:对象模型与三种常用写入姿势
3.1 三个核心对象与坐标系:Sheets、Worksheet、Cells
这套组件的对象模型不算复杂,但和 Excel 的 VBA 对象模型有点区别。最顶层是 TXLSReadWriteII6,也就是你拖到窗体上的主控组件。它里面有一个 Sheets 集合,集合里每个元素是一个工作表对象,再往下才是单元格对象。平时最常打交道的就这三个层次。
容易绕晕人的地方是坐标。Excel 本身的单元格坐标是从 1 开始计数的,A1 就是第一列第一行,但很多 Delphi 组件内部的 Cells 下标是从 0 开始计的。也就是说,Excel 里的 A1,在组件里可能是 Cells[0, 0];Excel 里的 B2,在组件里是 Cells[1, 1]。这个偏移量不搞清楚,导出数据时整张表都会错位,而且错得很隐蔽,因为只有到上线之后被别人打开文件才发现列对不上。
另外,Cells 的下标顺序是“列在前,行在后”,和 Excel 习惯里的“行在前,列在后”相反。我之前在循环里把行号写在第一个参数位置,结果数据全变成转置存放。这类问题我不觉得是组件设计不好,更像是 Delphi 组件一贯的坐标风格,用之前先花十分钟写两行测试代码把坐标验证掉,后面整个导出逻辑都稳。
3.2 创建新工作簿并写出第一行数据
从无到有生成一份 xlsx 是这个组件最常用的场景。我们直接写一个最小控制台程序,把两行三列的数据落盘:
program WriteDemo; {$APPTYPE CONSOLE} uses System.SysUtils, XLSReadWriteII6; var wb: TXLSReadWriteII6; ws: TXLSWorksheet; wsIdx: Integer; begin wb := TXLSReadWriteII6.Create(nil); try // 新建工作表并拿到它的下标 wsIdx := wb.Sheets.Add('周报'); ws := wb.Sheets[wsIdx]; // Cells[列, 行],下标从 0 开始 ws.Cells[0, 0].AsString := '姓名'; ws.Cells[1, 0].AsString := '成绩'; ws.Cells[0, 1].AsString := '某同学'; ws.Cells[1, 1].AsInteger := 97; // 显式指定 xlsx 格式,再写文件 wb.FileFormat := xlExcel2007; wb.WriteToFile('D:\temp\output.xlsx'); finally wb.Free; end; end;这段代码是完整的踩通流程。它先用Sheets.Add新建工作表,返回值是新增表的下标,然后通过下标再取工作表对象。为什么不直接拿返回值当工作表?因为不同小版本里 Add 的返回值不一样,有的返回下标,有的返回对象,用下标方式兼容性最稳。
写入数据用的是Cells[列, 行].AsString、AsInteger这样的属性。这个组件的设计是把单元格对象暴露出来,让你直接改它的类型化取值属性,字符串、整数、浮点数、日期时间各有各的入口。写字符串时如果单元格里已经有数字,组件会自动做转换,但转换失败时它抛异常不会静默吞掉,这个特性在排错时很省力。
最后两行是重点:FileFormat := xlExcel2007告诉组件按 OOXML 格式保存,WriteToFile才真正落盘。我不建议省略 FileFormat 赋值,因为默认格式跟具体资源包的编译配置有关,有人依赖扩展名自动判断,结果生成的文件扩展名是 xlsx,内部却是老版 xls,Excel 打开就报修复。
3.3 读取外部 xlsx 并遍历指定区域
读取比写入更考验边界处理,因为外部文件不可控。下面这段代码把第一个工作表的所有有效行和列打印出来:
program ReadDemo; {$APPTYPE CONSOLE} uses System.SysUtils, XLSReadWriteII6; var wb: TXLSReadWriteII6; r, c: Integer; begin wb := TXLSReadWriteII6.Create(nil); try wb.Filename := 'D:\temp\input.xlsx'; wb.Read; // LastRow 和 LastCol 返回有效区域的边界 for r := 0 to wb.Sheets[0].LastRow - 1 do begin for c := 0 to wb.Sheets[0].LastCol - 1 do begin if wb.Sheets[0].Cells[c, r].AsString <> '' then WriteLn(Format('行 %d, 列 %d = %s', [r, c, wb.Sheets[0].Cells[c, r].AsString])); end; end; finally wb.Free; end; end;LastRow和LastCol这个两个属性返回的是有效数据区域的边界。注意它们是 0 基的,所以循环条件里减了 1,否则最后一个空行会被多打印一次。我见过有人直接用 LastRow 作为循环上限,结果每次都多读一行空数据,在数据校验场景里直接造成误报。
读完文件后,单元格的取值仍然可以用AsString统一取,这样处理混合类型时最不容易出错。但有一个前提:如果单元格里是真数字、日期,用AsString拿到的可能是经过格式化的文本。想拿原始数值,要按列的真实类型分别用 AsFloat、AsDateTime、AsInteger。最稳妥的做法是先看单元格类型再决定取值,别一个 AsString 走天下。
注意:Read 方法会一次性把整个工作簿读进内存。几十 MB 的大文件还好,几百 MB 的超大文件会明显卡顿。如果源文件特别大,后面 4.4 那条避坑记录里会单独讲。
3.4 把 DataSet 循环导出到 Excel 的常规写法
实际项目里最常遇到的是把数据库查询结果导出成 Excel。下面这个封装函数展示了基本套路,把数据集字段名先写进第一行,再把每条记录逐行写入:
procedure ExportDataSetToExcel(DS: TDataSet; wb: TXLSReadWriteII6); var row, col: Integer; ws: TXLSWorksheet; begin ws := wb.Sheets[0]; // 先写表头 for col := 0 to DS.FieldCount - 1 do ws.Cells[col, 0].AsString := DS.Fields[col].DisplayLabel; // 再逐行写数据 row := 1; DS.First; while not DS.Eof do begin for col := 0 to DS.FieldCount - 1 do ws.Cells[col, row].AsString := DS.Fields[col].AsString; Inc(row); DS.Next; end; end;这个函数有几个值得注意的地方。第一,它没有处理每一列的数据类型,全部按字符串写。这样做的好处是代码简单、不会被类型转换绊倒,坏处是 Excel 里的数字会变成文本,后续做统计函数会受影响。如果你有明确的数值列,应该在循环里判断字段类型,用 AsFloat 写数值。
第二,它是逐行逐列访问 Cells,这种写法在数据量小时没有任何问题,但到十万行以上会明显变慢。原因在于每次访问 Cells 都会经过组件内部的对象调度,相当于每个格子的寻址都有额外开销。想要提速,可以在循环里把单元格地址换算成行列后做批量操作,或者分块写入,也就是每写 1000 行就主动把对象池清理一次,避免内存堆积。这个技巧我放在第四部分展开。
第三,表头用DisplayLabel而不是FieldName,这是为了保留数据集里已经设置好的中文列名。有些项目忘了给数据集字段设 DisplayLabel,导出出来一列英文字段名,交付时又得返工。写导出代码时先把表头打好,后面数据行再慢也不会乱。
3.5 样式设置:列宽、行高、边框与字体加粗
报表光有数据没有样式,交付时通常过不了验收。这套组件支持直接设置单元格样式,我用最常碰到的几个需求做示例:
// 设置表头样式:加粗、居中、带边框 ws.Cells[0, 0].FontStyles := [fsBold]; ws.Cells[0, 0].Alignment := haCenter; ws.Cells[0, 0].Border := cbBox; // 设置列宽和行高 ws.ColumnWidth[0] := 24; ws.RowHeight[0] := 28; // 合并单元格:把 A1:B1 合并成一个大表头 ws.Merge(0, 0, 1, 0);这里的cbBox是库自带边框常量,意思是对单元格四周全部加边框。haCenter是水平居中。FontStyles是字体样式集合,可以同时加粗加下划线。ws.Merge(0, 0, 1, 0)是把第 0 行、第 0 列到第 1 列的区域合并,最常用于表头横跨多列的场景。
样式设置有个隐藏的坑:重复设置同一个单元格的样式,会不断产生新的内部样式对象。如果在一个大循环里对每个单元格重复设置边框,内存会被样式对象占掉一大截,文件也会变大。正确做法是只对表头这类固定区域设置样式,数据区除非必要,否则不要每个格子都设一遍边框。遇到这种需求,先把样式定义好,再复制给目标区域,比在循环里反复构造样式要节省得多。
4. 避坑排查:Tokyo 10.2 下最常见的五个翻车点与对症处理
4.1 安装后 IDE 找不到包,或提示 Package not found
现象:组件面板上什么都没出现,或者在打开工程时弹窗提示某个 BPL 找不到。再严重一点,IDE 启动到一半直接崩掉,关了重开还是一样。
原因:设计期包没有复制到 IDE 的包目录,或者系统里残留了旧版本的 BPL,IDE 按照库搜索顺序加载到了错误版本。大多数情况是装新包前没卸载旧包,旧版本遗留的 dcl 文件和新的运行时包版本对不上。
解决:先卸载 IDE 包列表里的所有 XLSReadWrite 相关条目,再把旧 BPL 从 IDE 包目录里清掉,然后重新执行第二章里的命令行编译安装流程。注意编译前一定要/t:Clean,否则旧时间戳的 DCU 可能不被重新编译。安装后重启 IDE 再打开工程验证。我自己的习惯是准备一个最小空工程专门做组件验证,每次安装任何包都先在这个空工程里引用一遍,确认通过再进正式工程。
4.2 中文字符变成问号
现象:从老系统里读入的 xls 文件,代码里取出的中文全是问号,或者空白。写出的 xlsx 在别的机器打开,中文正常,但用旧版 Excel 打开时乱码。
原因:xls 文件是 Excel 97-2003 格式时,内部并没有强制写清楚代码页。如果生成这个文件的系统是英文环境,没有把代码页信息写进 BIFF8 流里,读出来再按 Unicode 解析时就会出问题。写出的文件乱码通常是编码转换没做,把 AnsiString 直接塞进单元格,组件按 Unicode 处理后个别字符丢失。
解决:读入时,先确认老文件的原始编码。先在文本里声明目标代码页,或者在读完后立即对 AsString 做一次转换测试。如果确认是代码页缺失,最省事的办法是要求上游先转成 xlsx 或 CSV UTF-8 再交接。写出的文件,统一使用 UnicodeString 赋值,不要用 AnsiString。Delphi 10.2 默认字符串就是 Unicode,但如果你从老的第三方控件拿数据,它返回的可能是 AnsiString,赋值前用string强制转一下。
4.3 文件在 Excel 里提示损坏
现象:程序跑完,文件也生成了,双击打开后 Excel 弹“发现不可读取的内容”,问你要不要修复。点修复后内容还在,但每次都要走一遍这个过程,很影响体验。
原因:多数是 FileFormat 没有显式设置。组件内部默认的写入格式可能还是老的 BIFF8,也就是 xls 格式,但文件名扩展名是 .xlsx。Excel 2007 以后对“扩展名是 xlsx、内容却是 xls”的文件会直接报告损坏,因为它按 ZIP 容器标准去解包,结果打开失败。还有一种是单元格里写了 Excel 不允许的控制字符,比如 ASCII 0 到 8 的不可见字符,写进 XML 后也会被 Excel 判定为内容异常。
解决:写文件前显式设置FileFormat := xlExcel2007,这是根治方案。同时清理输入数据里的控制字符,写一个简单的过滤函数,把 Ord 值小于 9 的字符全部剔除。这样能避开九成以上的“文件损坏”提示。如果问题还在,用压缩工具打开生成的 xlsx,看看解包后是否能看到正常的 xl 目录结构,打不开或结构缺失说明生成流程有断点。
4.4 写入十万行内存失控
现象:导出十万行、几十列数据,程序内存占用一路从几十兆涨到几百兆,甚至接近 1 GB。导出完成后内存也没明显回落,导致同一进程里后续操作变慢。
原因:组件为了支持随机访问,单元格对象不会立即释放,而是挂在工作表的对象池里。你在循环里每访问一次 Cells,它都可能创建或保留一个内部对象,即使这个单元格只是临时读取。再加上如果每个单元格都设置了独立样式,样式对象又占一块内存,数据量一大就非常可观。
解决:先把“是否需要逐格设置样式”放一边。绝大多数报表数据区不需要独立样式,等整份数据写完,再统一设置列宽和表头格式。如果确实是数据量特别大,考虑按批导出:先写 5000 行到第一个工作表,写完即保存,再清空当前工作表对象并继续下一个工作表,避免单次工作簿里堆积太多单元格对象。
注意:任务管理器里的内存数字高,不代表内存泄漏。组件在 Free 之后如果内存能正常回落,就还在安全范围。真正要警惕的是连续多次导出后内存仍不归还,那就该查是不是你自己在代码里保留了全局引用。
4.5 写入时报文件被占用
现象:第一次运行程序导出成功,第二次再运行,WriteToFile 抛出“文件正在被另一进程使用”之类的异常。但检查了一遍代码,没发现哪里显式占用了文件。
原因:Excel 如果打开了同一个文件,Windows 的文件锁会让写入失败,这是最常见的情况。但有些时候不是 Excel,而是上面某次读取操作使用的 FileStream 没有释放。Delphi 内存管理不会因为对象脱离了作用域就立刻释放句柄,如果你在某段代码里用 TFileStream 或数据库组件打开了同一个文件没有关闭,后续写入就会撞锁。
解决:程序里统一遵守“读文件的组件负责关闭文件”原则。读取时,如果源文件不需要长期占用,先复制到临时目录再读,读完后立刻释放组件。写入时,如果目标文件存在,先尝试以独占方式打开做一次探测,确认没被占用再进行写入。这样至少在报错时能知道是谁占了文件,而不是干等着 Excel 用户把文件关掉。
5. 两个压箱底的验证技巧:可读性自检与 Excel 列号换算
5.1 用一行封装“读前自检”
在生成文件之后、交付之前,我习惯先用组件自己把文件读一遍,作为完整性自检。这个方法成本极低,但能拦下 4.3 里提到的文件损坏问题。封装成一个函数后,各处都能复用:
function XlsxIsReadable(const fn: string): Boolean; var tmp: TXLSReadWriteII6; begin Result := False; tmp := TXLSReadWriteII6.Create(nil); try tmp.Filename := fn; tmp.Read; Result := True; except // 组件已经在异常消息里说明了问题行列 end; tmp.Free; end;这个函数真正起作用的不是返回值,而是异常消息。组件在 Read 时如果遇到无法解析的结构,会在异常文本里给出大致的位置信息。我在批量导出任务里,每个文件写完后都调用一次自检,失败时记录文件名和异常文本,交付前就能把问题文件筛出来,而不是等用户打开才发现打不开。
5.2 把 Excel 列字母换算成单元格下标
调试了一半才发现边界问题,源文件里明明是 B 列,我代码里却写成了 Cells[2, r],整整偏了一列。后来我写了一个小函数,专门把 Excel 的列字母转成组件里的列下标:
function ExcelColToIdx(const colName: string): Integer; var i: Integer; begin Result := 0; for i := 1 to Length(colName) do Result := Result * 26 + (Ord(UpCase(colName[i])) - Ord('A') + 1); end;逻辑很简单:从字符串最左边开始,按 26 进制累加。比如“B”会得到 2,但组件下标从 0 开始,所以真要传给 Cells 时还要减 1:“A”是 1,对应的 Cells 下标是 0。这也解释了我之前犯的错误——Excel 是 1 基坐标,组件是 0 基坐标,中间的偏移量经常被忽略。这个函数可以放在一个公共工具单元里,调试时直接打印出坐标映射,省得每次手算。
这个坑我吃过两次。第一次因为偏移量错位,整张报表的列对不上,上线后才发现;第二次是懒得验证直接复用旧版本包,IDE 坏了重装一遍。从那以后,每次拿到新的 6.00.16 组件包,我都强制自己按固定顺序走一遍:解压后先读目录结构、命令行编译两个平台、用最小工程做冒烟验证,最后再进正式代码。这套流程看着多花了十几分钟,实际上比瞎试一晚上省得多。希望这篇记录对你也有帮助。
本文还有配套的精品资源,点击获取