Windows 业务应用程序的打印与 PDF 输出 ── System.Drawing.Printing / WPF / 报表库的取舍选型

· · CSharp, .NET, WinForms, WPF, 打印, PDF, 报表, Windows 开发, 技术咨询

Windows 业务应用程序的打印,并不是「用 PrintDocument 随便画一画就完事」这么简单的事情。分页失效、留白跑偏、预览和实际输出不一致、只想要 PDF 却硬是弹出打印对话框 ── 这些问题,大多是因为在还没理解打印机制本身的情况下就动手实现所导致的。

本文将从「按需求该如何取舍」的角度,梳理 WinForms 的 System.Drawing.Printing、WPF 的 FlowDocument/FixedDocument、PDF 输出库,以及通过 Excel/Word 生成报表这四种方案。

1. 先说结论

  • 简单的一览表,或是现有 WinForms 资产的延伸,用 PrintDocument 就够用。 分页的机制是反复调用 PrintPage 事件,直到 HasMorePages 变为 false 为止,忘记处理这一点,第二页之后就不会输出。12
  • 打印的 DPI 和屏幕的 DPI 是两件不同的事。 PageSettings.PrinterResolution 代表打印机的分辨率,留白则分成两层:PageSettings.HardMarginX/HardMarginY(打印机本身无法打印的物理区域)与 Margins(应用程序指定的逻辑留白)。如果直接把屏幕坐标套用到打印上,就会因为这两套 DPI、坐标系统的差异而使排版跑偏。345
  • 在 WPF 中,Windows 标准的打印路径分成 GDI 打印路径与 XPS 打印路径两套。 WPF 应用程序本来就使用 XPS 打印路径,若要发送到不支持 XPSDrv 的打印机,系统会自动转换成 GDI 格式。67
  • WPF 的文档在「流式排版」的 FlowDocument 与「固定版式」的 FixedDocument 之间,设计思路完全不同。 FlowDocument 以可读性优先,会随窗口大小、分辨率重新排版;FixedDocument 则以显示、打印设备的还原度优先。89
  • 纸张与纸盒的指定,在 WPF 中用 PrintDialogPrintTicket/PrintQueue 来处理。 打印前用 PrintQueue.MergeAndValidatePrintTicket 针对打印机的实际能力进行校验与合并,是标准做法。1011
  • Microsoft Print to PDF 是以交互式操作为前提设计的打印驱动程序。 它的定位是操作系统内置的打印队列与驱动程序包,基本机制是每次执行都会弹出对话框让你选择保存位置,因此不适合无人值守的批处理。12
  • 从服务器或服务进行的 Office COM 自动化,微软官方明确表示不推荐、不支持。 Office 的设计前提是交互式桌面与已登录用户的配置文件,若在无人值守、非交互式环境中运行,可能出现不稳定行为或死锁。13
  • Windows 服务在 Session 0 中运行,因此无法显示对话框,也不能依赖绑定在用户会话上的默认打印机。 在设计涉及打印的常驻处理时,请先意识到这个边界。14
  • 长时间运行时忘记释放 GDI 对象(句柄),会达到每个进程的上限而导致崩溃。 这个上限是可以通过注册表 GDIProcessHandleQuota 调整的有限数值,并不是可以无限获取的。15

判断表 ── 需求 × 方案

需求 适合的方案 理由
想打印简单的一览表、单据等少量页面(WinForms) PrintDocument 只用标准类即可完成,可直接沿用现有的 GDI+ 绘图资产
线条很多的报表、多种纸张、复杂版式 报表库,或用 FixedDocument 做固定版式 自行编写坐标计算、分页控制、中文排版的成本很高
大批量批处理打印(夜间无人执行) 以编程方式调用 PrintDocument.Print() /直接发送到 XpsDocumentWriter 必须完全不弹出任何对话框,也要避免依赖默认打印机
只需要保存 PDF(不使用打印功能) 用 PDF 生成库直接生成 PDF 不依赖打印对话框、默认打印机、队列状态
必须有预览、公司内部确认流程很重要 PrintPreviewDialog(WinForms)、DocumentViewer(WPF) 可直接沿用打印前确认的标准 UI
想沿用现有的 Excel 模板 用 Open XML SDK 等方式直接生成,或有限度地使用 COM 自动化 可重复利用现有资产,但应避免在常驻服务中使用 COM 自动化

下面针对各项目详细说明。

2. 了解 Windows 打印架构的最低限度知识

Windows 的打印架构由打印队列和各打印机专用的驱动程序组成。应用程序只需要调用不依赖设备的函数,就能把打印任务发送到激光打印机、矢量绘图仪、点阵打印机、传真机等各种输出设备。16

打印路径大致分为两套。

  • GDI 打印路径。 Win32 GDI 应用程序打印时,GDI 图形引擎会把绘图指令以 EMF(增强型图元文件)方式排入队列,或是与打印机驱动程序配合,直接渲染可打印的图像后发送到队列。传统的 WinForms 应用程序走的就是这条路径。166
  • XPS 打印路径。 这是给以 XPS(XML Paper Specification)为基础的打印机驱动程序使用的路径,WPF 应用程序本来就使用这条路径,以 XPS 文档的格式发送到队列。如果目标打印机不是 XPSDrv 驱动程序,就会自动转换成 GDI 格式后再发送。67

为什么屏幕上看到的效果和打印结果会不一致? 最常见的原因是搞混了 DPI。屏幕的 Graphics 通常以 96 DPI 左右建立坐标系统,但打印机的 Graphics 是由 PageSettings.PrinterResolution 决定的,多数情况下是 600 DPI 左右、远高于屏幕的分辨率。3 如果把为屏幕计算好的像素坐标直接交给打印用的 Graphics,绘制出来的结果就会比预期小很多(或按高分辨率设置而变大)。打印时应该始终以「百分之一英寸」或实际尺寸(毫米、英寸)为坐标基准,让设计不依赖 Graphics 的分辨率,这样更安全。

另一个容易被忽视的地方,是打印机本身具有的物理不可打印区域。多数打印机在纸张边缘几毫米范围内无法打印。这个物理限制可以通过 PageSettings.HardMarginX/HardMarginY 获取,官方文档也说明这代表「打印机所设置的物理留白」。4 应用程序端指定的逻辑留白则是 PageSettings.Margins(默认值为上下左右各 1 英寸),但即使把它设为 0,也无法输出到比打印机硬边界更内侧的位置。5 「明明把留白设成 0,边缘却被裁掉/位置比预期偏移」这类问题,大多就是没有意识到硬边界的存在所导致的。

3. WinForms:System.Drawing.Printing 的实务

WinForms 的打印以 PrintDocument 组件为核心来搭建。基本流程是:创建 PrintDocument,设置 PrinterSettingsDefaultPageSettings,在 PrintPage 事件中进行实际绘图,再调用 Print() 方法开始打印。1

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Printing;
using System.Windows.Forms;

public sealed class ReportPrinter
{
    // 要打印的数据。假设每一行对应一条单据明细
    private readonly IReadOnlyList<string> _lines;
    private int _lineIndex;

    public ReportPrinter(IReadOnlyList<string> lines) => _lines = lines;

    public void Print(string printerName)
    {
        using var document = new PrintDocument();
        document.DocumentName = "订单明细";
        if (!string.IsNullOrEmpty(printerName))
        {
            document.PrinterSettings.PrinterName = printerName;
        }

        // 指定了不存在的打印机名称时,这里会变成 false(不能用来检测离线状态)
        if (!document.PrinterSettings.IsValid)
        {
            throw new InvalidOperationException(
                $"找不到指定的打印机: {printerName}");
        }

        _lineIndex = 0;
        document.PrintPage += Document_PrintPage;
        document.Print();
    }

    private void Document_PrintPage(object? sender, PrintPageEventArgs e)
    {
        // MarginBounds 是反映逻辑留白(Margins)后的可绘制区域。
        // PageBounds 是整张纸,即使画在硬边界内侧也可能被裁切
        using var font = new Font("Microsoft YaHei", 10);
        float lineHeight = font.GetHeight(e.Graphics);
        float y = e.MarginBounds.Top;

        while (_lineIndex < _lines.Count)
        {
            if (y + lineHeight > e.MarginBounds.Bottom)
            {
                // 这一页画到这里为止,还有剩余内容,换到下一页
                e.HasMorePages = true;
                return;
            }

            e.Graphics.DrawString(_lines[_lineIndex], font, Brushes.Black,
                e.MarginBounds.Left, y);
            y += lineHeight;
            _lineIndex++;
        }

        // 所有行都画完了,这就是最后一页
        e.HasMorePages = false;
    }
}

「第二页没有输出」这类问题的典型模式,是忘记把 HasMorePages 明确设为 false(默认值本身就是 false),或者相反,条件判断写错,导致一直保持 false 就跳出去了。PrintPage 事件的机制是会反复触发,直到这个属性变为 false 为止,请务必先明确判断「是否还有尚未画完的行」,再设置这个属性。12 如果想针对不同页面应用不同设置(比如纸张尺寸或方向),除了 PrintPage 之外,也可以善用 QueryPageSettings 事件。

「留白跑偏」这类问题的典型模式,是以 e.Graphics.VisibleClipBoundse.PageBounds 为基准来组合坐标,而忽略了 MarginBounds(反映逻辑留白后的区域)与 HardMarginX/HardMarginY(打印机的物理限制)。4 特别容易出问题的是:开发机的打印机硬边界较小,恰好没有暴露问题,但交付对象换了另一款机型、硬边界较大时,线条或项目就会被切掉一部分显得不完整。跨机型的打印验证,正是开发阶段容易被忽视的一个环节。

如果要提供预览功能,可以使用 PrintPreviewDialog,只要把同一个 PrintDocument 赋值给 Document 属性,就能直接沿用 PrintPage 事件的逻辑。1 若要按打印机分辨率切换输出结果,可以从 PrinterSettings.PrinterResolutions 获取可选列表,再设置到 PageSettings.PrinterResolution3

4. WPF:FlowDocument / FixedDocument 与 PrintDialog

在 WPF 中,该以 FlowDocument 还是 FixedDocument 为主轴,取决于文档本身的性质。

  • FlowDocument 按窗口大小、字体设置等运行时变量,动态重新排版内容,是以可读性为优先的文档,标准内置搜索、分页、切换显示模式等功能。8
  • FixedDocument 由应用程序端完全掌控版式,是以还原度优先的 WYSIWYG 设计,适合需要对显示、打印设备有高精度还原需求的场景。9

以流式排版优先考虑可读性的公司内部报告,适合用 FlowDocument;需要把线条位置、项目位置固定到像素级别的单据或标签,则适合用 FixedDocument,这是基本的判断准则。

基本的打印用 System.Windows.Controls.PrintDialog 来进行。这个类和 System.Windows.Forms.PrintDialog 是不同的东西,是 WPF 专用的类。10

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;

public static class FlowDocumentPrinter
{
    public static void Print(FlowDocument document, string jobName)
    {
        var printDialog = new PrintDialog();

        // 只有用户点击确定时才进行打印
        if (printDialog.ShowDialog() != true)
        {
            return;
        }

        // FlowDocument 在显示用途与打印用途上,页面尺寸的前提并不相同,
        // 因此要把打印用的页面尺寸、留白,对齐打印机的可打印区域
        var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
        paginator.PageSize = new Size(
            printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);

        printDialog.PrintDocument(paginator, jobName);
    }
}

如果想细化指定纸张尺寸、进纸盒、双面打印等设置,可以使用 PrintQueuePrintTicket/PrintCapabilitiesPrintTicket 代表对打印任务的指示,PrintCapabilities 代表打印机实际支持的功能,标准做法是先用 PrintQueue.MergeAndValidatePrintTicket 把指定的设置合并、校验成该打印机专属的有效值,再拿来使用。711

using System.Linq;
using System.Printing;

public static class DuplexPrintTicketBuilder
{
    public static PrintTicket BuildDuplexTicket(PrintQueue printQueue)
    {
        PrintCapabilities capabilities = printQueue.GetPrintCapabilities();

        // 事先确认打印机是否支持双面打印(长边装订)
        bool supportsDuplex = capabilities.DuplexingCapability
            .Contains(Duplexing.TwoSidedLongEdge);

        var requestedTicket = new PrintTicket();
        if (supportsDuplex)
        {
            requestedTicket.Duplexing = Duplexing.TwoSidedLongEdge;
        }

        // 针对用户的默认 PrintTicket,只合并双面打印这一项要求。
        // 不支持的项目会自动被替换成有效值
        ValidationResult result = printQueue.MergeAndValidatePrintTicket(
            printQueue.UserPrintTicket, requestedTicket);

        return result.ValidatedPrintTicket;
    }
}

如果要用 FixedDocument 组装固定版式的报表,直接发送到 XPS 打印路径,可以用 XpsDocumentWriterWrite/WriteAsync 方法把任务添加到 PrintQueue。如果目标是不支持 XPSDrv 的打印机,系统内部会自动转换成 GDI 格式后再发送,调用端不需要关心路径上的差异。7

5. PDF 输出的可选方案

「想要输出 PDF」这个需求,实际上会因为它指的是下面三种场景中的哪一种而使设计大不相同。

  1. 作为打印的延伸,让用户可以手动选择保存为 PDF 即可
  2. 想从应用程序内,通过打印对话框生成 PDF
  3. 完全不需要打印,只需要在无人值守的批处理中大批量生成 PDF 文件

Microsoft Print to PDF 是为场景 1 和 2 设计的功能。 这是 Windows 10 以后标准内置、定位为打印队列与驱动程序包的功能,从打印对象应用程序的角度看,它就是一台普通打印机。12 只不过它的实质是「每次打印都会询问保存文件名」这种以交互式操作为前提的机制,官方文档也仅提供「管理员可以从镜像中移除该打印队列与驱动程序包」这种粒度的控制手段。12 若要在无人值守、大批量的批处理中固定文件路径、安静地输出 PDF,也就是场景 3 的用途,这个功能在设计上并不适合。要实现场景 3,比较直接的做法是不经过打印管线,改用直接生成 PDF 的库。

选择 PDF 生成库时的比较维度,建议在推荐特定产品之前,先按下面的维度梳理自家需求,选型时就不容易摇摆。

维度 应确认的事项
许可证 是 OSS(MIT/Apache 系,还是 GPL/AGPL 系)还是商业许可证。如果自家应用程序要对外部分发、销售,务必以一手资料(许可证条款原文、提供方官方网站)确认库的许可条件(是否有源代码公开义务、再分发条件、能否商用)是否与自家的分发形式相冲突
中文字体的处理 是否支持中文字体的子集嵌入,是否能在不嵌入字体的情况下让 PDF 本身携带字体实体(在浏览端没有该字体时是否会出现乱码),如果业务需要竖排版或异体字则要确认其支持情况
报表相关功能 是否支持以模板为基础的报表定义、条形码/二维码生成、电子签名、PDF/A(长期保存规范)支持、能否轻松把现有纸质报表的版式直接迁移过来
依赖关系、运行环境 能否在服务器环境(无 GUI 的 Windows Server、容器)中运行,是否依赖原生库,在 .NET Core/.NET(非 Framework)环境下的实际运行情况
性能 大量页面、大批量任务生成时的内存占用、并发生成时的表现

许可条件会因产品和版本而异,务必在采用前确认提供方的官方文档或许可条文本身。本文不推荐特定库的名称或产品,只呈现比较时应考虑的「维度」。

6. 通过 Excel / Word 输出报表的方案

如果已经有用 Excel 模板运作的现有报表,比起从零重新打造,沿用现有资产的架构有时反而更现实。方式大致分为两种。

  • 用 Open XML SDK 等方式直接生成、编辑 xlsx/docx。 不启动 Excel/Word 的可执行文件,直接读写文件格式(ZIP + XML),因此在服务器环境也能稳定运行。这种做法很适合沿用模板的线条、格式,只填入数值的架构,详细内容整理在「Excel 报表输出的实现方法 - COM / Open XML / 模板」。
  • 用 Excel COM Interop 实际操作 Excel.Application。 这适合想直接沿用带有宏或复杂重新计算逻辑的现有 Excel 工作簿,但容易因为 COM 引用没有释放而导致 EXCEL.EXE 进程残留,对策与判断基准整理在「C# 操作 Excel 时 EXCEL.EXE 进程残留的问题」。

这里重要的是,微软官方明确表示,在服务器端或 Windows 服务中进行 Office COM 自动化属于「不推荐、不支持」。Office 应用程序的设计前提是交互式桌面与已登录用户的配置文件,若在无人值守、非交互式环境中运行,可能出现不稳定行为或死锁。13 在授权方面,通过服务器端自动化把 Office 功能提供给客户端使用的用法,通常也不在一般 EULA 的预期范围之内。13

因此,如果想把报表输出纳入常驻服务或批处理任务,即使沿用现有的 Excel 模板,也应该避免在运行时真的启动 Excel.exe 的架构(COM Interop),改用 Open XML SDK 等方式直接生成文件更为安全。如果确实需要 COM Interop,比较现实的做法是把它设计成在交互式登录的用户会话中运行的常驻应用程序(而不是服务)。

7. 实务中的陷阱

从 Windows 服务、Session 0 进行打印

想把打印处理放进常驻服务的咨询并不少见,但 Windows 服务是在Session 0 中运行的。Session 0 是 Windows Vista 以后专门保留给服务与不涉及交互用户的进程使用的会话,与用户的交互会话是分离的。14 由于这个分离机制,服务即使显示对话框,用户也看不到,如果编写了等待用户输入的代码(例如等待打印对话框显示),看起来就会像卡死了一样。14

实务上另一个容易踩坑的地方,是网络打印机或用户各自映射的打印机的可见性。用户登录后连接的网络打印机,大多是绑定在该用户的交互会话上才能看到,从其他会话(在 Session 0 运行的服务进程上下文)看起来往往不一样。涉及打印的常驻处理,一定要先确认「用哪个账户、哪个会话,能看到哪台打印机」。会话隔离的整体概念,整理在「如何理解 Windows 的会话隔离」;服务的实现与运维基础则整理在「Windows 服务的创建与运维」。

打印机不存在、离线时的行为

如果在 PrinterSettings.PrinterName 中指定了不存在的打印机名称,或是在没有设置默认打印机的环境中运行,尝试打印的那一刻就会发生异常或失败。至少应该做到两点:先用 PrinterSettings.IsValid 提前校验,以及针对打印失败设计好重试与错误通知的机制。网络打印机暂时离线时,即使任务已经排进队列,实际输出可能仍会停滞不前,因此不要把打印结果只当成「已发送」就算结束,必要时应设计确认任务状态的机制。

依赖默认打印机的风险

不明确指定 PrinterSettings.PrinterName、一律依赖「默认打印机」的做法,随着运行时间拉长,风险也会越来越高。原因是用户可能把默认打印机改成别的设备,或是把笔记本电脑带到别的地方,导致默认打印机发生变化,结果被发送到非预期的输出设备。业务上重要的报表,应该用配置文件或应用程序内的设置界面明确保存打印目标打印机名称,设计成不会被默认打印机的变化所牵动。

长时间运行下的 GDI 句柄泄漏

如果在打印处理中没有用 usingDispose() 确实释放 GraphicsFontBrushPen 等 GDI/GDI+ 资源,长时间运行后 GDI 对象的句柄会耗尽,导致与绘图相关的 API 调用开始失败。GDI 对象的句柄在每个进程上都有默认上限,可以通过注册表的 GDIProcessHandleQuota 在 256 到 65,536 之间调整,是一种有限的资源。15 打印频率越高的常驻应用程序,这种泄漏问题就越容易变成只在生产环境才会显现的问题。句柄泄漏的排查与定位方法,在以工业相机长时间运行崩溃案例为主题的「工业相机长期运行崩溃调查 - handle 泄漏篇」中有具体讲解。

相关文章

相关咨询领域

合同会社小村软件(KomuraSoft LLC)承接 Windows 业务应用程序打印、报表输出功能的设计、利用现有 Excel 资产重新打造报表,以及从常驻服务进行打印、输出设计的技术咨询。

参考链接

  1. Microsoft Learn, PrintDocument Class。关于 PrintDocument 的作用(设置 DocumentNamePrinterSettings,再用 Print() 开始打印),以及从 WPF 打印时应使用 System.Printing 命名空间的说明。  2 3 4

  2. Microsoft Learn, PrintPageEventArgs.HasMorePages Property。关于默认值为 false,以及 PrintPage 事件会反复触发直到这个属性变为 false 为止。  2

  3. Microsoft Learn, PageSettings.PrinterResolution Property。关于页面打印分辨率的默认值是打印机的默认分辨率,以及可以从 PrinterSettings.PrinterResolutions 获取可选分辨率列表。  2 3

  4. Microsoft Learn, PageSettings.HardMarginX Property。关于硬边界代表打印机所设置的物理留白。  2 3

  5. Microsoft Learn, PageSettings.Margins Property。关于页面留白的默认值是上下左右各 1 英寸,以及在 PrintPage 事件中可以搭配 Bounds 属性计算可打印区域。  2

  6. Microsoft Learn, Windows Print Path Overview。关于 Windows 存在 GDI 打印路径(源自 Win32 应用程序)与 XPS 打印路径(源自 WPF 应用程序或 XPS Print API)两条主要打印路径。  2 3

  7. Microsoft Learn, Printing documents overview。关于 WPF 应用程序使用 XPS 打印路径、通过 PrintDialog 进行的基本打印、通过 PrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriter 进行的高级打印,以及对不支持 XPSDrv 打印机自动转换为 GDI 格式的说明。  2 3 4

  8. Microsoft Learn, Flow Document Overview。关于 FlowDocument 是按窗口大小、分辨率等运行时变量重新排版内容、以可读性为优先的文档。  2

  9. Microsoft Learn, FixedDocument Class。关于 FixedDocument 是以还原显示、打印设备为优先的 WYSIWYG 设计,与 FlowDocument 的设计思路不同。  2

  10. Microsoft Learn, PrintDialog Class。关于 System.Windows.Controls.PrintDialog 是按用户输入配置 PrintTicketPrintQueue 的对话框,以及它与 System.Windows.Forms.PrintDialog 是不同的类。  2

  11. Microsoft Learn, How to: Validate and Merge PrintTickets。关于用 PrintQueue.GetPrintCapabilities 确认打印机的支持功能,再用 MergeAndValidatePrintTicket 把要求内容合并、校验成打印机专属有效 PrintTicket 的步骤。  2

  12. Microsoft Learn, RemoveMPDW。关于 Microsoft Print to PDF 是默认安装的可选功能,并以打印队列与驱动程序包为单位进行配置、移除。  2 3

  13. Microsoft Support, Considerations for server-side Automation of Office。关于微软不推荐、不支持从包含 ASP/ASP.NET/DCOM/NT 服务的无人值守、非交互式客户端应用程序进行 Office 自动化,以及 Office 的设计前提是交互式桌面与用户配置文件。  2 3

  14. Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation。关于 Windows Vista 以后,Session 0 专门保留给服务与不涉及交互用户的应用程序使用,服务因此也无法直接显示对话框。  2 3

  15. Microsoft Learn, GDI Objects。关于 GDI 对象的句柄在每个进程上有默认上限,可以通过注册表 GDIProcessHandleQuota 在 256 到 65,536 之间调整上限。  2

  16. Microsoft Learn, Introduction to printing。关于 Windows 的打印架构由打印队列与打印机驱动程序组成,以及 Win32 GDI 应用程序的绘图指令以 EMF 方式排入队列的机制。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

PrintDocument 和 WPF 的打印,应该用哪一种来做?
如果界面是 WinForms,就用 PrintDocument;如果是 WPF,就用 FlowDocument/FixedDocument,这是最直接的判断。不需要混用两种技术,配合现有应用程序的界面框架才是实务上的判断基准。如果是全新选用 WPF,建议先确定报表是以流式排版为主、还是以固定版式为主,再决定以 FlowDocument 还是 FixedDocument 为主轴,之后就不会犹豫不决。
报表库到底要不要买?
如果是线条很多的复杂报表、需要支持多种纸张尺寸、需要嵌入中文字体或竖排版等,自己用 GDI+/WPF 绘图来实现的成本很高,那么引入报表库的成本通常反而更低。反过来说,如果只是 A4 一览表这种简单报表,用 PrintDocument 或 FixedDocument 自行实现往往就够用了,建议先评估需求的复杂度,再做判断会比较稳妥。
只生成 PDF、不使用打印功能的架构可行吗?
完全可行。PDF 归根到底只是输出格式的一种,不依赖打印对话框或默认打印机、直接用 PDF 库生成文件的做法,通常和批处理、自动化的兼容性更好。Microsoft Print to PDF 是以交互式操作为前提设计的驱动程序,并不适合无人值守的 PDF 生成作业。
可以支持标签打印机或小票打印机吗?
可以支持,但仅靠通用的 PrintDocument/FixedDocument 延伸做法,有时会难以应对。多数标签打印机拥有自己的指令语言或 SDK,往往需要通过与 Windows 标准打印路径不同的方式来控制。建议先确认目标机型是以 Windows 标准打印机驱动程序的方式工作,还是需要通过专用 SDK 来控制,再决定后续设计。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表