资讯详情

Delphi原生读写Excel:XLSReadWriteII组件避坑指南

📅 2026/11/5 5:54:22 | 华诺云谱 👁 阅读
Delphi原生读写Excel:XLSReadWriteII组件避坑指南
简介XLSReadWriteII v6.0.0.47 是面向 Delphi 开发者的 Excel 文件读写组件兼容 D7 至 D10.3 Rio适合需要在桌面应用中直接生成、解析和修改 Excel 工作簿的开发者。它不依赖本机安装 Excel内置面向单元格值优化的内存管理器并提供 Direct Read 与 Direct Write 模式便于处理超出内存容量的大型文件。压缩包为 rar 格式共 596 个文件约 2.59MB以 pas 源码、html 帮助文档、dfm 窗体、dpr/dproj 工程文件为主另有示例配置、图片和资源文件目录结构便于按示例与文档检索。已有 302 人学习。随包提供完整源代码便于集成、调试与二次开发示例覆盖图表、自动筛选、交互式透视表等场景可帮助读者理解 Excel 读写流程、稳定性设计与大文件处理思路。1. Delphi 存量项目要读写 Excel我最后留下了这套组件场景很具体一台不能装 Office 的生产机一个还在维护的 D7 工程财务每周要一份带表头样式和日期列的 Excel 报表。OLE 自动化那条路在这里直接走不通机器上根本没有 Excel 进程可以调。XLSReadWriteII 这类原生读写库就是为这种场景存在的——它自己解析 BIFF 和 OOXML 结构不依赖 Office、不启动外部进程直接生成 .xls 或 .xlsx。这次拆的 v6.0.0.47 覆盖 Delphi 7 一直到 10.3 Rio同一套源码要同时照顾 AnsiString 和 UnicodeString 两个时代的编译器这个跨度本身就是它最大的价值也是大部分坑的来源。适合还在维护 Delphi 存量系统、需要稳定导出 Excel 报表的团队如果你只在新版 Delphi 上做一次性小导出、能接受 CSV那没必要上这套。2. 装进 IDED7 与 D10.3 Rio 两套包怎么共存2.1 先搞清楚它替代了什么很多项目一开始都是用 OLE 自动化CreateOleObject(Excel.Application)导出报表的写起来顺手出问题的时候很难排查。我在三个不同项目里踩过同一类问题最后都换成了原生库第一是运行环境依赖。OLE 方案要求目标机器装了 Excel而且版本要匹配。客户的瘦客户端、Windows Server 上跑的服务、偶尔的绿色版 Office任何一个环节不合规导出就直接抛异常而且异常信息通常只有一句未指定的错误。第二是进程残留。哪怕你老老实实写了 Quit 和 FreeAndNil只要中间有一句抛异常Excel.exe 就挂在后台。跑一天定时任务任务管理器里能堆几十个 Excel 进程最后内存被吃干净。这是 OLE 的黑匣子特性你没法从代码层面彻底兜住。第三是并发和权限。服务账号以非交互方式运行时OLE 调用会因为桌面会话缺失而失败这类问题在部署到正式环境之前基本发现不了。维度OLE 自动化原生读写库运行依赖必须安装 Excel无纯 DLL/静态链接导出 1 万行耗时秒级到十几秒随版本波动通常百毫秒级进程残留风险高无外部进程服务/无桌面环境不可用可用格式控制粒度靠 Excel 对象模型全但慢常用格式够用2.2 源码包目录与编译安装拿到的是一个源码分发包解压后常见的目录结构是Source.pas 单元、Packages.dpk 设计期/运行期包、Demos示例工程、HelpAPI 文档。安装的核心就两步让编译器找得到单元让 IDE 加载到组件页。具体操作把解压目录放到一个不含空格、不含中文的路径下比如D:\libs\xlsrw\。D7 时代留下来的老工具链对中文路径的容忍度很差这一步省不掉。打开 IDE在 Tools → Environment Options → Library 里把D:\libs\xlsrw\Source加进 Library Path。注意 D7 只有一份全局 Library Path而 Rio 是按 Win32/Win64、Debug/Release 分开的需要分别配置。在 D7 里打开 Packages 目录下对应 D7 的那个 .dpk先 Build 再 Install在 Rio 里打开对应 10.3 的 .dpk同样先 Build 再 Install。两套包不能混用因为 RTL 版本不同接口里的 string 语义也不一样。安装成功后组件面板会多出一个页签里面是 TXLSReadWriteII5 之类的组件。如果面板上没出现但编译能过说明是运行期包装好了、设计期包没装回去检查 .dpk 里的 Register 过程有没有被编译进去。提示v6 的几个小版本之间单元名和包名偶有差异有的叫 XLSReadWriteII6不要凭记忆写 uses装完之后去 Source 目录dir *.pas看一眼最稳。2.3 单元引用与最小可运行例子装好之后先跑一个二十行的例子确认整条链路通再往工程里塞业务代码。下面这段是生成一个两列小表并存成 .xls 的最小实现uses XLSReadWriteII5; // 单元名以安装目录 Source 下实际文件为准 procedure TForm1.MakeMiniReport; var XLS: TXLSReadWriteII5; begin XLS : TXLSReadWriteII5.Create(nil); try // 索引 0 是第一个工作表空表时先补一个 if XLS.Sheets.Count 0 then XLS.Sheets.Add; XLS.Sheets[0].Name : 日报; XLS.Sheets[0].AsString[0, 0] : 日期; // 列, 行 —— 注意是 0 基 XLS.Sheets[0].AsString[1, 0] : 产量; XLS.Sheets[0].AsDateTime[0, 1] : Now; XLS.Sheets[0].AsFloat[1, 1] : 1234.5; XLS.Filename : D:\out\daily.xls; XLS.Version : xvExcel8; // BIFF8对应 .xls XLS.Write; finally XLS.Free; end; end;逻辑说明整个过程是建对象 → 建表 → 逐格赋值 → 指定文件名和格式 → 一次性 Write。所有单元格操作都在内存里完成Write 之前磁盘上什么都没有所以中间任何一步抛异常都不会留下半截文件这一点比 OLE 方案省心很多。参数说明AsString[Col, Row]的两个下标是列在前、行在后和 Excel 里 A1 的直觉顺序相反这是新手最容易写反的地方Version决定最终写出的二进制格式xvExcel8是 BIFF8要出 .xlsx 就选枚举里对应 OOXML 的那一项常量名在不同小版本里不一致打开 .pas 看一眼 TXLSVersion 的声明最快Filename的扩展名要和Version对得上否则 Excel 打开时会提示文件格式与扩展名不一致。3. 对象模型拆解Sheet、Cell 与 0 基索引3.1 三层结构和访问路径这套组件的对象模型很扁就三层最上面是 TXLSReadWriteII5代表整个工作簿管文件名、格式版本、读写动作中间是 Sheets 集合每一项是一个工作表管名字、可见性、行列尺寸最下面才是单元格通过工作表上的索引器直接访问不强制你持有 Cell 对象。要建立的心智模型是工作簿是一张二维数组的集合而不是一堆对象树。绝大多数操作用XLS.Sheets[i].AsXxx[Col, Row]这一条路径就能完成不需要 Create 任何子对象也不需要手动释放。这一点对从 Excel COM 迁过来的同学很不习惯——那边每个 Range、每个 Worksheet 都要 Release这边完全不用管。关于索引只有一条铁律列和行都从 0 开始。Excel 界面上的 A1 单元格对应代码里的[0, 0]界面上第 5 行是 A5代码里是[0, 4]。我在三个项目里见过同一个 bug循环写成for Row : 1 to Count do结果整张表往下错一行最后一行还丢了因为数据只写了 Count-1 行。解决办法是统一在数据源层就转成 0 基别在写 Excel 的地方做 1/-1 的算术。注意下面出现的属性名以安装目录 Source 下的 .pas 接口声明为准。v6 的几个小版本之间偶有重命名思路和参数含义不变遇到编译不过先去看接口。3.2 写一张带表头样式的报表业务代码里真正麻烦的从来不是写数据是写得能看。表头加粗、列宽合适、数字有千分位、日期显示成 yyyy-mm-dd这四件事占了导出模块八成的工作量。procedure TForm1.WriteStyledReport(const Rows: Integer); var XLS: TXLSReadWriteII5; Sheet: TXLSSheet; Col, Row: Integer; begin XLS : TXLSReadWriteII5.Create(nil); try if XLS.Sheets.Count 0 then XLS.Sheets.Add; Sheet : XLS.Sheets[0]; Sheet.Name : 产量日报; // 表头加粗 居中 Sheet.AsString[0, 0] : 日期; Sheet.AsString[1, 0] : 产线; Sheet.AsString[2, 0] : 产量; for Col : 0 to 2 do begin Sheet.Cell[Col, 0].Font.Bold : True; Sheet.Cell[Col, 0].HorizAlignment : haCenter; end; // 数据区日期列指定显示格式数值列用千分位 for Row : 1 to Rows do begin Sheet.AsDateTime[0, Row] : Date - Row 1; Sheet.AsString[1, Row] : L IntToStr(Row mod 3 1); Sheet.AsFloat[2, Row] : 800 Random(400); end; // 列宽单位是字符宽度不是像素 Sheet.ColWidth[0] : 12; Sheet.ColWidth[1] : 8; Sheet.ColWidth[2] : 10; XLS.Filename : D:\out\report.xls; XLS.Version : xvExcel8; XLS.Write; finally XLS.Free; end; end;逻辑说明先铺表头再循环数据最后统一调列宽顺序不能反——列宽是按列索引设的跟数据无关但放最后可以避免中间插入列时被覆盖。样式设置用的是Cell[Col, Row]这条路径取到单元格对象后再改 Font、对齐这些子属性值的读写走AsXxx索引器两条路径指向同一个格子混用没问题。参数说明ColWidth的单位是标准字符宽度12 大约等于能放下 12 个数字不是像素所以设成 30 也宽不到哪去别按像素思维调HorizAlignment的枚举值有 haLeft/haCenter/haRight 等不同版本可能多一两个值用常见的三个够了。日期列如果只写AsDateTime而不设显示格式Excel 会按默认格式渲染可能显示成2024/1/1 0:00:00这种带时间的样式正式报表里要单独设 NumberFormat。3.3 读文件时的类型判定顺序读比写容易出问题因为你不知道对面那个文件是谁生成的、里面存的是什么类型。常见的做法是按字符串 → 日期 → 数值的顺序逐格判定而不是无脑 AsFloat。procedure TForm1.ReadReport(const FileName: string); var XLS: TXLSReadWriteII5; Sheet: TXLSSheet; Col, Row, LastRow: Integer; S: string; begin XLS : TXLSReadWriteII5.Create(nil); try XLS.Filename : FileName; XLS.Read; // 读失败会抛异常外面要 try Sheet : XLS.Sheets[0]; LastRow : Sheet.LastRow; // 有的版本叫 LastCellRow for Row : 1 to LastRow do begin S : Trim(Sheet.AsString[0, Row]); if S then Continue; // 空行跳过别当数据 Memo1.Lines.Add(Format(%s | %s | %.2f, [S, Sheet.AsString[1, Row], Sheet.AsFloat[2, Row]])); end; finally XLS.Free; end; end;逻辑说明Read是一次性把整个文件解析进内存之后所有访问都是内存操作所以循环里怎么读都不慢。空行判定一定要做因为 Excel 用户习惯在表格末尾敲几个回车LastRow会把它们算进去不判空就会多出一堆空记录。参数说明AsString对数值单元格返回的是格式化后的文本受 NumberFormat 影响对日期单元格返回的是日期字符串所以不要用 AsString 的结果再做数值计算会踩到千分位逗号和小数点。要数值就老老实实 AsFloat要日期就 AsDateTime。另外Read遇到损坏文件会直接抛异常调用方必须包 try否则一个坏文件能把整个批处理任务带崩。4. xls 还是 xlsx格式、日期与中文的三条分水岭4.1 保存格式由 Version 决定扩展名只是外壳这是最容易踩的一个认知坑很多人以为把 Filename 改成 .xlsx 就是 xlsx 了。实际写出的二进制格式完全由Version属性决定扩展名只是文件名的一部分。如果 Version 还是 BIFF8文件内容就是 .xls 结构Excel 打开时会弹文件格式与扩展名不一致的警告。维度xlsBIFF8xlsxOOXML单表行数上限655361048576体积较小压缩包结构通常也小老版本 Excel 兼容全兼容2007 以前打不开生成速度略快略慢需打包 zip 结构复杂格式保留稳定稳定但部分老特性映射有损适合场景内网老系统、行数可控对外交付、大数据量选型建议很实在数据量稳定在几万行以内、交付对象是老系统就 .xls省事一旦有超过 65536 行的可能必须 .xlsx不然写到第 65537 行会直接异常或者静默截断。我一般会在导出前先算一下行数超过 6 万就强制切 xlsx并且把这条判断写成断言跑测试的时候能立刻发现。注意切换 Version 之后要回归一遍样式。同一个 Font、同一个 NumberFormat 字符串在两种格式里的映射不完全一致尤其是自定义格式串和条件格式。4.2 日期不是日期是浮点数Excel 内部没有日期类型日期就是一个浮点数整数部分是从 1900-01-01 起的天数小数部分是当天的时间比例。所以AsDateTime写进去的其实是这个序列值显示成什么样完全取决于单元格的 NumberFormat。这解释了两个现象。第一为什么有些工具读出来的日期是 45000 这样的数字——它没解析格式直接把底层值给你了。第二为什么你明明写了日期打开却是数字——NumberFormat 没设。正确的做法是值用 AsDateTime 写格式单独设// 值为 TDateTime 序列值显示格式单独指定 Sheet.AsDateTime[0, Row] : IncDay(Date, -Row); Sheet.Cell[0, Row].NumberFormat : yyyy-mm-dd; // 数值列千分位 两位小数 Sheet.AsFloat[2, Row] : 12345.6; Sheet.Cell[2, Row].NumberFormat : #,##0.00;格式串的语法跟 Excel 自定义格式完全一致可以直接抄 Excel 里设置单元格格式看到的那串。要提醒的是格式串本身是字符串在 D7 里是 AnsiString在 Rio 里是 UnicodeString如果格式串里混了中文比如0.00元两个编译器的处理不一样最好只写 ASCII。还有一个隐蔽的点1900 和 1904 两种日期系统。Windows 版 Excel 默认 1900Mac 老版本默认 1904两者相差 1462 天。组件默认按 1900 处理如果你要读的文件来源不明读出来的日期整体差四年先怀疑这个。4.3 中文与两个编译器时代的字符串v6 号称一套源码支持 D7 到 10.3 Rio这句话的代价就体现在字符串上。D7 的string是 AnsiString按系统代码页编码Rio 的string是 UnicodeStringUTF-16。同一个.pas在两个编译器下SizeOf(Char)一个 1 一个 2。组件内部做了兼容但你的业务代码未必。血泪经验有三条其一凡是把字符串传给 Windows API 或者写文件的地方D7 下用 PAnsiCharRio 下用 PWideChar别用裸 PChar 跨版本。更省事的办法是用TEncodingRio 有D7 没有或者干脆避开全部走组件提供的 AsString 接口让库自己去处理转换。其二中文路径。D7 下XLS.Filename接受 AnsiString系统代码页是 GBK 时中文路径没问题但如果程序跑在英文系统上代码页不是 936中文路径就会变成乱码甚至找不到文件。稳妥做法是把导出目录配成纯英文加数字路径里出现中文迟早出事。其三读取别人生成的文件时如果内容是繁体或者生僻字AsString 返回的可能是问号。这种情况先确认文件的 BIFF 版本和代码页记录再确认编译目标最后才怀疑组件本身——我遇到过的几次全都是源文件保存时被转成了 ANSI 丢字。5. 避坑排查五个让我返工的现场问题5.1 编译期报找不到单元现象装完包、组件面板上也能看到组件一拖到 Form 上就报File not found: XLSReadWriteII5.dcu或者提示某个依赖单元找不到。原因有两个。一是 Library Path 只加到了 IDE 的搜索路径但工程自己的 Search Path 里有更早版本的旧路径编译器优先命中了旧目录。二是这个包依赖同目录下的几个基础单元比如公共类型、压缩相关只加了 Source 根目录没加子目录。解决先看编译输出里那个实际去找的完整路径多半能一眼看出问题。然后把工程选项里的 Search Path 清空统一由 IDE 全局路径提供如果 Source 下有子目录用递归加通配的方式一次性加进去。最后做一件很多人懒得做的事——Clean 一次再 BuildDelphi 的增量编译对 .dcu 的依赖判断不算可靠。5.2 打开文件提示格式与扩展名不一致现象代码跑完没报错文件也生成了双击打开时 Excel 弹警告点是之后内容能显示但用户不敢用。原因就是 4.1 说的Filename 是 .xlsx 而 Version 还是 BIFF8或者反过来。也有一种情况是版本枚举设对了但文件是从旧版本复制过来的模板内部结构本来就是老的。解决把 Version 和扩展名的对应关系写成一个函数导出时只调这个函数不手写扩展名。另外在写完之后加一步自检重新用组件 Read 一遍刚生成的文件能读通再认为成功。这一步多花几十毫秒能挡掉九成的格式错配。5.3 整张表错位一行一列现象数据都写进去了但整体往下偏一行、往右偏一列A1 永远空着最后一行的数据还丢了。原因只有一个索引基准搞混。Excel 界面是 1 基组件是 0 基中间某处做了 1 但循环边界没跟着改或者数据源是从 1 开始的数组直接透传。解决在数据层和写表层之间加一个明确的边界数据层统一 0 基写表函数里禁止出现任何 ±1 的算术。我一般的做法是写一个WriteCell(Col, Row: Integer)的内部方法所有写入必须过它方法里加一句断言Assert((Col 0) and (Row 0))越界立刻炸而不是安静地错位。5.4 日期列显示成 45000 这类数字现象代码里明明赋的是Now打开文件看到的是一串五位数字。原因是只写了值没写格式。AsDateTime 存的是 TDateTime 的序列值单元格的 NumberFormat 是常规Excel 就按数字显示。解决值走 AsDateTime格式走 NumberFormat两件事分开做不要指望组件自动推断。如果文件是给第三方系统读的再确认一下对方是读显示值还是读底层值——读底层值的系统不认格式串这时候应该把日期格式化成字符串写进去而不是写 TDateTime。5.5 大表导出卡死或内存飙升现象一千行秒出五万行跑了两分钟还没结束任务管理器里内存一路涨到几个 G。原因通常不是组件慢是写法问题每写一格就去访问一次Sheets[0]每次都要过一层索引查找或者在循环里反复设同一个样式对象或者开了公式自动重算每写一格全表重算一遍。解决把 Sheet 对象取到局部变量里循环内只用局部变量访问样式在循环外设一次或者先设好第一行再整列复制格式数据量大时先关掉自动重算全部写完再统一触发一次。另外数据源那边尽量一次查完放进数组别在写表的循环里查数据库——那种写法在测试环境看不出问题到生产环境数据库一慢整个导出就雪崩。6. 进阶封成一个单元再用第二种工具回读验证导出逻辑散在十几个 Form 里是这类项目最常见的技术债。我一般会抽一个TReportExport出来构造函数只接文件名和格式对外只暴露AddHeader、AddRow、Save三个方法把 Sheet 引用、列宽、格式串全部封在内部。这样换组件版本的时候只改一个文件不会牵一发动全身。封装之后有一个必须补的环节验证。自己写的文件自己读读通了不算数因为读写用的是同一套解析逻辑一起错的时候看不出来。我的做法是用第二种工具回读——Python 的 openpyxl 足够装上就能用from openpyxl import load_workbook # data_onlyTrue 读的是公式缓存值没有缓存会得到 None正好用来验公式 wb load_workbook(rD:\out\daily.xlsx, data_onlyTrue) ws wb.active assert ws.title 日报, ws.title assert ws[A1].value 日期 assert ws[B1].value 产量 assert abs(float(ws[C2].value) - 1234.5) 1e-6, ws[C2].value print(rows, ws.max_row, cols, ws.max_column)这段脚本跑通说明文件结构、单元格值、工作表名都对得上不是只有自家的库能读的私有格式。data_onlyTrue这个参数是关键它读的是公式的计算结果缓存如果导出时写了公式却没写缓存值这里会拿到 None而自家组件回读往往不报错——这就是交叉验证的价值所在。我固定会跑三条自检行数对不对拿数据库 count 比、首末行的关键字段对不对防错位和截断、随机抽三格的值对不对防类型串位。三条都过这个导出才敢上生产。说个教训。早些年我图省事导出完只看文件大小不为零就交付结果一次版本升级后所有日期列变成数字业务方用了两周才发现回头补数据补了三天。从那以后我每次改导出逻辑都强制先跑一遍 openpyxl 回读再提测宁可在本地多花五分钟。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑