LINUX.ORG.RU

Печать элемента Qml,расположенного в QQuickWidget

 , ,


1

3

Использую Qt5.5.1 и есть задача печатать отображаемый на экране элемент. Вот тут есть небольшая проблема: когда вызываем grabToImage на элемент из qml пишет «Item::grabToImage: item's window is not visible». Что пишет понятно, нет явного создания элемента Window, поэтому и не может. Есть корявый вариант по сигналу из qml в c++ вызывать grab на весь QQuickWidget, потом его резать по координатам. Генерить рисунок отдельно вариант ещё хуже. Может кто-нибудь сталкивался с такими делами и более адекватное решение?


нет явного создания элемента Window

Мне кажется проблема не в этом, а что элемент пока ещё не связан с контекстом OpenGL, в котором его можно было бы нарисовать. Этот элемент вообще видим в данный момент?

Dendy ★★★★★
()
#include "ItemGrabber.h"

#include <QTimer>

#include <private/qquickitem_p.h>
#include <private/qsgrenderer_p.h>

class BindableFbo : public QSGBindable
{
public:
    BindableFbo(QOpenGLFramebufferObject *fbo)
        : m_fbo(fbo)
    {
    }
    virtual void bind() const { m_fbo->bind(); }
private:
    QOpenGLFramebufferObject *m_fbo;
};

ItemGrabber::ItemGrabber(QQuickItem *parent)
    : QQuickItem(parent)
{
    setFlag(ItemHasContents, false);
}

void ItemGrabber::grab(QVariantList items)
{
    m_items.clear();
    for (const auto &i : items) {
        auto obj = qobject_cast<QQuickItem *>(i.value<QObject *>());
        if (obj) {
            m_items.append(obj);
        }
    }

    // Delay the actual rendering to give all elements on the page
    // a chance to be fully rendered.
    QTimer::singleShot(500, this, [this]() {
        if (!isGrabbing && !m_items.isEmpty()) {
            isGrabbing = true;
            setFlag(QQuickItem::ItemHasContents);
            update();
        }
    });
}

QSGNode *ItemGrabber::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *updatePaintNodeData)
{
    Q_UNUSED(updatePaintNodeData);
    if (!(flags() & QQuickItem::ItemHasContents)) {
        isGrabbing = false;
        return oldNode;
    }
    setFlag(QQuickItem::ItemHasContents, false);

    auto toInt = [](qreal value)->int { return static_cast<int>(value); };
    QSize size(toInt(width()), toInt(height()));

    QSGRootNode root;
    QList<QSGNode *> nodes, parentNodes, siblingNodes;
    const int count = m_items.size();
    for (int i = 0; i < count; ++i) {
        QSGNode *node = QQuickItemPrivate::get(m_items[i])->itemNode();
        nodes.append(node);
        QSGNode *parentNode = node->QSGNode::parent();
        parentNodes.append(parentNode);
        QSGNode *previousSiblingNode = node->previousSibling();
        siblingNodes.append(previousSiblingNode);
        if (parentNode) {
            parentNode->removeChildNode(node);
        }
        root.appendChildNode(node);
    }
    QSGRenderer *renderer = QQuickItemPrivate::get(this)->sceneGraphRenderContext()->createRenderer();
    renderer->setRootNode(static_cast<QSGRootNode *>(&root));

    QOpenGLFramebufferObject fbo(size);
    renderer->setDeviceRect(size);
    renderer->setViewportRect(size);
    renderer->setProjectionMatrixToRect(QRectF(QPointF(), size));
    renderer->setClearColor(Qt::transparent);
    renderer->renderScene(BindableFbo(&fbo));
    fbo.release();

    QImage image = fbo.toImage().scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); // Format_ARGB32_Premultiplied

    for (int i = 0; i < count; ++i) {
        root.removeChildNode(nodes[i]);
    }
    renderer->setRootNode(nullptr);
    delete renderer;

    for (int i = 0; i < count; ++i) {
        if (siblingNodes[i]) {
            parentNodes[i]->insertChildNodeAfter(nodes[i], siblingNodes[i]);
        } else {
            parentNodes[i]->prependChildNode(nodes[i]);
        }
    }

    emit grabFinished(image);

    isGrabbing = false;
    return oldNode;
}
fluorite ★★★★★
()
Ответ на: комментарий от Dendy

Этот элемент вообще видим в данный момент?

Да, виден, конечно. Я как понимаю он требует созданного QQuickWindow, но в случае встраивания в виджет он не создается

SvSon
() автор топика
Ответ на: комментарий от SvSon

Ты не понял кода, по grab вызывается update(), который приводит к вызову updatePaintNode(), в котором из интересующей тебя области отрисовки (которую занимает ItemGrabber) вытаскивается рендер всех элементов, заданных в grab(). Картинка отправляется через сигнал grabFinished.

fluorite ★★★★★
()
Ответ на: комментарий от fluorite

Понял, спасибо, буду пробовать

SvSon
() автор топика
Ответ на: комментарий от fluorite

Хм.. интересно, а какая производительность у такого подхода? С какой частотой можно граббить? Потянет оно например 60fps? Какая загрузка CPU при этом будет?

Недавно нужно было сделать что-то типа live-stream (граббить QML-ное окошко со всем содержимым в Image по изменениям в нем) и передавать изменения в виде картинок куда-то там и там уже эти картинки отрисовывать (что-то типа RDP и прочего). Но я не осилил это в QML, пришлось переползать на QWidgets.

kuzulis ★★
()
Последнее исправление: kuzulis (всего исправлений: 2)
Ответ на: комментарий от kuzulis

Да собственно qquickshadereffectsource примерно так и работает, не вижу особо причин не работать с частотой отрисовки scene graph. Идею подсмотрел у webos, а она вроде на телевизорах работает, так что и с cpu ок должно быть. Тестируй :)

fluorite ★★★★★
()
Ответ на: комментарий от fluorite

Как я понимаю, этот айтем ItemGrabber должен быть корневым айтемом всего окошка (т.е другие визуальные айтемы должны быть внутри него) ?

kuzulis ★★
()
Ответ на: комментарий от kuzulis

Нет, он берёт SG ноды айтемов, которые ему передали в grab.

grab([item1id,item2id,item3id])
Но FramebufferObject, в котором отрендерятся эти ноды, будет размера граббера, хотя это легко поменять.
QSize size((int)width(), (int)height());
QOpenGLFramebufferObject fbo(size);
Суть в том, что во время updatePaintNode мы собираем желаемые SG ноды выбранных QQuickItem'ов (любой айтем, который умеет рисоваться, имеет QSGNode), рендерим в своём буфере, после чего возвращаем ноды обратно в основной qml рендер (на экран). Примерно так накладываются на айтемы шейдерные эффекты типа тени, см. метод setSourceItem в qquickshadereffectsource. Тот же update() в конце, который приведёт в updatePaintNode(). (Но там поприличнее код, конечно).

fluorite ★★★★★
()
12 декабря 2017 г.
Ответ на: комментарий от fluorite

Подниму тему, наконец то дошли руки, попробовал, и вот, что получается:

1. fbo.toImage() - оочень медленный, у меня заняло ~24 мсек чтобы преобразовать в картинку 800х600. так дело не пойдет...

2. непонятно как отследить момент «перерисовки» целевого item, чтобы сграббить его в этот момент.. ну не граббить же периодически...

ЗЫ: Мож кто занимался этой темой?

PS: На виджетах оно быстро и просто делается:

bool MyWidget::event(QEvent *event)
{
    if (event->type() == QEvent::UpdateRequest)
        myGrab();
    return QWidget::event(event);
}

void MyWidget::myGrab()
{
    ...
    QBackingStore *store = backingStore();
    Q_ASSERT(store);

    QPaintDevice *pdev = store->paintDevice();
    const auto image = dynamic_cast<QImage *>(pdev);
    ...
}

а вот, с qml/opengl прям какой-то трабл... :(

kuzulis ★★
()
Последнее исправление: kuzulis (всего исправлений: 2)
Ответ на: комментарий от fluorite

аналог твоего кода для виджетов делается ещё быстрее и проще

Где там оно быстрее и проще?

QQuickRenderControl::grab() внутри дергает то же самое что и QOpenGLFramebufferObject::toImage(), через qt_gl_read_framebuffer(...).

Набросал тут код чтобы мониторить конец перерисовки окна и после этого граббить содержимое нужного айтема (со всеми его детьми):

#include "grabber.h"

#include <QDebug>
#include <QElapsedTimer>
#include <QSGRootNode>
#include <QOpenGLFramebufferObject>

#include <private/qquickitem_p.h>
#include <private/qsgrenderer_p.h>

class BindableFbo final : public QSGBindable
{
public:
    explicit BindableFbo(QOpenGLFramebufferObject *fbo)
        : m_fbo(fbo)
    { }

    void bind() const final { m_fbo->bind(); }

private:
    QOpenGLFramebufferObject *m_fbo = nullptr;
};

Grabber::Grabber(QQuickItem *parent)
    : QQuickItem(parent)
{
    setFlag(QQuickItem::ItemHasContents);

    connect(this, &Grabber::windowChanged,
            this, &Grabber::scheduleWindowChange);
}

void Grabber::setSourceItem(QQuickItem *sourceItem)
{
    if (sourceItem == m_sourceItem)
        return;
    m_sourceItem = sourceItem;
    emit sourceItemChanged(m_sourceItem);
    update();
}

QQuickItem *Grabber::sourceItem() const
{
    return m_sourceItem;
}

QSGNode *Grabber::updatePaintNode(QSGNode *oldNode,
                                  UpdatePaintNodeData *updatePaintNodeData)
{
    Q_UNUSED(updatePaintNodeData);

    if (!m_sourceItem)
        return oldNode;

    m_running = true;

    const auto itemNode = QQuickItemPrivate::get(m_sourceItem)->itemNode();
    const auto parentNode = itemNode->parent();
    const auto siblingNode = itemNode->previousSibling();

    if (parentNode)
        parentNode->removeChildNode(itemNode);

    const QScopedPointer<QSGRenderer> renderer(
                QQuickItemPrivate::get(this)->
                sceneGraphRenderContext()->createRenderer());

    QSGRootNode rootNode;
    rootNode.appendChildNode(itemNode);
    renderer->setRootNode(&rootNode);

    const QSize size(m_sourceItem->width(), m_sourceItem->height());
    renderer->setDeviceRect(size);
    renderer->setViewportRect(size);
    renderer->setProjectionMatrixToRect(QRectF(QPointF(), size));
    renderer->setClearColor(Qt::transparent);

    QOpenGLFramebufferObject fbo(size);
    renderer->renderScene(BindableFbo(&fbo));
    fbo.release();

    rootNode.removeChildNode(itemNode);

    if (parentNode) {
        if (siblingNode) {
            parentNode->insertChildNodeAfter(itemNode, siblingNode);
        } else {
            parentNode->prependChildNode(itemNode);
        }
    }

    QElapsedTimer et;
    et.start();
    const QImage image = fbo.toImage(); // TOO LONG!
    static int count = 0;
    qDebug() << "Elapsed:" << et.elapsed() << count;

    ++count;

    //image.save(tr("test%1.png").arg(count++));

    return oldNode;
}

void Grabber::scheduleWindowChange(QQuickWindow *window)
{
    disconnect(m_updateConnection);
    if (window) {
        m_updateConnection = connect(window, &QQuickWindow::afterRendering,
                                     this, &Grabber::scheduleUpdate);
    }
}

void Grabber::scheduleUpdate()
{
    if (!m_running)
        update();
    else
        m_running = false;
}

Юзать как то так:

import QtQuick 2.9
import QtQuick.Window 2.2

import com.semsotec.horex 1.0

Window {
    visible: true
    width: 800
    height: 600
    title: qsTr("Hello World")

    Item {
        id: root
        anchors.fill: parent

        Rectangle {
            width: 200
            height: 200
            color: "red"

            Behavior on x {
                NumberAnimation { from: 0; to: 800; duration: 2000; loops: Animation.Infinite }
            }
            Component.onCompleted: x = 100

            Text {
                id: name
                text: qsTr("text")
            }
        }

        Rectangle {
            y: root.height / 1.5
            width: 20
            height: 20
            color: "green"

            Behavior on x {
                NumberAnimation { from: 800; to: 0; duration: 500; loops: Animation.Infinite }
            }
            Component.onCompleted: x = 100
        }
    }

    Grabber {
        sourceItem: root
    }
}

В данном случае у меня fbo.toImage() тратит ~8 мсек при размере окна 800х600, и ~30 мсек при 1920х1080 (подразумевается, что захватываемая Item имеет размер на все окно). Т.е. получаются результаты около 120 fps и 30 fps соответственно, быстрее не получается. Я на винде проверял пока что только, но нужно будет сделать это для андройда, и, думаю, там результаты будут похуже.

Все-равно получается как-то убого, и не понятно как прикрутить QQuickRenderControl чтобы граббить нужный айтем... Везде в примерах (а имеется только один пример в examples) все не то... блин...

kuzulis ★★
()
Последнее исправление: kuzulis (всего исправлений: 2)
Ответ на: комментарий от kuzulis

Зачем ты мне у меня скопипащенный код показываешь? :) Мониторить перерисовку надо подписавшись на сигнал QQuickWindow::beforeSynchronizing() Прекрасный пример использования QQuickRenderControl есть в видосе, который я кидал выше. Готового решения нету, да.

зы а не с двагиса ли ты?

fluorite ★★★★★
()
Ответ на: комментарий от fluorite

Зачем ты мне у меня скопипащенный код показываешь? :)

Потому что немного окультурил его и упростил.

Мониторить перерисовку надо подписавшись на сигнал QQuickWindow::beforeSynchronizing()

Почему?

Прекрасный пример использования QQuickRenderControl есть в видосе

Ну там немного не то.

зы а не с двагиса ли ты?

нет

kuzulis ★★
()
Ответ на: комментарий от kuzulis

Блин, только что заметил, что этот код рендерит айтемы в обратном порядке их «глубины».. т.е самый верхний айтем будет отрендерен как самый нижний.. елки зеленые... как это все достало.. :(

kuzulis ★★
()
Ответ на: комментарий от kuzulis

Я даже баг QTBUG-65187 создал на всякий случай, может кто поможет разобраться.

В качестве бреда — а что если взять Qt3D

Охх.. слишком много зависимостей уже, да и лень разбираться. Попробую пока что просто зделать «реверс» узлов. т.е. поменяю местами все ноды (последние зделаю первыми и наоборот). По идее должно помочь.. хотя хз.

kuzulis ★★
()
Последнее исправление: kuzulis (всего исправлений: 1)
Ответ на: комментарий от kuzulis

Попробую пока что просто зделать «реверс» узловю

Вроде пока работает (коряво, конечно). Сначала, перед отрисовкой, рекурсивно меняю местами всех детей у каждого узла, а потом, после отрисовки, восстанавливаю порядок узлов как было.

kuzulis ★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.