Введение
Когда содержимое документа определённо не уместится на одной странице — длинный список транзакций, многостраничный отчёт, каталог товаров — нужна явная логика разбиения контента на страницы при прямой отрисовке через QPainter (в отличие от QTextDocument, который делает это автоматически, но с ограничениями, разобранными в статье 193). В статье построим переиспользуемый паттерн постраничной печати произвольного списка записей с повторяющимся заголовком на каждой странице.
Концепция
Базовый алгоритм многостраничной печати: вычисляется, сколько элементов содержимого (строк таблицы, записей списка) умещается на одной странице с учётом высоты заголовка, отступов и высоты одной строки; контент делится на группы такого размера; для каждой группы, кроме первой, вызывается printer->newPage() перед началом отрисовки новой страницы; на каждой странице повторно рисуется заголовок (название документа, номер страницы, иногда повторяющиеся заголовки колонок таблицы), и далее соответствующая порция данных.
Пример кода: Многостраничная печать и
#include <QPrinter>
#include <QPainter>
#include <QVector>
struct TransactionRecord
{
QString date;
QString description;
double amount;
};
class PagedTransactionPrinter
{
public:
void print(QPagedPaintDevice *device, const QVector<TransactionRecord> &records,
const QString &reportTitle)
{
QPainter painter;
if (!painter.begin(device)) return;
const double headerHeight = 120;
const double rowHeight = 25;
const double footerHeight = 40;
const double pageHeight = device->height();
const double pageWidth = device->width();
const double margin = device->logicalDpiX() / 2;
const double availableHeight = pageHeight - headerHeight - footerHeight - 2 * margin;
const int rowsPerPage = qMax(1, static_cast<int>(availableHeight / rowHeight));
const int totalPages = (records.size() + rowsPerPage - 1) / rowsPerPage;
for (int page = 0; page < totalPages; ++page) {
if (page > 0) {
device->newPage(); // переход на новую страницу для всех, кроме первой
}
drawPageHeader(painter, pageWidth, margin, reportTitle, page + 1, totalPages);
const int startIdx = page * rowsPerPage;
const int endIdx = qMin(startIdx + rowsPerPage, records.size());
double y = margin + headerHeight;
for (int i = startIdx; i < endIdx; ++i) {
drawRecordRow(painter, margin, y, pageWidth - 2 * margin, records[i]);
y += rowHeight;
}
drawPageFooter(painter, pageWidth, pageHeight, margin, page + 1, totalPages);
}
painter.end();
}
private:
void drawPageHeader(QPainter &painter, double pageWidth, double margin,
const QString &title, int pageNum, int totalPages)
{
QFont titleFont("Arial", 16, QFont::Bold);
painter.setFont(titleFont);
painter.drawText(QRectF(margin, margin, pageWidth - 2 * margin, 40),
Qt::AlignLeft, title);
QFont columnHeaderFont("Arial", 10, QFont::Bold);
painter.setFont(columnHeaderFont);
const double headerY = margin + 60;
painter.drawText(QRectF(margin, headerY, 100, 20), "Дата");
painter.drawText(QRectF(margin + 110, headerY, 300, 20), "Описание");
painter.drawText(QRectF(pageWidth - margin - 120, headerY, 120, 20),
Qt::AlignRight, "Сумма, руб.");
painter.drawLine(QPointF(margin, headerY + 25), QPointF(pageWidth - margin, headerY + 25));
}
void drawRecordRow(QPainter &painter, double margin, double y, double width,
const TransactionRecord &record)
{
QFont rowFont("Arial", 10);
painter.setFont(rowFont);
painter.drawText(QRectF(margin, y, 100, 20), record.date);
painter.drawText(QRectF(margin + 110, y, 300, 20), record.description);
painter.drawText(QRectF(margin + width - 120, y, 120, 20),
Qt::AlignRight, QString::number(record.amount, 'f', 2));
}
void drawPageFooter(QPainter &painter, double pageWidth, double pageHeight,
double margin, int pageNum, int totalPages)
{
QFont footerFont("Arial", 9);
painter.setFont(footerFont);
painter.drawText(QRectF(margin, pageHeight - margin - 20, pageWidth - 2 * margin, 20),
Qt::AlignRight, QString("Страница %1 из %2").arg(pageNum).arg(totalPages));
}
};
Пояснения к коду
rowsPerPage вычисляется заранее на основе доступной высоты страницы за вычетом заголовка и подвала, делённой на высоту одной строки — это позволяет точно определить totalPages до начала фактической отрисовки, что необходимо для корректного отображения «Страница X из Y» в подвале каждой страницы (без знания общего числа страниц заранее эта информация была бы недоступна на момент печати первой страницы).
Главный цикл for (int page = 0; page < totalPages; ++page) вызывает device->newPage() для всех страниц, кроме первой (if (page > 0)) — QPagedPaintDevice::newPage() завершает текущую страницу и начинает новую, сохраняя состояние QPainter, но сбрасывая текущую позицию отрисовки на начало новой пустой страницы. На каждой странице повторно вызывается drawPageHeader(), что воссоздаёт заголовок и заголовки столбцов таблицы заново — именно этот повторяющийся вызов решает проблему, упомянутую как ограничение QTextDocument в статье 193.
Подводные камни
- Неточный расчёт
rowsPerPage, не учитывающий реальную высоту текста при разных шрифтах или включающий «припас» недостаточного размера. Если реальная отрисованная высота строки текста (с учётом межстрочного интервала конкретного шрифта) немного отличается от заложенного в расчёт значенияrowHeight, последняя строка на странице может визуально «наезжать» на подвал или следующий элемент — стоит либо использоватьQFontMetricsдля точного измерения реальной высоты строки текущего шрифта, либо оставлять заведомый запас в расчёте. - Запись с очень длинным текстом описания, не уместившимся в отведённую ширину колонки (
drawTextбезQt::TextWordWrapв примере, ради простоты) — длинный текст будет просто обрезан границейQRectF, а не перенесён на следующую строку внутри той же ячейки, что может сделать описание транзакции нечитаемым; для реальных отчётов с переменной длиной текста нужна дополнительная логика обработки многострочных ячеек, дополнительно усложняющая расчётrowsPerPage(поскольку строки разной высоты делают равномерное деление невозможным). - Жёстко закодированные пиксельные значения отступов между колонками (
margin + 110,pageWidth - margin - 120) без относительного, процентного распределения ширины — при изменении общей ширины страницы (другой формат бумаги, другая ориентация) эти абсолютные смещения могут не масштабироваться пропорционально, что нужно явно тестировать при поддержке нескольких форматов страниц. - Производительность при очень большом числе записей (десятки тысяч строк отчёта) — хотя сам алгоритм пагинации линейный и не должен создавать принципиальных проблем, фактическое время рендеринга каждой отдельной страницы через множество вызовов
drawText()может стать заметным при экспорте действительно больших отчётов в PDF; для таких случаев стоит измерить реальное время генерации и, при необходимости, рассмотреть пакетную обработку или индикацию прогресса для пользователя во время длительной генерации.