ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Delphi版Excel公式引擎源码解析:AST解析与自定义函数实现

Delphi版Excel公式引擎源码解析:AST解析与自定义函数实现

简介:本资源为DevExpress ExpressSpreadSheet v1.38版本的完整Delphi与C++ Builder(BCB)源代码包,面向中高级桌面开发工程师及组件定制需求者,解决电子表格功能深度集成、性能调优与UI主题适配等实际开发难题。包内共382个文件,涵盖73个Pascal单元(pas,核心逻辑与引擎实现)、45个包工程文件(dpk/bpk,组件编译与依赖管理)、38个C++源码(cpp,GDI+图形渲染与底层交互)、26个窗体描述(dfm,界面布局)、以及XP Theme Manager、ExpressLibrary和ExpressGDI+相关模块,完整呈现DevExpress组件架构设计与跨平台视觉一致性实现机制。资源压缩包仅2.81MB,结构精炼,含大量演示项目(如FeaturesDemo、Stocks、Flying等BDSPROJ工程),便于逆向学习公式解析、数据绑定与自定义函数扩展。目前已有446人学习下载,是研究商业级VCL/FMX组件源码、开展二次开发与疑难问题定位的高价值参考资料。

1. 这不是个“控件安装包”,而是一份能让你看懂 Excel 引擎内核的 Delphi 源码切片

你有没有在 Delphi 项目里用过 DevExpress 的 SpreadSheet 控件?拖一个 TdxSpreadSheet 到窗体,绑定数据、导出 Excel、加条件格式——一切丝滑。但一旦遇到「公式计算结果和 Excel 不一致」「自定义函数注册后不触发」「单元格样式在打印预览中丢失」这类问题,官方文档只告诉你Use this method,却不告诉你它内部怎么调用TdxSpreadSheetFormulaEngine、怎么解析=SUM(A1:A10)的 AST 节点、甚至TdxSpreadSheetCell的FStyleIndex是如何映射到共享样式表的。这份DevExpress.ExpressSpreadSheet.v1.38.for.Delphi.BCB源代码,就是那个被封装在.dcu和.bpl黑匣子底下的真实世界:它不是 SDK 文档,不是示例工程,而是1.38 版本中 SpreadSheet 核心模块(非 UI 层)的完整 Delphi 源码级实现,覆盖公式引擎、单元格存储、样式管理、文件读写(XLSX/XLS)四大主干。适合两类人:一类是正在维护十年以上 Delphi 企业报表系统、需要修复兼容性 bug 的老手;另一类是想从零理解电子表格底层机制(比如如何避免浮点精度导致的=0.1+0.2≠0.3显示问题)的算法向开发者。它不能直接编译成新控件,但你能把它当“活体解剖标本”——改一行TdxSpreadSheetFormulaParser.ParseNumber就能验证自己对 Excel 数字解析逻辑的理解是否正确。


2. 架构拆解:四个核心单元与它们的依赖边界

这个源码包不是单一大杂烩,而是按职责清晰切分的四个 Delphi 单元(.pas文件),每个单元解决一类确定性问题。我把它比作一辆车的发动机舱:你可以不修它,但得知道油路、电路、气门各自在哪,否则一冒烟就只能换整车。

2.1 公式引擎:dxSpreadSheetFormulaEngine.pas—— Excel 公式的 Delphi 实现

这是整个包里最硬核的部分。它没用字符串正则匹配,而是构建了完整的语法树(AST)解析器。关键入口是TdxSpreadSheetFormulaEngine.Evaluate,它接收一个TdxSpreadSheetCell和公式字符串(如"=IF(A1>0,SUM(B1:B10),A1*2)"),返回Variant类型结果。

function TdxSpreadSheetFormulaEngine.Evaluate( ACell: TdxSpreadSheetCell; AFormula: string): Variant; var LParser: TdxSpreadSheetFormulaParser; LNode: TdxSpreadSheetFormulaNode; begin LParser := TdxSpreadSheetFormulaParser.Create; try LNode := LParser.Parse(AFormula); // ← 关键:生成 AST 节点树 try Result := EvaluateNode(LNode, ACell); // ← 递归求值 finally LNode.Free; end; finally LParser.Free; end; end;

提示:EvaluateNode是递归核心,对TdxSpreadSheetFormulaFunctionNode(函数节点)会查FFunctionRegistry字典,里面存着'SUM'、'IF'、'VLOOKUP'等所有内置函数的TdxSpreadSheetFormulaFunction实例。你若想加自定义函数=MYROUND(A1,2),只需继承TdxSpreadSheetFormulaFunction并注册进该字典——不用动解析器,也不用改 AST 结构。

2.2 单元格与工作表模型:dxSpreadSheetModel.pas—— 内存中的 Excel 表格

这里定义了TdxSpreadSheetWorksheet(工作表)、TdxSpreadSheetRow(行)、TdxSpreadSheetColumn(列)、TdxSpreadSheetCell(单元格)四层对象。重点不是“有这些类”,而是它们的内存组织方式:

  • TdxSpreadSheetWorksheet.FCells是一个TList<TdxSpreadSheetCell>,但不是按行列顺序存储,而是用稀疏数组(Sparse Array)思想:只存非空单元格,空单元格不占内存;
  • TdxSpreadSheetCell.FValue是Variant,但FValueType字段明确区分vtEmpty、vtInteger、vtDouble、vtString、vtDateTime—— 这直接决定了公式计算时的类型提升规则(比如1 + "2"返回"12"还是3);
  • 所有单元格的样式索引FStyleIndex指向TdxSpreadSheetWorkbook.FStyles中的TdxSpreadSheetCellStyle对象,实现样式复用。

2.3 样式系统:dxSpreadSheetStyles.pas—— 从字体颜色到条件格式的统一管理

Excel 的样式极其复杂,但此模块用三层抽象收口:

  • TdxSpreadSheetCellStyle:单个样式定义(字体、边框、填充、对齐);
  • TdxSpreadSheetStyleManager:样式缓存与去重(相同字体+边框+填充的样式只存一份,返回唯一StyleIndex);
  • TdxSpreadSheetConditionalFormatting:条件格式规则集合,每条规则是TdxSpreadSheetConditionalFormatRule,含Formula(如"$A1>100")、AppliesToRange、StyleIndex。

关键设计在于:样式不绑定到单元格,只绑定到样式索引。所以当你调用ACell.Style := MyStyle,实际是ACell.FStyleIndex := StyleManager.Add(MyStyle)。这解释了为什么大量单元格共用同一套样式时内存占用极低。

2.4 文件读写:dxSpreadSheetIO.pas—— XLSX 解包与重装的底层逻辑

它不依赖第三方 ZIP 库,而是用TdxZipFile(包内自带)解压.xlsx的[Content_Types].xml、xl/workbook.xml、xl/worksheets/sheet1.xml等文件。核心流程是:

  1. TdxSpreadSheetWorkbookReader.ReadFromFile→ 解压并解析workbook.xml获取工作表列表;
  2. 对每个<sheet>标签,调用TdxSpreadSheetWorksheetReader.ReadFromXML→ 解析sheet1.xml中的<row>和<c>(cell)节点;
  3. 每个<c>节点的s="5"属性被转为FStyleIndex := StrToInt(AttrValue),再从styles.xml中查对应样式。

注意:dxSpreadSheetIO.pas里没有写 Excel 2003(.xls)的 BIFF 解析,只支持.xlsx和.csv。如果你的客户还在用 Office 2003,这部分需自行扩展TdxSpreadSheetBiffReader(包里未提供)。


3. 编译与运行:三步走通 Delphi 10.4+ 环境

别被“源代码”吓住——它不是让你重编整个 DevExpress 安装包,而是作为可调试、可断点、可 Patch 的引用库接入现有项目。以下步骤经实测(Delphi 10.4 Sydney + Windows 10 x64):

3.1 环境准备:确认你的 Delphi 支持 BCB 兼容模式

v1.38.for.Delphi.BCB中的 “BCB” 指 Borland C++ Builder 兼容头,不代表你要装 C++ Builder。它只是说明此版本源码使用了{$IFDEF BCB}条件编译块,用于处理 C++ Builder 的特定类型别名(如AnsiStringvsUnicodeString)。Delphi 10.2+ 默认启用{$IFDEF BCB}兼容模式,无需额外设置。验证方法:新建 VCL Forms Application,在uses加dxSpreadSheetFormulaEngine,编译无Undeclared identifier错误即通过。

3.2 工程集成:添加源码路径而非 DCU

将下载包解压后的Source\目录(含全部.pas文件)加入工程搜索路径:

  • 右键项目 → Options → Delphi Compiler → Search path
  • 在末尾追加:$(ProjectDir)..\DevExpress.SpreadSheet.Source\

提示:不要把.pas文件直接 Add to Project!否则会导致单元重复声明(dxSpreadSheetModel在多个.pas中被uses,Delphi 会报Unit 'dxSpreadSheetModel' is not in the search path)。只加路径,让编译器按需加载。

3.3 最小可运行 Demo:显示一个带公式的单元格

新建空白窗体,放一个TdxSpreadSheet(需已安装 DevExpress VCL 组件),然后:

procedure TForm1.FormCreate(Sender: TObject); var LWorkbook: TdxSpreadSheetWorkbook; LWorksheet: TdxSpreadSheetWorksheet; LCell: TdxSpreadSheetCell; begin LWorkbook := TdxSpreadSheetWorkbook.Create; try LWorksheet := LWorkbook.Worksheets.Add('Demo'); LCell := LWorksheet.Cells[0, 0]; // A1 LCell.Value := 100; LCell := LWorksheet.Cells[0, 1]; // B1 LCell.Value := 200; LCell := LWorksheet.Cells[0, 2]; // C1 LCell.Formula := '=A1+B1'; // ← 关键:设公式而非值 dxSpreadSheet1.Workbook := LWorkbook; // 绑定到控件 except on E: Exception do ShowMessage('初始化失败:' + E.Message); end; end;

运行后,C1 单元格应显示300。此时你在dxSpreadSheetFormulaEngine.pas的EvaluateNode第一行下断点,F7 进入,就能看到公式如何被拆解为AddNode并递归求值——这才是源码的价值:所见即所得的调试深度。


4. 避坑指南:五个血泪经验换来的排错清单

刚拿到源码跑 demo 时,我踩了至少七次坑。以下是高频、隐蔽、且官方文档绝不会提的五条:

4.1 现象:编译报错E2003 Undeclared identifier: 'TdxZipFile'

原因:dxSpreadSheetIO.pas依赖dxZip.pas(ZIP 压缩解压工具),但该文件不在本源码包中,而是 DevExpress 安装目录下的Source\dxCore\子目录。v1.38 版本要求dxZip.pas必须是同版本(即 1.38),混用 1.37 或 1.39 会导致TdxZipFile接口不兼容。
解决:从你本地安装的C:\Program Files\Developer Express\VCL\Source\dxCore\下复制dxZip.pas到本项目Source\目录,并确保其unit dxZip;声明顶部有{$I dxVer.inc}且dxVer.inc中DX_VERSION = 138。

4.2 现象:运行时报Access violation at address XXXX in module 'dxSpreadSheetFormulaEngine.dll'

原因:你把dxSpreadSheetFormulaEngine.pas加进了项目(Add to Project),导致 Delphi 同时链接了你编译的.dcu和 DevExpress 安装包自带的.dll中同名单元,内存地址冲突。
解决:立即从项目中移除所有.pas文件(Project → Options → Deployment 中删掉),只保留 Search Path 设置。Delphi 会自动按需编译.pas,绝不手动 Add。

4.3 现象:公式=NOW()返回0,="Hello"&A1返回空字符串

原因:TdxSpreadSheetFormulaEngine默认禁用易失性函数(volatile functions)和文本连接运算符&,需显式启用。源码中TdxSpreadSheetFormulaEngine.FEnableVolatileFunctions默认False。
解决:在创建引擎后设置:

dxSpreadSheet1.Workbook.FormulaEngine.EnableVolatileFunctions := True; dxSpreadSheet1.Workbook.FormulaEngine.EnableTextConcatenation := True;

4.4 现象:导入 XLSX 后中文显示为方块或乱码

原因:dxSpreadSheetIO.pas解析sharedStrings.xml时,对 UTF-8 BOM 处理不健壮。某些 Excel 导出的.xlsx在sharedStrings.xml开头有EF BB BFBOM,而TdxXmlReader默认按 ANSI 读取。
解决:在TdxSpreadSheetWorkbookReader.ReadFromFile方法中,找到读取sharedStrings.xml的代码段(约第 420 行),将:

LStream := TMemoryStream.Create; LZipFile.ExtractFile('xl/sharedStrings.xml', LStream); LStream.Position := 0; LXmlDoc.LoadFromStream(LStream); // ← 问题在此

改为:

LStream := TMemoryStream.Create; LZipFile.ExtractFile('xl/sharedStrings.xml', LStream); LStream.Position := 0; // 强制 UTF-8 读取 LXmlDoc.LoadFromStream(LStream, TEncoding.UTF8);

4.5 现象:修改dxSpreadSheetStyles.pas后重新编译,样式不生效

原因:TdxSpreadSheetStyleManager使用TObjectDictionary<Integer, TdxSpreadSheetCellStyle>缓存样式,Integer键是HashCode计算值。你改了TdxSpreadSheetCellStyle.GetHashCode逻辑(比如加了新字段),但旧.dcu文件仍用老哈希算法,导致缓存命中失败或崩溃。
解决:每次修改dxSpreadSheetStyles.pas后,必须删除整个__history和Win32\Debug目录,并清理 IDE 的Search Path缓存(Tools → Options → Environment Options → Delphi Options → Library → Library Path → 点右下角 Reset)。


5. 二次开发实战:给公式引擎加一个=ISPRIME(n)自定义函数

这才是源码交付的终极价值——不是看,而是改。下面带你从零实现一个判断质数的 Excel 函数,全程不碰 DevExpress 安装包,只改源码包里的三个文件。

5.1 步骤一:定义函数类TdxSpreadSheetIsPrimeFunction

在dxSpreadSheetFormulaEngine.pas文件末尾(implementation段),新增:

type TdxSpreadSheetIsPrimeFunction = class(TdxSpreadSheetFormulaFunction) public function Execute(const AArgs: array of Variant): Variant; override; end; function TdxSpreadSheetIsPrimeFunction.Execute( const AArgs: array of Variant): Variant; var LNum: Int64; i: Integer; begin Result := False; if Length(AArgs) <> 1 then Exit; if not VarIsNumeric(AArgs[0]) then Exit; LNum := Trunc(VarAsType(AArgs[0], varInteger)); if LNum < 2 then Exit; if LNum = 2 then begin Result := True; Exit; end; if (LNum mod 2) = 0 then Exit; for i := 3 to Trunc(Sqrt(LNum)) do if (LNum mod i) = 0 then Exit; Result := True; end;

5.2 步骤二:注册函数到引擎

仍在dxSpreadSheetFormulaEngine.pas,找到TdxSpreadSheetFormulaEngine.Create构造函数(约第 180 行),在inherited Create之后、FFunctionRegistry := TDictionary<string, TdxSpreadSheetFormulaFunction>.Create之前,插入:

// 注册自定义函数 FFunctionRegistry.Add('ISPRIME', TdxSpreadSheetIsPrimeFunction.Create);

同时,在TdxSpreadSheetFormulaEngine.Destroy中释放它(约第 220 行):

if FFunctionRegistry.TryGetValue('ISPRIME', LFunc) then LFunc.Free;

5.3 步骤三:在 Demo 中验证

回到你的窗体代码,在设置公式前加:

// 确保引擎已初始化 dxSpreadSheet1.Workbook.FormulaEngine.RegisterFunction('ISPRIME', TdxSpreadSheetIsPrimeFunction); // 然后设公式 LCell := LWorksheet.Cells[1, 0]; // A2 LCell.Formula := '=ISPRIME(17)'; // 应返回 TRUE LCell := LWorksheet.Cells[1, 1]; // B2 LCell.Formula := '=ISPRIME(25)'; // 应返回 FALSE

运行,A2 显示TRUE,B2 显示FALSE。此时你在TdxSpreadSheetIsPrimeFunction.Execute打断点,F7 进入,就能看到LNum := 17、循环i := 3 to 4、最终Result := True的全过程——你亲手给 Excel 引擎加了一条新指令。

从那以后我每次要加自定义函数,都强制走一遍这个三步流程:先写Execute逻辑(专注业务),再注册进FFunctionRegistry(专注耦合点),最后用最小 Demo 验证(专注反馈闭环)。不跳步,不抄近路,因为少走一步,下次升级 v1.39 时就得重挖三天坑。希望帮到你。

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

返回列表