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