8.3. Вывод на печать.

Процедура вывода изображений на печать в Qt очень похожа на рисование по поверхности виджетов. Вкратце, процесс печати можно представить следующими шагами:

  1. Создается экземпляр класса QPrinter, который будет представлять "устройство для рисования".

  2. Вызывается функция QPrinter::setup(), которая покажет пользователю диалог выбора принтера.

  3. Создается экземпляр класса QPainter, который будет взаимодействовать с объектом QPrinter.

  4. Средствами QPainter рисуется изображение на странице.

  5. Вызывается метод QPrinter::newPage(), чтобы прокрутить страницу.

  6. Повторять действия, описанные в пунктах 4 и 5, пока не будут отпечатаны все страницы.

В операционных системах Windows и Mac OS X, QPrinter использует системные драйверы. В Unix страницы генерируются в формате PostScript и затем передаются устройству печати lp или lpr (или любой другой программе, которая будет назначена вызовом QPrinter::setPrintProgram()).

Рисунок 8.15. Пример вывода на печать виджетов OvenTimer, QCanvas и QImage.


Начнем обсуждение с простого примера, который печатает одну страницу. Для начала напечатаем виджет OvenTimer: void PrintWindow::printOvenTimer(OvenTimer *ovenTimer) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); int side = QMIN(rect.width(), rect.height()); painter.setViewport(0, 0, side, side); painter.setWindow(-50, -50, 100, 100); ovenTimer->draw(&painter); } } Здесь мы исходим из того, что класс PrintWindow содержит переменную-член printer, класса QPrinter. В противном случае можно было бы создать экземпляр QPrinter на стеке, но в этом случае у нас отсутствовала бы возможность сохранить пользовательские настройки принтера.

Мы вызываем setup(), чтобы запустить диалог выбора принтера. Она возвращает true, если пользователь нажал на кнопку OK. После вызова setup(), объект QPrinter готов к работе.

Далее создается QPainter, который будет рисовать на QPrinter. Потом настраивается область просмотра (viewport) и назначается система координат окна (-50, -50, 100, 100) -- прямоугольник, который ожидает получить OvenTimer, и в завершение выполняется рисование виджета, вызовом функции draw(). Если не установить размеры области просмотра, то виджет OvenTimer будет вытянут на всю высоту страницы.

По-умолчанию QPainter устанавливает размеры окна такими, чтобы они соответствовали разрешению экрана (обычно где-то между 72 и 100 точками на дюйм), но в данном случае это не имеет большого значения, так как мы сами установили систему координат окна.

Пример вывода на печать виджета OvenTimer не имеет особой практической ценности, потому что он предназначен, в первую очередь, для вывода на экран и взаимодействия с пользователем. Но для других виджетов, таких как Plotter, который был разработан нами в Главе 5, этот пример приобретает определенный смысл.

Более практичный пример -- вывод на печать QCanvas. Приложения, которые его используют, очень часто нуждаются в возможности вывода на печать того, что нарисует пользователь.

void PrintWindow::printCanvas(QCanvas *canvas) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = canvas->size(); size.scale(rect.size(), QSize::ScaleMin); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(canvas->rect()); painter.drawRect(painter.window()); painter.setClipRect(painter.viewport()); QCanvasItemList items = canvas->collisions(canvas->rect()); QCanvasItemList::const_iterator it = items.end(); while (it != items.begin()) { --it; (*it)->draw(painter); } } } На этот раз мы установили систему координат окна в соответствии с размерами канвы и ограничили область просмотра тем же самым соотношением сторон. Для этого мы использовали функцию QSize::scale(), задав в качестве второго аргумента ScaleMin. Например, если канва имела размер 640 X 480, а область просмотра QPainter -- 5000 X 5000, в результате получится область просмотра с размерами 5000 X 3750.

Функция collisions() возвратит список видимых элементов канвы, отсортированный по значению координаты z. Список просматривается в цикле, начиная с конца, и выполняется рисование элементов списка вызовом QCanvasItem::draw(). Таким образом, чем выше в списке стоит элемент, тем позднее он будет нарисован.

Третий пример -- печать картинки из QImage.

void PrintWindow::printImage(const QImage &image) { if (printer.setup(this)) { QPainter painter(&printer); QRect rect = painter.viewport(); QSize size = image.size(); size.scale(rect.size(), QSize::ScaleMin); painter.setViewport(rect.x(), rect.y(), size.width(), size.height()); painter.setWindow(image.rect()); painter.drawImage(0, 0, image); } } Мы установили размеры окна в соответствии с размерами изображения и размеры области просмотра (viewport), чтобы соблюсти отношения сторон, после чего нарисовали изображение, начиная с позиции (0, 0).

Печать компонентов, которые занимают не более одной страницы, достаточно проста. Но нередко приходится сталкиваться с необходимостью вывода на печать многостраничных документов. В таких случаях нужно вывести на печать одну страницу, затем вызвать функцию newPage() и напечатать следующую страницу. Однако здесь возникает проблема определения окончания каждой из страниц.

Qt предлагает два варианта вывода на печать многостраничных документов:

Далее мы рассмотрим оба варианта.

В качестве примера напечатаем справочник цветовода, который содержит названия цветов и их краткое описание. Каждая статья справочника хранится в виде "название: описание", например:

Miltonopsis santanae: Самая опасная разновидность орхидеи. Поскольку каждая статья представлена одной строкой, то весь справочник можно представить как список строк -- QStringList.

Следующий фрагмент кода выводит на печать содержимое справочника, предварительно "перегнав" его в формат HTML:

void PrintWindow::printFlowerGuide(const QStringList &entries) { QString str; QStringList::const_iterator it = entries.begin(); while (it != entries.end()) { QStringList fields = QStringList::split(": ", *it); QString title = QStyleSheet::escape(fields[0]); QString body = QStyleSheet::escape(fields[1]); str += "<table width=\"100%\" border=1 cellspacing=0>\n" "<tr><td bgcolor=\"lightgray\"><font size=\"+1\">" "<b><i>" + title + "</i></b></font>\n<tr><td>" + body + "\n</table>\n<br>\n"; ++it; } printRichText(str); }

Рисунок 8.16. Пример вывода на печать справочника цветовода, с помощью QSimpleRichText.


На первом шаге выполняется преобразование справочника в формат HTML. Каждая статья представляется в виде HTML таблицы с двумя ячейками. Функция QStyleSheet::escape() заменяет специальные символы '&', '<', '>' их аналогами HTML ("&amp;", "&lt;", "&gt;"). И в заключение выводим на печать то, что получилось, вызовом printRichText(). const int LargeGap = 48; void PrintWindow::printRichText(const QString &str) { if (printer.setup(this)) { QPainter painter(&printer); int pageHeight = painter.window().height() - 2 * LargeGap; QSimpleRichText richText(str, bodyFont, "", 0, 0, pageHeight); richText.setWidth(&painter, painter.window().width()); int numPages = (int)ceil((double)richText.height() / pageHeight); int index; for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < numPages; ++j) { if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = numPages - j - 1; } else { index = j; } printPage(&painter, richText, pageHeight, index); } } } } Сначала мы рассчитываем высоту одной страницы, отталкиваясь от размера окна и размера пространства, которое резервируется под нижний и верхний колонтитулы. Затем создается объект класса QSimpleRichText, содержащий HTML текст. Последний аргумент, в конструкторе QSimpleRichText -- это высота страницы. Класс QSimpleRichText использует эту величину, чтобы вставить разрывы страниц.

Рисунок 8.17. Раскладка страницы справочника цветовода.


После этого выполняется печать каждой страницы. Внешний цикл for отсчитывает количество копий, запрошенных пользователем. В большинстве своем, драйверы принтеров поддерживают печать нескольких копий документа, в этом случае функция QPrinter::numCopies() вернет 1, в противном случае -- количество копий, запрошенных пользователем. В предыдущих примерах, с целью упрощения кода, мы игнорировали этот параметр.

Внутренний цикл for отсчитывет страницы. если страница не является первой, то вызывается функция newPage(). Для вывода очередной страницы на печать вызывается функция printPage().

Диалог выбора принтера позволяет пользователю заказать печать страниц в обратном порядке, мы так же соблюдаем и это требование.

В данном примере предполагается, что printer, bodyFont и footerFont -- это переменные-члены класса PrintWindow.

void PrintWindow::printPage(QPainter *painter, const QSimpleRichText &richText, int pageHeight, int index) { QRect rect(0, index * pageHeight + LargeGap, richText.width(), pageHeight); painter->saveWorldMatrix(); painter->translate(0, -rect.y()); richText.draw(painter, 0, LargeGap, rect, colorGroup()); painter->restoreWorldMatrix(); painter->setFont(footerFont); painter->drawText(painter->window(), AlignHCenter | AlignBottom, QString::number(index + 1)); } Функция printPage() выводит на печать (index + 1)-ую страницу. Она содержит HTML-код и номер страницы в нижнем колонтитуле.

Мы выполняем смещение системы координат и вызываем draw(), чтобы нарисовать текст, с нужной позиции. После этого, в нижнем колонтитуле, по центру страницы, выводится ее номер. Если бы нам потребовалось выводить что нибудь в верхнем колонтитуле, то мы добавили бы еще один вызов drawText().

Константа LargeGap равна числу 48. Если исходить из предположения, что разрешение экрана срставляет 96 точек на дюйм, то число 48 соответствует половине дюйма (12.7 мм). Чтобы найти точное значение для константы, в каждом конкретном случае, можно воспользоваться услугами класса QPaintDeviceMetrics:

QPaintDeviceMetrics metrics(&printer); int LargeGap = metrics.logicalDpiY() / 2; Ниже приводится один из вариантов инициализации bodyFont и footerFont в конструкторе PrintWindow: bodyFont = QFont("Helvetica", 14); footerFont = bodyFont; А теперь покажем, как напечатать справочник с помощью QPainter. Ниже приводится измененный вариант функции printFlowerGuide(): void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector<QStringList> pages; int index; paginate(&painter, &pages, entries); for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < (int)pages.size(); ++j) { if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = pages.size() - j - 1; } else { index = j; } printPage(&painter, pages, index); } } } } Первое, что нужно сделать после настройки принтера и QPainter -- это вызвать вспомогательную функцию paginate(), чтобы определить разбивку справочника по страницам. Результат работы функции -- массив QStringList, в котором каждый из элементов хранит статьи справочника для одной страницы.

Например, допустим, что справочник содержит всего 6 статей, которые мы обозначим как A, B, C, D, E и F. Теперь предположим, что статьи A и B располагаются на первой странице, C, D и E -- на второй, а F -- на третьей. Таким образом, массив pages, в элементе с индексом 0, будет содержать статьи A и B, статьи C, D и E -- в элементе с индексом 1 и статью F -- в элементе с индексом 2.

В остальном, функция printFlowerGuide() практически идентична приведенному ранее варианту. Однако, функция printPage() имеет существенные отличия, но об этом немного позже.

void PrintWindow::paginate(QPainter *painter, vector<QStringList> *pages, const QStringList &entries) { QStringList currentPage; int pageHeight = painter->window().height() - 2 * LargeGap; int y = 0; QStringList::const_iterator it = entries.begin(); while (it != entries.end()) { int height = entryHeight(painter, *it); if (y + height > pageHeight && !currentPage.empty()) { pages->push_back(currentPage); currentPage.clear(); y = 0; } currentPage.push_back(*it); y += height + MediumGap; ++it; } if (!currentPage.empty()) pages->push_back(currentPage); } Функция paginate() распределяет статьи справочника по страницам, основываясь на результатах функции entryHeight(), которая вычисляет высоту одной статьи.

Рисунок 8.18. Вывод справочника цветовода с помощью QPainter.


Она в цикле проходит по всем статьям справочника и добавляет их в конец текущей страницы пока не закончится доступное пространство. После этого текущая страница добавляется в массив pages и начинается заполнение новой страницы.

Рисунок 8.19. Раскладка одной статьи справочника.


int PrintWindow::entryHeight(QPainter *painter, const QString &entry) { QStringList fields = QStringList::split(": ", entry); QString title = fields[0]; QString body = fields[1]; int textWidth = painter->window().width() - 2 * SmallGap; int maxHeight = painter->window().height(); painter->setFont(titleFont); QRect titleRect = painter->boundingRect(0, 0, textWidth, maxHeight, WordBreak, title); painter->setFont(bodyFont); QRect bodyRect = painter->boundingRect(0, 0, textWidth, maxHeight, WordBreak, body); return titleRect.height() + bodyRect.height() + 4 * SmallGap; } Функция entryHeight(), с помощью QPainter::boundingRect(), вычисляет высоту статьи на странице. На рисунке 8.19 показана раскладка статьи справочника и назначение констант SmallGap и MediumGap. void PrintWindow::printPage(QPainter *painter, const vector<QStringList> &pages, int index) { painter->saveWorldMatrix(); painter->translate(0, LargeGap); QStringList::const_iterator it = pages[index].begin(); while (it != pages[index].end()) { QStringList fields = QStringList::split(": ", *it); QString title = fields[0]; QString body = fields[1]; printBox(painter, titleFont, title, lightGray); printBox(painter, bodyFont, body, white); painter->translate(0, MediumGap); ++it; } painter->restoreWorldMatrix(); painter->setFont(footerFont); painter->drawText(painter->window(), AlignHCenter | AlignBottom, QString::number(index + 1)); } Функция printPage() обходит в цикле все статьи справочника и печатает их в два приема: первый раз функция printBox() вызывается для печати заголовка статьи (название цветка) и второй раз -- для печати описания (тела статьи). В заключение печатается номер страницы, внизу по центру. void PrintWindow::printBox(QPainter *painter, const QFont &font, const QString &str, const QBrush &brush) { painter->setFont(font); int boxWidth = painter->window().width(); int textWidth = boxWidth - 2 * SmallGap; int maxHeight = painter->window().height(); QRect textRect = painter->boundingRect(SmallGap, SmallGap, textWidth, maxHeight, WordBreak, str); int boxHeight = textRect.height() + 2 * SmallGap; painter->setPen(QPen(black, 2, SolidLine)); painter->setBrush(brush); painter->drawRect(0, 0, boxWidth, boxHeight); painter->drawText(textRect, WordBreak, str); painter->translate(0, boxHeight); } Функция printBox() рисует прямоугольник, а затем внутри него -- текст.

Если на печать выводится большой документ, или пользователь заказал несколько копий одного документа, то неплохо было бы показать индикатор хода выполнения задания -- QProgressDialog. Ниже приводится модифицированный вариант функции printFlowerGuide(), которая выводит перед пользователем индикатор хода выполнения задания:

void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector<QStringList> pages; int index; paginate(&painter, &pages, entries); int numSteps = printer.numCopies() * pages.size(); int step = 0; QProgressDialog progress(tr("Printing file..."), tr("Cancel"), numSteps, this); progress.setModal(true); for (int i = 0; i < (int)printer.numCopies(); ++i) { for (int j = 0; j < (int)pages.size(); ++j) { progress.setProgress(step); qApp->processEvents(); if (progress.wasCanceled()) { printer.abort(); return; } ++step; if (i > 0 || j > 0) printer.newPage(); if (printer.pageOrder() == QPrinter::LastPageFirst) { index = pages.size() - j - 1; } else { index = j; } printPage(&painter, pages, index); } } } } Когда пользователь нажимает на кнопку Cancel -- вызывается QPrinter::abort(), которая останавливает процесс печати.