技术文章

在 C++Builder 中动态创建和释放 HotPDF 组件

在设计时将 THotPDF 放到窗体上对于快速原型制作是不错的,但这将组件绑定到了窗体的生命周期上,生产代码很少这样需要。每一次按钮点击运行一次的报表生成器,批量进行过夜导出的服务线程,根本没有窗体的助手类:在所有这些情况中,你想要的是该组件仅仅存在一个 PDF 任务所需的时长然后消失。这意味着运行时分配,而在编写第一行代码之前了解两点是很有价值的:谁拥有该对象,以及当出现问题时如何执行清理。

VCL 中的所有者语义

每个 VCL 组件构造函数都采用 TComponent* 类型的 Owner 参数。传递 this(表单)将用表单的所有组件列表注册新对象,这样如果表单被销毁而该组件仍存活,VCL 会自动释放它。传递 nullptr 意味着没有所有者:你将为指针承担唯一责任,并且如果异常在你明确调用 delete 之前引发栈展开,没有任何东西会为你清理它。

对于在单一函数内完成的一次性导出,这两种选择均可行,但它们具有不同的失败模式。若以 this 充当所有者,只要窗体最终关闭就不可能发生内存泄漏;若以 nullptr 充当,指针必须抵达 __finally 块。在实践中,nullptr__finally 这种模式对短生命周期对象更为清爽,因为它使生命周期边界一目了然,并避免了窗体累积本该是临时用途的拥有的对象。

异常安全的结构

PDF 生成可能由于与 API 完全无关的原因而失败:输出目录为只读、缺失字体文件、流提前冲刷、或者是调用者提供的数据达到了长度上限。不管起因是什么,必须运行清理路径。保证这一点的惯用 C++Builder 方式是 try/__finally

#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
#pragma link "HPDFDoc"
#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    THotPDF* Pdf = new THotPDF(nullptr);
    try
    {
        Pdf->FileName = "output.pdf";
        Pdf->Compression = cmFlateDecode;
        Pdf->FontEmbedding = true;
        Pdf->BeginDoc();
        Pdf->CurrentPage->SetFont("Arial", TFontStyles(), 12);
        Pdf->CurrentPage->TextOut(72, 720, 0, L"Hello from C++Builder");
        Pdf->EndDoc();
    }
    __finally
    {
        delete Pdf;
    }
}

清单中有一些内容值得指出。所有者是 nullptr,使得生命周期变得明确。CompressionFontEmbeddingBeginDoc 之前被设置:它们都是文档级别的选项,HotPDF 会在文档打开时提交它们,而在那之后给它们赋值没有效果。TextOut 采用的坐标单位为点,从页面的左下角开始测量,Y 轴向上增加;72, 720 这对坐标将文本置于有着一英寸左边距的信纸大小页面的左上角附近。__finally 块中的 delete Pdf 会运行,不管 BeginDoc、绘制或 EndDoc 是否引发了异常。

避免在 delete 之后调用 Pdf 上的任何方法。如果指针储存在成员变量中,在删除后立即将其设为 nullptr,这样后续任何意外访问都会引发干净的崩溃,而非静默的破坏。

项目配置

C++Builder 通过组合包含路径、库路径和 pragma 指令来定位 THotPDF。生成的头文件与 HotPDF 源目录中的 HPDFDoc.pas 放在一起;把该目录添加到 Project > Options > C++ Compiler > Include path 中。#pragma link "HPDFDoc" 指令告诉链接器提取已编译的单元而无需在项目文件中手动将其列出。如果您使用运行时包而不是静态链接,请首先安装 HotPDF 设计和运行时包;这个 pragma 仍然适用。

保持单元名 HPDFDoc 不变。C++Builder 会根据 Pascal 单元名推导头文件名,所以重命名该文件或在 pragma 中使用路径别名,会在暗中破坏查找过程。

作用域和多文档作业

对于由用户操作触发的单次导出,将其范围限定到按钮处理程序的局部变量是正确的做法:在一次调用帧之内创建、使用、并销毁,对于之后阅读代码的人来说意图很明显。当相同的窗体驱动一个连续的工作流时,设计时替代方案才有其正当性,比如一个当用户改变设置就重建文档的打印预览面板;在那种情况下,保持组件的存活并重复调用 BeginDoc/EndDoc 破坏性会比重复分配和释放堆对象来得小。

对于按顺序生成许多文档的批处理作业,为每个文档界定一个 THotPDF 的作用域是值得产生分配开销的。如果没有对象可供携带状态,那么状态就不会在文档之间传递,而且这是一类您永远不必调试的间歇性漏洞。分配、生成、删除、重复。

出现在好几个 HotPDF 演示里的一个属性是 AutoLaunch,它在 EndDoc 之后立刻于系统 PDF 阅读器中打开生成的文件。在编写排版初稿时它非常有用。但在生产环境中,跳过它:直接打开输出路径,验证该文件存在且不是零大小,记录下结果,并让调用工作流来决定是否需要打开阅读器。在批处理作业里,AutoLaunch 会为每个文档发起一个阅读器窗口,并且会在一些系统上导致等待阅读器关闭时进程受阻。

这里展示的 THotPDF 组件及所有的绘制调用都是用于 Delphi 和 C++Builder 的 HotPDF 组件的一部分。