Печать и вывод PDF в Windows-бизнес-приложениях — выбор между System.Drawing.Printing, WPF и библиотеками отчётов

· · CSharp, .NET, WinForms, WPF, Печать, PDF, Отчёты, Разработка Windows, Техническая консультация

Печать в Windows-бизнес-приложении не заканчивается фразой «нарисуем что-нибудь PrintDocument-ом». Не работают переносы страниц, смещаются поля, preview не совпадает с реальным выводом, между приложением и нужным PDF появляется диалог печати — обычно такие проблемы начинаются с реализации без понимания механизма печати.

Здесь систематизированы четыре подхода: System.Drawing.Printing в WinForms, FlowDocument/FixedDocument в WPF, библиотеки генерации PDF и отчёты через Excel/Word. Главный вопрос — как выбирать их по требованиям.

1. Итог сначала

  • Для простых списков или продолжения существующих WinForms-активов достаточно PrintDocument. Перенос страниц устроен как повторение события PrintPage, пока HasMorePages не станет false; если забыть это, страницы после второй не выйдут. 12
  • DPI печати — не DPI экрана. PageSettings.PrinterResolution описывает разрешение принтера, а поля состоят из PageSettings.HardMarginX/HardMarginY — физически непечатаемой зоны принтера — и Margins — логических полей приложения. Простое повторное использование экранных координат сдвигает layout из-за разницы DPI и координатных систем. 345
  • В WPF стандартный путь печати Windows разделён на GDI и XPS. WPF-приложение изначально использует XPS-путь; при отправке принтеру без XPSDrv вывод автоматически преобразуется в GDI. 67
  • Документы WPF имеют разную философию: FlowDocument — для потока, FixedDocument — для фиксированного layout. FlowDocument ставит читаемость выше и перераскладывается по размеру окна и разрешению, а FixedDocument ставит выше точность на устройствах отображения и печати. 89
  • В WPF бумага и лоток задаются через PrintDialog и PrintTicket/PrintQueue. Перед печатью принято проверить и слить запрос с реальными возможностями принтера через PrintQueue.MergeAndValidatePrintTicket. 1011
  • Microsoft Print to PDF — драйвер печати, рассчитанный на интерактивную работу. Это поставляемая ОС очередь и пакет драйвера; как правило, при каждом запуске появляется выбор пути сохранения, поэтому для незаметного пакетного процесса он не подходит. 12
  • Microsoft не поддерживает и не рекомендует Office COM automation на сервере или из службы. Office проектировался для интерактивного рабочего стола и профиля пользователя; в unattended/non-interactive окружении возможны нестабильность и deadlock. 13
  • Службы Windows работают в session 0, поэтому не могут показывать диалог и не должны зависеть от принтера по умолчанию из пользовательской сессии. Учитывайте эту границу в самом начале при проектировании резидентной печати. 14
  • Если в долгоживущем процессе не освобождать GDI-объекты (handles), можно достичь предела на процесс и получить падение. Предел конечен и настраивается через GDIProcessHandleQuota; бесконечно выделять объекты нельзя. 15

Таблица выбора: требование × средство

Требование Подходящее средство Причина
Напечатать несколько простых списков или накладных (WinForms) PrintDocument Достаточно стандартных классов и можно использовать существующую GDI+-отрисовку
Отчёты с большим числом линий, разной бумагой, сложным layout Библиотека отчётов или фиксированный layout на FixedDocument Самостоятельный расчёт координат, переносов и японской типографики дорог
Массовая пакетная печать без оператора Программный PrintDocument.Print() / прямая отправка в XpsDocumentWriter Нужно закончить без диалогов и не зависеть от принтера по умолчанию
Нужен только PDF без печати Прямо генерировать PDF библиотекой Нет зависимости от диалога, принтера по умолчанию и состояния spooler
Нужен preview и важна внутренняя проверка PrintPreviewDialog (WinForms), DocumentViewer (WPF) Можно использовать стандартный UI проверки перед печатью
Нужно использовать существующий шаблон Excel Прямая генерация Open XML SDK или ограниченный COM automation Активы переиспользуются, но COM не следует запускать в резидентной службе

Далее разберём пункты подробнее.

2. Самое необходимое о печати в Windows

Архитектура печати Windows состоит из spooler и драйверов отдельных принтеров. Приложение вызывает независимые от устройства функции и может послать задачу самым разным выходам: лазерному принтеру, векторному plotter, растровому принтеру или факсу. 16

Есть два главных пути печати.

  • GDI-путь. Когда печатает Win32 GDI-приложение, графический движок GDI кладёт команды рисования как EMF (enhanced metafile) в spooler либо вместе с драйвером сразу рендерит печатаемое изображение. По этому пути идут традиционные WinForms-приложения. 166
  • XPS-путь. Это путь для драйверов на основе XPS (XML Paper Specification). WPF-приложения изначально используют его, отправляя в spooler XPS-документы. Если целевой принтер не имеет XPSDrv, данные автоматически преобразуются в GDI перед отправкой. 67

Почему экран и распечатка отличаются. Самая частая причина — путаница DPI. Экранный Graphics обычно работает около 96 DPI, а Graphics принтера — с разрешением PageSettings.PrinterResolution, часто около 600 DPI. 3 Если передать в печатный Graphics пиксельные координаты, рассчитанные для экрана, результат будет намного меньше — либо при другой настройке больше — ожидаемого. Для печати безопаснее строить координаты в hundredths of an inch или физических единицах (миллиметры/дюймы), не завися от разрешения Graphics.

Другая часто упускаемая вещь — физически непечатаемая зона принтера. Многие принтеры не могут печатать несколько миллиметров от края бумаги. Это ограничение доступно через PageSettings.HardMarginX/HardMarginY; документация описывает его как физические поля, установленные принтером. 4 Логические поля приложения — PageSettings.Margins (по умолчанию один дюйм со всех сторон); даже если поставить их в ноль, печатать внутри hard margin нельзя. 5 Многие дефекты «поле поставили нулевым, но край режется или всё смещается» происходят от игнорирования этих физических полей.

3. WinForms: практическое использование System.Drawing.Printing

Печать WinForms строится вокруг PrintDocument: создаётся документ, настраиваются PrinterSettings и DefaultPageSettings, в событии 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("Segoe UI", 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.VisibleClipBounds или e.PageBounds, игнорируя MarginBounds (зона с логическими полями) и HardMarginX/HardMarginY (физическое ограничение). 4 В частности, у принтера разработки физическое поле может быть маленьким и скрыть проблему, а у модели заказчика быть больше, из-за чего обрезаются линии и поля. Проверка на нескольких моделях — этап, который легко пропустить.

Чтобы дать preview, присвойте тот же PrintDocument свойству Document у PrintPreviewDialog; логика PrintPage будет использоваться без изменений. 1 Если нужно менять вывод по разрешению принтера, получите варианты из PrinterSettings.PrinterResolutions и задайте их в PageSettings.PrinterResolution. 3

4. WPF: FlowDocument / FixedDocument и PrintDialog

В WPF выбор между FlowDocument и FixedDocument зависит от природы документа.

  • FlowDocument. Документ, который динамически перераскладывает содержимое по переменным выполнения, таким как размер окна или настройки шрифта, и ставит читаемость выше. Он имеет штатные возможности поиска, pagination и смены режима отображения. 8
  • FixedDocument. WYSIWYG-документ, в котором приложение полностью контролирует layout и ставит выше точность на устройстве отображения и печати. Подходит, когда нужна высокая воспроизводимость. 9

Для внутреннего отчёта, где важна читаемость, берите FlowDocument; для накладной или этикетки, где положение линий и полей должно быть фиксировано до пикселя, — FixedDocument.

Базовая печать делается через System.Windows.Controls.PrintDialog, отдельный WPF-класс, не равный System.Windows.Forms.PrintDialog. 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();

        // Печатать только если пользователь нажал OK.
        if (printDialog.ShowDialog() != true)
        {
            return;
        }

        // FlowDocument использует разные предпосылки для размера страницы при отображении и печати, поэтому
        // размер страницы и поля печатаемого содержимого нужно привести к области печати принтера.
        var paginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
        paginator.PageSize = new Size(
            printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);

        printDialog.PrintDocument(paginator, jobName);
    }
}

Для точного задания размера бумаги, лотка и двусторонней печати используйте PrintQueue и PrintTicket/PrintCapabilities. PrintTicket описывает инструкцию задания, 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;
    }
}

Если отчёт с фиксированным layout строится как FixedDocument и отправляется прямо по XPS-пути, задание добавляется в PrintQueue методами Write/WriteAsync XpsDocumentWriter. Для принтера без XPSDrv внутри выполняется автоматическое преобразование в GDI, так что вызывающей стороне не нужно учитывать путь. 7

5. Варианты вывода PDF

Требование «нужно вывести PDF» меняет проектирование в зависимости от одного из трёх смыслов.

  1. Как продолжение печати достаточно, чтобы пользователь вручную выбрал сохранение PDF.
  2. Нужно генерировать PDF из приложения через диалог печати.
  3. Не печатать вообще, а массово создавать только PDF-файлы в unattended batch.

Microsoft Print to PDF подходит для случаев 1 и 2. Это функция, поставляемая с Windows 10: очередь печати и пакет драйвера, которую приложение видит как обычный принтер. 12 Но она предполагает интерактивное поведение с запросом имени файла назначения при каждой печати; официально контроль предоставлен лишь как управление этой очередью и пакетом драйвера. 12 Для случая 3, где путь фиксирован и PDF тихо массово выводятся, механизм не спроектирован. Здесь естественнее библиотека, генерирующая PDF напрямую без конвейера печати.

При выборе PDF-библиотеки до рекомендации конкретного продукта упорядочьте требования компании по следующим осям.

Ось Что проверить
Лицензия OSS (MIT/Apache либо GPL/AGPL) или коммерческая. При внешнем распространении/продаже приложения проверить в первоисточниках — тексте лицензии и официальном сайте поставщика — что обязанность раскрыть код, условия перераспространения и коммерческое использование не противоречат своей модели
Японские шрифты Встраивание subset японских шрифтов, возможность не получать mojibake на компьютере без шрифта, а при необходимости бизнеса — вертикальное письмо и варианты IVS
Возможности отчётов Template-based определения, barcode/QR, электронная подпись, PDF/A для долгого хранения и простота переноса существующего бумажного layout
Зависимости и среда Работа на сервере без GUI (Windows Server, containers), зависимости от native library и опыт работы в .NET Core/.NET вне Framework
Производительность Использование памяти при batch generation многих страниц/задач и поведение при параллельном создании

Условия лицензирования меняются по продукту и версии. Перед выбором всегда подтверждайте официальную документацию или сам текст лицензии. Здесь не рекомендуется конкретная библиотека или продукт — показаны только критерии сравнения.

6. Отчёты через Excel / Word

Если уже есть отчёт, управляемый шаблоном Excel, часто реалистичнее использовать этот актив, чем создавать всё с нуля. Есть два крупных способа.

  • Генерировать или редактировать xlsx/docx напрямую через Open XML SDK и т. п. Исполняемый файл Excel/Word не запускается, ZIP + XML читается и пишется напрямую, поэтому подход стабилен на сервере. Он хорошо сочетается с сохранением линий и оформления шаблона при подстановке только значений; подробнее — в «Как строить вывод Excel-отчёта: COM/Open XML/templates».
  • Управлять настоящим Excel.Application через Excel COM Interop. Этот путь выбирают, когда требуется переиспользовать существующую книгу с macros или сложным recalculation, но распространена проблема, что EXCEL.EXE остаётся из-за неосвобождённых COM-references. Меры и критерии приведены в статье о сохранении EXCEL.EXE при работе с Excel из C#.

Важно, что Microsoft официально заявляет: Office automation на сервере или из Windows-службы не рекомендуется и не поддерживается. Office спроектирован под интерактивный desktop и профиль пользователя, поэтому в unattended/non-interactive окружении возможны нестабильность и deadlock. 13 Лицензии также обычно не предполагают предоставление клиентам функций Office автоматизацией на стороне сервера. 13

Поэтому при встраивании вывода отчётов в резидентную службу или batch job, даже если используется шаблон Excel, безопаснее не запускать Excel.exe в runtime через COM Interop, а перейти к прямой генерации файлов посредством Open XML SDK. Если COM Interop совершенно необходим, реалистично отделить его как резидентное приложение, а не службу, в интерактивной вошедшей пользовательской сессии.

7. Практические ловушки

Печать из Windows-службы и session 0

Запросы встроить печать в резидентную службу встречаются часто, но службы Windows выполняются в session 0. Начиная с Windows Vista session 0 зарезервирована для служб и процессов, не связанных с интерактивными пользователями, и отделена от пользовательской сессии. 14 Поэтому диалог, который показывает служба, пользователь не увидит, а код, ожидающий его ввода — например диалог печати, — выглядит зависшим. 14

Другой практический риск — видимость сетевых или назначенных отдельному пользователю принтеров. Сетевой принтер, подключённый пользователем, часто виден в его интерактивной сессии, но не обязательно в другом контексте, например в процессе службы session 0. Для резидентного процесса с печатью в начале проектирования подтвердите, какие принтеры видны какой учётной записи и сессии. Разделение сессий описано в «Как понимать isolation сессий Windows», а базовая реализация и эксплуатация служб — в «Как создавать и эксплуатировать службы Windows».

Когда принтера нет или он offline

Если задать несуществующее имя в PrinterSettings.PrinterName или запустить в окружении без default printer, попытка печати может привести к исключению или неудаче. Как минимум заранее проверяйте через PrinterSettings.IsValid и проектируйте повторные попытки и уведомление об ошибке. Когда сетевой принтер временно offline, job может остаться в spooler без реального вывода; не заканчивайте проверку на «отправить удалось», а при необходимости проверяйте состояние job.

Риск зависимости от default printer

Реализация, которая не задаёт PrinterSettings.PrinterName и всегда полагается на «принтер по умолчанию», со временем накапливает риск. Пользователь может его изменить, ноутбук может сменить default в другом месте, и важный документ уйдёт не туда. Для значимых бизнес-отчётов явно храните имя принтера назначения в конфигурации или экране настроек, чтобы не зависеть от смены default.

Утечка GDI handles при долгой работе

Если в коде печати не освобождать через using или Dispose() GDI/GDI+ ресурсы вроде Graphics, Font, Brush и Pen, после долгой работы исчерпываются handles GDI-объектов и вызовы рисования начинают отказывать. Handles имеют предел на процесс; это конечный ресурс, который через реестр GDIProcessHandleQuota можно настроить от 256 до 65 536. 15 Чем чаще печатает резидентное приложение, тем выше шанс, что такая утечка проявится только в production. Практика исследования и изоляции разобрана на примере в статье о падении промышленной камеры от утечки handles.

Связанные статьи

Связанные области консультаций

KomuraSoft LLC проводит технические консультации по проектированию функций печати и вывода отчётов в Windows-бизнес-приложениях, перестройке отчётов с использованием существующих Excel-активов и проектированию печати/вывода из резидентных служб.

Справочные ссылки

  1. Microsoft Learn, PrintDocument Class. О роли PrintDocument — задать DocumentName и PrinterSettings, начать печать Print() — и указании использовать namespace System.Printing для печати WPF.  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. О том, что hard margin представляет физическое поле, установленное принтером.  2 3

  5. Microsoft Learn, PageSettings.Margins Property. О полях по умолчанию в один дюйм со всех сторон и расчёте печатаемой области с Bounds в PrintPage 2

  6. Microsoft Learn, Windows Print Path Overview. О двух главных путях Windows: GDI для Win32-приложений и XPS для WPF либо XPS Print API.  2 3

  7. Microsoft Learn, Printing documents overview. О XPS-пути WPF, базовой печати PrintDialog, расширенной печати PrintTicket/PrintCapabilities/PrintQueue/XpsDocumentWriter и автоматическом преобразовании в GDI для принтеров без XPSDrv.  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 строит PrintTicket и PrintQueue на основе ввода пользователя и отличается от System.Windows.Forms.PrintDialog 2

  11. Microsoft Learn, How to: Validate and Merge PrintTickets. О проверке возможностей через PrintQueue.GetPrintCapabilities и слиянии/проверке запроса с MergeAndValidatePrintTicket 2

  12. Microsoft Learn, RemoveMPDW. О Microsoft Print to PDF как установленной по умолчанию optional feature, настраиваемой/удаляемой как очередь печати и пакет драйвера.  2 3

  13. Microsoft Support, Considerations for server-side Automation of Office. О том, что Microsoft не рекомендует и не поддерживает автоматизацию Office из unattended/non-interactive приложений, включая ASP/ASP.NET/DCOM/NT services, и что Office предполагает интерактивный desktop и user profile.  2 3

  14. Microsoft Learn, Service Changes for Windows Vista - Session 0 Isolation. О том, что с Windows Vista session 0 зарезервирована для служб и приложений, не связанных с интерактивными пользователями, а службы не могут напрямую показывать диалоги.  2 3

  15. Microsoft Learn, GDI Objects. О том, что handles GDI-объектов имеют предел на процесс, а GDIProcessHandleQuota позволяет менять его от 256 до 65 536.  2

  16. Microsoft Learn, Introduction to printing. О составе архитектуры печати Windows из spooler и драйверов и о том, как команды рисования Win32 GDI отправляются в spooler как EMF.  2

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Не только appsettings.json — практическое руководство по управлению конфигурацией Windows-бизнес-приложений (настройки окружений, секреты и место записи)

Практический разбор управления конфигурацией Windows-бизнес-приложений: слои appsettings.json, выбор между IConfiguration и семейством IO...

Как сделать сайт заметным по запросам с названием района — практическое руководство по локальному SEO для малого и среднего бизнеса

Для компаний, которые не появляются по запросу «название района + отрасль», собран порядок исправлений для локального SEO: подготовка Goo...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Что выбрать: PrintDocument или печать WPF?
Если экран построен на WinForms, естественный выбор — PrintDocument; если на WPF — FlowDocument/FixedDocument. Не нужно смешивать технологии: практический критерий — framework экранов существующего приложения. Для нового WPF-проекта заранее решите, будет ли отчёт преимущественно поточным или с фиксированным layout, и выберите соответственно FlowDocument либо FixedDocument.
Нужно ли покупать библиотеку отчётов?
Если высока стоимость самостоятельного создания сложных отчётов с большим числом линий, несколькими размерами бумаги, встраиванием японских шрифтов или вертикальным письмом средствами GDI+/WPF, стоимость библиотеки отчётов часто оказывается ниже. Для простого списка A4 собственный PrintDocument или FixedDocument обычно достаточен. Безопаснее сначала оценить сложность требований.
Можно ли генерировать только PDF и не пользоваться печатью?
Да. PDF — лишь один из форматов вывода; прямая генерация библиотекой PDF, не зависящей от диалога печати и принтера по умолчанию, часто лучше подходит для пакетной обработки и автоматизации. Microsoft Print to PDF — драйвер для интерактивных операций и не годится для создания PDF без участия пользователя.
Поддерживаются ли принтеры этикеток и чековые принтеры?
Это возможно, но не всегда решается простым продолжением PrintDocument/FixedDocument. У многих принтеров этикеток есть собственный язык команд или SDK, и управление идёт не по стандартному пути печати Windows. Сначала выясните, работает ли целевая модель как обычный драйвер Windows или требует специального SDK.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог