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

Превью печати: QPrintPreviewWidget и QPrintPreviewDialog

Введение

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

Концепция

QPrintPreviewDialog оборачивает QPrintPreviewWidget и связывается с QPrinter через сигнал paintRequested(QPrinter*) — вместо того, чтобы рисовать содержимое один раз «напрямую» в main() или обработчике кнопки, логика формирования документа выносится в отдельный слот, подключаемый к этому сигналу, и вызывается диалогом предпросмотра каждый раз, когда нужно перерисовать страницы (включая повторные вызовы при изменении масштаба или ориентации страницы пользователем в самом диалоге).

Пример кода: Превью печати

#include <QPrintPreviewDialog>
#include <QPrinter>
#include <QPainter>

class InvoiceDocument
{
public:
    void render(QPrinter *printer)
    {
        QPainter painter;
        if (!painter.begin(printer)) return;

        const QRectF pageRect(0, 0, printer->width(), printer->height());
        const int margin = printer->resolution() / 2;

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

        QFont bodyFont("Arial", 12);
        painter.setFont(bodyFont);
        painter.drawText(margin, margin + 100, "Клиент: ООО \"Ромашка\"");
        painter.drawText(margin, margin + 130, "Сумма: 15 400 руб.");

        painter.end();
    }
};

void showPrintPreview(QWidget *parentWidget)
{
    QPrinter printer(QPrinter::HighResolution);
    printer.setPageSize(QPageSize(QPageSize::A4));

    auto *document = new InvoiceDocument; // должен жить достаточно долго (см. подводные камни)

    QPrintPreviewDialog previewDialog(&printer, parentWidget);
    previewDialog.setWindowTitle("Предпросмотр печати — Счёт № 2024-105");

    QObject::connect(&previewDialog, &QPrintPreviewDialog::paintRequested,
                      [document](QPrinter *printerToRender) {
        document->render(printerToRender);
    });

    previewDialog.exec();

    delete document;
}

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

InvoiceDocument::render() инкапсулирует всю логику формирования визуального содержимого документа в отдельном методе, не привязанном напрямую к диалогу предпросмотра — это позволяет вызывать ровно тот же код и для предпросмотра, и для финальной реальной печати (через обычный QPrintDialog, как в статье 190), и для сохранения в PDF (через QPdfWriter, статья 191), поскольку все три сценария принимают один и тот же тип QPagedPaintDevice для рисования.

Лямбда, подключённая к сигналу paintRequested, вызывается QPrintPreviewDialog каждый раз, когда нужно перестроить отображаемые страницы предпросмотра — это может произойти несколько раз за один показ диалога (например, при изменении пользователем ориентации страницы прямо в интерфейсе предпросмотра), поэтому код рендеринга должен быть детерминированным и не иметь побочных эффектов, которые были бы нежелательны при повторном вызове.

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

  • Захват указателя на временный объект документа в лямбде без гарантии его времени жизни. Если document был бы создан как локальная переменная на стеке и удалён до вызова previewDialog.exec() (что не так в примере, но легко допустить при рефакторинге), лямбда обратилась бы к уже разрушенному объекту в момент срабатывания paintRequested — поскольку этот сигнал может сработать многократно при интерактивном взаимодействии с диалогом, объект документа должен переживать весь жизненный цикл диалога предпросмотра.
  • Побочные эффекты внутри обработчика paintRequested (например, инкремент счётчика, запись в файл, изменение общего состояния приложения) — поскольку сигнал может сработать несколько раз за одно открытие диалога предпросмотра, любой код с побочными эффектами, рассчитывающий на однократное выполнение, будет работать некорректно при повторном рендеринге, инициированном самим пользователем внутри диалога.
  • Тяжёлые вычисления внутри render(), выполняемые синхронно при каждом перерисовывании предпросмотра. Если формирование содержимого документа требует дорогих операций (запрос к базе данных, сложные вычисления), повторный вызов при каждом взаимодействии пользователя с диалогом предпросмотра (масштабирование, смена ориентации) может ощутимо замедлять отзывчивость самого диалога — для таких случаев стоит заранее закэшировать дорогие промежуточные результаты вне самого метода рендеринга, вызывая дорогую часть один раз, а не при каждом перерисовывании.
  • QPrintPreviewDialog::exec() — модальный блокирующий вызов, аналогично любому другому Qt-диалогу — если предпросмотр должен быть немодальным (не блокирующим взаимодействие с остальным приложением), нужно использовать show() вместо exec() и явно обрабатывать его закрытие через сигналы, что требует дополнительного кода управления жизненным циклом диалога по сравнению с простым блокирующим вызовом.