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

Печать произвольной графики через QPainter на QPrinter

Введение

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

Концепция

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

Пример кода: Печать произвольной графики

#include <QPrinter>
#include <QPainter>
#include <QPdfWriter>
#include <QPixmap>
#include <QChart> // условно, диаграмма из Qt Charts или любой другой источник QPixmap

void printChartCenteredAndScaled(QPagedPaintDevice *device, const QPixmap &chartImage)
{
    QPainter painter;
    if (!painter.begin(device)) return;

    const QRectF pageRect(0, 0, device->width(), device->height());
    const int margin = device->logicalDpiX() / 2; // отступ ~0.5 дюйма

    const QRectF availableRect = pageRect.adjusted(margin, margin, -margin, -margin);

    // Вычисляем коэффициент масштабирования, сохраняющий пропорции изображения
    const double scaleX = availableRect.width() / chartImage.width();
    const double scaleY = availableRect.height() / chartImage.height();
    const double scale = qMin(scaleX, scaleY); // вписываем целиком, не искажая пропорции

    const double scaledWidth = chartImage.width() * scale;
    const double scaledHeight = chartImage.height() * scale;

    // Центрируем изображение в доступной области
    const double offsetX = availableRect.left() + (availableRect.width() - scaledWidth) / 2.0;
    const double offsetY = availableRect.top() + (availableRect.height() - scaledHeight) / 2.0;

    painter.setRenderHint(QPainter::SmoothPixmapTransform);
    painter.drawPixmap(QRectF(offsetX, offsetY, scaledWidth, scaledHeight), chartImage,
                        chartImage.rect());

    // Подпись под изображением
    QFont captionFont("Arial", 10);
    painter.setFont(captionFont);
    painter.drawText(QRectF(availableRect.left(), offsetY + scaledHeight + 20,
                            availableRect.width(), 30),
                      Qt::AlignHCenter, "Рис. 1. Динамика продаж за квартал");

    painter.end();
}

void exportChartToPdf(const QString &filePath, const QPixmap &chartImage)
{
    QPdfWriter writer(filePath);
    writer.setPageSize(QPageSize(QPageSize::A4));
    writer.setResolution(300);
    printChartCenteredAndScaled(&writer, chartImage);
}

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

Ключевой расчёт в функции — вычисление scale как минимума из scaleX и scaleY: если бы использовалось любое из этих значений по отдельности (или они применялись бы независимо к ширине и высоте), изображение было бы искажено, растянутым или сжатым неравномерно по разным осям — выбор минимума гарантирует, что изображение целиком вписывается в доступную область, сохраняя исходное соотношение сторон, с возможным появлением пустого пространства по одной из осей (что и компенсируется последующим центрированием через offsetX/offsetY).

painter.setRenderHint(QPainter::SmoothPixmapTransform) включает билинейную интерполяцию при масштабировании растрового изображения — без этой подсказки масштабирование может выполняться через простое, более грубое (но быстрое) ближайшее соседство, что для печати, особенно при увеличении мелкого изображения, даёт заметно худшее визуальное качество с явными «ступеньками» на диагональных линиях.

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

  • Использование chartImage.width()/height() без учёта devicePixelRatio() изображения, полученного со экранов с высоким DPI. Если QPixmap была захвачена с виджета на Retina-дисплее или аналогичном экране высокой плотности, её физический размер в пикселях может быть кратно больше логического размера виджета — расчёт масштаба без учёта этого соотношения приведёт к тому, что изображение при печати окажется значительно меньше ожидаемого относительно остального контента страницы.
  • Печать растрового изображения низкого исходного разрешения при печати с высоким setResolution() принтера/PDF. Растяжение маленького исходного изображения (скажем, 400×300 пикселей) на большую физическую область печати с разрешением 300 DPI неизбежно даёт видимую пикселизацию независимо от качества интерполяции — для печати качественной растровой графики исходное изображение должно изначально иметь достаточное разрешение для целевого физического размера на странице.
  • Отсутствие учёта ориентации страницы при расчёте availableRect. Если ориентация страницы (портретная/альбомная) определяется динамически или может быть изменена пользователем через диалог печати уже после того, как код расчёта геометрии написан с предположением о конкретной ориентации, контент может оказаться неоптимально расположен (например, узкая высокая диаграмма на широкой альбомной странице) — для надёжности расчёт device->width()/device->height() должен происходить непосредственно перед отрисовкой, когда ориентация уже окончательно определена.
  • Векторная графика, отрисованная напрямую через примитивы QPainter (а не как растровое изображение), масштабируется без потери качества при изменении разрешения устройства вывода — если есть возможность перерисовать диаграмму непосредственно через примитивы QPainter для печати (а не использовать готовый растровый снимок экранного виджета), это даёт существенно лучшее качество печати, особенно для линий и текста, по сравнению с масштабированием уже растеризованного изображения.