打印机扩展

重要

新式打印平台是 Windows 与打印机通信的首选方式。 建议使用 Microsoft 的 IPP 收件箱类驱动程序以及打印支持应用 (PSA) 来自定义 Windows 10 和 11 中的打印体验,以便进行打印机设备开发。

有关详细信息,请参阅新式打印平台打印支持应用设计指南

用户在 Windows 桌面上运行现有应用程序时,打印机扩展应用支持打印首选项和打印机通知。

打印机扩展可以采用任何支持 COM 的语言生成,但经过优化,现可使用 Microsoft .NET Framework 4 生成。 如果打印机扩展支持 XCopy 且不依赖于操作系统附带以外的外部运行时(例如 .NET),则可能会随打印驱动程序包一起分发打印机扩展。 如果打印机扩展应用不符合这些条件,则可在 setup.exe 或 MSI 包中分发,并可使用 v4 清单中指定的 PrinterExtensionUrl 指令在打印机的设备阶段体验中播发。 通过 MSI 包分发打印机扩展应用时,可以选择将打印驱动程序添加到包或将其分开分发并单独分发驱动程序。 打印机首选项体验上会显示 PrinterExtensionUrl。

IT 管理员拥有一些用于管理打印机扩展分发的选项。 如果在 setup.exe 或 MSI 中打包应用程序,则 IT 管理员可以使用标准软件分发工具(如 Microsoft Endpoint Configuration Manager),也可以将应用程序包含在标准 OS 映像中。 如果 IT 管理员编辑 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\Printer\Printer\<print 队列名称>\PrinterDriverData\PrinterExtensionUrl,IT 管理员也可以覆盖在 v4 清单中指定的 PrinterExtensionUrl。

如果企业选择完全阻止打印机扩展,则可以通过名为“计算机配置\管理模板\打印机\不允许 v4 打印机驱动程序显示打印机扩展应用程序”的组策略来达到此目的。

生成打印机扩展

在开发打印机扩展时,必须注意六个主要重点领域。 以下列表中显示了这些重点领域。

  • 注册

  • 启用事件

  • OnDriverEvent 处理程序

  • 打印引用

  • 打印机通知

  • 管理打印机

注册

通过指定一组注册表项或在 v4 清单文件的 PrinterExtensions 节中指定应用程序信息,可以向打印系统注册打印机扩展。

有指定的 GUID 支持打印机扩展的每个不同入口点。 无需在 v4 清单文件中使用这些 GUID,但必须知道 GUID 值才能将注册表格式用于 v4 驱动程序安装。 下表显示了两个入口点的 GUID 值。

入口点 GUID
打印引用 {EC8F261F-267C-469F-B5D6-3933023C29CC}
打印机通知 {23BB1328-63DE-4293-915B-A6A23D929ACB}

需要使用注册表注册在打印机驱动程序外部安装的打印机扩展。 这可确保无论后台处理程序的状态或客户端计算机上的 v4 配置模块是哪个,都可以安装打印机扩展。

PrintNotify 服务启动后,它将检查 [OfflineRoot] 路径下是否存在注册表项,并处理任何挂起的注册或取消注册。 完成任何挂起的注册或取消注册后,将实时删除注册表项。 如果正在使用脚本或迭代进程来放置注册表项,则每次指定 \[PrinterDriverId] 项时,可能都需要重新创建 \[PrinterExtensionID] 项。 不完整或格式不正确的项不会被删除。

仅在首次安装时才需要此注册。 以下示例展示了用于注册打印机扩展的正确注册表项格式。

注意

[OfflineRoot] 用作 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Print\OfflinePrinterExtensions 的简写形式。

[OfflineRoot]
    \[PrinterExtensionId] {GUID}
           AppPath=[PrinterExtensionAppPath] {String}
           \[PrinterDriverId] {GUID}
                  \[PrinterExtensionReasonGuid]
(default) = ["0"|"1"] {REG_SZ 0:Unregister, 1:Register}
                  \…
                  \[PrinterExtensionReasonGuidN]
           \[PrinterDriverId2]
                  \[PrinterExtensionReasonGuid2.1]
                  \…
                  \[PrinterExtensionReasonGuid2.Z]
           …
           \[PrinterDriverIdM]
    \[PrinterExtensionId2]
    …
    \[PrinterExtensionIdT]

例如,以下项集会向 {PrinterExtensionIDGuid} PrinterExtensionID 注册打印机扩展,并向打印机首选项和打印机通知原因注册 {PrinterDriverID1Guid} 和 {PrinterDriverID2Guid} PrinterDriverID 的“C:\Program Files\Fabrikam\pe.exe”可执行文件的完全限定路径。

[OfflineRoot]
    \{PrinterExtensionIDGuid}
           AppPath="C:\Program Files\Fabrikam\pe.exe"
           \{PrinterDriverID1Guid}
                 \{EC8F261F-267C-469F-B5D6-3933023C29CC}
            (default) = "1"
                 \{23BB1328-63DE-4293-915B-A6A23D929ACB}
            (default) = "1"
           \{PrinterDriverID1Guid}
                 \{EC8F261F-267C-469F-B5D6-3933023C29CC}
            (default) = "1"
                 \{23BB1328-63DE-4293-915B-A6A23D929ACB}
            (default) = "1"

要卸载同一打印机扩展,应指定以下项集。

[OfflineRoot]
    \{PrinterExtensionIDGuid}
           AppPath="C:\Program Files\Fabrikam\pe.exe"
           \{PrinterDriverID1Guid}
                 \{EC8F261F-267C-469F-B5D6-3933023C29CC}
            (default) = "0"
                 \{23BB1328-63DE-4293-915B-A6A23D929ACB}
            (default) = "0"
           \{PrinterDriverID1Guid}
                 \{EC8F261F-267C-469F-B5D6-3933023C29CC}
            (default) = "0"
                 \{23BB1328-63DE-4293-915B-A6A23D929ACB}
            (default) = "0"

由于打印机扩展在用户启动的上下文和事件启动的上下文中都可以运行,因此能够确定打印机扩展的运行上下文非常有用。 例如,如果已针对通知或打印首选项启动应用,则这可以允许应用枚举所有队列上的状态。 Microsoft 建议,在“开始”菜单快捷方式上或在注册期间在注册表中填充的 AppPath 项中,与驱动程序(例如,使用 MSI 或 setup.exe)分开安装的打印机扩展应使用命令行开关。 由于将随驱动程序一起安装的打印机扩展安装到 DriverStore,因此不会在打印首选项或打印机通知事件之外启动这些扩展。 因此,在这种情况下不支持指定命令行开关。

为当前 PrinterDriverID 注册打印机扩展时,它必须在 AppPath 中包括 PrinterDriverID。 例如,对于名称为 printerextension.exe 的打印机扩展应用和 {GUID} 的 PrinterDriverID 值,[PrinterExtensionAppPath] 将如下例所示:

"C:\program files\fabrikam\printerextension.exe {GUID}"

启用事件

在运行时,打印机扩展必须为当前 PrinterDriverID 启用事件触发。 这是通过 args[] 数组传递到应用的 PrinterDriverID,它允许打印系统提供适当的事件上下文,用于处理打印首选项或打印机通知等原因。

因此,应用程序应为当前 PrinterDriverID 创建新 PrinterExtensionManager,注册委托以处理 OnDriverEvent 事件,并使用 PrinterDriverID 调用 EnableEvents 方法。 以下代码片段演示了此方法。

PrinterExtensionManager mgr = new PrinterExtensionManager();
mgr.OnDriverEvent += OnDriverEvent;
mgr.EnableEvents(new Guid(PrinterDriverID1));

如果应用未在 5 秒内调用 EnableEvents,Windows 将超时并启动标准 UI。 为了缓解这个问题,打印机扩展应遵循最新的性能最佳做法,具体包括:

  • 延迟尽可能多的应用初始化,直到调用 EnableEvents 之后。 之后,通过使用异步方法并在初始化期间不阻止 UI 线程,优先考虑 UI 响应能力。

  • 在安装过程中使用 ngen 生成本机映像。 有关详细信息,请参阅本机映像生成器

  • 在加载时使用性能测量工具查找性能问题。 有关详细信息,请参阅 Windows 性能分析工具

DriverEvent 处理程序

注册 OnDriverEvent 处理程序并启用事件后,如果已启动打印机扩展来处理打印首选项或打印机通知,则将调用该处理程序。 在前面的代码片段中,已将名为 OnDriverEvent 的方法注册为事件处理程序。 在以下代码片段中,PrinterExtensionEventArgs 参数是支持构造打印首选项和打印机通知方案的对象。 PrinterExtensionEventArgsIPrinterExtensionEventArgs 的包装器。

static void OnDriverEvent(object sender, PrinterExtensionEventArgs eventArgs)
{
    //
    // Display the print preferences window.
    //

    if (eventArgs.ReasonId.Equals(PrinterExtensionReason.PrintPreferences))
    {
        PrintPreferenceWindow printPreferenceWindow = new PrintPreferenceWindow();
        printPreferenceWindow.Initialize(eventArgs);

        //
        // Set the caller application's window as parent/owner of the newly created printing preferences window.
        //

        WindowInteropHelper wih = new WindowInteropHelper(printPreferenceWindow);
        wih.Owner = eventArgs.WindowParent;

        //
        // Display a modal/non-modal window based on the 'WindowModal' parameter.
        //

        if (eventArgs.WindowModal)
        {
            printPreferenceWindow.ShowDialog();
        }
        else
        {
            printPreferenceWindow.Show();
        }
    }

    //
    // Handle driver events.
    //

    else if (eventArgs.ReasonId.Equals(PrinterExtensionReason.DriverEvent))
    {
        // Handle driver events here.
    }
}

为了防止与崩溃或打印机扩展速度缓慢关联的不良用户体验,如果未在启动应用后短时间内调用 EnableEvents,Windows 将实现超时。 要启用调试,如果有调试器附加到 PrintNotify 服务,则会禁用此超时。

但是,在大多数情况下,我们感兴趣的所有应用相关代码都在 OnDriverEvent 回调期间或之后运行。 在开发过程中,在从 OnDriverEvent 回调中启动打印首选项或打印机通知体验之前,显示 MessageBox 可能也很有用。 出现 MessageBox 时,返回到 Visual Studio 并选择调试>附加到进程,然后选择进程的名称。 最后,返回到 MessageBox,然后选择“确定”以恢复。 这将确保看到异常,并从该点开始命中任何断点。

将来可能支持新的 ReasonId。 因此,打印机扩展必须显式检查 ReasonID,并且不得使用“else”语句来检测最后一个已知的 ReasonID。 如果收到未知的 ReasonID,应用应正常退出。

打印首选项由 PrintSchemaEventArgs.Ticket 对象驱动。 此对象同时封装用于描述设备功能和选项的 PrintTicket 和 PrintCapabilities 文档。 虽然基础 XML 也可用,但对象模型更便于使用这些格式。

在每个 IPrintSchemaTicketIPrintSchemaCapabilities 对象中,都有 功能 (IPrintSchemaFeature) 和选项 (IPrintSchemaOption)。 尽管不考虑来源时用于功能和选项的接口相同,但行为会因基础 XML 而略有不同。 例如,PrintCapabilities 文档为每个功能指定许多选项,而 PrintTicket 文档仅指定选定(或默认)选项。 同样,PrintCapabilities 文档指定本地化的显示字符串,而 PrintTicket 文档则不指定。

有关 WPF 中的数据绑定的详细信息,请参阅数据绑定概述

为了最大限度地提升性能,Microsoft 建议仅当需要更新 PrintCapabilities 文档时,才能执行 GetPrintCapabilities 调用。

当用户使用数据绑定 ComboBox 控件进行选择时,会自动更新 PrintTicket 对象。 当用户最终单击“确定”时,将开始一系列异步验证和完成操作。 此异步模式被广泛使用,以防止在 UI 线程上执行长期任务,并导致打印首选项 UI 或正在打印的应用挂起。 下面是在用户单击“确定”后用于处理 PrintTicket 更改的步骤列表。

  1. 可使用 IPrintSchemaTicket::ValidateAsync 方法以异步方式验证 PrintSchemaTicket。

  2. 异步验证完成后,公共语言运行时 (CLR) 将调用 PrintTicketValidateCompleted 方法。

    1. 如果验证成功,它将调用 CommitPrintTicketAsync 方法,而 CommitPrintTicketAsync 将调用 IPrintSchemaTicket::CommitAsync 方法。 成功完成更新 PrintTicket 时,这将调用 PrintTicketCommitCompleted 方法,该方法又会调用将调用 PrinterExtensionEventArgs.Request.Complete 方法的便利方法,以指示打印首选项已完成,然后它将关闭应用。

    2. 否则,它会呈现 UI,供用户处理约束情况。

如果用户直接单击了“取消”或关闭了“打印首选项”窗口,打印机扩展会使用错误日志的相应 HRESULT 值和消息,调用 IPrinterExtensionEventArgs.Request.Cancel。

如果打印机扩展的进程已关闭,并且未调用前面段落中所述的 Complete 或 Cancel 方法,则打印系统将自动回退到使用 Microsoft 提供的 UI。

为了检索设备状态信息,打印机扩展可以使用 Bidi 查询打印设备。 例如,要显示设备的墨迹状态或其他类型的状态,打印机扩展可以使用 IPrinterExtensionEventArgs.PrinterQueue.SendBidiQuery 方法向设备发出 Bidi 查询。 获取最新的 Bidi 状态是一个两步过程,涉及为 OnBidiResponseReceived 事件设置事件处理程序,以及使用有效的 Bidi 查询调用 SendBidiQuery 方法。 以下代码片段显示了此两步过程。

PrinterQueue.OnBidiResponseReceived += new
EventHandler<PrinterQueueEventArgs>(OnBidiResponseReceived);
PrinterQueue.SendBidiQuery("\\Printer.consumables");

收到 Bidi 响应后,将调用以下事件处理程序。 此事件处理程序还具有模拟墨迹状态实现,当设备不可用时这可能对开发很有用。 PrinterQueueEventArgs 对象包括 HRESULT 和 Bidi XML 响应。 有关 Bidi XML 响应的详细信息,请参阅 Bidi 请求和响应架构

private void OnBidiResponseReceived(object sender, PrinterQueueEventArgs e)
{
    if (e.StatusHResult != (int)HRESULT.S_OK)
    {
        MockInkStatus();
        return;
    }

    //
    // Display the ink levels from the data.
    //

    BidiHelperSource = new BidiHelper(e.Response);
    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs("BidiHelperSource"));
    }
    InkStatusTitle = "Ink status (Live data)";
}

打印机通知

打印机通知的调用方式与打印首选项完全相同。 在 OnDriverEvent 处理程序中,如果 IPrinterExtensionEventArgs 指示 ReasonID 与 DriverEvents GUID 匹配,则可以生成处理此事件的体验。

以下变量最有助于处理功能打印机通知体验。

  • PrinterExtensionEventArgs.BidiNotification – 这包含导致触发事件的 Bidi XML。

  • PrinterExtensionEventArgs.DetailedReasonId – 这包含驱动程序事件 xml 文件中的 eventID GUID。

通知 IPrinterExtensionEventArgs 对象中最重要的属性是 BidiNotification 属性。 这包含导致触发事件的 Bidi XML。 有关 Bidi XML 响应的详细信息,请参阅 Bidi 请求和响应架构

管理打印机

为了支持打印机扩展作为可用作管理/维护打印机的中心的应用的角色,可以枚举为其注册当前打印机扩展的打印队列,并获取每个队列的状态。 这未在 PrinterExtensionSample 项目中演示,但可以将以下代码片段添加到 App.xaml.cs Main 方法中,以注册事件处理程序。

mgr.OnPrinterQueuesEnumerated += new EventHandler<PrinterQueuesEnumeratedEventArgs>(mgr_OnPrinterQueuesEnumerated);

枚举队列后,将调用事件处理程序,并可执行状态操作。 此事件会在应用的生存期内定期触发,以确保所枚举打印队列的列表为最新,即使用户自打开队列以来已安装更多队列也是如此。 因此,事件处理程序在每次执行时都不会创建新窗口,这一点很重要,以下代码片段显示了这一点。

static void mgr_OnPrinterQueuesEnumerated(object sender, PrinterQueuesEnumeratedEventArgs e)
{
    foreach (IPrinterExtensionContext pContext in e)
    {
        // show status
    }
}

为了使用打印机扩展执行维护任务,Microsoft 建议如以下伪代码概述的那样使用旧 WritePrinter API。

OpenPrinter
    StartDocPrinter
        StartPagePrinter
          WritePrinter
        EndPagePrinter
    EndDocPrinter
ClosePrinter

打印机扩展性能最佳做法

为了确保获得最佳用户体验,应将打印机扩展设计为可尽可能快地加载。 打印机扩展示例项目是一个 .NET 应用程序,这意味着它会内置到中间语言 (IL) 中,必须在运行时将该语言编译为适合本机处理器体系结构的格式。 在安装期间,Microsoft 建议根据最佳做法安装打印机扩展,以确保已为本机系统体系结构编译应用。 有关代码编译和安装最佳做法的详细信息,请参阅提升桌面应用程序的启动性能

Microsoft 还建议打印机扩展推迟初始化任务,例如加载资源,直到调用 EnableEvents 方法之后。 这样可最大程度地减少在打印机扩展的 5 秒超时之前应用调用 EnableEvents 的可能性。

在 OnDriverEvent 调用后,打印机扩展应尽快初始化其 UI 并绘制,从而尽可能使用异步方法来确保响应能力。 打印机扩展不应依赖于网络调用或 Bidi,以便为打印首选项或打印机通知创建初始窗口状态。

当用户使用影响 PrintTicket 的屏幕 UI 进行选择时,打印机扩展应使用 IPrintSchemaTicket::ValidateAsync 方法,以便尽早验证更改。 最后,打印机扩展应使用 IPrintSchemaTicket::CommitAsync 方法,以便提交 PrintTicket 更改。

打印机扩展始终从调用它们的进程的进程外执行。 因此,在开发打印机扩展时,必须牢记窗口行为:

打印机扩展示例演示如何创建一个通常作为最顶部窗口启动的 UI。 但在某些情况下,不会在前台显示 UI,例如,导致调用 UI 的进程会在不同的完整性级别运行,或者在为不同的处理器体系结构编译进程时运行。 在这种情况下,打印机扩展应调用 FlashWindowEx,以通过闪烁任务栏中的图标来请求访问前台的用户权限。

Bidi 请求和响应架构

数据绑定概述

提升桌面应用程序的启动性能

本机映像生成器

打印架构接口

Windows 性能分析工具