Печать и PDF в Windows-приложениях: PrintDocument, WPF и библиотеки отчётов

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

История изменений (2 обновлений, последнее 31 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Текст статьи исправлен, чтобы соответствовать японскому оригиналу.
Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21619980)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). Печать и PDF в Windows-приложениях: PrintDocument, WPF и библиотеки отчётов. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-app-printing-pdf-guide/

DOI (зарегистрированный архив)
10.5281/zenodo.21619980
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21619981

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

В этой статье четыре варианта разобраны с точки зрения выбора по требованиям. Дальше на них ссылаюсь по этим номерам.

№ Вариант Где разбирается
① System.Drawing.Printing в WinForms (PrintDocument) глава 3
② FlowDocument / FixedDocument в WPF глава 4
③ Прямая генерация библиотекой вывода PDF глава 5
④ Печатные формы через Excel / Word (Open XML SDK, автоматизация COM) глава 6

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 задают через PrintDialog и PrintTicket / PrintQueue. Перед печатью принято слить запрос с реальными возможностями принтера и проверить его через PrintQueue.MergeAndValidatePrintTicket. 1011
  • Microsoft Print to PDF — драйвер печати, рассчитанный на интерактивную работу. Это очередь печати и пакет драйвера, которые поставляются с ОС. Как правило, при каждом запуске появляется диалог выбора пути сохранения, поэтому для пакетной обработки без оператора он не подходит. 12
  • Автоматизацию Office через COM с сервера или из службы Microsoft официально не поддерживает и не рекомендует. Office рассчитан на интерактивный рабочий стол и профиль пользователя; в необслуживаемой неинтерактивной среде возможны нестабильная работа и взаимная блокировка. 13
  • Служба Windows работает в сеансе 0, поэтому не может показать диалог и не должна опираться на принтер по умолчанию из сеанса пользователя. Если проектируете печать из постоянно работающего процесса, эту границу нужно учитывать с самого начала. 14
  • Если в долго работающем процессе не освобождать объекты GDI (дескрипторы), можно упереться в лимит на процесс и получить сбой. Лимит конечен: его можно менять через GDIProcessHandleQuota в реестре, бесконечно выделять объекты нельзя. 15

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

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

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

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 21, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Минимум о печати в Windows

Сначала определим термины, которые дальше используются в этой главе.

Термин Смысл
DPI (dots per inch) Точек на дюйм. Единица разрешения. У экрана обычно около 96 DPI, у принтера около 600 DPI — порядок другой
Диспетчер печати (spooler) Служба Windows, которая принимает задания печати от приложения, ставит их в очередь и по очереди отдаёт драйверу принтера. Даже если принтер остановился, приложение может идти дальше
EMF (расширенный метафайл) Формат, в котором записаны команды рисования GDI. По пути печати GDI в диспетчер печати уходит именно он
WYSIWYG (What You See Is What You Get) То, что видно на экране, совпадает с тем, что выходит на выходе. Для печати это значит: предпросмотр совпадает с бумагой
Сеанс 0 Начиная с Windows Vista — сеанс, зарезервированный для служб и процессов, не связанных с интерактивным пользователем. Он отделён от экрана вошедшего пользователя 14
Аппаратные поля Полоса у края листа, на которой принтер физически не печатает. Приложение сузить её не может

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

Путей печати два.

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

На схеме это выглядит так. Каким бы путём ни шли, в конце всё равно есть диспетчер печати и драйвер.

принтер с XPSDrvпринтер без XPSDrvприложение WinFormsвариант ① PrintDocumentпуть печати GDIкоманды рисования уходят в spool как EMFприложение WPFвариант ② FlowDocument и FixedDocumentпуть печати XPSв spool как XPS-документдиспетчер печатиавтопреобразование в формат GDIдрайвер принтерапринтер, факсMicrosoft Print to PDF и др.

Рис. 1. Путь печати GDI и путь печати XPS. Если WPF отправляет задание на принтер без XPSDrv, данные по пути преобразуются в формат GDI.

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

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

Важно, что эти два значения задаются независимо. Аппаратные поля определяет принтер, а MarginBounds считается только из Margins, которые задало приложение. MarginBounds сам по себе к области печати не округляется. На схеме это не вложенность, а так:

PageBounds — размер всего листаобласть печатиPrintableArea, HardMarginX, HardMarginYзадаёт принтер; приложение сузить не можетMarginBoundsсчитается только из Marginsк области печати не округляетсягарантированно печатаемая область — пересечение двух

Рис. 2. MarginBounds и аппаратные поля задаются независимо. Если Margins меньше аппаратных полей, MarginBounds выходит за область печати.

  • PageBounds: размер всего листа.
  • Аппаратные поля: физическая область, в которой принтер не печатает. Её можно прочитать через HardMarginX / HardMarginY; приложение сузить её не может. 4 Саму область печати даёт PageSettings.PrintableArea (в сотых долях дюйма). 17
  • MarginBounds: область рисования с учётом Margins, которые задало приложение (по умолчанию один дюйм со всех сторон). 5

То есть если логические поля меньше аппаратных — как при Margins равных нулю — MarginBounds выходит за область печати. Если рисовать, опираясь на такой MarginBounds, содержимое в выпирающей полосе не напечатается и край обрежется. В этом и состоит «поля обнулили, а край режется».

Чтобы уйти в безопасную сторону, берут один из двух приёмов.

  • Сделать логические поля не меньше аппаратных. Каждую сторону Margins сравнивают с HardMarginX / HardMarginY и меньшую поднимают.
  • Прямоугольник для рисования получить как пересечение с PrintableArea. MarginBounds как есть не используют, а берут общую часть с областью печати через Rectangle.Intersect.
// Внутри события PrintPage. Все единицы — сотые доли дюйма
private void OnPrintPage(object? sender, PrintPageEventArgs e)
{
    PageSettings page = e.PageSettings;

    // Область печати от левого верхнего угла листа. Левый верх — сами аппаратные поля
    var printableOnPaper = new RectangleF(
        page.HardMarginX, page.HardMarginY,
        page.PrintableArea.Width, page.PrintableArea.Height);

    // MarginBounds — расчёт из Margins, к области печати он не округлён.
    // Пересечение даёт диапазон, который не обрежется даже на модели с большими аппаратными полями.
    // До этой точки начало координат — левый верхний угол листа
    Rectangle safeOnPaper = Rectangle.Intersect(
        e.MarginBounds, Rectangle.Round(printableOnPaper));

    // Здесь легко ошибиться. Когда OriginAtMargins по умолчанию false, начало
    // координат e.Graphics — левый верх области печати, а не листа. Если рисовать
    // в координатах листа, рисунок уедет вправо-вниз на величину аппаратных полей,
    // правый и нижний края выйдут за область печати и обрежутся — как раз тот
    // симптом, который этот пример должен предотвратить.
    // Перед рисованием переносим в координаты Graphics
    Rectangle safe = safeOnPaper;
    safe.Offset(
        -(int)Math.Round(page.HardMarginX),
        -(int)Math.Round(page.HardMarginY));

    e.Graphics!.DrawRectangle(Pens.Black, safe);
}

Если поставить OriginAtMargins = true, начало координат смещается, и эта поправка тоже меняется. Сначала решите, в какой системе рисуете, и только потом собирайте координаты. Если смешать подходы, позиция будет съезжать только на другой модели — такой сбой трудно искать.

Ещё одно место, где путают начало координат. У PrintDocument.OriginAtMargins значение по умолчанию — false. Тогда начало координат Graphics стоит в левом верхнем углу области печати, а Margins на это начало не влияет. 18 Если кажется, что «Margins задали, а сдвига нет» или наоборот «сдвиг лишний», сначала проверьте это. Чтобы начало оказалось внутри полей, явно поставьте OriginAtMargins = true.

3. WinForms: практика System.Drawing.Printing

Печать WinForms собирают вокруг компонента PrintDocument. Обычный порядок: создать 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 (для проверки offline это не годится)
        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 Особенно часто на принтере разработки аппаратные поля малы, и проблема не проявляется, а у заказчика модель с большими аппаратными полями — и линии с полями обрезаются. Проверка на нескольких моделях — этап, который на разработке легко пропустить.

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

4. WPF: FlowDocument / FixedDocument и PrintDialog

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

  • FlowDocument. Документ, который динамически перестраивает содержимое под переменные выполнения — размер окна, настройки шрифта — и ставит читаемость выше. Поиск, разбиение на страницы и смена режима отображения есть из коробки. 8
  • FixedDocument. WYSIWYG-документ, в котором вёрстку полностью контролирует приложение и важнее точность на устройстве отображения и печати. Подходит, когда нужна высокая воспроизводимость. 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;
    }
}

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

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

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

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

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

Когда выбираете библиотеку генерации PDF, до рекомендации конкретного продукта разложите требования компании по таким осям.

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

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

Как искать кандидатов и сравнивать их. Если искать только по осям, вывод зависит от даты статьи, поэтому заранее зафиксируйте порядок обращения к первоисточникам.

Этап Куда смотреть Что смотреть конкретно
1. Собрать кандидатов Тег pdf на NuGet Gallery, поиск по Categories (Tags) Порядок величины числа загрузок. Если порядки отличаются на два и больше, это уже разница в фактическом применении
2. Проверить, жив ли пакет «Version History» на странице пакета NuGet Был ли выпуск за последний год, не слишком ли редки релизы
3. Посмотреть, как идёт разработка Репозиторий GitHub (ссылка «Source repository» на странице NuGet) Дата последнего коммита, насколько застаиваются Open Issue, отвечает ли maintainer
4. Проверить поддержку .NET Поле «Frameworks» на странице NuGet Есть ли среди целей ваш net8.0 и аналоги. Пакет только с net472 в .NET (линейка Core) не заработает
5. Проверить лицензию Сам текст по ссылке «License» на странице NuGet Можно ли коммерчески распространять, есть ли обязанность раскрыть код. Читать текст, а не пересказ
6. Проверить на живом коде Небольшой пробный проект Встраивание японских шрифтов, время генерации и память на ожидаемом числе страниц

Чаще всего пропускают этапы 4 и 5. Решить по обзорной статье, а потом обнаружить «поддерживался только .NET Framework» или «оказался AGPL, в свой продукт вставить нельзя» — значит выбросить всю работу на проверку. Пока не сузили список до одного кандидата, пройдите хотя бы 4 и 5 — так меньше возвратов.

6. Печатные формы через Excel / Word

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

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

Важно, что автоматизацию Office через COM на сервере и из службы Windows Microsoft официально называет нерекомендуемой и неподдерживаемой. Приложения Office рассчитаны на интерактивный рабочий стол и профиль уже вошедшего пользователя; в необслуживаемой неинтерактивной среде возможны нестабильная работа и взаимная блокировка. 13 И по лицензии обычный EULA не предполагает отдавать клиентам функции Office за счёт автоматизации на стороне сервера. 13

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

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

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

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

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

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

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

Риск зависимости от принтера по умолчанию

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

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

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

8. Итог

Дерево решений: какой из четырёх вариантов брать

Варианты ①–④ из начала статьи сужаются, если задавать вопросы в таком порядке.

  1. Печать вообще нужна? Если на выходе достаточно PDF-файла, а на бумагу пользователь печатает сам по желанию, самый прямой путь — ③ (прямая генерация библиотекой PDF), без конвейера печати. Microsoft Print to PDF предполагает интерактивный вопрос о пути сохранения и для пакетной обработки без оператора не подходит (глава 5).
  2. Нужно ли переиспользовать существующий шаблон Excel или Word? Тогда ④. Автоматизацию COM из постоянно работающей службы или пакетного задания Microsoft явно не поддерживает, поэтому склоняйтесь к прямой генерации через Open XML SDK (глава 6).
  3. Экраны на WinForms или на WPF? Ориентируйтесь на фреймворк существующего приложения. WinForms — ①, WPF — ②. Смешивать технологии не нужно (главы 3 и 4).
  4. Если выбран WPF: перетекание или фиксированная вёрстка? Внутренний отчёт, где важнее читаемость, — FlowDocument; накладная или этикетка, где фиксируют положение линий, — FixedDocument (глава 4).

Что проверить до реализации

  • Сначала зафиксируйте единицы и систему координат. Экранные пиксельные координаты в печать как есть не переносите. Опирайтесь на сотые доли дюйма или на физический размер в миллиметрах и дюймах (глава 2).
  • Поля — два уровня. Рисуйте от MarginBounds, не от PageBounds. Даже при Margins равных нулю вывести можно только внутри аппаратных полей (глава 2, рис. 2).
  • Явно задавайте HasMorePages. Сначала решите, остались ли строки для рисования, и только потом ставьте свойство. Значение по умолчанию — false; пропуск настройки — типичная причина «вторая страница не выходит» (глава 3).
  • Явно храните принтер назначения. Не опирайтесь на принтер по умолчанию: имя держите в конфигурации или на экране настроек приложения (глава 7).
  • Ресурсы GDI всегда освобождайте через using. Утечка Font, Brush, Pen проявляется только при долгой работе в production (глава 7).
  • Печать из службы с самого начала сверяйте с границей сеанса. В сеансе 0 диалог показать нельзя, и сетевые принтеры пользователя тоже не видны (глава 7).
  • Проверяйте на нескольких моделях. Размер аппаратных полей зависит от модели: на машине разработки всё может быть в порядке, а у заказчика обрежутся линии (глава 3).

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

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

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

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

  1. Microsoft Learn, PrintDocument Class. О роли PrintDocument — задать DocumentName и PrinterSettings, начать печать через 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 ↩4

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

  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 как необязательной функции, установленной по умолчанию, которую настраивают и удаляют как очередь печати и пакет драйвера. ↩ ↩2 ↩3

  13. Microsoft Support, Considerations for server-side Automation of Office. О том, что Microsoft не рекомендует и не поддерживает автоматизацию Office из необслуживаемых неинтерактивных клиентских приложений, включая ASP, ASP.NET, DCOM и службы NT, и что Office рассчитан на интерактивный рабочий стол и профиль пользователя. ↩ ↩2 ↩3

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

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

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

  17. Microsoft Learn, PageSettings.PrintableArea Property. О том, что свойство задаёт область, в которой принтер может печатать, в сотых долях дюйма, и что с его помощью можно печатать снаружи полей страницы, но внутри области печати. ↩

  18. Microsoft Learn, PrintDocument.OriginAtMargins Property. О значении по умолчанию false и о том, что при false начало координат определяется только областью печати, а PageSettings.Margins игнорируется. ↩

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

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

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

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

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

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

Об авторе

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

Го Комура

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

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

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

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