Основы печати в WinRT

187

Если в любой программе, приводившейся ранее, вызвать панель чудо-кнопок и нажать кнопку "Устройства", на экране появляется панель, на которой ничего не сказано о принтерах. Ваше приложение должно зарегистрироваться в Windows 8, сообщив о том, что оно может что-то напечатать.

В области печати задействованы три пространства имен:

Пространство имен Windows.UI.Xaml.Printing

Содержит класс PrintDocument и поддержку его событий. Как следует из имени, класс PrintDocument представляет некий документ, который пользователь вашей программы хочет вывести на печать.

Пространство имен Windows.Graphics.Printing

Содержит класс PrintManager - интерфейс к панели Windows 8 со списком принтеров и их настроек, а также классы PrintTask, PrintTaskRequest и PrintTaskOptions. «Задача» (task) печати - то же самое, что и «задание» (job), то есть конкретное использование принтера для печати конкретного документа.

Пространство имен Windows.Graphics.Printing.OptionDetails

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

Большая часть API работы с принтером связана со служебными функциями вместо непосредственного определения текста и графики для печатной страницы. В самом деле, приложение Windows 8 определяет страницу печати так же, как оно рисует на экране: с использованием визуального дерева экземпляров классов, производных от UIElement. Как правило, корневым элементом является Border или Panel с потомками. Визуальное дерево может определяться в XAML, но, вероятно, чаще строится в программном коде.

При определении элементов для отображения на экране обычно можно ориентироваться на разрешение 96 пикселов на дюйм. Для печати происходит почти то же самое, с одним исключением: эквивалентность считается точной. Независимо от фактического разрешения, принтер всегда рассматривается как устройство с разрешением 96 DPI.

Чтобы заставить Windows 8 выводить список принтеров при нажатии чудо-кнопки "Устройства", прежде всего следует назначить обработчик события:

PrintManager pmanager = PrintManager.GetForCurrentView();
pmanager.PrintTaskRequested += pmanager_PrintTaskRequested;

Эти две строки можно объединить:

PrintManager.GetForCurrentView().PrintTaskRequested += 
    pmanager_PrintTaskRequested;

Статический метод GetForCurrentView получает экземпляр PrintManager, связанный с окном вашей программы. Назначая обработчик для события PrintTaskRequested, ваша программа объявляет о своей доступности для печати. Обработчик выглядит следующим образом:

private void pmanager_PrintTaskRequested(PrintManager sender,
            PrintTaskRequestedEventArgs e)
{
    // ...
}

Этот обработчик вызывается, когда пользователь щелкает на чудо-кнопке "Устройства" (или нажимает Windows+K), но (как вы вскоре увидите) для вывода списка принтеров он должен вызвать другой метод с указанием функции обратного вызова.

Обработчик PrintTaskRequested должен назначаться только в том случае, если приложение действительно готово к выводу. Если ему необходимо предварительно запросить информацию у пользователя или загрузить документ, обработчик не должен связываться с событием PrintTaskRequested. А когда программа снова окажется в положении, в котором не готова вывести на печать что-то осмысленное, обработчик следует отсоединить:

PrintManager.GetForCurrentView().PrintTaskRequested -= 
    pmanager_PrintTaskRequested;

В последующих примерах в основном назначаю и отсоединяю обработчик события в переопределениях OnNavigatedTo и OnNavigatedFrom, что является символическим представлением этого процесса.

Обработчик события PrintTaskRequested - один из пяти методов обратного вызова и обработчиков событий, которые должны поддерживаться программами, выполняющими простую печать. Все пять методов являются обязательными. Более того, еще перед инициированием события PrintTaskRequested ваша программа должна подготовиться к печати: создать объект PrintDocument и связать с ним три обработчика событий.

Итак, рассмотрим полную программу, которая печатает документ из одной страницы. Страница содержит один элемент TextBlock с сообщением «Привет, принтер!» Файл XAML в проекте HelloPrinter не играет роли в логике программы; он просто информирует нового пользователя о том, как вывести что-нибудь на печать:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <TextBlock FontSize="48" HorizontalAlignment="Center"
                   TextAlignment="Center" VerticalAlignment="Center">
            Привет, принтер!
            <LineBreak />
            <Run FontSize="24">
                (выберите устройства и принтер)
            </Run>
        </TextBlock>
    </Grid>
</Page>

Файл фонового кода определяет три поля. Одним из них является элемент , выводимый программой на печать:

using Windows.Graphics.Printing;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.UI.Xaml.Printing;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        PrintDocument printDocument;
        IPrintDocumentSource printDocumentSource;

        // Элемент UIElement для печати
        TextBlock txtblk = new TextBlock
        {
            Text = "Привет, принтер!",
            FontFamily = new FontFamily("Times New Roman"),
            FontSize = 48,
            Foreground = new SolidColorBrush(Colors.Black)
        };

        // ...
    }
}

Объект PrintDocument представляет документ, выводимый приложением на печать. Обычно программа создает один объект PrintDocument и использует его для всех заданий печати. В некоторых ситуациях есть смысл поддерживать в программе сразу несколько объектов PrintDocument (например, для печати всего документа, для печати сводки документа и для печати миниатюр), но не стоит создавать новые документы PrintDocument для каждой задачи печати. (Как вы увидите, к моменту запроса задачи печати создавать PrintDocument уже поздно!) Также можно определить класс, производный от PrintDocument, в котором инкапсулируется часть логики печати, но при этом в PrintDocument нет ничего, что вам стоило бы переопределить.

В программе, работающей с одним типом документа, вероятно, стоит определить PrintDocument и IPrintDocumentSource в виде полей (как это сделано у меня) и создать объект PrintDocument во время инициализации:

// ...

public MainPage()
{
    this.InitializeComponent();

    // Создание объекта PrintDocument и назначение обработчиков
    printDocument = new PrintDocument();
    printDocumentSource = printDocument.DocumentSource;
    printDocument.Paginate += OnPrintDocumentPaginate;
    printDocument.GetPreviewPage += OnPrintDocumentGetPreviewPage;
    printDocument.AddPages += OnPrintDocumentAddPages;
}
        
// ...

Второе поле - объект типа IPrintDocumentSource - берется из объекта PrintDocument. Кроме того, необходимы обработчики трех событий, определяемых PrintDocument. Они отвечают за предоставление количества страниц, а также страниц для предварительного просмотра и печати.

Программа HelloPrinter назначает обработчик события PrintTaskRequested для PrintManager во время выполнения OnNavigatedTo и отсоединяет его во время OnNavigatedFrom. При этом в первом случае используются две команды, а во втором - одна (просто для разнообразия).

// ...

protected override void OnNavigatedTo(NavigationEventArgs e)
{
    // Присоединить обработчик
    PrintManager printManager = PrintManager.GetForCurrentView();
    printManager.PrintTaskRequested += OnPrintManagerPrintTaskRequested;

    base.OnNavigatedTo(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
    // Отсоединение обработчика
    PrintManager.GetForCurrentView().PrintTaskRequested 
        -= OnPrintManagerPrintTaskRequested;

    base.OnNavigatedFrom(e);
}
        
// ...

В реальной программе этот обработчик будет назначаться тогда, когда ваша программа готова к печати, и отсоединяться при отсутствии данных для печати. Когда обработчик назначен, а пользователь проводит пальцем у правого края экрана и выбирает "Устройства", вызывается обработчик событий PrintTaskRequested. Стандартная обработка этого события выглядит так:

// ...

private void OnPrintManagerPrintTaskRequested(PrintManager sender, 
    PrintTaskRequestedEventArgs e)
{
    e.Request.CreatePrintTask("Привет принтер", OnPrintTaskSourceRequested);
}
        
// ...

Аргументы события PrintTaskRequested включают свойство типа Request, а программа обычно реагирует вызовом метода CreatePrintTask этого объекта Request, передавая ему имя задачи печати (имя приложения или имя документа, выводимого на печать приложением) и функцию обратного вызова. Метод CreatePrintTask возвращает объект PrintTask, но обычно в сохранении этого объекта нет необходимости.

Список доступных принтеров

Метод обратного вызова, который я назвал OnPrintTaskSourceRequested, вызывается при выборе пользователем одного из принтеров в списке. В простейшем случае обработчик может отреагировать, вызывая метод SetSource для аргументов события с передачей объекта IPrintDocumentSource, полученного ранее от объекта PrintDocument:

// ...
private void OnPrintTaskSourceRequested(PrintTaskSourceRequestedArgs e)
{
    e.SetSource(printDocumentSource);
}
        
// ...

Когда этот метод возвращает управление Windows, на экране отображается панель настройки печати для конкретного принтера:

Настройка печати

Слева отображается предварительное изображение печатаемой страницы. Если документ состоит из нескольких страниц, вы можете выбрать просматриваемую страницу в нижнем поле. Впрочем, проще будет перебирать страницы, проводя пальцем в нужном направлении.

Общее количество страниц берется из обработчика события Paginate - одного из трех событий, определяемых PrintDocument. Обработчики всех трех событий были назначены в конструкторе MainPage. В программе HelloPrinter обработчик Paginate реализован очень просто:

// ...

private void OnPrintDocumentPaginate(object sender, 
    PaginateEventArgs e)
{
    printDocument.SetPreviewPageCount(1, PreviewPageCountType.Final);
}
        
// ...

Обработчик Paginate дает приложению возможность подготовить все страницы для печати, а затем вызвать метод PrintDocument с указанием количества страниц и признаком предварительного/окончательного значения (если всю работу невозможно или неудобно выполнить за один раз, ситуация немного усложняется).

Предварительное изображение печатной страницы предоставляется обработчиком события GetPreviewPage, который также определяется PrintDocument и назначается ранее в конструкторе MainPage:

// ...
private void OnPrintDocumentGetPreviewPage(object sender, 
    GetPreviewPageEventArgs e)
{
    printDocument.SetPreviewPage(e.PageNumber, txtblk);
}
        
// ...

Нумерация страниц в свойстве PageNumber аргументов события начинается с 1, а значения лежат в диапазоне от 1 до количества, указанного в вызове SetPreviewPageCount. В этой конкретной программе оно всегда равно 1. Программа реагирует на событие вызовом метода SetPreviewPage объекта PrintDocument, которому передается номер страницы и элемент TextBlock, который я определил как поле. Эта информация отображается при предварительном просмотре печати.

При нажатии кнопки "Печать" вызывается последний обработчик события:

// ...

private void OnPrintDocumentAddPages(object sender, AddPagesEventArgs e)
{
    printDocument.AddPage(txtblk);
    printDocument.AddPagesComplete();
}
        
// ...

Обработчик события AddPages отвечает за вызов AddPage для каждой страницы в документе. Обычно для печати используются те же объекты, которые передавались методу SetPreviewPage, но при желании их можно изменить. Процедура завершается вызовом AddPagesComplete. Панель печати исчезает, и вскоре (если повезет) вы услышите знакомый звук запускающегося принтера.

Будьте внимательны! Обработчик события Paginate может вызываться многократно, особенно если пользователь экспериментирует с разными настройками принтера. Если разбиение документа на страницы в вашей программе сопряжено со значительной работой, вероятно, ее лучше избежать, если макет страницы остается неизменным. В реальной программе все страницы обычно собираются в объект List в обработчике Paginate, а затем передаются обработчикам GetPreviewPage и AddPages.

Элементу TextBlock, который выводится программой HelloPrinter, назначен размер шрифта (FontSize) 48. На экранах разных размеров и разрешений размер TextBlock может выглядеть по-разному. Однако на печати 48 является точным значением, представляющим 48/96 дюйма, то есть половину дюйма, или 36 пунктов.

Обратите внимание на то, что свойству Foreground выводимого на печать элемента TextBlock задан черный цвет. Поскольку программа использует темную тему оформления, по умолчанию для свойства Foreground используется белый цвет. Без явного задания цвета будет использовано значение по умолчанию, а текст окажется невидимым на белой бумаге. Над такими ошибками порой приходится биться по несколько дней! Во время экспериментов с кодом печати стоит использовать нестандартные цвета, например красный и синий, чтобы снизить вероятность вывода белого текста.

Возможно, во время просмотра кода HelloPrinter вам покажется, что его можно упростить в паре мест - например, что объект PrintDocument не обязательно создавать изначально и сохранять в поле, а можно создать его в методе OnPrintTaskSourceRequested, назначить три обработчика событий и извлечь объект IPrintDocumentSource. Разные обработчики событий PrintDocument смогут получить доступ к PrintDocument через аргумент sender.

Однако такое решение не работает. Операции создания и обращения к объекту PrintDocument должны выполняться в потоке пользовательского интерфейса, а обработчик PrintTaskRequested и метод обратного вызова, который я назвал OnPrintTaskSourceRequested, не выполняются в потоке пользовательского интерфейса. Создание PrintDocument не может быть отложено до выдачи события PrintTaskRequested.

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