Введение
Перед тем как реально отправить документ на печать (расходуя бумагу и время) или сохранить как 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()и явно обрабатывать его закрытие через сигналы, что требует дополнительного кода управления жизненным циклом диалога по сравнению с простым блокирующим вызовом.