Печать
132WPF --- Периферия WPF --- Печать
Хотя WPF включает в себя десятки классов, имеющих отношение к печати (большинство из которых находятся в пространстве имен System.Printing), существует одна начальная точка, которая призвана облегчить жизнь: класс PrintDialog.
PrintDialog заключает в себе знакомое диалоговое окно Print, которое позволяет пользователю выбрать принтер и установить несколько стандартных настроек печати, такие как количество копий. Однако класс PrintDialog — это нечто больше, чем просто симпатичное окно; он также обладает встроенной способностью инициировать вывод на печать:
Чтобы отправить задание на печать посредством класса PrintDialog, необходимо воспользоваться одним из двух описанных ниже методов:
- PrintVisual()
Работает с любым классом, унаследованным от System.Windows.Media.Visual. Сюда относится любая графика, нарисованная вручную, и любой элемент, помещенный в окно.
- PrintDocument()
Работает с любым объектом DocumentPaginator. Сюда относятся те, что используются для разбиения на страницы FlowDocument (или XpsDocument), и все специальные объекты DocumentPaginator, которые создаются для работы с данными.
В следующих разделах рассматриваются разнообразные стратегии, которые можно применять для создания вывода на печать.
Печать элемента
Простейший подход к печати состоит в использовании преимущества модели, которая уже используется для визуализации на экране. С помощью метода PrintDialog.PrintVisual() можно отправлять любой элемент в окне (и все его дочерние элементы) прямо на принтер.
Чтобы увидеть пример в действии, взгляните на окно, показанное на рисунке ниже. Оно содержит контейнер Grid, в котором располагаются все элементы. В верхней строке находится контейнер Canvas, содержащий в себе элементы TextBlock и Path (которые визуализируются как прямоугольник с эллиптическим отверстием в центре):
Отправить Canvas вместе со всем содержимым на принтер можно с помощью следующего фрагмента кода, выполняемого по щелчку на кнопке "Печать":
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
printDialog.PrintVisual(canvas, "Распечатываем элемент Canvas");
}
Первый шаг предусматривает создание объекта PrintDialog. Следующий шаг — вызов ShowDialog() для отображения диалогового окна Print. Метод ShowDialog() возвращает булевское значение. Возврат true указывает на то, что пользователь щелкнул на кнопке OK, false — на кнопке Cancel (Отмена), а значение null означает, что диалоговое окно было просто закрыто без щелчка на какой-либо кнопке.
При вызове методу PrintVisual() передаются два аргумента. Первый — это элемент, который необходимо напечатать, а второй — строка, используемая для идентификации задания печати. Она будет отображаться в очереди печати Windows (в столбце Document Name (Имя документа)). При печати подобным образом вывод особо не управляется. Элемент всегда располагается в левом верхнем углу страницы. Если элемент не включает ненулевых значений Margin, то граница содержимого может оказаться в непечатаемой области страницы и, следовательно, просто не будет распечатана.
Недостаток контроля над полями — лишь первое из ограничений этого подхода. Кроме того, невозможно разбить длинное содержимое на страницы, поэтому если окажется, что оно не уместилось на одну страницу, оно будет просто потеряно за нижней ее гранью. Наконец, нельзя управлять масштабированием, используемым при визуализации задания для печати. Вместо этого WPF использует некоторую независимую от устройства систему визуализации, основанную на единицах величиной в 1/96 дюйма. Например, прямоугольник шириной в 96 единиц на мониторе будет иметь 1 дюйм в ширину (при условии, что применяется стандартная установка Windows в 96 dpi) и тот же 1 дюйм на печатной странице. Часто это меньше, чем требуется.
Очевидно, что WPF выведет намного больше деталей на печатную страницу, потому что вряд ли найдется принтер со столь малым разрешением, как 96 dpi (намного более распространены разрешения принтеров в 600 dpi и 1200 dpi). Однако WPF будет поддерживать на печатной копии тот же размер содержимого, что и на мониторе.
На следующем рисунке показан полностраничный печатный вывод Canvas из окна:
Класс PrintDialog скрывает в себе низкоуровневый внутренний класс .NET по имени Win32PrintDialog, который, в свою очередь, заключает в себе диалоговое окно Print, представленное API-интерфейсом Win32. К сожалению, эти дополнительные уровни лишают гибкости.
Потенциальная проблема заключена в способе, которым класс PrintDialog работает с модальными окнами. В недоступном коде Win32PrintDialog имеется логика, которая всегда делает диалоговое окно Print модальным по отношению к главному окну приложения. Это приводит к странной проблеме, когда отображается модальное окно из главного окна и затем из него вызывается метод PrintDialog.ShowDialog(). Хотя ожидается, что диалоговое окно Print будет модальным по отношению ко второму окну, на самом деле оно модально по отношению к главному окну, а это значит, что пользователь может вернуться ко второму окну и взаимодействовать с ним (даже щелкать на кнопке Print, открывая несколько экземпляров диалогового окна Print)!
Несколько неуклюжее решение состоит в том, чтобы вручную изменить главное окно приложения на текущее окно, прежде чем вызвать PrintDialog.ShowDialog(), и затем по завершении немедленно переключить его обратно.
Существует и другое ограничение, связанное с работой класса PrintDialog. Поскольку главный поток приложения владеет печатаемым содержимым, невозможно запустить печать в фоновом потоке. Это становится заметно, если выполнение логики печати занимает некоторое время.
Есть два возможных решения. Когда визуальные компоненты, которые должны быть напечатаны, конструируются в фоновом потоке (а не извлекаются из существующего окна), то в фоновом потоке можно запустить и печать. Однако более простое решение предусматривает использование диалогового окна PrintDialog для того, чтобы позволить пользователю указать настройки печати, и затем с помощью класса XpsDocumentWriter выполнение действительной печати содержимого вместо вызова методов печати класса PrintDialog. Класс XpsDocumentWriter поддерживает возможность отправки содержимого на принтер асинхронно.
Трансформация печатного вывода
Возможно, вы помните, что за счет присоединения объекта Transform к свойству RenderTransform или LayoutTransform любого элемента можно изменить способ его визуализации. Объекты Transform помогают решить проблему негибкого вывода на печать, потому что с их помощью можно изменять размеры элемента (ScaleTransform), перемещать его по странице (TranslateTransform) либо делать то и другое (TransformGroup).
К сожалению, визуальные компоненты могут размещать себя только одним способом в каждый конкретный момент времени. Это значит, что масштабировать элемент одним способом в окне, а другим — в выводе на печать не удастся. Вместо этого любой объект Transform, который будет применен, изменит и печатный вывод, и экранное представление элемента.
Если не хотите идти на компромиссы, эту проблему можно обойти различными способами. Суть идеи — в применении трансформации непосредственно перед созданием печатного вывода с последующим ее удалением. Чтобы предотвратить появление на экране элемента с измененным размером, он может быть временно скрыт.
Может показаться, что для сокрытия элемента следует изменить его свойство Visibility, но это скроет его как в окне, так и в печатном выводе, что очевидно не подходит. Возможное решение состоит в изменении Visibility родительского элемента (в данном примере — контейнера компоновки Grid). Это работает, потому что метод PrintVisual() учитывает только указанный элемент и его дочерние элементы, а не родительский.
Ниже приведен код для вывода на печать элемента Canvas, показанного ранее, который имеет впятеро большие размеры:
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// Скрыть Grid
grid.Visibility = Visibility.Hidden;
// Увеличить вывод в 5 раз
canvas.LayoutTransform = new ScaleTransform(5, 5);
// Напечатать элемент
printDialog.PrintVisual(canvas, "Распечатываем элемент Canvas");
// Удалить трансформацию и снова сделать элемент видимым
canvas.LayoutTransform = null;
grid.Visibility = Visibility.Visible;
}
В этом примере не хватает одной детали. Хотя Canvas (и его содержимое) растягивается, контейнер Canvas все равно использует информацию о компоновке из включающего его Grid. Другими словами, Canvas все равно полагает, что в его распоряжении лишь столько пространства, сколько отводит ему ячейка Grid, в которую он помещен.
В рассматриваемом примере это не представляет проблемы, потому что Canvas не ограничивает себя доступным пространством (в отличие от некоторых других контейнеров). Однако проблемы возникнут в ситуации, когда есть текст, который должен заворачиваться в рамках печатной страницы, или если в Canvas используется фон (который в данном примере будет иметь меньшие размеры ячейки Grid, а не размеры всей области под Canvas).
Решить эту проблему просто. После установки LayoutTransform (но перед печатью Canvas) понадобится инициировать процесс компоновки вручную, используя методы Measure() и Arrange(), наследуемые каждым элементом от класса UIElement. Трюк состоит в том, что при вызове этим методам передаются размеры страницы, и контейнер Canvas растягивается, чтобы заполнить страницу. (Кстати, поэтому устанавливается свойство LayoutTransform, а не RenderTransform, т.к. нужно, чтобы компоновка принимала во внимание новый растянутый размер.) Для получения размеров страницы служат свойства PrintableAreaWidth и PrintableAreaHeight.
На основе имен свойств имеет смысл предположить, что PrintableAreaWidth и PrintableAreaHeight отражают печатаемую область страницы, другими словами — часть страницы, которую принтер действительно печатает. (Большинство принтеров не могут печатать очень близко к границам листа, обычно из-за устройства подающих роликов.) Но в действительности PrintableAreaWidth и PrintableAreaHeight просто возвращают полную ширину и высоту страницы в независимых от устройства единицах. Для листа бумаги А4 это будет 816 и 1056. (Разделив эти числа на 96 dpi, можно получить полный размер листа в дюймах.)
В следующем примере демонстрируется использование свойств PrintableAreaWidth и PrintableAreaHeight. Для красоты у границ страницы остаются поля шириной 10 единиц (около 0,1 дюйма):
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// Скрыть Grid
grid.Visibility = Visibility.Hidden;
// Увеличить размер в 5 раз
canvas.LayoutTransform = new ScaleTransform(5, 5);
// Определить поля
int pageMargin = 5;
// Получить размер страницы
Size pageSize = new Size(printDialog.PrintableAreaWidth - pageMargin * 2,
printDialog.PrintableAreaHeight - 20);
// Инициировать установку размера элемента
canvas.Measure(pageSize);
canvas.Arrange(new Rect(pageMargin, pageMargin, pageSize.Width, pageSize.Height));
// Напечатать элемент
printDialog.PrintVisual(canvas, "Распечатываем элемент Canvas");
// Удалить трансформацию и снова сделать элемент видимым
canvas.LayoutTransform = null;
grid.Visibility = Visibility.Visible;
}
Конечным результатом будет печать любого элемента и масштабирование в соответствии с потребностями, как показано на рисунке ниже. Этот подход работает исключительно хорошо, однако в его основе лежит несколько запутанный механизм.
Печать элементов без их отображения
Поскольку способ, по которому отображаются данные в приложении, и способ, которым они должны выводиться на печать, часто отличаются, и иногда имеет смысл создать визуальный элемент программно (вместо использования того, что уже есть в существующем окне). Например, следующий код создает находящийся в памяти объект TextBlock, заполняет его текстом, устанавливает режим переноса, задает размер для заполнения печатной страницы и затем печатает ее:
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// Создать текст
Run run = new Run ("Это простой текст, тестируем функциональность " +
"печати в Windows Presentation Foundation.");
// Поместить его в TextBlock
TextBlock visual = new TextBlock();
visual.Inlines.Add(run);
// Использовать поля для получения рамки страницы
visual.Margin = new Thickness(5);
// Разрешить перенос для заполнения всей ширины страницы
visual.TextWrapping = TextWrapping.Wrap;
// Увеличить TextBlock по обоим измерениям в 5 раз.
// (В этом случае увеличение шрифта дало бы тот же эффект,
// потому что TextBlock — единственный элемент)
visual.LayoutTransform = new ScaleTransform(5, 5);
// Установить размер элемента
Size pageSize = new Size (printDialog.PrintableAreaWidth, printDialog.PrintableAreaHeight);
visual.Measure(pageSize);
visual.Arrange(new Rect(0, 0, pageSize.Width, pageSize.Height));
// Напечатать элемент
printDialog.PrintVisual(visual, "Распечатываем текст");
}
Этот подход позволяет захватывать нужное содержимое из окна, но отдельно настраивать его внешнее представление для печати. Однако это не поможет, если содержимое должно быть разбито на страницы (понадобится техника печати, описанная в следующих разделах).
Печать документа
Метод PrintVisual(), может быть, наиболее многоцелевой метод печати, но класс PrintDialog также включает и другую возможность. С использованием метода PrintDocument() можно печатать содержимое из потокового документа. Преимущество этого подхода в том, что потоковый документ может обрабатывать огромный объем сложного содержимого и разбивать его на множество страниц (как это делается на экране).
Можно было предположить, что метод PrintDialog.PrintDocument() требует объект FlowDocument, но на самом деле он принимает объект DocumentPaginator. Класс DocumentPaginator — это специализированный класс, единственное назначение которого — брать содержимое, разбивать его на множество страниц и поставлять каждую страницу по мере поступления запросов. Каждая страница представлена объектом DocumentPage, который на самом деле — просто оболочка для единственного объекта Visual с несколькими удобными средствами. В классе DocumentPage доступны только три дополнительных свойства: Size возвращает размер страницы, ContentBox — размер прямоугольника, в который помещается содержимое на странице после добавления полей, a BleedBox — область, куда помещаются торговые марки, водяные знаки и авторские логотипы, находящаяся вне границ страницы.
Это значит, что PrintDocument() работает почти так же, как PrintVisual(). Отличие в том, что он печатает несколько визуальных компонентов — по одному на каждую страницу.
Хотя можно было бы разбить содержимое на отдельные страницы без помощи DocumentPaginator и выполнять повторяющиеся вызовы PrintVisual(), но это неудачный подход. В таком случае каждая страница становится отдельным заданием печати.
Каким же образом получить объект DocumentPaginator для FlowDocument? Трюк заключается в приведении FlowDocument к IDocumentPaginatorSource с последующим использованием DocumentPaginator. Вот пример:
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
printDialog.PrintDocument(
((IDocumentPaginatorSource)docViewer.Document).DocumentPaginator,
"A Flow Document");
}
Этот код может привести к желаемому результату, а может и не привести, в зависимости от контейнера, в котором в данный момент находится документ. Если документ расположен в памяти (а не в окне) или если он хранится в RichTextBox либо FlowDocumentScrollViewer, этот код работает хорошо. Будет получен многостраничный печатный вывод с двумя столбцами (на стандартном листе формата А4 в портретной ориентации). Это тот же результат, который получается с помощью команды ApplicationCommands.Print.
Как известно, некоторые элементы управления включают встроенную привязку команд. Контейнер FlowDocument (подобно использованному ранее FlowDocumentScrollViewer) входит в число таких элементов. Он обрабатывает команду ApplicationCommands.Print для выполнения базовой печати. Этот жесткий код печати подобен коду, показанному ранее, хотя использует объект XpsDocumentWriter.
Однако если документ хранится в FlowDocumentPageViewer или FlowDocumentReader, результат не так хорош. В этом случае документ разбивается на страницы так же, как текущее представление в контейнере. Таким образом, если для того, чтобы уместить содержимое в текущее окно, понадобится 24 страницы, то получатся именно 24 страницы печатного вывода, каждая с крошечным окошком с данными. Опять-таки, решение несколько запутано, но оно работает. (По сути — это то же решение, которое использует команда ApplicationCommands.Print.) Трюк состоит в том, чтобы заставить FlowDocument разбивать себя на страницы. Это можно сделать, установив свойства FlowDocument.PageHeight и FlowDocument.PageWidth в границы страницы, а не границы контейнера.
В таких контейнерах, как FlowDocumentScrollViewer, эти свойства не устанавливаются, потому что разбиение на страницы не используется. Вот почему средство печати работает без задержек — контейнер разбивает себя на страницы автоматически, когда создается печатный вывод.
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
FlowDocument doc = docViewer.Document;
doc.PageHeight = printDialog.PrintableAreaHeight;
doc.PageWidth = printDialog.PrintableAreaWidth;
printDialog.PrintDocument(
((IDocumentPaginatorSource)docViewer.Document).DocumentPaginator,
"A Flow Document");
}
Может также понадобиться установить такие свойства, как ColumnWidth и ColumnGap, чтобы получить нужное количество столбцов. В противном случае их будет столько же, как в текущем окне.
Единственная проблема этого подхода в том, что как только эти свойства изменены, они применятся к контейнеру, который отображает документ. В результате получается сжатая версия документа, которая, скорее всего, будет слишком мала, чтобы его можно было прочитать в текущем окне. Правильное решение учитывает это, сохраняя значения, изменяя их и затем восстанавливая в исходном виде.
Ниже приведен полный код, печатающий двухстолбцовый вывод с общими полями (добавленными через свойство FlowDocument.PagePadding):
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
FlowDocument doc = docViewer.Document;
// Сохранить все имеющиеся настройки
double pageHeight = doc.PageHeight;
double pageWidth = doc.PageWidth;
Thickness pagePadding = doc.PagePadding;
double columnGap = doc.ColumnGap;
double columnWidth = doc.ColumnWidth;
// Привести страницу FlowDocument в соответствие с печатной страницей
doc.PageHeight = printDialog.PrintableAreaHeight;
doc.PageWidth = printDialog.PrintableAreaWidth;
doc.PagePadding = new Thickness(0);
// Использовать два столбца
doc.ColumnGap = 25;
doc.ColumnWidth = (doc.PageWidth - doc.ColumnGap
- doc.PagePadding.Left - doc.PagePadding.Right) / 2;
printDialog.PrintDocument(
((IDocumentPaginatorSource)doc).DocumentPaginator, "A Flow Document");
// Восстановить старые настройки
doc.PageHeight = pageHeight;
doc.PageWidth = pageWidth;
doc.PagePadding = pagePadding;
doc.ColumnGap = columnGap;
doc.ColumnWidth = columnWidth;
}
}
Этот подход имеет ряд ограничений. Хотя можно изменять свойства, которые влияют на поля и количество столбцов, все же контроль ограничен. Конечно, можно программно модифицировать FlowDocument (например, временно увеличивая его FontSize), но не удастся подстроить такие детали печатного вывода, как номера страниц. В следующем разделе рассматривается один способ, позволяющий обойти это ограничение.
Манипуляции страницами в печатном выводе документа
Чтобы обрести больший контроль над печатью FlowDocument, можно создать собственный DocumentPaginator. Класс DocumentPaginator разделяет содержимое документа на отдельные страницы для вывода на печать (или отображения в средстве постраничного просмотра FlowDocument). Класс DocumentPaginator отвечает за возврат общего количества страниц на основе заданного размера страницы и предоставляет скомпонованное содержимое для каждой страницы в виде объекта DocumentPage.
Объект DocumentPaginator не должен быть очень сложным: фактически он может просто упаковывать DocumentPaginator, который предоставлен FlowDocument, и позволяет ему выполнять всю необходимую работу по разбиению текста на страницы. Однако DocumentPaginator можно использовать для внесения небольших поправок, таких как добавление заголовка и нижнего колонтитула.
Основной фокус состоит в перехвате каждого запроса страницы, который осуществляет PrintDialog, с последующим изменением этой страницы перед передачей дальше.
Первой частью этого решения является построение класса HeaderFlowDocumentPaginator, унаследованного от DocumentPaginator. Поскольку DocumentPaginator — абстрактный класс, HeaderFlowDocumentPaginator должен реализовать несколько методов. Тем не менее, HeaderFlowDocumentPaginator может передать большую часть работы стандартному DocumentPaginator, который предоставляется экземпляром FlowDocument:
public class HeaderedFlowDocumentPaginator : DocumentPaginator
{
// Реальный класс разбиения на страницы (выполняющий всю работу по разбиению)
private DocumentPaginator flowDocumentPaginator;
// Сохранить класс разбиения на страницы FlowDocument из заданного документа
public HeaderedFlowDocumentPaginator(FlowDocument document)
{
flowDocumentPaginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
}
public override bool IsPageCountValid
{
get { return flowDocumentPaginator.IsPageCountValid; }
}
public override int PageCount
{
get { return flowDocumentPaginator.PageCount; }
}
public override Size PageSize
{
get { return flowDocumentPaginator.PageSize; }
set { flowDocumentPaginator.PageSize = value; }
}
public override IDocumentPaginatorSource Source
{
get { return flowDocumentPaginator.Source; }
}
public override DocumentPage GetPage(int pageNumber)
{
// ...
}
}
Поскольку HeaderedFlowDocumentPaginator передает свою работу приватному DocumentPaginator, код не показывает, как работают свойства PageSize, PageCount и IsPageCountValid. Свойство PageSize устанавливается потребителем DocumentPaginator (кодом, использующим DocumentPaginator). Это свойство сообщает DocumentPaginator, сколько еще места доступно на каждой печатаемой странице (или на экране). Свойства PageCount и IsPageCountValid предоставляются потребителю DocumentPaginator для отображения результата разбиения на страницы. При каждом изменении PageSize объект DocumentPaginator заново вычисляет размер каждой страницы.
Метод GetPage() — место, где происходит действие. Это код вызывает метод GetPage() реального объекта DocumentPaginator и затем выполняет работу над страницей. Базовая стратегия состоит в извлечении объекта Visual из страницы и помещении его в новый объект ContainerVisual. К этому ContainerVisual можно впоследствии добавить нужный текст. Наконец, допускается создать новый класс DocumentPage, который упаковывает ContainerVisual с вновь вставленным заголовком:
public override DocumentPage GetPage(int pageNumber)
{
// Получить запрошенную страницу
DocumentPage page = flowDocumentPaginator.GetPage(pageNumber);
// Поместить страницу в объект Visual. После этого можно
// будет применять трансформации и добавлять другие элементы
ContainerVisual newVisual = new ContainerVisual();
newVisual.Children.Add(page.Visual);
// Создать заголовок
DrawingVisual header = new DrawingVisual();
using (DrawingContext dc = header.RenderOpen())
{
Typeface typeface = new Typeface("Times New Roman");
FormattedText text = new FormattedText("Страница " +
(pageNumber + 1) .ToString(), System.Globalization.CultureInfo.CurrentCulture,
FlowDirection.LeftToRight, typeface, 14, Brushes.Black);
// Оставить четверть дюйма пространства между краем страницы и текстом
dc.DrawText(text, new Point(96*0.25, 96*0.25));
}
// Добавить заголовок к объекту Visual
newVisual.Children.Add(header);
// Поместить объект Visual в новую страницу
DocumentPage newPage = new DocumentPage(newVisual);
return newPage;
}
В этой реализации предполагается, что добавление заголовка не приводит к изменению размеров страницы. Вместо этого предполагается, что на полях есть достаточно места, чтобы вместить заголовок. Если этот код применить с небольшими полями, заголовок будет напечатан поверх содержимого документа. Именно так работают заголовки в таких программах, как Microsoft Word. Они не считаются частью главного документа и позиционируются отдельно от главного содержимого документа.
Здесь присутствует небольшой нюанс. Добавить объект Visual для страницы к ContainerVisual не удастся до тех пор, пока он отображается в окне. Обходной путь предусматривает его временное удаление из контейнера, выполнение печати и последующий возврат объекта на место:
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
FlowDocument doc = docViewer.Document as FlowDocument;
docViewer.Document = null;
doc.PagePadding = new Thickness(96 * 0.25, 96 * 0.75, 96 * 0.25, 96 * 0.25);
HeaderedFlowDocumentPaginator paginator =
new HeaderedFlowDocumentPaginator(doc);
printDialog.PrintDocument(paginator, "Headered Flow Document");
docViewer.Document = doc;
}
Объект HeaderedFlowDocumentPaginator используется для печати, но не присоединен к FlowDocument, так что он не сможет изменить способ отображения документа на экране: