2

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

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

在不涉及我的项目的许多细节的情况下,让我介绍一个非常简单的打印程序,它失败了:

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异常处理,所以实际结果略有不同,但与这个结果基本相似。结果显示了不稳定和/或内存泄漏的特征,而没有太多的错误消息。

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

其他人有类似的问题吗?

4

7 回答 7

4

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

  • 它正在被摧毁。
  • 它的NumCopies, Orientation, 或PrinterIndex属性已设置。
  • 它的SetPrinter()方法被调用(在内部由PrinterIndex属性设置器和SetToDefaultPrinter()方法,以及由TPrintDialogand TPrinterSetupDialog)。

如果不这样做,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();
    }
}
于 2016-08-18T02:35:06.827 回答
3

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

TPrinter当其Printing属性为 false时对其执行以下操作之一时,会发生“打印机当前未打印”错误:

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

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

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

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

因此,鉴于您显示的测试代码,有两种可能性:

  • 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不是线程安全的。

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

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

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

于 2016-08-11T21:20:57.900 回答
2

尝试删除以下 Windows 更新:

Microsoft Windows 安全更新 (KB3177725)

MS16-098:Windows 内核模式驱动程序的安全更新说明:2016 年 8 月 9 日

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

于 2016-08-12T13:50:13.813 回答
1

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

我做了一些快速调试,发现是对 StartDoc() 的调用返回了一个 <= 0 的值。

在我弄清楚真正导致此问题的原因之前的临时修复是通过调用在 Printers 中重新创建 Printer 对象

Vcl.Printers.SetPrinter(TPrinter.Create).Free;

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

调用 EndDoc() 时似乎没有正确释放某些内容

于 2016-08-11T22:46:48.683 回答
1

看来这确实是微软的问题,他们应该修复这个错误的更新。您可以在公司网站上找到更多信息。

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

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

于 2016-08-13T20:42:52.820 回答
0

在 Windows 10 64 位系统上运行内置于 XE7 的 32 位 Delphi 应用程序时,我或多或少地同时开始遇到同样的奇怪行为。

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

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

于 2016-08-12T11:20:50.730 回答
0

问题中代码的以下变体将解决问题,但需要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();
    }
}

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

至于“为什么”,我目前最好的猜测是,在 2016 年 8 月 10 日实施Microsoft Windows 安全更新 (KB3177725)TPrinter后,单独使用并没有从 Windows 访问所有必要资源的所有必要权限。不知何故,一个电话to (或)在调用之前设置成功执行的必要条件。TPrinterSetupDialogTPrintDialogBeginDoc()TPrinter

于 2016-08-13T13:15:48.840 回答