[QML] Visualizzare un flusso di immagini con QQuickImageProvider

In questo articolo mostrerò un esempio di come visualizzare un flusso di immagini da C++ a QML. Qt fornisce una classe, QQuickImageProvider, per l’esposizione d’immagini verso QML. Tuttavia, l’esempio e la documentazione di Qt sono frammentarie, manca un esempio realmente completo che vada dal provider fino al QML. Vediamo quindi di unire tutti i pezzi.

Lo schema che utilizzerò sarà questo:

La classe ImageSender sarà la classe delegata di fornire la sequenza di immagini. Per questo esempio sarà semplicemente un QObject che notificherà una nuova immagine presa da una cartella ogni tot milllisecondi. Sender e Provider sono separati in modo tale che il provider sia riutilizzabile a prescindere dalla sorgente di dati.

Definire il Provider

Veniamo adesso a ImageProvider. Dovrà possedere un metodo per settare la nuova immagine disponibile, un segnale per notificare verso QML che una nuova immagine è pronta e l’interfaccia di providing dell’immagine tramite QQuickImageProvider. Dato che QQuickImageProvider non eredita da QObject, questa non può inviare direttamente segnali e pertanto ImageProvider dovrà ereditare sia da QObject che da QQuickImageProvider, ricordando sempre che in caso di ereditarietà multipla QObject deve essere dichiarata per prima per potere generare correttamente i file moc.

#include <QObject>
#include <QImage>
#include <QQuickImageProvider>

class ImageProvider : public QObject, public QQuickImageProvider
{
   Q_OBJECT

public:
   ImageProvider(QObject* parent = nullptr);

   QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override;

   void setImage(const QImage& image);

signals:
   void imageUpdated();

private:
   QImage _image;
};
ImageProvider::ImageProvider(QObject* parent)
   : QObject(parent), QQuickImageProvider(QQuickImageProvider::Image)
{
}

QImage ImageProvider::requestImage(const QString& /*id*/, QSize* size, const QSize& requestedSize)
{
   if(size) *size = _image.size();

   if(requestedSize.width() > 0 && requestedSize.height() > 0 ){
      _image = _image.scaled(requestedSize.width(), requestedSize.height(), Qt::KeepAspectRatio);
   }

   return _image;
}

void ImageProvider::setImage(const QImage& image)
{
   if(!image.isNull()){
      _image = image;
      emit imageUpdated();
   }
}

Come prima cosa vediamo che nel costruttore di QQuickImageProvider è necessario specificare il tipo d’immagine che si andrà a fornire. Questa enumerazione indicherà quale metodo dovremo andare a reimplementare per fornire l’immagine. Andremo a fornire una QImage quindi il tipo scelto sarà QQuickImageProvider::Image e il metodo da implementare requestImage().

Il parametro id rappresenta l’identificativo dell’immagine che si sta richiedendo, nel caso ce ne fossero più disponibili. Il path della fonte del componente Image di QML avrà questa struttura

source: "image://provider/stringa"

dove image è il tipo di fonte richiesta, provider è il nome con cui viene registrato il componente del provider e dopo l’ultimo / sarà il parametro passato come id. Se si fornisce una sola immagine alla volta questo è irrilevante, ma supponiamo ad esempio di salvare un buffer con le ultime 10 immagini. In questo modo passando come id 1, 2, ecc… potremo richiedere facilmente richiedere l’immagine corrispondente a quell’indice.

Il parametro sizeRequested indica il parametro QML di Image sourceSize, per cui se questo è un valore valido l’immagine deve ritornare con quella dimensione. Size infine deve essere sempre settato alla grandezza originale dell’immagine, nel caso la dimensione non venga esplicitamente richiesta lato QML.

Il main

Il main è abbastanza autoesplicativo. Il provider va registrato come un un qualunque altro componente, mentre per il provider c’è un metodo specifico dell’engine, addImageProvider(). Una volta collegato il sender col provider il gioco è fatto. Si presenta così

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>

#include "image_provider.h"
#include "image_sender.h"

int main(int argc, char *argv[])
{
   QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
   QGuiApplication app(argc, argv);

   ImageSender sender;
   ImageProvider* provider = new ImageProvider;

   QQmlApplicationEngine engine;
   engine.rootContext()->setContextProperty("imageProvider", provider);
   engine.addImageProvider("stream", provider);

   QObject::connect(&sender, &ImageSender::imageReady, provider, &ImageProvider::setImage);
   sender.start(60);
   engine.load(url);

   return app.exec();
}

QML

Per collegare il provider è necessario impostare come fonte l’indirizzo del provider ad un elemento Image, stando attenti a disabilitare il caching.

    Image {
        id: image
        source: "image://stream/";

        anchors.fill: parent
        fillMode: Image.PreserveAspectFit
        cache: false
    }
}

L’indirizzo ha la forma come spiegato prima, tipo di ritorno del provider, nome del provider ed eventualmente una stringa di id. Siccome non stiamo utilizzando nessun id quello spazio può essere lasciato vuoto.

Adesso dobbiamo far sì che il componente Image ricarichi la nuova immagine. Il modo più semplice è quello di creare una funzione che setti nuovamente la fonte in modo tale da richiamare il caricamento dell’immagine, che conterrà però una nuova immagine fornita dal provider

function reload() {
  var oldSource = providerSource;
  source = "";
  source = oldSource;
}

Manca solo l’ultima cosa: collegare il segnale di immagine pronta del provider con la funzione reload() del componente Image. Per questo utilizzeremo un componente Connection:

    Connections {
        target: imageProvider
        function onImageUpdated() { image.reload() }
    }

Adesso è tutto pronto. Il file QML completo si presenta così

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    id: root
    width: 640
    height: 480
    visible: true
    title: qsTr("QML Image Provider")

    Connections {
        target: imageProvider
        function onImageUpdated() { image.reload() }
    }

    Image {
        id: image

        readonly property string providerSource: "image://stream/image";

        anchors.fill: parent
        fillMode: Image.PreserveAspectFit
        cache: false

        function reload() {
            var oldSource = providerSource;
            source = "";
            source = oldSource;
        }
    }
}

Avete notato che non ho inserito l’indirizzo del provider nella source, ma l’ho esposto tramite una property? Il motivo è che se al momento del caricamento dell’applicazione non ha pronta la prima immagine, si riceverà un errore a console di provider non pronto o di immagine non valida. In questo modo si garantisce che il primo set della fonte dell’immagine sia pronto quando ci sia effettivamente un’immagine pronta.

Tutti i pezzi sono al loro posto. Ed ecco il risultato

Soddisfacente, no?

Il sorgente completo del progetto può essere trovato su GitHub.

One Reply to “[QML] Visualizzare un flusso di immagini con QQuickImageProvider”

Lascia un commento