
简介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 里操作路径大致是这几步打开 IDE 的 Tools → Options。找到 Environment Options → Delphi Options → Library。在 Library path 里追加 Sources 和 Common 两个目录。确认 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 和 .dcpInstall 才会把控件注册进 IDE 工具面板。对依赖链很长的包正确顺序是按依赖关系把所有包 Compile 完最后再 Install 那些真正对应控件的设计期包。设计期包决定工具面板里出现哪些控件运行期包永远不应该被 Install。如果我在 IDE 里手工操作会这样做打开底层包的 .dpk右键 Compile等状态栏出现 Success。按依赖顺序重复直到所有运行期包编译通过。打开名字里带 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 对象检查器能显示的名称和顺序不会骗人。这一步花五分钟后面少走两小时弯路。放好控件后写一个最简单的 FormCreateprocedure 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 里按 CtrlC 复制、CtrlV 粘贴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 老包装不进新 IDEDelphi 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 写入的公式显示 0AutoCalc 没开或引错了行号现象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凡是数字、公式、边界条件改动都能立刻看到。这套小工具我现在还在用。遇到老控件包第一件事就是把文本导出写好然后才谈样式和公式顺序别颠倒不然你永远不知道到底哪里坏了。希望帮到你。本文还有配套的精品资源点击获取