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

QPdfWriter: программная генерация PDF без диалога печати

Введение

Не каждая задача «печати» подразумевает реальный физический принтер или диалог выбора — часто нужно просто сгенерировать PDF-файл программно: автоматический отчёт по расписанию, экспорт документа по нажатию кнопки «Сохранить как PDF», пакетная генерация счетов для множества клиентов без какого-либо взаимодействия с пользователем на каждый файл. QPdfWriter решает именно эту задачу — он реализует тот же интерфейс QPagedPaintDevice, что и QPrinter, но напрямую пишет в PDF-файл, минуя систему печати ОС целиком. В статье разберём, как использовать QPdfWriter для программной, неинтерактивной генерации PDF.

Концепция

QPdfWriter создаётся с указанием пути к выходному файлу и поддерживает настройку размера страницы, разрешения, метаданных документа (заголовок, автор) — всё это устанавливается программно, без диалогов. Поскольку QPdfWriter, как и QPrinter, наследует QPagedPaintDevice, один и тот же код отрисовки, написанный через QPainter, можно использовать взаимозаменяемо для печати на реальный принтер или для сохранения в PDF — это удобно, когда приложение должно поддерживать оба сценария без дублирования логики формирования содержимого документа.

Пример кода: QPdfWriter

#include <QPdfWriter>
#include <QPainter>
#include <QPageSize>
#include <QDateTime>
#include <QDebug>

bool generateInvoicePdf(const QString &filePath, const QString &clientName,
                        double amount, const QStringList &items)
{
    QPdfWriter writer(filePath);
    writer.setPageSize(QPageSize(QPageSize::A4));
    writer.setResolution(300);
    writer.setTitle("Счёт для " + clientName);
    writer.setCreator("MyApp Invoice Generator");

    QPainter painter;
    if (!painter.begin(&writer)) {
        qWarning() << "Не удалось создать PDF-файл:" << filePath;
        return false;
    }

    const QRectF pageRect(0, 0, writer.width(), writer.height());
    const int margin = writer.resolution() / 2; // отступ примерно 0.5 дюйма

    QFont titleFont("Arial", 22, QFont::Bold);
    painter.setFont(titleFont);
    painter.drawText(QRectF(margin, margin, pageRect.width() - 2 * margin, 60),
                      Qt::AlignLeft, "Счёт на оплату");

    QFont infoFont("Arial", 12);
    painter.setFont(infoFont);
    int y = margin + 100;
    painter.drawText(margin, y, "Клиент: " + clientName);
    y += 30;
    painter.drawText(margin, y, "Дата: " + QDateTime::currentDateTime().toString("dd.MM.yyyy"));
    y += 50;

    QFont itemFont("Arial", 11);
    painter.setFont(itemFont);
    for (const QString &item : items) {
        painter.drawText(margin, y, "• " + item);
        y += 25;
    }

    y += 30;
    QFont totalFont("Arial", 14, QFont::Bold);
    painter.setFont(totalFont);
    painter.drawText(margin, y, QString("Итого к оплате: %1 руб.").arg(amount, 0, 'f', 2));

    painter.end();
    qDebug() << "PDF успешно сгенерирован:" << filePath;
    return true;
}

void demonstrateUsage()
{
    generateInvoicePdf("invoice_2024_105.pdf", "ООО \"Ромашка\"", 15400.0,
                       {"Консультационные услуги — 10 000 руб.",
                        "Разработка макета — 5 400 руб."});
}

Пояснения к коду

writer.setResolution(300) устанавливает разрешение в точках на дюйм, влияющее на то, как координаты, переданные в QPainter (в логических единицах QPdfWriter), отображаются на физические размеры страницы — это явно отличается от поведения экранного виджета, где аналогичная настройка не требуется, поскольку DPI экрана определяется системой автоматически. setTitle()/setCreator() записывают метаданные непосредственно в структуру PDF-файла, видимые, например, в свойствах документа при открытии в Adobe Reader или аналогичной программе.

Вычисление margin как половины значения resolution() — простой способ получить отступ примерно в полдюйма независимо от выбранного разрешения; такой относительный подход к расчёту координат избавляет от необходимости пересчитывать абсолютные значения отступов при изменении настроек разрешения документа.

Подводные камни

  • Путь к выходному файлу, на запись в который у процесса нет прав (системная защищённая директория, файл уже открыт и заблокирован другим процессом для чтения) приводит к тому, что painter.begin(&writer) вернёт false без более конкретной диагностики причины — стоит явно проверять права на запись или перехватывать типичные ситуации (заблокированный файл) до попытки генерации, чтобы дать пользователю понятное сообщение об ошибке.
  • Жёстко заданные пиксельные координаты вместо относительных, основанных на resolution() и размере страницы. Код, написанный с абсолютными числами координат для одного конкретного разрешения, выглядит некорректно (слишком мелко или со смещением) при изменении setResolution() на другое значение — пример демонстрирует более устойчивый подход через вычисление отступов и позиций относительно текущего разрешения.
  • Отсутствие обработки переполнения страницы при динамическом, заранее не известном количестве элементов (items в примере) — если список содержит больше позиций, чем уместится на одной странице A4 при заданном размере шрифта, текст просто продолжит печататься за пределами видимой области страницы без вызова ошибки, и эта часть содержимого будет потеряна визуально; для произвольного числа элементов нужна логика разбиения на несколько страниц, разбираемая в статье 195.
  • QPdfWriter не поддерживает добавление страниц к уже существующему PDF-файлу — каждый вызов создаёт новый файл с нуля; для объединения нескольких сгенерированных частей в один PDF-документ или для дополнения существующего файла новыми страницами нужны либо генерация всего содержимого за один проход через многостраничную логику (writer.newPage()), либо использование отдельных инструментов слияния PDF, не входящих в стандартный API QPdfWriter.