简介:这份资源是 DevExpress 旗下 ExpressSpreadSheet 电子表格组件 v1.38 的完整源代码包,面向使用 Delphi 与 C++ Builder 的桌面应用开发者。它适合已经熟悉 VCL 组件体系、希望深入组件内部实现或进行二次开发的中高级程序员,可用于研究表格引擎、公式解析、数据绑定与界面渲染的具体做法,并据此定制符合项目需求的表格功能。压缩包为 rar 格式,共 382 个文件,约 2.81MB,以 res 资源、pas 单元、dpk 与 bpk 包工程、cpp 与 h 源文件、dfm 窗体文件为主,另含 dpr、bdsproj、cbproj 等示例工程配置以及若干 xls 演示表格、bmp 与 jpg 图片、hlp 帮助文档,覆盖从核心库到演示项目的多个层次。已有 447 人学习关注。源码中包含 ExpressGDI+ 图形绘制库、ExpressLibrary 通用模块、XP Theme Manager 主题适配以及丰富的 demo 工程,读者可借此理解组件架构与复用模式,定位渲染、主题与计算逻辑的实现细节,为排查疑难问题或裁剪功能提供直接依据。
1. 先别急着装控件:一份表格控件套件的 Delphi/C++Builder 源代码到底能解决什么
拿到一份表格控件套件的 Delphi/C++Builder 源代码,第一反应往往是双击包文件往 IDE 里装。真到现场就会发现,这类源码包不是普通组件压缩包:它同时牵着运行时包、设计期包、C++ 头文件、条件编译开关和交付时的运行库选项。它适合需要二次改控件行为、补导出/打印链路、查底层绘制逻辑的团队;只想要拖拽即用的人,先评估维护成本。下面按拆包、编译、注册、排错、参数、验证的顺序走一遍。
2. 拆源码包先看目录:Delphi/C++Builder 双栈的文件边界与依赖链
源码包最容易让人误判的地方,是目录里同时躺着.pas、.inc、.dcu、.bpl、.hpp、.lib和工程文件。只看名字会觉得都能编,实际参与编译的只有其中一部分;剩下的是设计期资源、预编译输出,或者给 C++Builder 工程用的导入文件。先按扩展名做一次盘点,比直接打开 IDE 更稳,因为 IDE 会自动把搜索路径、输出路径和历史 DCU 混在一起,后面报错很难定位。这个 v1.38 源码包的处理顺序也一样:先分清源码与产物,再谈编译参数。
2.1 包结构里哪些文件参与编译,哪些只是设计期资源
我一般先在包根目录跑一次扩展名统计,不急着打开工程。命令只扫两层目录,避免被深层输出目录里的 DCU 干扰。统计结果用来判断这份源码是“纯源码 + 工程文件”,还是“源码 + 预编译产物混放”。后者要先清输出,否则编译器可能拿到旧平台、旧 IDE 版本的 DCU,出现莫名其妙的找不到单元或重复符号。
# 只统计两层内的文件扩展名,先看源码和产物的大致分布 find . -maxdepth 2 -type f \ | sed 's#^./##' \ | awk -F. 'NF>1 {print $NF}' \ | sort \ | uniq -c \ | sort -nr这段命令里,-maxdepth 2限制扫描深度,防止把build/Win32/Release/dcu里的历史文件全算进来;awk -F.取最后一个点后的扩展名,没有扩展名的文件会被NF>1过滤掉;最后的sort | uniq -c | sort -nr按数量倒序排列。看到.pas、.inc、.dpk、.dproj占主导,说明源码结构还在;看到大量.dcu、.bpl、.lib,就要先怀疑输出目录是否被一起打包进来。
接下来定位真正的包工程文件。Delphi 侧常见.dpk和.dproj,C++Builder 侧常见.cbproj,有些老包还会保留.bpk。不要只看文件名里的 “Runtime” 或 “Design”,要以包文件内部的requires和contains为准。源码包被改过名、复制过多份时,文件名和实际依赖经常对不上。
# 找出所有包工程文件,先确认运行时包和设计期包各有几个 find . -type f \( -name "*.dpk" -o -name "*.dproj" -o -name "*.cbproj" -o -name "*.bpk" \) -print | sort-type f只看文件,避免目录名带.dpk造成误判;括号里的多个-name用-o连接,最后统一-print。如果结果里出现多个同名但不同目录的工程文件,先不要合并,按目录分组,分别确认它们对应的输出平台。源码包常见的一个坑是:Win32 和 Win64 的工程文件同名,输出目录却指向同一个位置,编译一次就把另一平台的 DCU 覆盖掉。
文件类型可以按下面的边界理解,不必逐个打开:
| 文件类型 | 作用 | 处理方式 |
|---|---|---|
.pas/.inc | 控件核心源码、条件编译片段 | 参与编译,重点看{$IFDEF}和单元引用 |
.dpk/.dproj | Delphi 包定义与工程配置 | 决定requires、contains、输出目录 |
.cbproj | C++Builder 工程配置 | 决定头文件搜索路径、库路径、运行库选项 |
.hpp/.h | 给 C++Builder 用的头文件 | 确认是源码生成还是手工维护,避免旧头文件覆盖 |
.dcu/.bpl/.lib | 编译产物或导入库 | 升级 IDE、换平台前先清掉,不要复用 |
.res/.rc | 设计期资源、图标、版本信息 | 一般随包编译,缺失时先查资源搜索路径 |
这张表里最需要警惕的是.dcu和.hpp。.dcu是编译器直接吃进去的二进制单元,IDE 版本、平台、条件编译开关任何一项不一致,都可能出现“源码明明有,编译却说找不到”的黑匣子现象。.hpp则常常是 Delphi 单元生成给 C++Builder 的桥接头,源码改动后没有重新生成,C++ 侧就会引用旧类布局,运行期表现成访问违例或方法入口错乱。
2.2 依赖链与搜索路径:为什么先编运行时包再编设计期包
这类表格控件源码包通常拆成运行时包和设计期包。运行时包放核心控件、绘制、数据模型和导出逻辑;设计期包负责把控件注册到 IDE 面板,并依赖运行时包。先编设计期包也能过编译,但安装时容易找不到运行时 BPL,或者 IDE 加载到一半报 “Cannot load package”。所以顺序不能反过来:先运行时包,再设计期包,最后才是业务工程引用。
先看包文件里的依赖声明,不要靠猜。下面这条命令只抓requires和contains开头的行,能把包与单元的关系快速拉出来。不同源码的缩进和大小写可能不同,所以用了[[:space:]]和-E。
# 抓取包文件里的依赖与包含单元,先确认运行时包和设计期包的边界 grep -n -E "^[[:space:]]*(requires|contains)[[:space:]]" ./*.dpk ./*.dproj 2>/dev/null-n输出行号,方便回到源文件核对;-E启用扩展正则;^[[:space:]]*允许行首缩进;2>/dev/null只是把不存在的文件报错压掉,不影响结果。如果requires里出现另一个包名,而那个包没有对应的工程文件,先别继续编,优先确认它是系统包、第三方依赖,还是源码包自带但被放进了子目录。
搜索路径也要按“源码目录、公共 inc 目录、输出目录”分开。常见做法是给每个平台建独立输出目录,源码目录只放源码,不把 DCU 写回去。这样清理时直接删build即可,不会误删源文件。下面这条命令只是建目录,路径名可以按你的工程改;关键是 Win32 和 Win64 不要共用同一个 DCU 输出。
# 运行时包和设计期包分开输出,平台也分开,后面排错会省很多事 mkdir -p build/Win32/Release/dcu build/Win32/Release/bpl mkdir -p build/Win64/Release/dcu build/Win64/Release/bpl-p保证父目录不存在时一起建,重复执行不会报错。实际工程里把DCC_DcuOutput指到build/<Platform>/Release/dcu,把包输出指到build/<Platform>/Release/bpl。C++Builder 侧还要把生成的头文件目录加到 include 路径,库目录加到 library 路径。搜索路径的顺序也有讲究:源码目录在前,输出目录在后,旧版本目录不要挂进来,否则编译器可能优先拿到旧.dcu或旧.hpp。
依赖链确认完,再决定是“一个包全编”还是“运行时/设计期分开编”。我的建议是保持源码原本的拆分,不要为了省事把设计期注册单元塞进运行时包。设计期单元往往会引用 IDE 内部单元,塞进运行时包后,业务工程静态链接时容易带进不该带的依赖;交付时也会把设计期资源打进运行库。分包的代价是多一步安装,但边界清楚,后面升级 IDE 或换平台时不容易翻车。
3. 编译与安装闭环:从运行时包到设计期包的最小可复现路径
确认目录和依赖后,下一步不是打开 IDE 点编译,而是先用命令行把运行时包编出来。命令行的好处是参数可见、输出可复现,失败了能直接看完整日志。IDE 里点编译也能成,但搜索路径、平台、输出目录都藏在工程选项里,换一台机器就容易出现“我这边能编,你那边不行”。这一章按运行时包、设计期包、C++Builder 引用三段走,每段都落一个可抄的命令或代码片段。
3.1 用命令行 msbuild 编运行时包:参数、平台与配置怎么下
先编 Release 的 Win32 运行时包,确认最小闭环。下面的工程文件名是占位符,换成你目录里实际的.dproj。不同 IDE 版本的输出目录属性名可能略有差异,先跑通/t:Build、/p:Config、/p:Platform三个核心参数,再补输出目录属性。不要一上来堆十几个参数,失败时不知道是哪一项起作用。
# 先编运行时包,Release + Win32,输出目录显式分开 msbuild RuntimePackage.dproj \ /t:Build \ /p:Config=Release \ /p:Platform=Win32 \ /p:DCC_OutputDir=..\\build\\Win32\\Release\\bpl \ /p:DCC_DcuOutput=..\\build\\Win32\\Release\\dcu/t:Build走完整构建,不做增量判断;/p:Config=Release关掉调试符号和范围检查,交付前更接近真实表现;/p:Platform=Win32明确目标平台,避免默认平台和依赖包不一致;DCC_OutputDir和DCC_DcuOutput分别控制包输出和 DCU 输出。属性名如果在你本地不生效,打开.dproj搜DCC_前缀,按实际节点名替换。编译日志里重点看两件事:有没有从旧目录拿 DCU,以及requires里的包是否都解析到了。
Win32 通过后,再编 Win64。不要在同一轮里同时编两个平台,日志混在一起很难看。C++Builder 工程同理,把.dproj换成.cbproj,平台参数保持一致。若源码包同时提供 Delphi 和 C++Builder 工程,建议先编 Delphi 运行时包,再编 C++Builder 工程,因为 C++ 侧往往依赖 Delphi 生成的.hpp和导入库。顺序反了,C++ 工程可能拿到上一次的旧头文件,编译能过,运行期却对不上。
# Win64 单独跑一遍,输出目录不要和 Win32 混用 msbuild RuntimePackage.dproj \ /t:Build \ /p:Config=Release \ /p:Platform=Win64 \ /p:DCC_OutputDir=..\\build\\Win64\\Release\\bpl \ /p:DCC_DcuOutput=..\\build\\Win64\\Release\\dcuPlatform=Win64会改变指针宽度和部分条件编译分支,源码里如果有{$IFDEF CPU64BITS}之类的开关,必须单独验证。输出目录分开后,可以用文件时间戳确认新产物真的生成了,而不是 IDE 复用了缓存。命令行编译失败时,先看第一条错误,不要从最后一条倒着看;包编译的错误经常是连锁的,第一个找不到的单元才是根因。
3.2 设计期包注册与 IDE 里的控件页验证
运行时包编过后,再编设计期包。设计期包一般会requires运行时包,所以编译前确认运行时 BPL 和 DCP 已经在搜索路径里。下面这条命令用来确认输出文件是否齐全,ls只是看文件,不负责安装;文件缺失时不要继续往 IDE 里装。
# 安装前确认运行时包和设计期包的关键产物都在 ls -l build/Win32/Release/bpl/*.bpl build/Win32/Release/bpl/*.dcp 2>/dev/null*.bpl是运行期包,*.dcp是编译期需要的包信息文件。设计期包编译时如果找不到 DCP,会报找不到单元或包引用;安装时如果找不到 BPL,会报加载失败。两个文件缺一不可,且平台要一致。Win32 的设计期包不要指向 Win64 的 BPL,IDE 本身是 32 位还是 64 位也要一致,否则安装入口能点,加载阶段直接失败。
安装动作放在 IDE 的包管理入口里做:先加运行时包,再加设计期包,顺序不要反。装完不要只看“包列表里有没有”,新建一个空白窗体,打开控件面板确认表格控件页是否出现,再拖一个控件到窗体上,看设计器能否正常绘制。设计期包注册成功但控件面板不出现,常见原因是注册单元没有被contains进去,或者包被编到了另一个平台目录。此时回到.dpk看contains,比在 IDE 里反复卸载重装更快。
3.3 在 C++Builder 工程里引用:头文件、库路径与运行库选项
C++Builder 侧最容易出问题的是头文件和运行库选项。源码包生成的.hpp要进 include 路径,导入库要进 library 路径;如果工程选择动态运行库,还要保证运行时 BPL 在系统或 IDE 的搜索路径里。下面这段 C++ 代码只是演示引用顺序和释放顺序,类名、头文件名以你源码实际声明为准,不要直接照抄类名。
// 只包含公开头文件,避免把内部单元的头文件拖进业务代码 #include <SpreadSheetControl.hpp> // 头文件名按源码实际生成结果替换 void BuildPreview(TForm* owner) { // 创建后立刻设 Parent,再设对齐,减少设计器重绘闪烁 TSpreadSheetControl* sheet = new TSpreadSheetControl(owner); sheet->Parent = owner; sheet->Align = alClient; // 业务代码只碰公开属性,内部单元留给控件自己维护 sheet->Enabled = true; }#include指向生成的头文件,不要手工改生成头文件,否则下次重编会被覆盖;owner传窗体,利用 VCL 的拥有关系自动释放,避免忘记delete;Parent和Align的顺序影响设计期表现,先挂父容器再对齐更稳。工程选项里重点核对三处:Include 路径是否包含头文件输出目录,Library 路径是否包含.lib或.bpl对应目录,Link with runtime packages 是否和运行时包策略一致。三处任何一处不一致,常见表现是编译通过、链接报找不到符号,或者运行时报缺 BPL。
4. 避坑与排查:源码包在 IDE 里翻车的五条血泪记录
这类源码包的报错往往不是“源码写错了”,而是环境、平台、输出目录和包引用互相打架。下面五条是我在类似包上踩过的典型翻车记录,按现象、原因、解决拆开写。每条都尽量落到可检查的文件或命令上,不靠玄学重启 IDE。
4.1 编译期三条记录:找不到单元、包加载失败、平台串用
记录一:源码里明明有.pas,编译却报找不到单元。现象是错误信息指向某个单元名,打开目录能看到同名.pas,但编译器就是不认。原因通常是搜索路径指向了旧输出目录,或者同名的.dcu先被找到,而那个 DCU 是另一个 IDE 版本、另一个平台编出来的。解决顺序是先清输出,再确认搜索路径只保留当前源码目录和当前平台输出目录。用下面的命令找同名 DCU,看到多个结果就先停。
# 找同名 DCU,确认没有从旧目录串进来 find . -type f \( -iname "*.dcu" -o -iname "*.bpl" \) -print | sort-iname忽略大小写,因为不同源码包的大小写习惯不一致;-type f只看文件。清理时不要直接删源码目录,只删build或明确的输出目录。清完重新编运行时包,再编设计期包,不要跳过运行时包直接装设计期包。
记录二:设计期包编译通过,安装时报 “Cannot load package”。现象是包管理入口里能看到包名,勾选后弹加载失败,有时还带一个找不到另一个 BPL 的提示。原因多半是运行时 BPL 不在 IDE 的搜索路径,或者设计期包requires的包名和实际生成的 BPL 文件名不一致。解决方法是先确认运行时 BPL 已生成,再把它的目录加到 IDE 的包搜索路径,最后重新安装设计期包。不要用复制 BPL 到系统目录的方式掩盖问题,交付时还会再犯。
记录三:Win32 编过了,切 Win64 就报重复符号或找不到入口。现象是同一份源码,Win32 正常,Win64 编译到链接阶段失败。原因通常是输出目录没分开,Win32 的 DCU 和 Win64 的 DCU 混在一起,或者 C++Builder 工程引用了 Win32 的导入库。解决方法是给每个平台独立输出目录,编译前清掉对应平台的旧产物,C++ 工程的 Library 路径按平台切换。不要在一套路径里同时挂 Win32 和 Win64 的库目录,链接器不一定会按你期望的顺序选。
4.2 设计期与运行期两条记录:控件重复、中文乱码
记录四:控件面板里出现两个同名控件,拖出来哪个都不对。现象是控件页重复,或者旧控件还在、新控件也注册了。原因通常是多个设计期包注册了同一个类,或者旧 BPL 还在 IDE 搜索路径里,卸载包时只移除了列表项,没有清掉旧文件。解决方法是先在设计期包列表里移除旧包,再删当前输出目录的 BPL,重新编译并安装;同时检查contains里有没有重复的注册单元。不要靠改控件类名绕过,类名改了,已有窗体上的引用也会断。
记录五:设计期正常,运行期中文变成问号或截断。现象是表格里填中文,设计器显示正常,运行后导出或打印出现乱码。原因通常是源码里的字符集条件编译和业务工程不一致,比如控件按 Unicode 编译,业务工程还在走 ANSI 兼容路径;或者导出接口收AnsiString,传入的是宽字符串。解决方法是统一条件编译开关,检查.dpk、.dproj、.cbproj里的UNICODE定义,再核对导出函数的参数类型。字符集问题不要靠改系统区域设置蒙混,交付到另一台机器还会翻车。
5. 参数与边界:字符集、包引用和运行时部署怎么定
源码包能编过只是第一步,真正决定能不能交付的是参数边界:字符集开关是否统一,运行时包是动态链接还是静态链接,IDE 升级后哪些产物必须重编。这三件事在开发机上容易被忽略,到了客户现场就会变成“少一个 BPL”“中文乱码”“换 IDE 后编不过”。这一章把参数选择和边界条件拆开,尽量用可检查的命令和条件编译片段说明。
5.1 字符集与条件编译:源码里的开关不是装饰
表格控件涉及单元格文本、导出 CSV、打印页眉页脚,字符集开关一旦不统一,表现不只是乱码,还可能是字符串长度计算错误。先扫一遍源码里的条件编译,重点看UNICODE、MSWINDOWS、CPU64BITS和调试开关。下面这条命令只抓定义行,不展开所有分支,先看哪些开关被频繁使用。
# 扫源码和 inc 文件里的条件编译定义,先看字符集和平台开关 grep -RIn --include="*.pas" --include="*.inc" \ -E "\{\$(IFDEF|IFNDEF|DEFINE)[[:space:]]+(UNICODE|MSWINDOWS|CPU64BITS|DEBUG)" . \ | head -n 40-R递归,-I忽略二进制,--include限定源码扩展名;-E让多个开关用|连接;head -n 40只是防止输出太长,实际排查时可以去掉。看到UNICODE分支,就要确认业务工程和包工程的定义一致。Delphi 新版本默认走 Unicode,但老包可能保留 ANSI 兼容分支,C++Builder 侧的头文件生成也会受影响。
条件编译的写法要跟源码风格一致,不要混用{$IFDEF}和{$IF Defined(...)}造成阅读混乱。下面这段只是示意字符集分支,实际函数名按源码替换;关键是让同一份业务代码在两种开关下都能明确走哪条路径。
{$IFDEF UNICODE} // Unicode 路径:内部统一用宽字符串,导出时再转目标编码 Result := WideStringToUTF8(CellText); {$ELSE} // ANSI 兼容路径:只在维护老工程时保留,不要和新代码混用 Result := AnsiStringToUTF8(CellText); {$ENDIF}UNICODE定义来自工程选项,不要在每个单元里手工DEFINE,否则同一工程的不同单元可能走不同分支。导出和打印接口的参数类型要跟着开关走,不能一个接口收WideString,另一个收AnsiString还互相赋值。字符集问题排查到最后,往往是工程选项不一致,而不是控件源码本身有缺陷。
5.2 运行时包与静态链接:交付体积和依赖的取舍
运行时包和静态链接没有绝对好坏,只看交付场景。动态链接的优点是多个模块共用一份 BPL,补丁时替换 BPL 即可;缺点是部署时必须带齐 BPL,路径不对就启动失败。静态链接的优点是单文件或少量文件交付,缺点是体积大,升级要重编业务工程。表格控件源码包通常两种都支持,关键是把包引用选项和实际交付物对齐。
| 策略 | 适用场景 | 要检查的文件 | 常见问题 |
|---|---|---|---|
| 动态运行时包 | 多模块共用、需要单独替换控件 | .bpl、.dcp、PATH | 缺 BPL、平台混用、路径未带 |
| 静态链接 | 单机交付、不想带一堆 BPL | .dcu、.lib、.obj | 体积大、重复符号、运行库选项冲突 |
| 混合 | 主程序静态、插件动态 | 两套输出目录 | 边界不清,调试时加载了旧包 |
先看工程文件里的包引用配置,再决定交付物。下面这条命令抓包引用相关字段,不同 IDE 版本的字段名可能不同,看到UsePackage、RuntimePackage、LinkPackage一类关键词就人工核对。
# 查工程里的包引用配置,确认动态/静态策略没有混 grep -n -E "UsePackage|RuntimePackage|LinkPackage|DCC_UsePackage" ./*.dproj ./*.cbproj 2>/dev/null-n带行号,-E支持多关键词,2>/dev/null忽略不存在的工程文件。动态链接时,交付清单里要列 BPL 和依赖的运行库;静态链接时,确认没有残留的包引用,否则链接器仍会去找 BPL。混合模式最怕主程序和插件用了不同版本的运行时包,调试时加载了 IDE 路径下的旧 BPL,现场却只带了新 BPL。
5.3 版本兼容边界:IDE 升级后先改哪几处
IDE 升级后,源码包最常见的边界是 DCU 不兼容、包引用名变化、C++ 头文件需要重生成。不要直接打开旧工程就编,先清输出,再按平台重编运行时包。下面这条命令只删构建目录,不动源码;执行前确认build是你自己的输出目录,不要对源码目录做递归删除。
# 升级 IDE 后先清构建目录,避免旧 DCU 和旧 BPL 干扰 rm -rf build mkdir -p build/Win32/Release/dcu build/Win32/Release/bpl mkdir -p build/Win64/Release/dcu build/Win64/Release/bplrm -rf build的前提是输出都集中在build,如果源码包里输出散落在各处,先按第 2 章的find结果确认再清。重编顺序仍然是运行时包、设计期包、C++Builder 工程。升级后如果设计期包安装失败,先看包引用名是否还匹配,再看 IDE 的包搜索路径是否还指向旧版本目录。C++Builder 侧还要确认.hpp是重新生成的,不是从旧输出目录复制过来的。
6. 用最小表格 Demo 验完整链路:导出、打印和一次干净重编
编过、装上也别急着接业务工程,先用一个最小 Demo 把创建、填值、导出、释放跑一遍。Demo 不追求界面,只验证控件生命周期和导出链路。下面这段 Pascal 代码是结构示意,类名和方法名按源码实际声明替换;重点是创建后设父容器、填两个单元格、调一次导出、最后释放。
procedure TForm1.RunSmokeTest; var Book: TSpreadSheetBook; Sheet: TSpreadSheetSheet; begin Book := TSpreadSheetBook.Create(nil); try Sheet := Book.Sheets.Add; Sheet.Cells[0, 0].AsString := '编号'; Sheet.Cells[0, 1].AsString := '金额'; Sheet.Cells[1, 0].AsString := 'A-001'; Sheet.Cells[1, 1].AsFloat := 128.50; // 导出方法名按源码实际声明替换,先验 CSV 再验打印 Book.ExportToCSV('smoke.csv'); finally Book.Free; end; end;Create(nil)表示不挂到窗体,适合无界面冒烟测试;Sheets.Add后立刻填值,避免空表导出时误判成功;AsString和AsFloat分别验证文本与数值单元格;ExportToCSV只验一条最简导出链路,打印链路再单独用一页纸测试。导出文件生成后打开看编码和列分隔符,打印预览看页边距和分页,不要只看返回值。
从那以后我每次拿到这类源码包,都先跑运行时包、再装设计期包、最后用最小 Demo 验导出和打印,不再直接往主工程里塞。希望帮到你。
本文还有配套的精品资源,点击获取