
简介Quick Report 4.05 是面向 BCB6Borland 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再往上叠 BandBand 里放标签和表达式运行时调 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的用法第三包工程文件格式是老的.bpkBCB和.dpkDelphi不是后来的.dproj。如果你打算把它迁到新版本 IDE这些点都是要逐个处理的。从架构上看Quick Report 的分层大致是这样层次主要单元职责报表定义层QuickRpt.pasTQuickRep、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.bpkBuild。设计期包会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里面放一个 TQRLabelCaption写点文字。然后放一个按钮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 到 TCanvasBand 本身不直接画东西它是个容器真正的绘制在子控件里。每个输出控件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 和 IFTQRExpr的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确认没把原有功能改坏再上自己的业务工程。这个“先跑示例再上业务”的步骤帮我省过好几次回滚的麻烦。希望帮到你。本文还有配套的精品资源点击获取