为什么今天TPrinter (XE7) 突然出问题了?

Why is TPrinter (XE7) suddenly having problems today?

我正在使用 C++ Builder XE7 VCL。

2016 年 8 月 11 日左右 2:00pm UTC,我开始收到用户群关于打印问题的多起投诉。这些打印模块中的大多数已被证明多年稳定,并且在过去 24 小时内没有对我的项目进行更新。我能够在我的 development/test 环境中重现类似的问题。

在不详细介绍我的项目的情况下,让我展示一个非常简单的失败打印程序:

void __fastcall TForm1::PrintButtonClick(TObject *Sender)
{
    // Test Print:
    TPrinter *Prntr = Printer();
    Prntr->Title = "Test_";
    Prntr->BeginDoc();
    Prntr->Canvas->Font->Size = 10;
    Prntr->Canvas->TextOut(300,1050,"* * * Printing Test * * *");
    if (Prntr->Printing) {
        Prntr->EndDoc();
    }
}

第一次尝试打印时,一切都按预期完美运行。如果我再次单击该按钮,TPrinter 会生成一个小的 PDF,但该 PDF 文件实际上已损坏并且似乎有一个文件句柄粘在上面。

如果我第三次单击该按钮,则没有打印,并出现以下错误消息:

Printer is not currently printing.

我自己的测试是使用PDF打印机驱动完成的,但我收到的用户投诉包括各种本地打印机、网络打印机、PDF打印机等

在我的实际项目中,有try/catch个异常处理,所以实际结果略有不同,但与这个结果大体相似。结果显示了不稳定 and/or 内存泄漏的标志,但没有太多错误消息。

我怀疑可能有一些 Microsoft Windows 更新与 Embarcadero DLL 纠缠在一起,但到目前为止我还无法验证这一点。

还有其他人遇到类似问题吗?

打印中没有涉及 Embarcadero DLL。 TPrinter 直接调用 Win32 API 基于 GDI 的打印函数。

TPrinterPrinting 属性 为 false 时,在执行以下操作之一时会出现 "Printer is not currently printing" 错误:

  • TPrinter::NewPage()
  • TPrinter::EndDoc()
  • TPrinter::Abort()
  • a TPrinter::Canvas sub属性 正在更改。
  • 正在绘制 TPrinter::Canvas

您在显示的测试代码中执行了这些操作的一半,但您没有指定哪一行代码实际引发了错误。

Printing 属性 只是 returns TPrinter::FPrinting 数据成员的当前值,仅在以下情况下设置为 false:

  • 最初创建 TPrinter 对象(Printer() 函数 returns 一个在可执行文件的生命周期内重复使用的单例对象)。
  • Win32 API StartDoc() 函数在 TPrinter::BeginDoc() 内部失败(FPrinting 在调用 StartDoc() 之前设置为真)。
  • Printing 为真时调用
  • TPrinter::EndDoc()

所以,根据你给出的测试代码,有两种可能:

  • StartDoc() 失败,您没有检查该条件。 BeginDoc() 不会抛出错误(VCL 错误?!?),它只会正常退出,但 Printing 将为假。为此添加一个检查:

    Prntr->BeginDoc();
    if (Prntr->Printing) { // <-- here
        Prntr->Canvas->Font->Size = 10;
        Prntr->Canvas->TextOut(300,1050,"* * * Printing Test * * *");
        Prntr->EndDoc();
    }
    
  • 当您正在打印某些内容时,Printing 属性 会过早地设置为 false。 代码中可能发生的方式是:

    • 随机内存被破坏,TPrinter恰好是受害者。
    • 多个线程同时操作同一个 TPrinter 对象。 TPrinter 不是线程安全的。

由于您可以在您的开发系统中重现该问题,我建议您在项目选项中启用调试 DCU,然后在调试器中 运行 您的应用程序,并在 TPrinter::FPrinting 数据成员。当 FPrinting 更改值时将命中断点,您将能够查看调用堆栈以准确了解是哪个代码进行了更改。

根据此信息,我将冒险猜测您的错误原因是 StartDoc() 失败。不幸的是,StartDoc() 没有记录为返回 为什么 它失败了。您当然不能为此使用 GetLastError()(大多数 GDI 错误不会由 GetLastError() 报告)。您 可能 能够使用 Win32 API Escape() or ExtEscape() 函数从打印驱动程序本身检索错误代码(使用 TPrinter::Canvas::Handle 作为 HDC查询)。但如果这不起作用,您将无法确定失败的原因,除非 Windows 在其事件日志中报告错误消息。

如果 StartDoc() 真的失败了,那是因为 Win32 API 失败,而不是 VCL 失败。打印机驱动程序本身很可能在内部出现故障(特别是如果 PDF 打印驱动程序将打开的文件句柄留给其 PDF 文件),或者 Windows 无法与驱动程序正确通信。无论哪种方式,它都在 VCL 之外。这与错误在您未对您的应用程序进行任何更改的情况下开始发生的事实是一致的。 Windows 更新可能导致打印驱动程序发生重大变化。

今天也开始在这里发生。在我的 Windows 10 设置中,这会在调用 TForm 的 Print() 3 次后发生。我尝试了 Microsoft Print to PDF 和 Microsoft XPS Document Writer,但都出现了同样的错误。

我进行了一些快速调试,发现 returns 值 <= 0 是对 StartDoc() 的调用。

在我弄清楚真正导致此问题的原因之前的临时修复是通过调用

在 Printers 中重新创建 Printer 对象
Vcl.Printers.SetPrinter(TPrinter.Create).Free;

在调用任何使用 Printer 对象的东西之后。可能不建议这样做,但它暂时解决了我的问题。

似乎在调用 EndDoc() 时没有正确释放某些东西

当 运行 32 位 Delphi 应用程序在 Windows 10 64 位系统上构建在 XE7 中时,我几乎同时开始遇到同样的奇怪行为。

卸载 Windows 10 (KB3176493) 的最新安全升级后,从这些应用程序打印又可以正常工作了。

卸载此更新的一个相当令人惊讶的副作用似乎是文件关联 - 这是处理特定文件类型的默认程序 - 正在恢复为 Microsoft Windows 默认值...

尝试删除以下 Windows 更新:

Microsoft Windows 安全更新 (KB3177725)

MS16-098: Description of the security update for Windows kernel-mode drivers: August 9, 2016

到目前为止,这似乎解决了几个测试用例的问题。

问题中的以下代码变体将解决问题,但需要将 TPrinterSetupDialog 组件添加到表单中:

void __fastcall TForm1::PrintButtonClick(TObject *Sender)
{
    // Test Print:
    TPrinter *Prntr = Printer();
    Prntr->Title = "Test_";
    PrinterSetupDialog1->Execute();
    Prntr->BeginDoc();
    if (Prntr->Printing) {
        Prntr->Canvas->Font->Size = 10;
        Prntr->Canvas->TextOut(300,1050,"* * * Printing Test * * *");
        Prntr->EndDoc();
    }
}

对于程序使用,用户将在继续打印之前看到打印机设置对话框。

至于 "why",我目前最好的猜测是单独使用 TPrinter 没有所有必要的权限来访问 Windows 之后的所有必要资源 Microsoft Windows (KB3177725) 更新已于 2016 年 8 月 10 日实施。在调用 [=15= 之前以某种方式调用 TPrinterSetupDialog(或 TPrintDialog) ]设置了TPrinter成功执行的必要条件。

看来这真的是Microsoft problem and they should fix this buggy update. You can find more info at this on the company site.

对于此 Microsoft 错误,使用打印机设置对话框只是一种变通方法,而不是真正的解决方案。确认后,打印机设置对话框总是为打印机创建一个新句柄。接下来的一两个打印作业将会成功。

补丁应该来自微软,而不是来自 Embracadero。数以千计的程序受到影响,如果 MS 更新中存在错误,那么在所有程序中实施变通方法将是一种巨大的时间和金钱浪费。

使用 TPrintDialogTPrinterSetupDialog "works" 修复错误的原因是因为它们强制单例 TPrinter 对象(由 Vcl.Printers.Printer()函数)将其当前句柄释放给打印机(如果有的话),从而导致 TPrinter.BeginDoc() 创建一个新句柄。 TPrinter 在下列情况下释放其打印机句柄:

  • 它正在被摧毁。
  • NumCopiesOrientationPrinterIndex 属性 已设置。
  • 它的 SetPrinter() 方法被调用(在内部由 PrinterIndex 属性 setter 和 SetToDefaultPrinter() 方法,以及 TPrintDialogTPrinterSetupDialog).

如果不这样做,多次调用 TPrinter.BeginDoc() 只会重复使用同一个打印机句柄。显然最近的 Microsoft 安全更新已经影响了句柄重用。

因此,简而言之,(无需卸载 Microsoft 更新)在调用 BeginDoc() 期间,您需要执行 某些操作 导致 TPrinter 发布并重新创建其打印机句柄,然后问题就会消失。至少在 Embarcadero 可以发布 TPrinter 补丁来解决这个问题之前。也许他们可以更新 TPrinter.EndDoc()TPrinter.Refresh() 以释放当前的打印机句柄(他们目前没有)。

因此,以下解决方法无需对用户界面进行任何更改即可解决打印问题:

void __fastcall TForm1::PrintButtonClick(TObject *Sender)
{
    // Test Print:
    TPrinter *Prntr = Printer();
    Prntr->Title = "Test_";
    Prntr->Copies = 1;  // Here is the workaround
    Prntr->BeginDoc();
    if (Prntr->Printing) {
        Prntr->Canvas->Font->Size = 10;
        Prntr->Canvas->TextOut(300,1050,"* * * Printing Test * * *");
        Prntr->EndDoc();
    }
}