news 2026/9/26 4:34:21

ExpressSpreadSheet v1.38源码编译与Delphi集成实战笔记

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ExpressSpreadSheet v1.38源码编译与Delphi集成实战笔记

简介:DevExpress.ExpressSpreadSheet v1.38 源代码是一套面向 Delphi 与 C++ Builder 开发者的完整电子表格组件源码,用于在桌面应用中集成类似 Excel 的数据处理、公式计算与可视化能力,并支持按项目进行二次定制。压缩包共 382 个文件,大小约 2.81MB,其中 83 个 res 资源文件保存图标与字符串资源,73 个 pas 与 38 个 cpp 构成核心逻辑,45 个 dpk 与 24 个 bpk 为工程定义文件,另有 dfm 窗体布局、h 头文件及 FeaturesDemo、CustomFunction 等示例项目,目录结构按功能模块划分,便于定位与学习。随包配套的 ExpressGDI+ 绘图库和 XP Theme Manager 模块,有助于理解电子表格渲染与系统外观适配的实现细节。目前已有 445 人浏览学习,可依据示例工程快速验证组件能力。对希望深入理解 DevExpress 架构、调试复杂表格逻辑或独立扩展功能的开发者而言,这份源码提供了直接的一手参考,也能有效提升 Delphi 高级组件开发与排错能力,尤其适合有一定基础并想做深度定制的开发者。

1. 为什么要拿到 ExpressSpreadSheet v1.38 源码:Delphi 表单里的 Excel 有多难替代

接手老项目最难的往往不是业务逻辑,而是把一套十年没动过的控件源码在新环境下重新编译起来。我最近就在折腾 DevExpress 的 ExpressSpreadSheet v1.38 for Delphi/BCB 源代码包。它的用途很直接:在 Delphi 表单里放一块像 Excel 的区域,能点选、能写公式、能存文件。你如果也在维护这类项目,或者想给桌面工具加自定义报表,这份源码的价值比闭源安装包大得多,因为所有实现细节都能在源码里直接搜到。不过拿到源码不等于能跑,这个版本太老,.dpk 的目标平台和本机 IDE 经常对不上。接下来我按自己拆包时的顺序写:目录、编译、用法、踩坑。

2. 源码包的目录结构与编译顺序:三条规则解决 80% 的“找不到单元”报错

2.1 先看目录,再双击 .dpk

解开这种老包的第一时间,别急着双击 .dpk。先花十分钟把目录结构看一遍,后面能少走很多弯路。我拆过的 DevExpress 老控件包,典型布局差不多是这样:

目录里面是什么安装时的角色
Sources.pas / .cpp 源码,控件本体编译时的单元搜索路径,必加
Lib预编译的 .dcu / .bpl只用来对照,别直接引用,平台可能不匹配
Demos / Examples示例工程查 API 写法的活字典,优先级最高
Packages.dpk / .bdsgroupIDE 的安装入口,决定编译顺序
Common公共单元很多包共享,漏了必报错

这套包是给 Delphi 和 BCB 双平台用的,所以 Sources 里往往同一份逻辑同时面向两个编译器。.dpk 工程给 Delphi 用,.bdsgroup 或 .bpg 给 BCB 用。先搞清楚哪个是主线、哪个是辅助,再动手。

判断依据很简单:凡是出现在 .dpk 的 contains 清单里的文件,都必须在 Sources 或 Common 里能被找到。我第一拆的时候贪快,直接打开主包点 Compile,结果报了一屏“Cannot find unit”,后来发现是没往工程属性里加 Sources 路径,纯属自找。

提示:先看包内的 requires 依赖,再看目录。顺序反了,后面每一步都在跟搜索路径搏斗。

2.2 编译顺序怎么定:看 Requires,而不是猜

老包之间的依赖关系不在 README 里,就在 .dpk 的 requires 段里。打开任意一个 .dpk,看它 requires 了哪些包名,这些被依赖的包必须先生成 .bpl 和 .dcp,否则反着编,高层包装完,底层 .dcp 还没生成,Link 时直接挂。

我一般会先把所有 .dpk 按依赖关系拉一个顺序,再用命令行批量编译,比在 IDE 里一个个点快得多,也容易复现。下面是常见做法:

# 进入源码根目录,假设包结构是 Packages 下放 .dpk cd Packages # 1) 编译底层核心包,先不安装 dcc32 -B -Q -U..\Sources;..\Common -LE..\Lib -LN..\Lib dxSpreadSheet_Core.dpk # 2) 编译上层功能包 dcc32 -B -Q -U..\Sources;..\Common -LE..\Lib -LN..\Lib dxSpreadSheet_DB.dpk # -B 全量重建,忽略旧的 .dcu,防止旧产物干扰 # -Q 安静模式,编译信息不刷屏,出错再看日志 # -U 单元搜索路径,分号分隔;Sources 和 Common 都要在 # -LE 编译出的 .bpl 输出目录,IDE 加载包时从这里找 # -LN 编译出的 .dcp 输出目录,后续包靠 .dcp 找接口

这里值得多说两句的是 -LN。.dcp 是 Delphi 编译包时生成的接口描述文件,后面的包通过它引用前面包里的单元。如果 -LN 没指到同一个目录,过一会儿就会报“Unit xxx was compiled with a different version of yyy”。我遇到的最典型场景就是 -LE 和 -LN 都缺省,.dcp 散落在和 .pas 同级的目录里,项目一换路径全爆。

命令行的包名不一定是 dxSpreadSheet_Core.dpk,以你手里源码包的实际情况为准。我在拆 v1.38 时发现包名和目录名对不上,差点把底层包当高层包装了。最稳妥的办法是把 .dpk 当文本文件打开,直接搜 requires 关键字,依赖关系一目了然。

2.3 单元搜索路径:加到全局 Library path,而不是单个工程

源码包里的 .pas 文件必须能被编译器找到,否则依赖再多也是白搭。我建议直接把 Sources 和 Common 加进 IDE 的全局库路径,而不是每个工程各自加。原因很简单:包编译和工程编译共享同一套单元搜索路径,写在全局,所有工程和包都受益。

在 Delphi 里操作路径大致是这几步:

  1. 打开 IDE 的 Tools → Options。
  2. 找到 Environment Options → Delphi Options → Library。
  3. 在 Library path 里追加 Sources 和 Common 两个目录。
  4. 确认 BPL 输出目录和 DCP 输出目录都指向包目录下的 Lib。

对 BCB 用户,入口和配置方式差不多,只是 C++ Builder 的 Library 配置界面里多了头文件路径。注意 BCB 侧解析头文件的顺序和 Delphi 不同,Sources 里如果有 .hpp 和 .pas 同名,容易把 IDE 绕晕。常见做法是让 Delphi 先编译一遍生成 .dcu 和 .dcp,再由 BCB 引用编译结果,而不是让 BCB 直接啃 .pas,这样能省掉一半的玄学报错。

2.4 Compile 和 Install 的区别:先 Build 全部,最后 Install 设计期包

很多人把 Compile 和 Install 混在一起,这是老包安装最大的误区。Compile 只生成 .bpl 和 .dcp;Install 才会把控件注册进 IDE 工具面板。对依赖链很长的包,正确顺序是按依赖关系把所有包 Compile 完,最后再 Install 那些真正对应控件的设计期包。设计期包决定工具面板里出现哪些控件,运行期包永远不应该被 Install。

如果我在 IDE 里手工操作,会这样做:

  1. 打开底层包的 .dpk,右键 Compile,等状态栏出现 Success。
  2. 按依赖顺序重复,直到所有运行期包编译通过。
  3. 打开名字里带 Design 或 Dsgn 的 .dpk,右键 Install,再等工具面板刷新。

这一步翻车率很高,因为很多包在 Install 时要求底层的 .bpl 已经在系统 PATH 或 IDE 的 Load path 里。如果 Compile 全过了,Install 却报“A required package is not found”,多半是 -LE 输出的 .bpl 目录没被 IDE 认到。我的习惯是把 Lib 目录加进系统 PATH,或者用 IDE 的 BPL output 指过去,一劳永逸。

2.5 顺手验证:用示例工程做冒烟测试

包装没装对,光看编译日志不算数。源码包一般都带示例工程,这是最好的冒烟测试。打开一个和 SpreadSheet 相关的 Demo,直接编译运行;能起来,说明包链没有断。我通常挑一个带网格、带工具栏的示例,因为覆盖面够大。如果示例编译报错,先看错误信息里第一个找不到的单元,按路径回推搜索路径漏了哪一项。这条路径比新建空工程更靠谱,因为示例里用的都是作者实测过的 API 组合,不会因为属性名记错而误导你。

3. 快速搭一个可运行的 SpreadSheet 表单:拖控件到 120 行代码存出可读台账

3.1 在窗体上放好 TdxSpreadSheet:先确认类定义,再写代码

安装成功后,在工具面板里搜 Spread 就能找到 TdxSpreadSheet。把它拖到窗体上,我一般第一件事是设Align := alClient,让网格占满整个客户区。想带一点工具栏,可以在窗体上方再放一个配套的工具栏控件;不过 v1.38 时代这类组件在不同版本里名字差异很大,最稳妥的办法是打开 Demos 工程看看作者自己怎么摆的。

真正的关键是去源码里确认 v1.38 的入口属性。老包的公开接口和你印象里的新版往往有出入,与其背属性名,不如直接在 Sources 里搜TdxSpreadSheet = class,把类定义拉出来看。published 属性就是 IDE 对象检查器能显示的,名称和顺序不会骗人。这一步花五分钟,后面少走两小时弯路。

放好控件后,写一个最简单的 FormCreate:

procedure TMainForm.FormCreate(Sender: TObject); begin // 确保至少有一个工作表,行列给个合理起点 dxSpreadSheet1.SheetCount := 1; dxSpreadSheet1.ActiveSheet.ColCount := 20; dxSpreadSheet1.ActiveSheet.RowCount := 100; // 显示网格线,观感更接近 Excel dxSpreadSheet1.ActiveSheet.OptionsView.GridLines := True; end;

SheetCount在部分版本里写成Worksheets.Count或Sheets.Count,以你搜到的类定义为准。ColCount和RowCount表示网格区域大小,从 0 到 N-1,不是“额外再留一行”。OptionsView.GridLines只控制网格线绘制,不影响数据。老版本里它可能挂在Options下,IDE 自动补全会告诉你确切位置。

3.2 用代码填充单元格:Value、Formula 和 0 基下标的规则

网格摆在窗台上只是空壳,能把数据填进去才是关键。v1.38 里单元格对象的核心属性我一般只用三个:Value放显示值,Formula放公式,DisplayText拿渲染后的文本。下标规则是列在前、行在后,从 0 开始;A1 就是Cells[0, 0]。

下面这段是典型的写入操作:

procedure TMainForm.ButtonFillClick(Sender: TObject); var S: TdxSpreadSheetSheet; // 当前活动工作表 begin S := dxSpreadSheet1.ActiveSheet; // 表头:A1、B1、C1 S.Cells[0, 0].Value := '产品名'; S.Cells[1, 0].Value := '数量'; S.Cells[2, 0].Value := '单价'; // 数据行:从第二行开始 S.Cells[0, 1].Value := '螺丝'; S.Cells[1, 1].Value := 200; S.Cells[2, 1].Value := 0.5; // 公式:数量 * 单价 S.Cells[3, 1].Formula := '=B2*C2'; end;

Value是 Variant 类型,传字符串和数字都可以,控件内部会按列类型处理。数字不要加引号,否则会被当字符串存进去。Formula必须以等号开头,公式里走的是 Excel 风格 A1 引用,和 Cells 下标不是同一个体系,这是很多人的第一个认知冲突点。写完公式后界面没显示结果,多半是重算没开,第 4 章会专门说。

注意:公式里的 A1 引用和 Cells 下标是两套坐标系,先确认示例工程里的写法,再大规模填公式。

3.3 把表格存成文件:LoadFromFile 与 SaveToFile 的参数细节

台账类工具最核心的功能是持久化,不然用户敲了半天,关窗体数据全没了。v1.38 的 TdxSpreadSheet 提供文件读写能力,常见写法是这样的:

// 导入旧版 Excel 97-2003 文件 dxSpreadSheet1.LoadFromFile('D:\data\import.xls', sfxXLS); // 导出成同一格式的文件 dxSpreadSheet1.SaveToFile('D:\out\export.xls', sfxXLS);

第一个参数是完整路径,第二个参数是文件格式枚举。sfxXLS表示 Excel 97-2003 格式;如果读到的源码里这个枚举不叫这个名字,在 Sources 里搜TdxSpreadSheetFileFormat就能列出所有候选值。这种老包大概率不支持新版 XLSX 的高阶特性,保存时优先选 XLS,兼容性最好。

还有两个很实用的操作是剪贴板。用户常在网格上选中一块区域,复制到 Excel 再粘贴,体验很顺。对应方法是CopyToClipboard和PasteFromClipboard。具体签名在不同版本里差得比较多,以示例工程为准;最常见的用法是在 FormKeyDown 里按 Ctrl+C 复制、Ctrl+V 粘贴:

procedure TMainForm.FormKeyDown( Sender: TObject; var Key: Word; Shift: TShiftState); begin // 粘贴前先判断是否带 Ctrl if (Key = Ord('V')) and (ssCtrl in Shift) then dxSpreadSheet1.PasteFromClipboard; if (Key = Ord('C')) and (ssCtrl in Shift) then dxSpreadSheet1.CopyToClipboard; end;

这个做法适合做数据录入台。注意PasteFromClipboard在部分老版本里只处理 Tab 分隔的纯文本,从 Excel 直接复制的带格式内容可能被简化成纯文本,这属于正常现象,不是 bug。

3.4 行高列宽与网格观感:把默认值调成 Excel 的样子

网格刚拖出来时,行高列宽和 Excel 默认的差很远。我一般会在初始化里把列宽和行高设到接近 Excel 的观感,否则用户第一次打开会以为软件坏了。相关设置通常挂在OptionsView下:

// 默认列宽 12 个字符,行高 20 磅 dxSpreadSheet1.ActiveSheet.OptionsView.DefaultColWidth := 12; dxSpreadSheet1.ActiveSheet.OptionsView.DefaultRowHeight := 20;

DefaultColWidth的单位是老版本的“字符数”或“点”,给 12 左右看起来像 Excel 默认列宽。DefaultRowHeight是高度,给 20 比较合适。如果示例工程的观感更好,直接抄示例里的数值。这个设置只影响默认尺寸,单独调整过的列不受影响。

4. 把核心能力拆开看:公式引擎、样式与事件的三类边界条件

4.1 公式引擎:自动重算什么时候开,什么时候关

ExpressSpreadSheet 的公式引擎不是摆设,但重算时机经常让人迷糊。表现就是 Formula 属性写进去了,界面还显示 0 或空。我踩过的原因有两个:自动重算没开,或者重算不是每改一格就立刻发生。

先看常见配置:

// 打开自动重算,单元格变化后自动更新 dxSpreadSheet1.OptionsBehavior.AutoCalc := True; // 改数据后界面没刷新,再手动算一次 dxSpreadSheet1.Calculate;

OptionsBehavior.AutoCalc在部分版本里不叫这个名字,搜 AutoCalc 或 AutoRecalc 能找到。Calculate也一样,有的版本叫Recalc。如果不想让用户每输入一个格子就触发一次全表重算,可以在批量导入时先把 AutoCalc 关掉,全部写完后开回去再手动 Calculate,性能差距非常明显。

这里多说一条血泪经验:千万别在非主线程里调Calculate。我做过一个批量导入,为了不卡界面把计算丢进后台线程,结果界面没崩,数据和公式对不上,查了半天发现 VCL 控件不是线程安全的。正确做法是在工作线程里解析出纯数组,回到主线程再写进表格,最后统一 Calculate 一次。

4.2 循环引用与跨工作表引用:老引擎的容忍度有限

公式引擎的边界通常在两个地方露馅:循环引用和跨表引用。循环引用上,Excel 会给出警告,而 v1.38 这类老实现可能是算到最大迭代次数后留下一个错乱结果,不报错。所以写公式时要做一次自检,凡是 A 引 B、B 又引回 A 的模式直接在设计期避开。

跨工作表引用的写法在不同版本里区别不小。常见形式是=Sheet2!A1,如果源码里支持Sheets[...]风格,手写别名时要小心空格和大小写。我通常先在示例工程里搜!的用法,确认当前版本支持哪种风格,再写进业务代码。这个动作十分钟内能完成,能避免大量“为什么公式算出来是 0”的排查。

4.3 样式、合并单元格与条件格式:v1.38 能做和不能做的

要做报表,光有数据不够,还要有表头颜色、边框和合并单元格。v1.38 对基础样式支持够用,但条件格式能力偏弱,别拿它当新版 Excel 用。

下面是基础样式代码:

// 表头加粗、黄底、加边框 S.Cells[0, 0].Font.Bold := True; S.Cells[0, 0].Color := clYellow; // 合并 A1:C1,作为整行标题 S.MergeCells(0, 0, 2, 0);

Font.Bold和Color是单元格对象上的直接属性,大多数版本都支持。MergeCells的参数是左上角和右下角的列、行坐标,同样 0 基;我接触过的版本里顺序是左、上、右、下,但有的版本是列、行、宽度、高度,先搜定义再调用。

条件格式方面,v1.38 时代一般只支持简单的字体和颜色变化,数据条、图标集这类功能多半没有。遇到这种需求,我的替代方案是在写数据时自己算好颜色,直接把Color设置成目标值,效果稳定,又不依赖条件格式引擎。

4.4 事件机制:OnCellChanged 的触发边界与递归风险

网格控件的事件最容易当成黑匣子。v1.38 里OnCellChanged是常用的单元格内容变化事件,写法大致如下:

procedure TMainForm.dxSpreadSheet1CellChanged( Sender: TObject; ACol, ARow: Integer); begin // 只做展示,不要在这里写回同一个单元格 StatusBar1.SimpleText := Format('当前位置: %d, %d', [ACol, ARow]); end;

这个事件的触发边界比想象中窄。样式变化、列宽调整、公式重算结果变化,很多版本里都不触发OnCellChanged。如果拿它做“用户改完就自动存盘”,一定不能只依赖它,还要配合保存事件或保存按钮整体处理。

更危险的写法是在事件里改同一行或同一列的单元格,改一次又触发一次,递归下去轻则卡顿,重则栈溢出。常见做法是只读数据、只更新外部状态;如果确实要联动写别的单元格,加一个布尔锁变量,写之前置锁,写之后解锁,事件进来先看锁。

procedure TMainForm.dxSpreadSheet1CellChanged( Sender: TObject; ACol, ARow: Integer); begin if FIsWriting then // FIsWriting 是窗体私有变量 Exit; FIsWriting := True; try // 联动写 B 列 dxSpreadSheet1.ActiveSheet.Cells[1, ARow].Value := '已读'; finally FIsWriting := False; end; end;

4.5 性能观察:大数据量写入时先 BeginUpdate

一次写 5 万行,如果逐格设置 Value,界面会卡到像死机。老包的渲染没有做节流,常见做法是用BeginUpdate包裹批量写入,写完了再EndUpdate一次性刷新。

dxSpreadSheet1.BeginUpdate; try for R := 0 to 49999 do dxSpreadSheet1.ActiveSheet.Cells[0, R].Value := R; finally dxSpreadSheet1.EndUpdate; end;

BeginUpdate/EndUpdate的语义是临时挂起界面刷新和重算,适合批量导入场景。注意要在 finally 里保证配对的 EndUpdate,否则界面会一直不刷新,甚至后续操作全变慢。这条对任何 VCL 网格控件都适用,不只是这一家。

5. 避坑 / 常见问题:编译报错、乱码与资源泄漏的五条血泪记录

5.1 “Cannot find unit 'dxSpreadSheetXXX'”:八成是搜索路径没到全局

现象:编译任何引用 SpreadSheet 单元的工程,报Cannot find unit 'dxSpreadSheetXXX',可包明明装过。

原因:单元搜索路径只在某个工程里配过,新工程或 IDE 重新搜索时找不到 Sources 目录。另一种可能是编译顺序错了,底层包还没生成 .dcp,上层包无从引用。

解决:把 Sources 和 Common 加进全局 Library path,重开工程再编译。如果还报错,就在错误信息里找到第一个找不到的单元名,在源码目录里搜这个 .pas 的物理位置,把它的父目录加进路径。做完这一步,多数“找不到单元”会消失。

5.2 中文导入后变成乱码:编码转换和源码文件编码是两层问题

现象:用打开对话框导入旧版 Excel 文件后,界面上的中文变成“锟斤拷”或问号。

原因:老包内部用 AnsiString 处理字符串,导入时默认按系统 ANSI 代码页转码;如果系统代码页不是中文,或者 xls 内部是 UTF-8 编码,就会出乱码。另一个隐蔽原因是源码包里的 .pas 文件是 GB2312 编码,在新版 IDE 里被按 UTF-8 打开,导致中文字面量在编译前就是乱的。

解决:先确认系统区域设置,中文环境用 GBK/GB2312;再在导入处手动转换,用AnsiToUtf8、Utf8ToAnsi或TEncoding统一编码。源码文件编码问题,用 IDE 的文件编码设置把 .pas 改成对应编码保存,或者干脆在代码里避免中文字面量,全部从资源文件读取。

5.3 老包装不进新 IDE:Delphi 12 / 13 的兼容检查顺序

现象:把 v1.38 的 .dpk 拖进高版本 IDE,编译报一堆“版本不兼容”“Package was compiled with a different version of RTL”。

原因:.dpk 的 Requires 段里记录的包版本号和旧 RTL 版本号,跟当前 IDE 里的新版本对不上。老代码里还可能有被废弃的单元或条件编译分支,直接在新编译器下过不去。

解决:先打开 .dpk 源文件,把 Requires 里列出的系统包版本号改成当前 IDE 自带版本;常见做法是把旧写的requires rtl;之类删掉,让 IDE 自动补版本。再处理条件编译指令,比如旧的{$IFDEF DELPHI10_UP}标识符改成当前 IDE 认的VERxxx。这个动作没有一键按钮,但大多数包靠这两步能过。实在过不去,就用命令行 dcc32 单独编底层包,配合日志逐个修单元。

5.4 关闭窗体后内存不降:网格对象没释放干净

现象:窗体关闭后,任务管理器里进程内存不降反涨,重复开关几次后内存明显升高。

原因:TdxSpreadSheet 内部持有工作表、样式对象和公式缓存,FormClose 时没有显式清理,部分对象要等引用计数缓慢释放,甚至直接漏掉。

解决:在窗体的 OnClose 里显式清理。先在源码里搜 TdxSpreadSheet 有没有Clear、Reset之类的方法,然后在关闭事件里调用一次。窗体本身释放时,后续子对象会跟着释放。我习惯把清理逻辑写进 OnClose,而不是等 Destroy,这样主动权在自己手里。

procedure TMainForm.FormClose( Sender: TObject; var Action: TCloseAction); begin // 如果源码里有 Clear,就调用它释放内部对象 dxSpreadSheet1.Clear; end;

5.5 写入的公式显示 0:AutoCalc 没开或引错了行号

现象:Formula 属性写进去,单元格马上显示 0,点击单元格才能在编辑栏看到公式是对的。

原因:自动重算没打开,或者公式里的行号写成了 1 基,和 Cells 下标的 0 基混了。还有一个常见原因:公式引用的单元格是空的或文本类型,数字运算结果自然为 0。

解决:先打开 AutoCalc 并手动 Calculate 一次,看结果变不变;再检查公式字符串里的行列引用是否和单元格填充位置一致。用DisplayText属性把结果输出比对,比肉眼盯界面可靠。

6. 把源码包装成报表导出工具:自检清单与文本导出验证技巧

6.1 用三行代码把 ActiveSheet 导出成 XLS

报表导出是最常见的落地场景。把网格内容写成文件,很多边界问题会在导出时暴露。最基础的做法是包一个过程:

procedure ExportActiveSheetToXLS(const AFileName: string); begin // 固定导出为 xls 97-2003 格式 dxSpreadSheet1.SaveToFile(AFileName, sfxXLS); end;

调用时目标目录必须存在,不然SaveToFile直接抛异常。文件名建议不要带中文,老版本的保存路径解析有时会出编码问题;真要带,先转一层编码。导出完成后别急着关窗体,用 Excel 打开一次,检查行列数和公式结果。

6.2 安装正确性的自检清单

把下面四条检查过一遍,比装完就跑稳得多:

检查点通过标准失败时先看哪里
单元搜索路径新建工程可直接引用 dxSpreadSheet 单元Library path 是否包含 Sources
设计期包工具面板能搜到 TdxSpreadSheet是否安装了带 Design 的包
示例工程Demos 能编译运行包依赖顺序是不是还对
数据往返写入、保存、再读取内容一致文件格式枚举是否选对

这四条是串联关系,前面挂后面全挂。我每次换机器重配环境都按这个顺序走,不跳步骤。

6.3 把网格导出成文本,用于自动化比对

最后一个技巧我几乎在每个项目里都用:把网格内容导出成 Tab 分隔的文本,方便自动化回归。公式计算结果和样式对不对,用文本 diff 一眼就能看出来,比打开 Excel 截图强得多。

procedure DumpSheetToText(S: TdxSpreadSheetSheet; Lines: TStrings); var C, R: Integer; Line: string; begin Lines.Clear; for R := 0 to S.RowCount - 1 do begin Line := ''; for C := 0 to S.ColCount - 1 do Line := Line + S.Cells[C, R].DisplayText + #9; Lines.Add(Line); end; end;

DisplayText拿的是渲染后的文本,公式也是计算结果,不是公式串,正好用来验证结果。#9是 Tab 分隔符,Excel 打开时能自动分列。导出两个版本的文件后,用文本比对工具 diff,凡是数字、公式、边界条件改动都能立刻看到。

这套小工具我现在还在用。遇到老控件包,第一件事就是把文本导出写好,然后才谈样式和公式,顺序别颠倒,不然你永远不知道到底哪里坏了。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 4:34:19

WinForms自绘CheckBox ComboBox:多选下拉控件实现与避坑指南

简介:在Windows桌面应用开发中,带复选框的下拉列表是多选场景的常见方案。这份WPF工程源码演示了如何从ComboBox基类派生自定义控件,核心思路是在内部嵌入一个ListBox,用CheckBox作为每个条目的选择标记;开发者需要重写…

作者头像 李华
网站建设 2026/9/26 4:34:06

AI应用架构实战:Provider抽象、RAG与Agent编排

1. 从单模型到多 Provider:为什么必须做这层抽象做过 AI 应用的人大概都有过这种体验:项目初期直接调一家大模型的接口,代码写得飞快,功能跑通就上线。结果没过多久,业务方说想换成另一家的模型试试效果,或…

作者头像 李华
网站建设 2026/9/26 4:33:49

NEU-DET钢材缺陷数据集VOC与YOLO双格式解析及YOLOv8训练实战

简介:本资源为NEU-DET钢材表面缺陷检测数据集,面向从事工业质检、缺陷识别与深度学习目标检测的开发者及研究人员,可用于训练与验证钢材表面六类缺陷的检测模型。数据集同时提供Pascal VOC与YOLO两种标注格式,包含jpg图片及对应的…

作者头像 李华
网站建设 2026/9/26 4:33:25

treg:CLI技能可信执行的轻量级注册与校验机制

1. 项目概述:treg 是什么?它解决的不是“密钥管理”,而是开发者工作流中的信任断点“treg”这个名称乍看像某个新出的 CLI 工具缩写,或是某家小众 API 平台的代号——但结合当前高频热搜词(OpenRouter、CLI、SKILL.md、…

作者头像 李华
网站建设 2026/9/26 4:32:58

独立开发者对象存储与CDN加速横评:七牛云与腾讯云COS

独立开发者对象存储与CDN加速横评:七牛云与腾讯云COS在独立产品(SaaS / Web App / 移动端)的静态资源托管、用户头像存储与周报长图/PDF 归档中,对象存储(Object Storage Service)与内容分发网络&#xff0…

作者头像 李华
网站建设 2026/9/26 4:32:47

随机森林预测空气质量:时间序列特征工程与避坑实战

简介:这是一套面向数据挖掘初学者及空气质量分析实践者的完整项目资料,围绕随机森林算法构建污染预测模型,覆盖数据清洗、特征探索、模型训练与结果评估的实战闭环,适合具备一定Python基础、想通过真实项目巩固机器学习流程的读者…

作者头像 李华