Введение
Печать произвольной графики через прямые вызовы QPainter, как в предыдущих статьях, хорошо подходит для документов с фиксированной, заранее продуманной вёрсткой, но становится утомительной для табличных отчётов с переменным числом строк и столбцов. QTextDocument — текстовый движок Qt, поддерживающий HTML-подобную разметку (включая таблицы, списки, форматирование текста) — может быть использован как промежуточное представление документа, которое затем печатается одним вызовом, автоматически разбивая контент на страницы при необходимости. В статье разберём построение табличного отчёта через QTextDocument.
Концепция
QTextDocument::setHtml() принимает HTML-разметку и формирует внутреннее представление документа с полноценной поддержкой таблиц (<table>), который затем печатается через QTextDocument::print(QPagedPaintDevice*) — этот единственный вызов сам заботится о разбиении содержимого на страницы там, где это необходимо, без необходимости вручную вычислять, сколько строк таблицы уместится на одной странице. Это значительно проще, чем вручную рисовать таблицу через QPainter::drawLine()/drawText(), особенно для отчётов с заранее неизвестным числом строк.
Пример кода: Печать таблиц и
#include <QTextDocument>
#include <QPrinter>
#include <QPrintDialog>
#include <QPdfWriter>
QString buildReportHtml(const QVector<QPair<QString, double>> &salesData)
{
QString html = R"(
<html><head><style>
body { font-family: Arial; }
h1 { text-align: center; color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { border: 1px solid #999; padding: 8px; text-align: left; }
th { background-color: #f0f0f0; }
.total-row td { font-weight: bold; background-color: #e8f4ff; }
</style></head><body>
<h1>Отчёт по продажам</h1>
<table>
<tr><th>Товар</th><th>Сумма продаж, руб.</th></tr>
)";
double total = 0.0;
for (const auto &row : salesData) {
html += QString("<tr><td>%1</td><td>%2</td></tr>")
.arg(row.first)
.arg(row.second, 0, 'f', 2);
total += row.second;
}
html += QString(R"(
<tr class="total-row"><td>Итого</td><td>%1</td></tr>
</table>
</body></html>
)").arg(total, 0, 'f', 2);
return html;
}
void printSalesReport(QWidget *parentWidget, const QVector<QPair<QString, double>> &salesData)
{
QTextDocument document;
document.setHtml(buildReportHtml(salesData));
document.setPageSize(QSizeF(595, 842)); // примерные размеры A4 в "точках" (points)
QPrinter printer(QPrinter::HighResolution);
QPrintDialog dialog(&printer, parentWidget);
if (dialog.exec() == QDialog::Accepted) {
document.print(&printer); // автоматически разбивает таблицу на страницы при необходимости
}
}
void exportSalesReportToPdf(const QString &filePath, const QVector<QPair<QString, double>> &salesData)
{
QTextDocument document;
document.setHtml(buildReportHtml(salesData));
QPdfWriter writer(filePath);
writer.setPageSize(QPageSize(QPageSize::A4));
document.setPageSize(QSizeF(writer.width(), writer.height()));
document.print(&writer);
}
Пояснения к коду
buildReportHtml() формирует HTML-таблицу со встроенными CSS-стилями (QTextDocument поддерживает подмножество CSS, достаточное для базового оформления таблиц, цветов фона и границ) — строка с итоговой суммой выделена отдельным классом .total-row для визуального акцента, демонстрируя, что обычная CSS-стилизация работает так же, как и в браузере, в пределах поддерживаемого Qt подмножества.
document.setPageSize() сообщает движку текста размер страницы, относительно которого будет производиться разбиение содержимого на страницы — это значение должно соответствовать реальному размеру страницы целевого устройства печати (принтера или PDF-writer’а), переданного в print(), иначе пропорции и разбивка по страницам будут рассчитаны неверно. Сам вызов document.print(&printer) (или &writer для PDF) — единственная строка, отвечающая за фактическую отрисовку всего документа с автоматическим переносом таблицы на следующую страницу, если она не уместилась на одной.
Подводные камни
- Несогласованность единиц измерения между
setPageSize()документа и реальным размером страницы устройства печати.QTextDocument::setPageSize()ожидает размер в «точках» документа (логических единицах текстового движка), тогда какQPrinter/QPdfWriterоперируют физическими размерами с учётом установленного разрешения — расчёт правильного соответствия требует аккуратности, и расхождение приводит к неверной разбивке на страницы (например, к появлению лишней почти пустой последней страницы или, наоборот, к обрезанию контента). - Ограниченная поддержка CSS в
QTextDocumentпо сравнению с полноценным веб-движком. Современные CSS-возможности (flexbox, grid, сложные псевдоклассы) не поддерживаются — попытка использовать продвинутую вёрстку, скопированную «как есть» из веб-дизайна, приведёт к игнорированию неподдерживаемых свойств без явной ошибки, и итоговый вид документа может заметно отличаться от ожидаемого по аналогии с обычным браузером. - Сборка большого HTML через многократную конкатенацию строк в цикле (как в примере с
html += ...для каждой строки таблицы) при очень большом числе строк отчёта (тысячи записей) может быть заметно медленнее, чем построение черезQTextStreamили предварительное резервирование размера строки — для действительно больших отчётов стоит измерить и при необходимости оптимизировать этот этап формирования HTML, отдельно от собственно печати. - Заголовки таблицы (
<th>), не повторяющиеся автоматически на каждой новой странице при переносе длинной таблицы — в отличие от специализированных инструментов формирования отчётов (Crystal Reports и аналоги),QTextDocumentне имеет встроенной концепции «повторяющегося заголовка таблицы» при разбиении на страницы; для длинных таблиц, где это важно для читаемости, нужен либо более сложный, специально написанный механизм пагинации с вставкой заголовка вручную на каждой странице, либо переход на прямую отрисовку черезQPainterс собственной логикой пагинации (статья 195).