Printing

Printing in Qt is similar to drawing on a widget or on a pixmap. It consists of the following steps:

  1. Create a QPrinter to serve as the "paint device".
  2. Call QPrinter::setup() to pop up a print dialog, allowing the user to choose a printer and to set a few options.
  3. Create a QPainter to operate on the QPrinter.
  4. Draw a page using the QPainter.
  5. Call QPrinter::newPage() to advance to the next page.
  6. Repeat steps 4 and 5 until all the pages are printed.

On Windows and Mac OS X, QPrinter uses the system's printer drivers. On Unix, it generates PostScript and sends it to lp or lpr (or to whatever program has been set using QPrinter::setPrintProgram()).

Figure 8.15. Printing an OvenTimer, a QCanvas, and a QImage

Let's start with some simple examples that all print on a single page. The first example prints an OvenTimer widget:

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); } }

We assume that the PrintWindow class has a member variable called printer of type QPrinter. We could simply have created the QPrinter on the stack in printOvenTimer(), but then it would not remember the user's settings from one print run to another.

We call setup() to pop up a print dialog. It returns true if the user clicked the OK button; otherwise, it returns false. After the call to setup(), the QPrinter object is ready to use.

We create a QPainter to draw on the QPrinter. Then we make the painter's viewport square and initialize the painter's window to (-50, -50, 100, 100), the rectangle expected by OvenTimer. We call draw() to do the painting. If we didn't bother making the viewport square, the OvenTimer would be vertically stretched to fill the entire page height.

By default, the QPainter's window is initialized so that the printer appears to have a similar resolution as the screen (usually somewhere between 72 and 100 dots per inch), making it easy to reuse widget-painting code for printing. Here, it didn't matter, because we set our own window to be (-50, -50, 100, 100).

Printing an OvenTimer isn't a very realistic example, because the widget is meant for on-screen user interaction. But for other widgets, such as the Plotter widget we developed in Chapter 5, it makes lots of sense to reuse the widget's painting code for printing.

A more practical example is printing a QCanvas. Applications that use it often need to be able to print what the user has drawn. This can be done in a generic way as follows:

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); } } }

This time, we set the painter's window to the canvas's bounding rectangle, and we restrict the viewport to a rectangle with the same aspect ratio. To accomplish this, we use QSize::scale() with ScaleMin as its second argument. For example, if the canvas has a size of 640 x 480 and the painter's viewport has a size of 5000 x 5000, the resulting viewport size that we use is 5000 x 3750.

We call collisions() with the canvas's rectangle as argument to obtain the list of all visible canvas items sorted from highest to lowest z value. We iterate over the list from the end to paint the items with a lower z value before those with a higher z value and call QCanvasItem::draw() on them. This ensures that the items that appear nearer the front are drawn on top of the items that are further back.

Our third example is to draw a 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); } }

We set the window to the image's rectangle and the viewport to a rectangle with the same aspect ratio, and we draw the image at position (0, 0).

Printing items that take up no more than a single page is simple, as we have seen. But many applications need to print multiple pages. For those, we need to paint one page at a time and call newPage() to advance to the next page. This raises the problem of determining how much information we can print on each page.

There are two approaches to handling multi-page documents with Qt:

We will review both approaches in turn.

As an example, we will print a flower guide: a list of flower names with a textual description. Each entry in the guide is stored as a string of the format "name:description", for example:

Miltonopsis santanae: An most dangerous orchid species.

Since each flower's data is represented by a single string, we can represent all the flowers in the guide using one QStringList.

Here's the function that prints a flower guide using Qt's rich text engine:

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 += " " "

" "" + title + "
" + body + "

"; ++it; } printRichText(str); }

Figure 8.16. Printing a flower guide using QSimpleRichText

The first step is to convert the data into HTML. Each flower becomes an HTML table with two cells. We use QStyleSheet::escape() to replace the special characters '&', '<', '>' with the corresponding HTML entities ("&", "<", ">"). Then we call printRichText() to print the text.

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); } } } }

The printRichText() function takes care of printing an HTML document. It can be reused "as is" in any Qt application to print arbitrary HTML.

We compute the height of one page based on the window size and the size of the gap we want to leave at the top and bottom of the page for a header and a footer. Then we create a QSimpleRichText object containing the HTML data. The last argument to the QSimpleRichText constructor is the page height; QSimpleRichText uses it to produce nice page breaks.

Figure 8.17. The flower guide's page layout

Then we print each page. The outer for loop iterates as many times as necessary to produce the number of copies requested by the user. Most printer drivers support multiple copies, so for those QPrinter::numCopies() always returns 1. If the printer driver doesn't support multiple copies, numCopies() returns the number of copies requested by the user, and the application is responsible for printing that amount. In the previous examples, we ignored numCopies() for the sake of simplicity.

The inner for loop iterates through the pages. If the page isn't the first page, we call newPage() to flush the old page and start painting on a fresh page. We call printPage() to paint each page.

The print dialog allows the user to print the pages in reverse order. It is our responsibility to honor that option.

We assume that printer, bodyFont, and footerFont are member variables of the PrintWindow class.

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)); }

The printPage() function prints the (index + 1)-th page of the document. The page consists of some HTML and of a page number in the footer area.

We translate the QPainter and call draw() with a position and rectangle specifying the portion of the rich text we want to draw. It might help to visualize the rich text as a single very long page that must be cut into smaller portions, each of height pageHeight.

Then we draw the page number centered at the bottom of the page. If we wanted to have a header on each page, we would just use an extra drawText() call.

The LargeGap constant is set to 48. Assuming a screen resolution of 96 dots per inch, this is half an inch (12.7 mm). To obtain a precise length regardless of screen resolution, we could have used the QPaintDeviceMetrics class as follows:

QPaintDeviceMetrics metrics(&printer); int LargeGap = metrics.logicalDpiY() / 2;

Here's one way we can initialize bodyFont and footerFont in the PrintWindow constructor:

bodyFont = QFont("Helvetica", 14); footerFont = bodyFont;

Let's now see how we can draw a flower guide using QPainter. Here's the new printFlowerGuide() function:

void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector 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); } } } }

The first thing we do after setting up the printer and constructing the painter is to call the paginate() helper function to determine which entry should appear on which page. The result of this is a vector of QStringLists, with each QStringList holding the entries for one page.

For example, let's suppose that the flower guide contains 6 entries, which we will refer to as A, B, C, D, E, and F. Now let's suppose that there is room for A and B on the first page, C, D, and E on the second page, and F on the third page. The pages vector would then have the list [A, B] at index position 0, the list [C, D, E] at index position 1, and the list [F] at index position 2.

The rest of the function is nearly identical to what we did earlier in printRichText(). The printPage() function, however, is different, as we will see shortly.

void PrintWindow::paginate(QPainter *painter, vector *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); }

The paginate() function distributes the flower guide entries into pages. It relies on the entryHeight() function, which computes the height of one entry.

Figure 8.18. Printing a flower guide using QPainter

We iterate through the entries and append them to the current page until we come to an entry that doesn't fit; then we append the current page to the pages vector and start a new page.

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() + body Rect.height() + 4 * SmallGap; }

The entryHeight() function uses QPainter::boundingRect() to compute the vertical space needed by one entry. Figure 8.19 shows the layout of a flower entry and the meaning of the SmallGap and MediumGap constants.

Figure 8.19. A flower entry's layout

void PrintWindow::printPage(QPainter *painter, const vector &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)); }

The printPage() function iterates through all the flower guide entries and prints them using two calls to printBox(): one for the title (the flower's name) and one for the body (its description). It also draws the page number centered at the bottom of the page.

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); }

The printBox() function draws the outline of a box, then draws the text inside the box.

If the user prints a long document, or requests multiple copies of a short document, it is usually a good idea to pop up a QProgressDialog to give the user the opportunity of canceling the printing operation (by clicking Cancel). Here's a modified version of printFlowerGuide() that does this:

void PrintWindow::printFlowerGuide(const QStringList &entries) { if (printer.setup(this)) { QPainter painter(&printer); vector 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); } } } }

When the user clicks Cancel, we call QPrinter::abort() to stop the printing operation.

Категории