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

Печать из QML-приложений

Введение

Весь предыдущий материал цикла строился на Qt Widgets и QPainter, напрямую интегрированном с этой технологией — но как быть с приложениями, построенными на Qt Quick/QML? В статье разберём два подхода к печати из QML-приложений: захват визуального содержимого QML-сцены как изображения для последующей печати, и более гибкий подход с явным формированием печатного документа на C++, вызываемым из QML через мост.

Концепция

Простейший подход — QQuickItem::grabToImage(), который асинхронно рендерит указанный QML-элемент (и всё его визуальное дерево) в QImage, который затем можно передать в уже знакомый из статьи 194 код печати произвольной растровой графики. Этот подход прост, но даёт растровый «снимок» интерфейса, что не идеально для текстовых документов, где предпочтительна векторная чёткость текста при печати. Более качественный, но более сложный подход — формирование отдельного, специально для печати предназначенного документа на стороне C++ (через QTextDocument или прямую отрисовку), не связанного напрямую с визуальным представлением QML-интерфейса, а лишь с теми же исходными данными.

Пример кода: Печать из QML-приложений

// Receipt.qml — QML-компонент, который нужно распечатать как растровое изображение
import QtQuick

Rectangle {
    id: receiptRoot
    width: 400; height: 300
    color: "white"

    Column {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 10

        Text { text: "Кафе \"Уютное место\""; font.pixelSize: 20; font.bold: true }
        Text { text: "Чек № 1042"; font.pixelSize: 14 }
        Text { text: "Капучино — 250 руб."; font.pixelSize: 14 }
        Text { text: "Круассан — 180 руб."; font.pixelSize: 14 }
        Text { text: "Итого: 430 руб."; font.pixelSize: 16; font.bold: true }
    }
}
// printbridge.h — мост для печати из QML
#pragma once
#include <QObject>
#include <QQmlEngine>
#include <QQuickItem>
#include <QPrinter>
#include <QPrintDialog>
#include <QPainter>

class PrintBridge : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    QML_SINGLETON

public:
    Q_INVOKABLE void printQuickItem(QQuickItem *item)
    {
        if (!item) return;

        // Асинхронный захват визуального содержимого элемента в изображение
        auto grabResult = item->grabToImage();
        connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult]() {
            const QImage image = grabResult->image();
            printGrabbedImage(image);
        });
    }

private:
    static void printGrabbedImage(const QImage &image)
    {
        QPrinter printer(QPrinter::HighResolution);
        QPrintDialog dialog(&printer);

        if (dialog.exec() != QDialog::Accepted) return;

        QPainter painter;
        if (!painter.begin(&printer)) return;

        const QRectF pageRect(0, 0, printer.width(), printer.height());
        const double scale = qMin(pageRect.width() / image.width(),
                                  pageRect.height() / image.height());

        painter.setRenderHint(QPainter::SmoothPixmapTransform);
        painter.drawImage(QRectF(0, 0, image.width() * scale, image.height() * scale), image);

        painter.end();
    }
};
// Использование в QML
import QtQuick
import QtQuick.Controls
import App.Printing 1.0

Column {
    Receipt { id: receipt }

    Button {
        text: "Распечатать чек"
        onClicked: PrintBridge.printQuickItem(receipt)
    }
}

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

QQuickItem::grabToImage() асинхронно (поскольку рендеринг Qt Quick происходит в отдельном потоке рендеринга, а не синхронно в момент вызова) формирует растровый снимок указанного элемента и всех его визуальных потомков — результат доступен через сигнал ready объекта QQuickItemGrabResult, а не сразу синхронно после вызова, что отражено в коде через connect() вместо немедленного использования результата.

PrintBridge зарегистрирован как QML-синглтон (паттерн, уже знакомый из статьи 88) и предоставляет единственный метод printQuickItem(), вызываемый прямо из QML с передачей ссылки на конкретный визуальный элемент (receipt в примере) — внутри метода полученное изображение масштабируется и центрируется на странице принтера тем же подходом, что разбирался в статье 194 для произвольной графики.

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

  • Качество печати ограничено разрешением, в котором был выполнен grabToImage() — по умолчанию захват происходит в логических пикселях самого QML-элемента без увеличения разрешения, и при печати на крупный физический размер страницы результат может оказаться размытым из-за недостаточного исходного разрешения; grabToImage() принимает необязательный параметр targetSize, который можно указать больше реального размера элемента для получения изображения с запасом разрешения, специально предназначенного для печати, а не для отображения на экране.
  • Асинхронность grabToImage() легко упустить из вида, особенно при переносе кода из синхронного мира Qt Widgets — попытка использовать grabResult->image() немедленно после вызова grabToImage(), без ожидания сигнала ready, вернёт пустое изображение, поскольку рендеринг в момент вызова ещё не завершён.
  • Растровый снимок интерфейса не подходит для документов, где важна чёткость печатного текста (договоры, официальные документы) — текст, отрендеренный как часть растрового изображения и затем напечатанный, не обладает той же чёткостью на физическом принтере, что векторный текст, нарисованный непосредственно через QPainter::drawText(); для таких документов более качественный, хотя и более трудоёмкий подход — формирование отдельного «печатного» представления документа на C++ напрямую из тех же исходных данных, а не захват визуального QML-интерфейса.
  • Время жизни QQuickItemGrabResult, удерживаемого через QSharedPointer внутри лямбды. В примере захват результата (grabResult) в лямбду по значению (через QSharedPointer, который и возвращает grabToImage()) продлевает его жизнь до момента вызова лямбды — забывание этой детали и захват, например, «сырого» указателя на промежуточный объект без должного управления временем жизни привело бы к обращению к уже разрушенному объекту к моменту срабатывания сигнала ready.