Пользовательский интерфейс в Qt

Печать таблиц и отчётов на основе QTextDocument

Введение

Печать произвольной графики через прямые вызовы 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).