Qt: Как объединить 2 QQuickItems в один перед сохранением в png

Из этого обсуждения в StackOverflow я вполне могу сохранить изображение из элемента QML в файл как png/jpeg.

Как я могу наложить или объединить два разных слоя qml и объединить их в один, чтобы сохранить его в формате png/jpeg?

Примечание. Я могу сохранить один файл QQuickItem. Просто нужно знать, как наложить 2 QQuickItems


person TheWaterProgrammer    schedule 10.07.2017    source источник


Ответы (2)


Просто сделайте два объекта qml дочерними элементами корня Item, а затем возьмите этот корневой элемент, и он захватит все его содержимое.

Просто убедитесь, что корневой элемент достаточно велик, чтобы вместить дочерние элементы, а дочерние элементы не находятся в отрицательном пространстве, потому что он захватит только то, что находится внутри посадочного места корневого элемента.

Вы также можете выполнять композицию вручную из C++ или даже QML.

Проблема, описанная в вашем комментарии, заключается в том, что вы не можете перемещать вещи, так что вы можете сделать? Вместо исходных объектов QML в качестве родителей одного и того же корня вы можете иметь два элемента Image, затем вы захватываете элемент A и устанавливаете результат захвата в качестве источника изображения A, затем делаете то же самое для элемента B и, наконец, , вы захватываете корневой элемент, который захватывает два изображения вместе.

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

ApplicationWindow {
  id: window
  visible: true
  width: 640
  height: 480

  Rectangle {
    id: s1
    visible: false
    width: 200
    height: 200
    color: "red"
  }
  Rectangle {
    id: s2
    visible: false
    width: 200
    height: 200
    color: "blue"
  }

  Row {
    id: joiner
    visible: false
    Image { id: t1 }
    Image { id: t2 }
  }

  Image {
    id: result
    y: 200
  }

  Timer {
    id: finish
    interval: 10
    onTriggered: joiner.grabToImage(function(res) {result.source = res.url})
  }

  Component.onCompleted: {
    s1.grabToImage(function(res) {t1.source = res.url})
    s2.grabToImage(function(res) {t2.source = res.url; finish.start() })
  }
}

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

Еще проще, вы можете использовать этот изящный маленький помощник, чтобы быстро объединить любое количество элементов в одном изображении:

  Item {
    id: joinHelper
    visible: false
    property Component ic: Image { }
    property var cb: null

    Row { id: joiner }

    Timer {
      id: finish
      interval: 100
      onTriggered: joiner.grabToImage(joinHelper.cb)
    }

    function join(callback) {
      if (arguments.length < 2) return // no items were passed
      var i
      if (joiner.children.length) { // clean previous captures
        for (i = 0; i < joiner.children.length; ++i) {
          joiner.children[i].destroy()
        }
      }
      cb = callback // set callback for later
      for (i = 1; i < arguments.length; ++i) { // for every item passed
        var img = ic.createObject(joiner) // create empty image
        // need to capture img by "value" because of JS scoping rules
        // otherwise you end up with only one image - the final one
        arguments[i].grabToImage(function(temp){ return function(res){temp.source = res.url}}(img))
      }
      finish.start() // trigger the finishing step
    }
  }

И вы используете это так:

joinHelper.join(function(res) { result.source = res.url }, s1, s2)

Он по-прежнему использует строку, но вы можете легко настроить ее для создания собственного макета. Он работает, передавая окончательный обратный вызов и все элементы, которые вы хотите захватить, внутри он создает изображение для каждого элемента, помещает их в контейнер, а затем запускает таймер завершения.

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

Я также аннотировал большинство вещей, чтобы их было легче понять.

person dtech    schedule 10.07.2017
comment
это звучит как вариант, но в моем приложении довольно много слоев qml. Я не могу позволить себе роскошь просить всех поместить компоненты qml для сохранения в один слой. Есть ли другой способ С++ для наложения разных слоев QML? - person TheWaterProgrammer; 10.07.2017
comment
не понял свежей правки, которую вы сделали. у вас есть пример кода, который иллюстрирует, что вы имеете в виду? Вы имеете в виду взять QImages из захватов и объединить их? Мне все еще нужен пример кода, чтобы проиллюстрировать, что вы имеете в виду. спасибо за ваше внимание на этом до сих пор. ценить - person TheWaterProgrammer; 10.07.2017
comment
позвольте мне понять и попробовать образец, который вы указали в своем ответе. - person TheWaterProgrammer; 10.07.2017
comment
Большое спасибо, пока. Последний вопрос: после того, как все будет сделано, я, очевидно, хочу передать окончательный результат методу C++, который, конечно же, является определенным слотом. Я могу сделать это в Timer: onTriggered? Что ты посоветуешь ? - person TheWaterProgrammer; 10.07.2017
comment
@ HAL9000-Kernel, то, что происходит с конечным результатом, определяется обратным вызовом, который вы передаете, замените result.source = res.url тем, что вы хотите сделать с результатом захвата. - person dtech; 10.07.2017
comment
Но да, вы можете изменить его, чтобы пропустить обратный вызов и вместо этого написать обратный вызов в обработчике таймера, если вы хотите ограничить его выполнением одной операции. То, как я это написал, дает вам возможность иметь произвольный обработчик результатов захвата. Если вы удаляете параметр обратного вызова, обязательно измените код, чтобы цикл параметров выполнялся с нуля, а не пропускался один. - person dtech; 10.07.2017
comment
мой слот для сохранения на стороне С++ точно такой, как определено в этой ссылке, которую я упомянул в вопросе. он принимает QQuickItem. Любое предложение о том, как я могу добавить это в качестве обратного вызова в Timer? - person TheWaterProgrammer; 10.07.2017
comment
Мой слот Capturer::save(QQuickItem *item) - person TheWaterProgrammer; 10.07.2017
comment
Затем вы удалите joiner.grabToImage(joinHelper.cb) и передадите joiner save(). Это означает, что вы можете удалить свойство callback и свойство cb, которое его содержит. Вам все еще нужно использовать таймер, чтобы дать всему этому время для завершения. - person dtech; 10.07.2017
comment
Я заставил вашу логику работать до того момента, когда я вижу оба изображения в сохраненном png в моей файловой системе. но похоже, что логика соединения размещает изображения рядом. Не перекрывается, как я хотел. Может ли это быть из-за элемента Row, который вы использовали? - person TheWaterProgrammer; 10.07.2017
comment
может быть, я неправильно выразился - мне нужно было, чтобы изображения накладывались друг на друга. - person TheWaterProgrammer; 10.07.2017
comment
Просто замените строку на элемент - person dtech; 10.07.2017
comment
Действительно невероятно. Оно работает. У меня есть проблема с неправильным наложением из-за того, что мои элементы qml имеют отдельные значения ширины и высоты. Но наложение работает, по крайней мере, для начала. бесконечно благодарен - person TheWaterProgrammer; 10.07.2017
comment
Вы можете использовать width: childrenRect.width; height: childrenRect.height в Item, чтобы он автоматически масштабировался, чтобы соответствовать всем дочерним элементам. - person dtech; 10.07.2017
comment
childrenRect.width; height выглядит очень умным способом сделать это. похоже что-то с привязкой не так. один из моих слоев qml размещает обработанное изображение Open GL, и мне нужно исправить привязку к нему. в любом случае спасибо - person TheWaterProgrammer; 10.07.2017
comment
следует ли учитывать размер наложенных слоев qml где-то в вашем коде? к моим слоям qml применен комплекс width, height & anchoring. Я все равно принял ваш ответ и извините за такое количество вопросов. на всякий случай, если у вас есть предложение. - person TheWaterProgrammer; 10.07.2017
comment
Давайте продолжим это обсуждение в чате. - person TheWaterProgrammer; 10.07.2017

Этот вопрос, по-видимому, тесно связан с этим

Решение состоит в том, чтобы визуализировать рассматриваемые Item в текстуру второго элемента, который вам не нужно отображать на экране. Вы можете скомпоновать эти Item по своему усмотрению, добавив несколько ShaderEffectSource в качестве дочерних элементов, расположив их относительно друг друга, как вам нравится, и установить их источники на Item, которые вы хотите захватить в изображение.

Затем вы берете Item для изображения.

Общий пример, который предоставляет функцию для захвата списка Item в одно изображение, где каждый Item накладывается друг на друга с непрозрачностью 0,2 каждый:

import QtQuick 2.0

Rectangle {
    id: root
    visible: false
    color: 'white'
    function grabMultipleToImage(url, objects) {
        imgRep.url = url
        width = Math.max.apply(root, objects.map(function(e) { return e.width }))
        height = Math.max.apply(root, objects.map(function(e) { return e.height }))
        imgRep.ready = 0
        imgRep.model = objects
    }

    Repeater {
        id: imgRep
        onReadyChanged: {
            if (ready > 0 && ready === model.length) {
                console.log(root.width, root.height, imgRep.url)
                root.grabToImage(function (res) { res.saveToFile(imgRep.url); model = null })
            }
        }


        property int ready: 0
        property string url
        delegate: ShaderEffectSource {
            sourceItem: modelData
            width: modelData.width
            height: modelData.height
            opacity: 0.2
            live: false
            Component.onCompleted: { imgRep.ready++ }
        }
    }
}

Использование этого будет таким:

import QtQuick 2.7
import QtQuick.Controls 2.0

ApplicationWindow {
    id: myWindow
    visible: true
    width: 600
    height: 600
    color: 'white'

    Button {
        text: 'grab'
        onClicked: {
            test.grabMultipleToImage('testimg.jpg', [rect1, rect2, rect3])
        }
    }

    ImageGrabber {
        id: test

    }

    Rectangle {
        x: 100
        y: 205
        id: rect1
        color: 'blue'
        width: 10
        height: 20
    }
    Rectangle {
        x: 250
        y: 12
        id: rect2
        color: 'green'
        width: 20
        height: 30
    }
    Rectangle {
        x: 100
        y: 100
        id: rect3
        color: 'red'
        width: 100
        height: 5
    }
}

Но если вам нужно более сложное слияние, вы также можете создать этот объект вручную и получить его, когда захотите.

Не замеряя его, согласно примечанию из документации

Примечание : эта функция отображает элемент на поверхности вне экрана и копирует эту поверхность из памяти графического процессора в память процессора, что может быть весьма дорогостоящим. Для «живого» предварительного просмотра используйте слои или ShaderEffectSource.

это решение должно быть более эффективным, чем использование Images с ItemGrabResults в качестве источников, поскольку оно сохраняет материал в памяти графического процессора до тех пор, пока он не будет захвачен и сохранен.

person derM    schedule 10.07.2017
comment
спасибо за добавление этого ответа. Я пробую варианты здесь. это тоже похоже на простое решение - person TheWaterProgrammer; 10.07.2017
comment
но один предмет рядом с другим здесь не является намерением. это наложение изображений поверх другого, учитывая, что в каждом из них есть некоторый контент с прозрачным фоном. - person TheWaterProgrammer; 10.07.2017
comment
Изменено в соответствии с вашим комментарием. - person derM; 10.07.2017
comment
Я пытаюсь использовать ваше решение, заменив res.saveToFile(imgRep.url) вызовом моего слота C++, который принимает QQuickItem, например ImageCapture.save(res). - person TheWaterProgrammer; 10.07.2017
comment
res не будет QQuickItem. Это ItemGrabResult, который наследует QObject, но не QQuickItem. Если вам нужно QQuickItem вам нужно пройти, что в моем примере root в ImageGrabber.qml или test в моем main.qml. - person derM; 11.07.2017