简介:Quick Report 4.05 是面向 BCB6(Borland C++ Builder 6)与 Delphi 7 开发者的报表生成控件包,主要解决可视化报表设计、打印预览与自定义纸张打印中的布局错乱问题。资源以完整源码形式提供,适合需要将报表功能集成进桌面应用、并希望深入理解报表引擎内部实现的中高级开发者。压缩包为 rar 格式,共 65 个文件,约 247KB,其中 34 个 pas 源码文件承载报表核心逻辑与各功能单元,15 个 dfm 界面文件记录设计器、编辑器、预览与向导等窗体布局,另有 res、dcr 等资源文件及 dpk、bpk 安装包文件,便于在 IDE 中直接编译安装。目前已有 288 人学习下载。借助源码与资源文件,读者可掌握报表控件从设计到打印的完整链路,按需修改纸张处理、导出过滤等模块,完成二次开发与业务定制。
1. 拿到一份 BCB6/D7 时代的报表控件源码,我为什么还愿意花时间拆它
前阵子整理旧工程目录,翻出一套 Quick Report 4.05 的源码包,配套的是 Borland C++ Builder 6 和 Delphi 7 两个版本。说实话,第一反应是“这玩意儿还有人用吗”,但真把压缩包解开、把包文件装进 IDE、跑通第一个打印预览之后,我改主意了。现在市面上讲打印报表控件源码的资源,要么是某个商业控件的使用手册,要么是零散的 Demo 片段,像这样把完整源码、包安装文件、示例工程一起给出来的,反而不多见。它解决的是一个很具体的问题:在 BCB6/D7 这类老 IDE 里,怎么不依赖外部运行库、纯靠 VCL 把报表设计、预览、打印这条链路走通。适合两类人——一类是手里还有老项目要维护、被报表模块卡住的开发者,另一类是单纯想看看早期报表控件内部怎么组织分页、怎么和打印机打交道的人。下面我按“这东西是什么、怎么跑起来、核心代码长什么样、怎么改”的顺序,把拆包过程完整写一遍。
2. 项目定位与技术选型:Quick Report 4.05 到底解决了什么问题
2.1 它不是一个独立软件,而是一组 VCL 组件包
Quick Report 4.05 的本质是一套 Delphi/C++ Builder 的组件包(Component Package),安装后会往 IDE 的组件面板里注册一批控件,比如 TQuickRep、TQRBand、TQRLabel、TQRDBText、TQRExpr 这些。你在窗体上拖一个 TQuickRep,再往上叠 Band,Band 里放标签和表达式,运行时调 Preview 或 Print 就能出报表。源码包里通常包含.pas单元文件、.dpk/.bpk包工程、.res资源文件,以及针对 BCB6 和 D7 分别准备的包安装工程。这一点很关键——它不是让你uses一个单元就完事,而是要走 IDE 的 Install Package 流程,把设计期控件注册进去。
为什么强调这个?因为很多人拿到源码包第一反应是“我直接编译不就行了”,结果发现设计期控件没注册,窗体上根本拖不出来。常见做法是:先编译运行期包(Runtime Package),再编译设计期包(Design Package),最后在 IDE 里 Install。运行期包只含逻辑,设计期包才负责往面板注册控件和属性编辑器。
2.2 技术栈锁定在 BCB6 / D7,意味着什么
BCB6 和 D7 都是 2002 年前后的产品,对应的 Object Pascal 和 C++ 编译器版本比较老。这意味着几件事:第一,源码里大量使用AnsiString而不是UnicodeString,字符串处理要留意编码;第二,VCL 的TCanvas、TPrinter接口和现在的新版本有差异,比如Printer.Canvas的坐标原点、SetMapMode的用法;第三,包工程文件格式是老的.bpk(BCB)和.dpk(Delphi),不是后来的.dproj。如果你打算把它迁到新版本 IDE,这些点都是要逐个处理的。
从架构上看,Quick Report 的分层大致是这样:
| 层次 | 主要单元 | 职责 |
|---|---|---|
| 报表定义层 | QuickRpt.pas | TQuickRep、TQRBand、TQRSubDetail 等容器控件 |
| 输出控件层 | QRLabel.pas、QRDBText.pas、QRExpr.pas | 具体显示元素,负责测量和绘制 |
| 打印调度层 | QRPrntr.pas | 和 TPrinter 交互,处理分页、页边距、打印机能力 |
| 表达式引擎 | QRExpr.pas 内的解析部分 | 解析SUM(...)、IF(...)这类表达式 |
| 预览层 | QRPreview.pas | 屏幕预览、缩放、翻页 |
这个分层决定了你改功能时该动哪个文件:改显示样式去输出控件层,改分页逻辑去打印调度层,改表达式语法去表达式引擎。
2.3 和同类方案比,它的取舍在哪
那个年代做报表,常见选择有 Quick Report、ReportBuilder、Rave Reports。Quick Report 的优势是轻、源码可得、和 VCL 结合紧;劣势是复杂报表(比如多层嵌套子报表、交叉表)支持弱,表达式引擎也比较简单。4.05 这个版本在 4.x 系列里属于比较成熟的,Band 类型已经比较全,支持 Title、PageHeader、Detail、SubDetail、Summary、PageFooter 这些。你如果要做的是“主从表打印、带页眉页脚和合计”这类常规需求,它够用;要做“动态列交叉表”,就得自己扩展。
3. 把源码跑起来:从解包到第一个预览窗口
3.1 解包后先看清目录,别急着编译
源码包解开后,目录结构通常长这样(不同打包方式略有差异,但核心一致):
QuickReport405/ ├── Source/ # 核心源码单元 │ ├── QuickRpt.pas │ ├── QRLabel.pas │ ├── QRDBText.pas │ ├── QRExpr.pas │ ├── QRPrntr.pas │ └── QRPreview.pas ├── Packages/ # 包工程 │ ├── BCB6/ │ │ ├── QR405_R.bpk # 运行期包 │ │ └── QR405_D.bpk # 设计期包 │ └── Delphi7/ │ ├── QR405_R.dpk │ └── QR405_D.dpk ├── Examples/ # 示例工程 │ ├── SimpleReport/ │ └── MasterDetail/ └── Lib/ # 预编译 DCU/OBJ(可选)先确认Source里的单元齐全,再看Packages下对应你 IDE 版本的包工程。如果只有 Delphi 的.dpk没有 BCB 的.bpk,那 BCB 用户需要自己新建包工程把源码加进去——这种情况我遇到过,后面避坑章节细说。
3.2 编译运行期包和设计期包
以 BCB6 为例,操作顺序是:
- 打开
Packages/BCB6/QR405_R.bpk,直接 Build。这一步只编译逻辑,不注册控件。 - Build 通过后,打开
QR405_D.bpk,Build。设计期包会requires运行期包,所以必须先有运行期包的编译结果。 - 两个都通过后,在
QR405_D.bpk的工程窗口右键,选 Install。
命令行方式也可以,但 IDE 里操作更直观。如果你习惯用命令行,大致是:
# 在 BCB6 安装目录的 bin 下,用 bpr2mak 和 make 组合 bpr2mak QR405_R.bpk make -f QR405_R.mak bpr2mak QR405_D.bpk make -f QR405_D.mak注意bpr2mak生成的.mak里路径是相对的,如果你把包工程挪了位置,要手动改INCLUDE和LIB路径。这一步翻车的概率不低,尤其是源码包解压到带空格的目录时,make会报找不到文件。
3.3 验证安装:拖一个 TQuickRep 试试
安装成功后,组件面板上会多出一个 QuickReport 页签。新建一个 VCL 工程,拖一个 TQuickRep 到窗体上,再拖一个 TQRBand,把BandType设成rbDetail,里面放一个 TQRLabel,Caption写点文字。然后放一个按钮,OnClick里写:
procedure TForm1.Button1Click(Sender: TObject); begin QuickRep1.Preview; // 打开预览窗口 end;如果预览窗口能弹出来并显示你写的文字,说明运行期和设计期都装好了。这一步看着简单,但它是后面所有调试的基础——预览通了,打印链路基本就通了,因为预览和打印走的是同一套绘制逻辑,只是输出设备不同。
3.4 示例工程怎么用
Examples下的工程是验证控件功能的最好材料。SimpleReport演示最基本的 Band 布局和表达式;MasterDetail演示主从表,用 TQRSubDetail 关联主表的 Detail Band。打开示例工程时注意:它的搜索路径可能指向源码包解压前的绝对路径,需要你在 Project Options 的 Directories/Conditionals 里把 Include 和 Library 路径改成你本地的Source目录。改完重新 Build,能跑起来就说明环境没问题了。
4. 核心模块拆解:分页、绘制、表达式三条链路
4.1 分页逻辑:TQuickRep 怎么决定在哪断页
分页是报表控件最核心也最容易出问题的地方。Quick Report 的分页逻辑集中在TQuickRep的Prepare和 Band 的BeforePrint/AfterPrint事件链里。简化后的流程是:Prepare先计算所有 Band 的总高度,然后从第一页开始,逐个 Band 往页面上放,放不下就触发NewPage,把当前 Band 挪到下一页。
关键代码在QuickRpt.pas里,大致逻辑如下(为便于理解做了简化):
procedure TQuickRep.Prepare; var Band: TQRBand; PageSpace: Integer; begin // 初始化第一页 NewPage; PageSpace := PageHeight - Margins.Top - Margins.Bottom; for Band in Bands do begin // 如果当前页剩余空间放不下这个 Band,就换页 if Band.Height > PageSpace then begin NewPage; PageSpace := PageHeight - Margins.Top - Margins.Bottom; end; // 记录 Band 在本页的绘制位置 Band.Position := PageHeight - Margins.Top - (PageHeight - PageSpace); Dec(PageSpace, Band.Height); end; end;这里有几个参数要留意:PageHeight来自打印机或纸张设置,Margins是页边距,Band.Height是 Band 在设计期或运行期算出来的高度。如果你发现分页位置不对,先查这三个值。常见问题是 Band 的Height在运行期被动态改了,但Prepare用的是改之前的值——解决办法是在Prepare之前把所有动态高度算好。
4.2 绘制链路:从 TQRBand 到 TCanvas
Band 本身不直接画东西,它是个容器,真正的绘制在子控件里。每个输出控件(TQRLabel、TQRDBText 等)都实现了一个Paint方法,接收TCanvas和绘制区域。以 TQRLabel 为例:
procedure TQRLabel.Paint(ACanvas: TCanvas; ARect: TRect); begin // 设置字体和对齐方式 ACanvas.Font.Assign(FFont); ACanvas.Brush.Style := bsClear; // 根据 Alignment 计算文字起始位置 case FAlignment of taLeftJustify: DrawText(ACanvas.Handle, PChar(FCaption), -1, ARect, DT_LEFT); taCenter: DrawText(ACanvas.Handle, PChar(FCaption), -1, ARect, DT_CENTER); taRightJustify: DrawText(ACanvas.Handle, PChar(FCaption), -1, ARect, DT_RIGHT); end; end;ACanvas在预览时是屏幕 Canvas,在打印时是Printer.Canvas。这就是为什么预览和打印能共用一套绘制代码——只要传不同的 Canvas 进去就行。ARect是控件在页面上的矩形区域,由 Band 在分页时算好传下来。你如果要自定义一个输出控件,继承TQRCustomControl并实现Paint是最直接的路子。
4.3 表达式引擎:QRExpr 怎么解析 SUM 和 IF
TQRExpr的Expression属性支持类似SUM(Table1.Field1)、IF(Table1.Field1 > 100, '高', '低')这样的写法。解析逻辑在QRExpr.pas里,走的是“词法分析 → 语法树 → 求值”三步。词法分析把表达式拆成 token(标识符、数字、运算符、括号),语法分析构建一棵简单的表达式树,求值时递归遍历这棵树。
核心求值函数大致是这样:
function TQRExpr.Evaluate: Variant; begin if FParseTree = nil then FParseTree := ParseExpression(FExpression); Result := EvalNode(FParseTree); end; function TQRExpr.EvalNode(Node: TExprNode): Variant; begin case Node.NodeType of ntConstant: Result := Node.Value; ntField: Result := GetFieldValue(Node.FieldName); ntFunction: Result := CallFunction(Node.FuncName, Node.Args); ntOperator: Result := ApplyOperator(Node.Op, EvalNode(Node.Left), EvalNode(Node.Right)); end; end;SUM这类聚合函数的特殊之处在于它需要跨记录累加,所以CallFunction里对SUM的处理不是简单求值,而是维护一个累加器,在每条记录打印时更新,在 Summary Band 里输出最终值。如果你要加自定义函数,扩展CallFunction里的case分支即可,但要注意聚合函数和非聚合函数的调用时机不同。
4.4 打印调度:QRPrntr.pas 和 TPrinter 的交互
QRPrntr.pas负责把绘制结果送到打印机。它封装了TPrinter,处理BeginDoc/EndDoc、页边距换算、打印机分辨率适配。一个容易忽略的点是:屏幕预览用的是像素坐标,打印机用的是点(point)或物理单位,QRPrntr里有一层坐标转换。如果你发现预览正常但打印出来位置偏移,多半是这层转换的ScaleX/ScaleY没对上。常见做法是在BeforePrint里根据Printer.Canvas.Font.PixelsPerInch动态调整缩放因子。
5. 避坑与排查:装包、编译、打印里最容易翻车的几处
5.1 装包时报“找不到 QR405_R.bpl”
现象:Install 设计期包时,IDE 提示找不到运行期包的.bpl文件。原因通常是运行期包编译输出的路径和设计期包requires里写的路径不一致。解决:打开设计期包工程,在 Project Options 的 Packages 页里检查QR405_R的引用路径,或者在系统 PATH 里把运行期包的输出目录加进去。我一般会把两个包的输出目录设成同一个,省得来回改。
5.2 BCB6 下源码编译报“Undefined symbol AnsiCompareText”
现象:Build 运行期包时,链接器报某个 RTL 函数未定义。原因是 BCB6 的 RTL 版本和源码里引用的单元版本不匹配,或者Source目录里混入了为 D7 准备的单元。解决:确认你用的是Packages/BCB6下的包工程,且Source目录没有被 D7 的编译产物污染。如果还不行,在包工程里显式#include对应的 RTL 头文件,或者把Lib目录下的预编译.obj加进链接路径。
5.3 预览正常,打印出来是空白页
现象:Preview 能看到内容,点打印后出来的纸是白的。原因通常是Printer的Canvas在BeginDoc之前就被访问了,或者EndDoc之前没有正确NewPage。解决:确保打印流程是BeginDoc → 逐页绘制 → EndDoc,中间不要提前访问Printer.Canvas。另外检查QuickRep1.PrinterSettings里的纸张大小是否和实际打印机匹配,不匹配时有些驱动会直接吐白纸。
5.4 中文显示成乱码或问号
现象:报表里的中文在预览时正常,打印时变成乱码。原因是字体在打印 Canvas 上没有正确设置字符集。解决:在输出控件的Paint里显式设置ACanvas.Font.Charset := GB2312_CHARSET,并确保Font.Name是打印机支持的字体(比如宋体)。如果用的是AnsiString存中文,还要确认源码文件本身的编码和编译器默认编码一致。
5.5 动态改 Band 高度后分页错乱
现象:运行期根据数据动态调整了 Detail Band 的高度,结果分页位置全乱了。原因是Prepare在计算分页时用的是 Band 的静态Height,动态改的值没被重新计算。解决:在调用Prepare之前,先遍历所有 Band 把动态高度算好并赋值给Height,或者重写Prepare让它在分页循环里实时读取Height。我一般选前者,改动小、可控。
6. 二次开发:从改一个输出控件到扩展表达式函数
拆到这里,你应该已经能跑起来、也能看懂核心链路了。接下来讲两个我实际改过的方向,都是小切口、能验证的。
第一个方向是自定义输出控件。假设你要一个“带边框和背景色的标签”,继承TQRLabel重写Paint就行:
type TQRColorLabel = class(TQRLabel) private FBackColor: TColor; FBorderColor: TColor; protected procedure Paint(ACanvas: TCanvas; ARect: TRect); override; published property BackColor: TColor read FBackColor write FBackColor default clWhite; property BorderColor: TColor read FBorderColor write FBorderColor default clBlack; end; procedure TQRColorLabel.Paint(ACanvas: TCanvas; ARect: TRect); begin // 先画背景 ACanvas.Brush.Color := FBackColor; ACanvas.Brush.Style := bsSolid; ACanvas.FillRect(ARect); // 再画边框 ACanvas.Pen.Color := FBorderColor; ACanvas.Rectangle(ARect.Left, ARect.Top, ARect.Right, ARect.Bottom); // 最后调父类画文字 inherited Paint(ACanvas, ARect); end;改完之后,把这个新控件注册到设计期包(在包的Register过程里加一行RegisterComponents('QuickReport', [TQRColorLabel])),重新 Install,面板上就能拖出来了。验证方法是拖一个到 Detail Band 上,设个背景色,预览看效果。
第二个方向是扩展表达式函数。比如加一个FORMATDATE函数,把日期字段格式化成指定样式。在QRExpr.pas的CallFunction里加分支:
function TQRExpr.CallFunction(const Name: string; Args: TList): Variant; begin if SameText(Name, 'SUM') then Result := DoSum(Args) else if SameText(Name, 'IF') then Result := DoIf(Args) else if SameText(Name, 'FORMATDATE') then // 第一个参数是日期值,第二个是格式串 Result := FormatDateTime(VarToStr(Args[1]), VarToDateTime(Args[0])) else raise EQRExprError.CreateFmt('未知函数: %s', [Name]); end;加完后重新编译运行期包和设计期包,在TQRExpr.Expression里写FORMATDATE(Table1.OrderDate, 'yyyy-mm-dd')就能用。验证时注意参数顺序和类型,VarToDateTime对空值会抛异常,稳妥做法是先判空。
这两个方向改完,你对 Quick Report 的扩展点基本就摸清了:显示层靠继承输出控件,逻辑层靠扩展表达式函数,分页层靠重写Prepare或调整 Band 高度。我自己的习惯是,每次改完源码包,先跑一遍Examples下的SimpleReport和MasterDetail,确认没把原有功能改坏,再上自己的业务工程。这个“先跑示例再上业务”的步骤,帮我省过好几次回滚的麻烦。希望帮到你。
本文还有配套的精品资源,点击获取