Печать документов XPS

64

Как известно, в WPF поддерживаются два взаимодополняющих типа документов. Потоковые документы обрабатывают гибкое содержимое. Документы XPS хранят готовое для печати содержимое, основанное на фиксированном размере страницы (фактически являясь аналогом PDF). Содержимое "заморожено" на месте и сохраняет свою точную исходную форму.

Как и можно было ожидать, распечатать документ XpsDocument довольно просто. Подобно FlowDocument, класс XpsDocument предоставляет DocumentPaginator. Однако объекту DocumentPaginator для XpsDocument мало что нужно делать, поскольку его содержимое уже скомпоновано на фиксированных, неизменных страницах.

Ниже приведен код, который можно использовать для загрузки файла XPS в память, отображения его в DocumentViewer с последующей отправкой на принтер:

// Не забудьте добавить ссылку на ReachFramework.dll
using System.IO;
using System.IO.Packaging;
using System.Windows.Xps;
using System.Windows.Xps.Packaging;

...

     PrintDialog printDialog = new PrintDialog();

     // Отобразить документ
     XpsDocument doc = new XpsDocument("filename.xps", FileAccess.ReadWrite);
     docViewer.Document = doc.GetFixedDocumentSequence();
     doc.Close();

     // Напечатать документ
     if (printDialog.ShowDialog() == true)
     {
           printDialog.PrintDocument(docViewer.Document.DocumentPaginator,
               "Фиксированный документ XPS");
     }

Очевидно, что отображать фиксированный документ в DocumentViewer перед его печатью не обязательно. В коде этот шаг предусмотрен потому, что это наиболее распространенный вариант. Во многих сценариях документ XpsDocument загружается для предварительного просмотра и печатается только после того, как пользователь щелкнет на соответствующей кнопке.

Как и средства просмотра для объектов FlowDocument, объект DocumentViewer также обрабатывает команду ApplicationCommands.Print, а это означает, что документ XPS можно отправлять из DocumentViewer на принтер без какого-либо кода.

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

В WPF доступна вся необходимая поддержка программного создания документов XPS. Создание документа XPS концептуально похоже на печать некоторого содержимого: после построения документа XPS выбран фиксированный размер страницы и компоновка "заморожена". Так зачем же нужен дополнительный шаг? На то есть две причины:

Базовый прием создания документа XPS предусматривает создание объекта XpsDocumentWriter с использованием статического метода XpsDocument.CreateXpsDocumentWriter(), например:

XpsDocument doc = new XpsDocument("filename.xps", FileAccess.ReadWrite);
XpsDocumentWriter writer = XpsDocument.CreateXpsDocumentWriter(doc); 

XpsDocumentWriter — усеченный класс, и его функциональность вращается вокруг методов Write() и WriteAsync(), которые записывают содержимое в документ XPS.

Оба эти метода многократно перегружены, позволяя писать разные типы содержимого, включая другой документ XPS, страницу, извлеченную из документа XPS, визуальный элемент (который позволяет записывать любой элемент) и DocumentPaginator. Вновь созданный документ XPS затем отображается в DocumentViewer, который служит для предварительного просмотра печати.

Запись в документ XPS, находящийся в памяти

Класс XpsDocument предполагает, что содержимое XPS записывается в файл. Это немного неудобно в ситуациях, подобных показанной выше, где документ XPS служит временным хранилищем, используемым для предварительного просмотра. Подобные проблемы происходят, когда содержимое XPS должно быть сериализовано в какое-то другое хранилище, например, в поле внутри записи базы данных.

Это ограничение можно обойти и записать содержимое XPS непосредственно в MemoryStream. Однако это потребует немного больше усилий, поскольку сначала понадобится создать пакет для содержимого XPS. Ниже приведен код, выполняющий эту работу:

// Подготовиться к сохранению содержимого в памяти
MemoryStream ms = new MemoryStream();

// Создать пакет, используя статический метод Package.Open()
Package package = Package.Open(ms, FileMode.Create, FileAccess.ReadWrite);

// Каждому пакету необходим URI. Использовать синтаксис pack://. 
// Действительное имя файла неважно
Uri documentUri = new Uri("pack://filename.xps");

// Добавить пакет
PackageStore.AddPackage(documentUri, package);

// Создать документ XPS на основе этого пакета. В то же время 
// выбрать нужный уровень сжатия для содержимого в памяти. 
XpsDocument xpsDocument = new XpsDocument(package, CompressionOption.Fast,
      documentUri.AbsoluteUri);

По завершении использования документа XPS можно закрыть поток для освобождения памяти.

Не используйте подход с хранением в памяти, если приходится работать с большим документом XPS (например, при генерации документа XPS из содержимого базы данных, с неизвестным количеством записей). Вместо этого следует воспользоваться методом вроде Path.GetTempFileName() для получения подходящего временного пути и создать документ XPS в файле.

Печать непосредственно на принтер через XPS

Как уже известно, поддержка печати в WPF построена на пути печати XPS. При использовании класса PrintDialog признаки этой низкоуровневой реальности могут быть и не видны. С другой стороны, если применяется XpsDocumentWriter, то не заметить их невозможно.

До сих пор вся печать запускалась через класс PrintDialog. Поступать так не обязательно. В действительности PrintDialog делегирует реальную работу XpsDocumentWriter. Трюк состоит в создании XpsDocumentWriter, который упаковывает PrintQueue вместо FileStream. Код записи печатного вывода идентичен — в нем используются методы Write() и WriteAsync().

Ниже приведен фрагмент кода, который отображает диалоговое окно Print, получает выбранный принтер и применяет его для создания XpsDocumentWriter, запускающего задание печати:

PrintDialog printDialog = new PrintDialog();
string filePath = System.IO.Path.Combine(Directory.GetCurrentDirectory(), "FlowDocument1.xaml");

if (printDialog.ShowDialog() == true)
{
       PrintQueue queue = printDialog.PrintQueue;
       XpsDocumentWriter writer = PrintQueue.CreateXpsDocumentWriter(queue);

       using (FileStream fs = File.Open(filePath, FileMode.Open))
       {
           FlowDocument flowDocument = (FlowDocument)XamlReader.Load(fs);
           writer.Write(((IDocumentPaginatorSource)flowDocument).DocumentPaginator);
       }
}

Интересно, что в этом примере все равно используется класс PrintDialog. Однако это делается просто для отображения стандартного диалогового окна Print, в котором пользователь может выбрать принтер. Реальная печать осуществляется через XpsDocumentWriter.

Асинхронная печать

Класс XpsDocumentWriter упрощает асинхронную печать. Фактически предыдущий пример можно преобразовать для использования асинхронной печати, просто заменив вызов метода Write() вызовом WriteAsync().

В Windows все задания печати выполняются асинхронно. Тем не менее, процесс отправки задания на печать происходит синхронно, если применяется метод Write(), и асинхронно — если WriteAsync(). Во многих случаях время на отправку задания на печать является несущественным, и это средство не понадобится. Другое дело, если требуется построить (и разбить на страницы) содержимое, которое должно печататься асинхронно — это зачастую наиболее затратная по времени операция, и если нужна такая возможность, то понадобится писать код, который запускает логику печати в фоновом потоке. Для относительного упрощения работы можно воспользоваться подходом с классом BackgroundWorker.

Сигнатура метода WriteAsync() соответствует сигнатуре метода Write(). Другими словами, WriteAsync() принимает объект разбивки на страницы, визуальный элемент или один из нескольких других типов объектов.

Вдобавок метод WriteAsync() имеет перегрузки, принимающие дополнительный второй параметр с информацией о состоянии. Информация о состоянии может быть представлена любым объектом, который будет использоваться для идентификации задания печати. Этот объект предоставлен посредством объекта WritingCompletedEventArgs при возникновении события WritingCompleted. Это позволяет запускать сразу несколько заданий печати, обрабатывать событие WritingCompleted для каждого из них в одном и том же обработчике и определять, какое именно задание было запущено при каждом возникновении события.

Выполнение асинхронного задания печати можно прервать, вызвав метод CancelAsync().

Класс XpsDocumentWriter также включает небольшой набор событий, которые позволяют реагировать на отправку задания печати, в том числе WritingProgressChanged, WritingCompleted и WritingCancelled. Имейте в виду, что событие WritingCompleted происходит тогда, когда задание печати записывается в очередь печати, но это не значит, что принтер немедленно начинает его печатать.

Пройди тесты
Лучший чат для C# программистов