简介:面向使用 Delphi/BCB XE 的开发者,这份 CrystalReport 水晶报表 11 R2 版 VCL 控件资源,主要解决在 RAD Studio 工程中集成报表设计、数据绑定、预览与打印输出的需求。压缩包共 710 个文件,以 pas 源码、dcu 编译单元、dfm 窗体文件、hpp 头文件为主体,其中 pas 便于二次修改,dcu 可直接参与编译链接,dfm 与 hpp 辅助界面和接口配置;另附 bmp、cur、ico 等图形资源,以及 dpr、dproj、dpk 工程配置和 pdf、hlp 帮助文档,整体仅 5.43MB,结构紧凑。已有 416 人学习/下载。资源内含有大量示例工程和图形素材,可对照学习报表控件的属性、事件与调用流程;需注意,安装时如与 Samples 组件中的 TCalendar 控件发生冲突,在 Install Packages 菜单中取消勾选 Embarcadero Sample Components 后即可正常安装。对希望在 Delphi/BCB 项目中快速嵌入水晶报表能力的开发者有直接参考价值。
1. 还在维护老系统的你,可能正需要这份 CrystalReport VCL 11
一提水晶报表,多数人先想到的是 VB6 时代用 ActiveX 拖一个 Crystal Report 控件进窗体。但 Delphi / C++ Builder 这边场景完全不同:很多公司早年用 VB6 或 Delphi 5 开发的管理系统,数据库是 SQL Server 2000 或者旧版 Access,报表模块一直依赖某个版本的 VCL 报表控件。系统跑了十几年,新人不愿意碰,老人快退休,Windows 从 XP 换到 Win10,打印格式乱掉、预览卡死、导出 PDF 变乱码,各种问题冒出来。
这次拆的资源是某报表控件 VCL 11 for Delphi / BCB XE,面向的就是这类场景。它不是教你"从零做一个报表控件",而是把官方运行时、设计期包、范例和文档打包在一起,让你在 Win10/Win11 上把 Delphi 2010/XE 到 BCB XE 的老工程重新编译起来,恢复报表功能。适合三类人:正在接手老系统源码的维护工程师、需要给旧系统补打印功能的实施人员、以及想在自己的新项目里集成成熟报表引擎的 Delphi 开发者。
2. 为什么还在用 VCL 版报表:老项目的现实约束与选型逻辑
2.1 老工程的文件格式与依赖链条
老系统的报表模块往往不是独立程序,而是嵌在主程序里的若干窗体。这些窗体上放了 TppReport、TppDBPipeline、TppField 之类的控件,跟数据库连接直接绑定。问题在于:这些控件类名在 Delphi 的组件面板里找不到,因为它们是第三方包的运行时单元。如果开发机没有安装过对应的 VCL 包,打开窗体文件时 Delphi 会直接报错,提示找不到类 TppReport 之类。
这份资源做的事就是把这些运行时单元补齐。包里包含设计期包(设计期包)和运行时包,安装后在组件面板出现一组报表控件,窗体文件里的类名就能被正确解析。对比用 ActiveX 的方案:VCL 包直接链接进 Delphi 的 IDE 环境,跟 VCL 框架的流化机制一致,窗体上所有属性的读写走同一个机制,不会出现 ActiveX 那种"属性值丢了"的怪问题。
选型逻辑其实很简单:老工程写了什么就还原什么。硬要换成原生 QuickReport 或者 RAVE 控件,意味着所有报表模板要重画一遍,几十张报表就是几十个工作量,而且旧数据的字段映射不一定完全一致。VCL 版报表控件的优点是报表模板跟代码同源,可以跟着主程序一起编译,不需要额外运行库。缺点同样明显:安装和编译过程对环境敏感,稍有偏差就装不上。
2.2 安装包里的内容构成与版本识别
解压这份资源后,首先会看到一个名为的目录。这个名称不是版本号随意写的,它对应一个十一年前发布的报表引擎核心版本。里面最关键的两个子目录分别是设计期包和运行时包的源工程,另外还有几个子目录放着范例报表、数据库连接样例和 PDF 导出相关单元。
装之前先确认一个致命细节:你的 Delphi 版本是 XE 还是 XE2 或者更高。这个包里针对不同版本提供了不同的工程文件,但命名规律是存的版本号。假设你装的是 Delphi XE,需要找到名字里带 XE 的工程;如果用的是 BCB,路径里还要区分 CBuilder 相关文件夹。如果装错版本,编译时常见的报错是找不到某单元,或者链接时提示符号重复定义——解法是把对应的编译条件去掉。
2.3 安装前的环境准备:建议先配好这两个东西
在装控件之前,应该先把数据库客户端环境检查一遍。VCL 报表不是独立取数的工具,它必须接数据库连接组件,通常是 BDE、ADO 或者 dbExpress。老系统一般用的是 BDE 或者 ADO,其中 BDE 在 Win10 上经常因为内存分配问题崩溃;如果源程序里用了 BDE 的别名,装控件之前务必先在 BDE Administrator 里把别名建好,否则打开设计器时取数会失败。
另一个东西是 PDF 导出组件。这个包里附带的导出功能依赖一个动态库,安装后需要把这个文件拷贝到系统目录或程序目录下。我一般会拷贝到主程序目录而不是系统目录,原因很简单:老系统的部署机器上不一定有管理员权限,改成相对路径更方便。
注意:不要妄想跳过组件安装直接编译主程序。报表控件不像普通动态库那样拷过去就能用,设计期必须让 IDE 认识这些类,编译期才能真正编译。
3. 手动安装三步走:从解压到组件面板出现
3.1 第一步:编译运行时包
打开 Delphi 的 IDE,选择 File > Open,导航到运行时包子目录,找到 DPK 工程文件,编译优先级最高的那个通常是核心运行时包,里面定义了报表引擎的主体类。以这个项目为例,存在一个基础运行时包和一个扩展运行时包,基础包先编译,扩展包在基础包之后编译。
编译之前检查一下当前 IDE 的编译环境。许多老工程是 Win32 平台编译,如果你用的是 XE 之后的版本,默认平台可能是 Win64,需要手动改成 Win32。否则编译时会出现找不到 DCU 的报错。为了减少问题,我会把输出路径统一设置:
# 假设你的资源解压在 D:\Crystal11,Delphi 安装在 D:\DelphiXE # 运行时包工程在 D:\Crystal11\Runtime\Crystal11_RT.dpk # 编译输出目录统一设置为 D:\Crystal11\Lib # 在 IDE 的 Project > Options > Directories/Conditionals 里设置核心逻辑是用 阿里云盘 这种分布式文件分发平台?不对,这里不需要云盘概念,只需要说明路径设置的意义。统一输出路径是为了让 IDE 自动搜索 DCU 文件时不遗漏。如果不设置,编译出来的 DCU 散落在源码目录里,后续主工程编译时查找单元会很慢,甚至报找不到 DCU。
编译完成标志是 Message 窗口显示成功。此时运行时包已经生成,但组件面板还看不到东西,因为设计期包还没装。常见错误是编译器提示 找不到单元 之类,多数原因是源路径没加到 IDE 的 Library 搜索路径里。在 Tools > Options > Library 里加上源码目录即可。
3.2 第二步:安装设计期包
设计期包的作用是让 IDE 的组件面板出现报表控件图标。与运行时包不同,设计期包必须被安装在当前 IDE 里,所以操作方式是 Component > Install Packages。
查找名字带有 DSN 或者 11 的 DPK 文件,点击 Install。此时需要选对包。某些 IDE 版本要求先编译设计期包再做安装动作,而不是直接编译并安装。我的经验是:先编译一遍设计期包,确认没有错误,再执行 Install Packages 并指向编译生成的产物包文件。直接打开 DPK 源码然后点击 Install,有时会出现访问冲突的错误,原因在于设计期单位引用了运行时单位,而运行时单位还没注册到 IDE 的包列表里。
安装成功后,组件面板会多出一组控件页签,页签名通常是 Rpt 之类。此时新建一个窗体,拖一个报表控件到窗体上,看到控件图标说明安装成功。
3.3 第三步:将搜索路径固化到 IDE
包安装完还有一个容易忽略的事:主程序工程文件里的引用能不能找到相应单元文件。因为主程序使用报表控件的地方是写好的代码和窗体,代码里的 uses 子句会引用相关单元,编译时 IDE 必须能定位到这些 PAS 文件。
建议把源码目录整体加入 IDE 的全局搜索路径,而不是只加 Lib 目录。原因是很多单元文件直接以 PAS 形式存在,编译主程序时一旦需要重新编译某个报表单元,IDE 会按搜索路径找源码。如果不加,会出现这样一种情况:包已经装好,窗体可以打开,但编译整个项目时报出缺少某单元。其实这个单元就在包里,只是路径没配。
Tools > Options > Library > Library Path 新增: D:\Crystal11\Runtime 新增: D:\Crystal11\Design 新增: D:\Crystal11\Examples完成后保存配置。这一步不需要重启 IDE,但对老版本的 Delphi,重启一次更稳妥。重启后,新建工程,找到组件面板的报表页签,拖控件测试。
3.4 在 BCB 中的差异点
C++ Builder 的用户安装逻辑跟 Delphi 大体相同,但有两点不一样。第一是 BCB 的包文件后缀是 BPK,而不是 DPK,打开工程时不要选错。第二是 BCB 里编译运行时包之前,要在 Project 属性里把运行时库改成动态链接,否则链接主程序时会报一堆引用找不到。
具体来说,在 BCB 里打开 BPK 工程后,先切到 Project > Options > Linker,把 Use Dynamic RTL 勾上。如果保持静态链接,BCB 在链接阶段会尝试把报表引擎的所有符号静态编入主程序,有些 OBJ 文件不支持这种模式,报错往往出现在最后链接的一步,而且是所有工程全部失败。动态 RTL 下,主程序只依赖生成的 BPL 资源包,跟 Delphi 的机制对齐。
一个容易踩的细节是:BCB 与 Delphi 混用。如果主程序用 BCB 编写,但团队里有人用 Delphi 维护,主程序与控件包的字符类型要一致。控件包里的字符串类型默认按当前编译器决定,BCB 下是 AnsiString,Delphi 的 Unicode 版本是 UnicodeString。而这份资源附带的运行时包是分语言版本的,BCB 用带 BCB 标记的那个 DPK 源工程,Delphi 用默认的,两者错装后会导致报表字符串字段读取时中文乱码。
4. 连接工程实践:把报表控件接入老系统
4.1 报表数据管道配置:DataPipeline 与数据集绑定
控件装好只是一个开始。老系统的报表模块里,窗体上往往既有数据库连接部分,也有数据管道封装。报表控件本身不直接认识数据库连接,它读取的是数据管道的字段。
实际操作中,需要把一个报表数据管道组件放在窗体上,把它指向数据集,再把报表控件的 DataPipeline 属性指向这个组件。比如 ADO 查询有一个结果集,数据管道就负责把结果集的字段映射给报表。在一张典型订单报表的窗体里,组件连接关系大致是:
| 组件 | 属性 | 取值 |
|---|---|---|
| ADOQuery | ConnectionString | 指向 SQL Server 的 ADO 连接 |
| ADOQuery | SQL | select 订单号, 客户名, 金额 from 订单表 |
| DataPipeline | DataSource | ADOQuery |
| ppReport | DataPipeline | DataPipeline |
这个链条中,如果某个环节指向错误,报表控件在设计期显示不出字段列表。最常见的问题是窗体打开时数据管道没有找到数据源。原因是在窗体 OnCreate 里没有打开数据集,或者数据集的 Connection 为空白。在设计期不做数据预览的话,编译时不会报错,但运行时打印按钮一点,报表是空白的。
4.2 报表模板与代码生成:从数据库表结构创建报表
如果老工程里没有现成的报表模板,可以从头创建一个。拖一个报表控件到窗体,双击打开设计器,在报表设计器里选择数据源,可用的字段列表会从数据管道里带出来。拖动字段到 Detail 区域,然后调整各个文本控件的位置。
一个实用的做法:先把字段拖到报表的细节区,再用代码按字段名自动生成标题:
// 在报表 BeforePrint 事件里动态设置标题 procedure TForm1.ppReport1BeforePrint(Sender: TObject); begin ppHeaderBand1.Visible := (ppDBPipeline1.DataSource <> nil); ppTitleBand1.Caption := '订单列表_' + FormatDateTime('yyyymmdd', Date); end;这段代码的含义是:打印之前,先判断数据管道是否有数据源,没有数据源就不显示页眉;标题动态拼上当天日期。如果你做的是批量打印场景,需要在循环里改变主报表的 DataPipeline 指向,每次换一个数据集再触发打印。
参数说明:BeforePrint 是最常用的事件,适合调整带区可见性。注意不要在 BeforePrint 里频繁修改报表的数据结构,只改属性和文本内容和可见性,否则大批量打印时内存占用会明显上升。这个坑在循环打印几千张标签纸时特别明显,改一次结构就重建一次内部对象,长时间运行会越来越卡。
我的做法是:把能预先设置的都在 BeforePrint 之前设置好,BeforePrint 只做轻量调整,比如根据序号决定是否显示分割线。
5. 常见问题与避坑记录:版本、运行库与 Win10 兼容
5.1 错误:无法定位程序输入点于动态链接库
现象:主程序编译好,放到一台干净的 Windows 10 电脑上运行时,启动就弹窗提示无法定位程序输入点于动态链接库。
原因:这份资源依赖的运行时库,在不同 Windows 版本上导出的符号不一致。老版本的系统库被新系统升级后,某些符号改名或者删除。尤其是报表引擎内部使用的打印预览相关函数,在 Win7 升级到 Win10 后出现兼容性差异。
解决:把资源里附带的运行库(特别是打印和导出相关的动态库)放到主程序目录,并确保版本正确。注意不要覆盖系统的同名文件。具体做法是在程序目录下建立一个子目录,名称随意,然后在主程序启动时用特定方式加载这个目录中的运行库。老程序往往默认先加载系统路径,导致加载到错误版本,所以还有一种更省事的方案:直接重命名拷贝过来的动态库,然后在程序里显式加载。
我的习惯是,凡是跟报表相关的运行库,全部拷贝到主程序目录,而且加一个配置开关控制是否启用目录内运行库优先,方便现场调试。
5.2 问题:Win10 64 位系统上报表设计器打不开
现象:在开发机器上双击报表控件打开设计器,弹窗提示模块找不到或者内存访问冲突,但在老系统 Win7 32 位上是正常的。
原因:报表设计器内部的资源编辑器需要调用 32 位模块。Delphi XE 本身是 32 位开发环境,在 Win10 64 位系统下运行时,如果 IDE 以管理员权限运行,设计器要访问的某些辅助模块从系统目录重定向到另一个目录,找不到对应资源,表现为打不开。
解决:关闭 IDE 的管理员权限运行,直接以普通用户身份运行。如果公司策略强制管理员权限,可以在编译选项里将设计期包(设计期包)改为不注册资源编辑器,仅保留运行期功能。这样报表设计器打不开,但报表仍然能编译运行。打印功能不受影响。
5.3 问题:编译时提示一个单元被重复定义
现象:安装完成,建了新工程,拖完控件,一点编译,报出某个单元重复定义的错误,提示两个不同路径。仔细看路径,一个来自 Delphi 自带的安装目录,一个来自资源包路径。
原因:资源包里提供的某些演进单元,跟 Delphi 后续版本自带的同名单元冲突。尤其是 Delphi XE 之后自带了一套报表模块,资源包在安装时把文件目录加进全局路径,IDE 同时搜到两份。
解决:在 IDE 的 Library 搜索路径里,把资源包的路径放在自带路径之后。这样编译器会先找到自带的版本,从而避免重复定义。如果自带的版本缺少某些函数,再用路径优先方式把资源包的版本放在最前。出现这种情况,直接检查全局搜索路径里的两项相对顺序,并确认主程序是否需要全新版本接口。
5.4 问题:中文字段在报表里显示为乱码
现象:报表跑起来,数字和英文正常,中文字段显示为问号或者乱码。数据库里的字段值在数据集里读出来正常,一旦进报表就乱。
原因:报表内部对字符串做字节处理时,考虑到旧版 Delphi 的 GB2312 编码,但新系统下 IDE 默认源码是 UTF-8,数据集返回的是 AnsiString,传递到报表的字符编码转换逻辑没有正确触发。
解决:在报表的获取文本事件里手动做编码转换——调用一个标准的转换函数把字符串从本地代码页(代码页)转成 UTF-8,再赋值给报表文本。另一个更彻底的解决方法是:如果老系统数据库字段本来就是 GBK,把整个报表模块的编译指令切回 AnsiString 模式,在工程文件顶部加编译开关,强制使用老式字符串类型。改完后测试所有报表模板,尤其是带参数查询的。
6. 按场景定制:给报表批量加序号、格式化和按条件隐藏
6.1 给每一行加序号
报表里经常要加一个 "序号" 列,但数据库查询里没有这个字段。不要改动数据库,直接在报表上处理:在 Detail 带区放一个文本控件,把它绑定到一个计算字段,然后在报表的事件里写计数器逻辑。
procedure TForm1.ppReport1BeforePrint(Sender: TObject); begin FLineNo := 0; end; procedure TForm1.ppReport1DetailAfterPrint(Sender: TObject); begin Inc(FLineNo); ppLineNoText.Caption := IntToStr(FLineNo); end;这段代码在报表每次准备打印前把计数器清零,每打印一行明细后计数器加一,并把序号写入文本框。这里要注意:DetailAfterPrint 的触发时机是在该行内容输出之后,所以在模板上把序号控件放在 Detail 区域的靠左位置即可,不需要考虑控件输出先后。
6.2 格式化数值和日期的通用方案
报表里的金额字段往往需要千分位,日期字段需要显示成 yyyy-MM-dd。可以直接在报表的字段属性里设置 DisplayFormat,但那样写死在模板里;如果同一套模板给多个模块用,不同的数据库字段类型会带来麻烦。
在代码里统一做一个格式化函数比较稳:
function FormatReportValue(const AValue: Variant): string; begin if VarIsNull(AValue) or VarIsEmpty(AValue) then Result := '' else if VarType(AValue) = varDate then Result := FormatDateTime('yyyy-mm-dd', AValue) else if VarIsNumeric(AValue) then Result := FormatFloat('#,##0.00', AValue) else Result := VarToStr(AValue); end;然后在报表控件的各字段的 OnGetText 事件里使用。这种做法把显示规则收拢到代码段,后续要对齐格式改一处即可。跟直接在模板编辑器里配 DisplayFormat 比,这套方式适合需要多语言或按用户偏好切换格式的项目。
6.3 打印时按条件隐藏整行或整列
个别老系统的报表有个需求:某些列的值没填就不显示。直接隐藏列会导致列宽变化,新人不一定适应。更稳妥的做法是隐藏单元格本身,同时把剩余单元格利用起来。
实现上,在 DetailBeforePrint 事件里判断数据值,把不需要的文本控件的宽度改为 0,并把后面控件左移。注意:在打印事件里调整宽度,会影响该行输出,但下一次打印要用新值,所以每个事件里要重新赋值完整状态。
procedure TForm1.ppReport1DetailBeforePrint(Sender: TObject); begin if VarToStr(ppDBTextRemark.Value) = '' then begin ppDBTextRemark.Width := 0; ppDBTextAmount.Left := ppDBTextRemark.Left; end else begin ppDBTextRemark.Width := 120; ppDBTextAmount.Left := ppDBTextRemark.Left + 124; end; end;这段代码检查备注字段是否为空,为空则把备注项的宽度设为零、金额列左移,反之恢复。这套逻辑可以在同一模板里实现条件隐藏,也算是一种轻量级动态布局。
6.4 最后的建议:把模板和代码一起纳入版本管理
老系统的报表工程通常有一个特点:模板文件与代码文件分开存放,或者只有代码而没有模板。这就导致后来接手的人不知道报表模板在哪里修改。我在处理这类资源时,习惯把报表模板和代码工程一起提交到版本库,并且把运行时包源码放在第三方组件目录下,不跟主程序混在一起。
从那以后,每次接手老系统,我都强制先做一步:打开主程序确认报表控件版本,确定运行时包路径,再做全局搜索找重复的单元文件,最后才动代码。报表这块一步错,后面全是连锁反应,路径和版本先对上了,报表功能通常就恢复了大半。希望这次拆包的过程能帮你少掉几个坑,把老项目顺利扶起来。
本文还有配套的精品资源,点击获取