简介DevExpress ExpressSpreadSheet v1.38 for Delphi/C Builder 源代码用于在应用中集成类 Excel 的表格、公式计算与数据可视化能力适合研读组件内部实现、开展二次开发或性能调优的中高级开发者。包内共 382 个文件、约 2.81MB以 pas 单元、dpk/bpk 工程与包文件、cpp/h 源码、dfm 窗体为主体辅以 res、bmp 界面资源与 xls 示例数据并附 nfo、diz 发布说明。目前已有 447 人学习。源码覆盖表格引擎、数据绑定、公式解析与样式格式设置ExpressGDI 负责单元格与图表渲染ExpressLibrary 提供共享核心模块XP Theme Manager 统一系统主题外观CustomFunction、FeaturesDemo、Stocks 等示例工程可直接对照学习自定义函数等场景的写法便于按模块裁剪定制、复用现成实现。1. 拿到 DevExpress SpreadSheet 源代码先弄清楚它能解决什么老系统要加一块「能在界面里直接编辑、带公式重算、还能导出」的表格Excel 控件不能嵌入商业表格控件按席位报价自己从 TStringGrid 造又造不出公式引擎。这份 DevExpress.ExpressSpreadSheet.v1.38.for.Delphi.BCB 源代码正好处在这个夹缝里VCL 原生表格控件Delphi 和 CBuilder 两套工具链都能用给的还是源码公式解析、单元格渲染、打印导出这几层都摆在明面上能看也能改。它的价值不在「拿来即用」而在于把编译链路、公式重算和坐标模型看清楚之后你能判断是整块嵌进现有工程还是只拆出公式引擎那一层自己用。下面按我拆包的顺序写目录怎么分层、包怎么编、组件怎么装、公式怎么跑、哪里最容易出错。2. 源码包的目录结构与编译链路从 .dpk 到 .bpl 的落地顺序拿到一套带源码的 VCL 控件很多人的第一反应是打开 IDE 点 Install然后被一串「找不到单元」拦住。ExpressSpreadSheet 这类包不是单文件组件它是一组互相 requires 的包工程编译顺序错了后面每一步都是无用功。2.1 先给单元分层哪几个文件是引擎哪几个只是门面我拆包时习惯先把 .pas 按职责分堆而不是按字母序看完。v1.38 这套源码大致是这么几层单元名以你手上目录为准不同小版本可能把 Core 和 Classes 合并单元/文件职责什么情况下需要动它dxSpreadSheet.pas控件门面TdxSpreadSheet 本体管 Sheet 集合、视图、事件要给控件加对外属性或事件dxSpreadSheetClasses.pas / Core表、行、列、单元格对象模型数据存储要加自定义单元格类型、改存储结构dxSpreadSheetFormulas.pas公式的词法分析、语法树、求值要改运算符优先级、支持新语法dxSpreadSheetFunctions.pas内置函数注册表SUM、IF、ROUND 这些要补函数或者改某个函数的边界行为dxSpreadSheetUtils.pasA1 引用、区域字符串、坐标换算工具要做 Excel 风格的地址解析dxSpreadSheetPrinting.pas打印、分页、页面设置要调打印缩放和分页dxSpreadSheetReg.pas设计期注册组件面板的分类和图标要改面板分组或加注册项分清这层关系的好处很直接你后面遇到的 90% 编译错误本质都是「引擎层没编过门面层却先编了」。门面单元引用 ClassesClasses 引用 Utils 和 FunctionsFunctions 又依赖基础库的字符串与集合工具链条是单向的。提示先把 dxSpreadSheetReg.pas 单独拎出来看一遍里面 RegisterComponents 的那一行会告诉你组件装在面板的哪一页、类名是什么比在 IDE 里瞎找快得多。2.2 编译顺序基础包 → 运行期包 → 设计期包ExpressSpreadSheet 不是孤立的它依赖同门的公共基础库包cxLibrary、dxCore 这一类。这套依赖关系写在 .dpk 的 requires 子句里但 IDE 不会替你自动补缺了就是一句「找不到单元 xxx」。我一般先不开 IDE用命令行把顺序跑一遍出错信息比 IDE 干净# 目录里通常成对出现设计期包和运行期包星号是 Delphi 版本号 ls -1 *.dpk # 第一步先编基础库的运行期包名字按你目录里的实际文件替换 dcc32 -B -Q cxLibraryR*.dpk # 第二步编 SpreadSheet 的运行期包它 requires 上面的基础包 dcc32 -B -Q dxSpreadSheetR*.dpk # 第三步最后编设计期包它 requires 运行期包 dcc32 -B -Q dxSpreadSheetD*.dpk这段命令里三个参数值得说清楚-B是强制全部重编译避免残留 .dcu 让问题藏起来-Q静默模式只输出错误和警告几千行提示里你只需要看红色的那几行包文件名不写死版本号是因为同一份源码在不同 Delphi 版本下后缀不同写成通配反而更稳。如果第二步报F2613 Unit dxCore not found别急着去改源码先确认基础库的 .dcu 输出到了哪个目录那个目录有没有进 Library Path。运行期包编译通过之后会在输出目录生成 .bpl 和 .dcp设计期包再编才会生成可安装的 .bpl。2.3 CBuilder 侧.hpp 和 .lib 是另外生成出来的Delphi 编出来的 .dcu 对 CBuilder 没有直接意义。BCB 要的是头文件和导入库.hpp 描述类布局.lib/.bpi 负责链接。这套东西不是在 .dpk 里手写的而是由编译器从 .pas 的接口段导出。常见做法是在 IDE 里对包工程执行一次「Generate all CBuilder files」或者在工程选项里勾上 C 输出。生成之后目录里会多出 dxSpreadSheet.hpp、dxSpreadSheetClasses.hpp 这一组文件以及对应的 .lib。BCB 工程要做的三件事把头文件目录加进 Include Path把 .lib 所在目录加进 Library Path决定是静态链接还是跟着运行期包走——跟着 BPL 走的话发布时必须把对应的 .bpl 一起带上否则客户机上第一次创建控件就报找不到包。这一步最容易出错的地方是「改了 .pas 但没重新生成 .hpp」。你在 Delphi 那边加了个 published 属性BCB 侧编译照过运行时属性就是读不到因为头文件还是旧的类布局。血泪经验只要动过接口段就把 C 输出重生成一遍。3. 把 TdxSpreadSheet 挂上工具栏路径配置、包安装与第一个真表编译通过只是第一步真正决定这套源码好不好用的是装完之后能不能在十分钟内跑出一个带公式的表。这一章按我实际落地的顺序来先配路径再装包最后写一个能验证公式的窗体。3.1 Library Path 只加源码目录别把输出目录混进去Delphi 找单元的顺序是当前工程目录 → Search Path → Library Path → 已安装包的 .dcp。很多人图省事把源码目录和 .dcu 输出目录一起塞进 Library Path结果出现一种很玄学的现象改了 .pas重新编译行为没变。原因是编译器优先命中了输出目录里那份旧的 .dcu源码根本没参与编译。我的配置习惯是Library Path 只放源码目录.pas 所在的那一层不含子目录就逐个加包的输出目录.dcu/.bpl/.dcp单独配置只在「使用已编译包」时生效32 位和 64 位分开配v1.38 这种老版本基本只能走 32 位别在 64 位平台上折腾一上午。注意如果机器上装过别的版本的 DevExpress 组件注册表里可能残留同名的包。新旧两套包同时被 IDE 加载编译期会报重复单元运行期会报类未注册症状看着像源码有问题其实是环境不干净。3.2 装设计期包组件面板出现 ExpressSpreadSheet 页路径配好之后编译设计期包并安装。IDE 里走 Component → Install Packages → Add选中刚生成的 .bpl或者直接打开 dxSpreadSheetD*.dpk 点 Install。装完在工具栏的 ExpressSpreadSheet 分类下应该能看到 TdxSpreadSheet。验证是否真的注册成功不看面板看这两点新建一个空窗体代码里能自动补全出 TdxSpreadSheet 类型把控件拖上去Object Inspector 里能看到 Sheets 属性并能打开表格编辑器。如果面板有图标但拖上去报「Class TdxSpreadSheet not found」说明设计期包加载了、运行期包没找到回到上一步检查 BPL 目录有没有在系统的 PATH 或 IDE 的运行时包列表里。3.3 第一个能验证公式的表填数、写公式、拿结果下面这段代码可以直接贴进按钮事件里作用是建三列数据、写一个乘法公式、加一个求和然后把计算结果读回来打印。跑通它说明控件、公式引擎、单元格模型三层都是活的。procedure TForm1.BuildDemoTable; var Sheet: TdxSpreadSheetTableView; I: Integer; begin dxSpreadSheet1.BeginUpdate; // 关掉重绘和重算批量写入必加 try Sheet : dxSpreadSheet1.Sheets[0] as TdxSpreadSheetTableView; // 表头 Sheet.Cells[0, 0].AsString : 单价; Sheet.Cells[1, 0].AsString : 数量; Sheet.Cells[2, 0].AsString : 金额; // 数据行第 1 行到第 5 行 for I : 1 to 5 do begin Sheet.Cells[0, I].AsFloat : 12.5 * I; // 数值走 AsFloat别用 AsString Sheet.Cells[1, I].AsFloat : I; Sheet.Cells[2, I].Formula : A IntToStr(I 1) *B IntToStr(I 1); end; // 合计行 Sheet.Cells[1, 6].AsString : 合计; Sheet.Cells[2, 6].Formula : SUM(C2:C6); finally dxSpreadSheet1.EndUpdate; // 这里才真正重算 end; // 读回结果公式单元格取 Value 而不是 Text ShowMessage(VarToStr(Sheet.Cells[2, 6].Value)); end;几个参数和写法需要交代清楚BeginUpdate/EndUpdate必须成对出现且放在 try..finally 里。它们管的是重绘抑制和重算合并少了这对5 行数据可能看不出差别5000 行就是几十秒和几毫秒的差别。数值单元格用AsFloat文本用AsString。这是最容易犯的错把数字写成 AsString单元格就变成文本类型公式里参与运算时要么被当 0要么直接报类型错。公式写在Formula里、以等号开头单元格随后由引擎解析求值要取计算后的结果用Value取用户看到的显示串用Text。不同小版本里公式属性可能叫 Formula 或 FormulaText敲代码时看补全提示逻辑是一样的。行列表头默认占掉第 0 行第 0 列所以数据从[0,1]开始如果你的表隐藏了表头坐标语义会变这点在写循环之前先确认。4. 公式引擎与能力边界重算时机、坐标换算和大表性能把表跑起来之后真正决定这套源码能不能上生产的是三个问题公式什么时候重算、A1 地址怎么和自己的坐标对上、大表为什么会卡。这一章逐个说。4.1 重算不是实时的理解 BeginUpdate 背后的合并策略ExpressSpreadSheet 的重算模型是「依赖脏标记 批量提交」。你改一个单元格的值它不会立刻把所有依赖这个格的公式都算一遍而是把受影响的区域标脏等到一次更新周期结束时统一处理。BeginUpdate/EndUpdate就是手动拉长这个周期的开关。这套机制在导入场景里特别重要。从数据库里拉两万行填进表如果每填一格触发一次重算扫描复杂度会往平方级走。正确写法是把整段填充包在 BeginUpdate 里让脏标记攒着最后一次性算完。反过来如果你的业务需要「每改一格立刻看到结果」——比如做参数试算面板——那就别包大段的 BeginUpdate或者在中途手动调一次重算。还有一种情况公式结果显示成#NAME?或者原样的字符串。两个原因一是函数名不在内置函数表里v1.38 的函数集比现在的 Excel 少一大截像 SUMIFS、XLOOKUP 这类肯定没有二是你把公式当文本写进了 AsString。前者要改 dxSpreadSheetFunctions.pas 加注册项后者改一行代码就行。4.2 A1 引用和内部坐标的换算自己写一个更踏实控件的Cells[Col, Row]是 0 基的而用户和 Excel 打交道用的是 A1 这种 1 基字母列名。业务里只要涉及到「从配置表读一个区域字符串」「把用户输入的地址解析成范围」就得自己写换算。源码里 Utils 单元有类似工具但我一般自己写一份短的行为可控// 把 AB 这样的列名转成 0 基列号A-0, Z-25, AA-26 function ColIndexFromA1(const A1: string): Integer; var I, N: Integer; begin N : 0; for I : 1 to Length(A1) do begin if not (A1[I] in [A..Z, a..z]) then Break; // 遇到数字就停后面是行号 N : N * 26 (Ord(UpCase(A1[I])) - Ord(A) 1); end; Result : N - 1; // 转成 0 基 end; // 反向0 基列号转列名导出 CSV 时常用 function A1FromColIndex(AIndex: Integer): string; begin Result : ; Inc(AIndex); while AIndex 0 do begin Result : Chr(Ord(A) (AIndex - 1) mod 26) Result; AIndex : (AIndex - 1) div 26; end; end;这段逻辑的关键在(AIndex - 1) mod 26和(AIndex - 1) div 26这两处减一。26 进制没有 0 这个「数字」A 到 Z 对应 1 到 26直接拿 0 基去取模会在 Z、AA、AZ 这些边界上错位一格。写完拿 A、Z、AA、AZ、BA 五个值单测一遍过了就没问题。行号部分注意 Excel 的 1 基和控件的 0 基差一位Cells[ColIndexFromA1(AB), StrToInt(RowStr) - 1]这种写法在代码里到处都是建议封一个CellAt(const A1: string)出来别让减一散落各处。4.3 大表的三段开销重算、渲染、导出要分开压一张表卡先分清卡在哪一段否则优化全是猜的。我用一个粗略的办法定位只填数据不写公式看耗时填完公式再测一次差值就是重算开销把控件 Visible 设 False 再跑一遍差值就是渲染开销。三段的对策完全不同。重算开销大说明公式依赖链太深或者引用区域过大比如整列引用SUM(C:C)在万行表上会让引擎反复扫描换成明确区域SUM(C2:C10000)立竿见影。渲染开销大说明没做批量更新或者在循环里改了行高列宽这类触发布局重算的属性。导出开销大通常是逐格读取 Value 拼字符串改成按区域整块取再拼装能快一个量级。打印那一段还有独立的坑分页是按列宽累加算的列宽设成自动适应之后不同机器的字体度量略有差异出来的分页位置会飘。要稳定的打印输出把列宽设成固定像素值别用自适应。5. 避坑与排查源码编译到运行时的五类翻车现场这套源码我前后装过三台机器翻车基本集中在下面五种。每条按「看到什么 → 为什么 → 怎么修」写照着对症状就行。5.1 报找不到单元 dxCore 或 dxSpreadSheetStrs现象编译运行期包时命令行红字F2613 Unit xxx not found但目录里明明有这个 .pas 文件。原因基础库包没编或者编了但输出目录没进搜索路径。ExpressSpreadSheet 的 requires 里列了基础包编译器需要的是 .dcu 或 .dcp不是 .pas 本身。解决先单独编基础库的运行期包确认输出目录里有 .dcu再把这个输出目录加进 IDE 的 Library Path只加这一层别加源码目录避免旧 .dcu 抢先命中最后回来编 SpreadSheet 包。顺序不能反反了就是重复劳动。5.2 组件面板出现了拖上去却报 Class not found现象Tool Palette 里有 ExpressSpreadSheet 页拖控件到窗体正常一运行就弹Class TdxSpreadSheet not found。原因设计期包装上了运行期包没被加载或者加载的是另一个路径下的旧版本 .bpl。IDE 里设计期能创建是因为它自己加载了设计期包运行时是另一套加载逻辑。解决Project → Options → Packages确认运行期包在列表里且路径唯一检查系统 PATH 里有没有同名的旧 .bpl把编译输出目录清理干净重来一遍。机器上同时装过两套控件时这个错几乎必现。5.3 改了源码重新编译后行为一点没变现象在 dxSpreadSheetFunctions.pas 里加了一个函数重新编译运行时仍然报未知函数。原因编译器命中了输出目录里的旧 .dcu或者 BCB 工程的 .hpp 没重新生成导致链接的还是旧类布局。解决Delphi 侧先-B全量重编确认 .dcu 时间戳更新BCB 侧重生成 C 输出文件如果用了运行期包确认发布的 .bpl 是刚生成的那一份而不是部署目录里的历史版本。5.4 公式不重算或者结果一直显示原始字符串现象Formula赋值成功界面上看到的还是A1*B1这串字符或者所有公式格都是#NAME?。原因三种可能——把公式写进了 AsString 变成文本BeginUpdate 之后没有 EndUpdate脏标记没提交函数名不在 v1.38 的内置表里。解决先看属性写没写错公式只走 Formula再看 BeginUpdate 是否成对最后打开 Functions 单元查函数名是否注册。真要补函数注册项里除了名称还要给参数个数和求值回调少一个都会解析失败。5.5 万行表卡到假死切窗口回来才刷出来现象导入一万行数据后界面白屏十几秒鼠标点不动切走再切回来突然全好了。原因逐格赋值每格都触发布局和重算加上控件 Visible 为 True渲染开销叠在写入上。解决整段填充包进 BeginUpdate/EndUpdate填充期间把控件 Visible 关掉填完再开公式列放在数据填完之后统一写入别边填边算真的大到几万行考虑只把可视区域的数据绑到控件底层数据留在自己的结构里。6. 用源码给自己加一层把公式引擎拉出来做回归验证源码型控件的最大好处是你能给它加一个「离线自检」——不打开窗体直接调公式引擎算一批用例。我在这套包上做过一次收益比预想大升级 Delphi 版本、改函数实现、换编译选项跑一遍用例就知道公式层有没有被带坏。做法是把函数表的注册和求值入口单独拎出来绕开控件和视图。关键点是先创建一个不挂界面的表对象再往里面写公式读值全程不涉及渲染// 不创建 TdxSpreadSheet 控件直接操作表对象做公式回归 procedure RunFormulaSmokeTest; var Sheet: TdxSpreadSheetTableView; I: Integer; Cases: array[0..3] of record Expr, Expect: string; end; begin Cases[0].Expr : 11; Cases[0].Expect : 2; Cases[1].Expr : SUM(1,2,3); Cases[1].Expect : 6; Cases[2].Expr : IF(10,10,20); Cases[2].Expect : 10; Cases[3].Expr : ROUND(2.345,2); Cases[3].Expect : 2.35; Sheet : TdxSpreadSheetTableView.Create(nil); // 具体构造方式以源码声明为准 try for I : Low(Cases) to High(Cases) do begin Sheet.Cells[0, I].Formula : Cases[I].Expr; if VarToStr(Sheet.Cells[0, I].Value) Cases[I].Expect then raise Exception.CreateFmt(公式 %s 期望 %s 实得 %s, [Cases[I].Expr, Cases[I].Expect, VarToStr(Sheet.Cells[0, I].Value)]); end; finally Sheet.Free; end; end;用例不用多十几条就够覆盖四则运算与优先级、括号嵌套、区域求和、条件函数的两条分支、舍入的边界2.345 这种浮点表示不精确的值最能暴露问题、除零、非法引用。这套用例我在两个场景复用得很舒服一是换编译器版本后先跑它确认引擎层没受影响再动业务代码二是要给 Functions 单元补函数时先写用例再写实现改完立刻知道有没有踩到旧函数的边界。还有个小技巧值得说把这套用例做成一个只在 Debug 配置下编译的单元挂在程序启动流程的断言里。线上不会执行开发机上每次跑起来自动过一遍比事后翻日志快得多。数据库连接、权限、界面这些都能糊弄公式算错是糊弄不过去的用户第一眼就看见。从那以后我每次拿到这种带源码的控件包都先写一个几十行的冒烟用例跑通再往业务工程里接——顺序反过来的代价我在一个项目上付过整整两天。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?