Qt-based BB10 API Examples Documentation

Contents

Image Loader Example

Files:

Description

The Image Loader example demonstrates how to offload work intensive tasks into seperate work threads, and than report show their results asynchronously.

Overview

In this example we'll learn how to use QNetworkAccessManager and QThread to download an image file asynchronously from the network and then offload the time-consuming task of converting the raw data into a QImage and scaling it down to a proper size into a separated worker thread.

The actual code for downloading and processing the images is encapsulated inside the ImageLoader class. For each image we'll have one instance of this class and all these instances are stored inside a QListDataModel to make them easily accessible to the UI.

The UI

When the application is started, a Button is shown at the center of the screen. If the user clicks on it, the Button is hidden and a ListView is shown instead which covers the complete screen. Additionally the loadImages() method is invoked on the App object, which has been exported to QML under the name '_app'.

    // The button to start the loading of the images
    Button {
        horizontalAlignment: HorizontalAlignment.Center
        verticalAlignment: VerticalAlignment.Center

        text: qsTr("Load images")

        onClicked: {
            _app.loadImages()
            visible = false
            listView.visible = true
        }
    }

The 'model' property of the App object is bound against the 'dataModel' property of the ListView, so each ImageLoader from the model is represented by one item in the ListView.

    // The ListView that shows the progress of loading and result images
    ListView {
        id: listView

        horizontalAlignment: HorizontalAlignment.Fill
        verticalAlignment: VerticalAlignment.Fill

        visible: false

        dataModel: _app.model

Since we want to show an ActivityIndicator while the image is downloaded and processed, an ImageView when the image is finally available or a Label that shows an error message if any occurs, we have to implement our own ListItemComponent.

We simply put the three controls into a Container and change their 'visible' property depending on the 'loading' and 'label' property of the ImageLoader.

    listItemComponents: ListItemComponent {
        type: ""
        Container {
            preferredHeight: 500
            preferredWidth: 768

            layout: DockLayout {}

            // The ActivityIndicator that is only active and visible while the image is loading
            ActivityIndicator {
                horizontalAlignment: HorizontalAlignment.Center
                verticalAlignment: VerticalAlignment.Center
                preferredHeight: 300

                visible: ListItemData.loading
                running: ListItemData.loading
            }

            // The ImageView that shows the loaded image after loading has finished without error
            ImageView {
                horizontalAlignment: HorizontalAlignment.Fill
                verticalAlignment: VerticalAlignment.Fill

                image: ListItemData.image
                visible: !ListItemData.loading && ListItemData.label == ""
            }

            // The Label that shows a possible error message after loading has finished
            Label {
                horizontalAlignment: HorizontalAlignment.Center
                verticalAlignment: VerticalAlignment.Center
                preferredWidth: 500

                visible: !ListItemData.loading && !ListItemData.label == ""
                text: ListItemData.label
                multiline: true
            }
        }
    }

The App class

The App class is the central class in this application which loads the UI and provides the interaction between C++ and QML scope throught the 'model' property and the invokable loadImages() method.

    class App : public QObject
    {
        Q_OBJECT

        // The model that contains the progress and image data
        Q_PROPERTY(bb::cascades::DataModel* model READ model CONSTANT)

    public:
        App(QObject *parent = 0);

        // This method is called to start the loading of all images.
        Q_INVOKABLE void loadImages();

    private:
        // The accessor method for the property
        bb::cascades::DataModel* model() const;

        // The model that contains the progress and image data
        bb::cascades::QListDataModel<QObject*>* m_model;
    };

Inside the constructor the model is created and filled with 10 ImageLoader objects, each responsible to load a different image from wikimedia.org. Since we want to access the properties (e.g. 'loading' and 'label') of the ImageLoader objects in QML, we also have to register this type to QML.

At the end we load the UI from the main.qml file.

    App::App(QObject *parent)
        : QObject(parent)
        , m_model(new QListDataModel<QObject*>())
    {
        // Register custom type to QML
        qmlRegisterType<ImageLoader>();

        m_model->setParent(this);

        // Fill the model with data
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/6/62/Peace_riding_in_a_triumphal_chariot_Bosio_Carrousel_-_2012-05-28.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/a/af/Crepuscular_rays_with_reflection_in_GGP.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/2/2a/Anodorhynchus_hyacinthinus_-Hyacinth_Macaw_-side_of_head.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/2/29/Bataille_Waterloo_1815_reconstitution_2011_cuirassier.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/e/ec/Armadillo_Aerospace_Pixel_Hover.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/f/f5/A_sculpture_at_the_entrance_to_the_palace_of_Versailles.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/6/6d/Firehole_river_at_Upper_Geyser_Basin-2008-june.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/7/7c/Peugeot_206_WRC.jpg", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/9/97/Toda_Hut.JPG", this));
        m_model->append(new ImageLoader("http://upload.wikimedia.org/wikipedia/commons/d/dc/Marriott_Center_1.JPG", this));

        // Create the UI
        QmlDocument* qml = QmlDocument::create("asset:///main.qml").parent(this);
        qml->setContextProperty("_app", this);

        AbstractPane *root = qml->createRootObject<AbstractPane>();
        Application::instance()->setScene(root);
    }

When the user clicks on the 'Load images' button in the UI, the loadImages() method is invoked. Inside this method we simply iterate over the ImageLoader objects inside the model and call their load() method to trigger the download of the image from the network.

    void App::loadImages()
    {
        // Call the load() method for each ImageLoader instance inside the model
        for (int row = 0; row < m_model->size(); ++row) {
            qobject_cast<ImageLoader*>(m_model->value(row))->load();
        }
    }

The ImageLoader class

The ImageLoader class encapsulates the loading and processing of the images. The current state is made available to the UI through the 'loading' property, any error message through 'label' and the final image data through the 'image' property.

To download the image the QNetworkAccessManager class is used and afterwards the raw image data are converted and scaled inside a thread. Thread contexts are represented by the QThread class.

    class ImageLoader : public QObject
    {
        Q_OBJECT

        Q_PROPERTY(QVariant image READ image NOTIFY imageChanged)
        Q_PROPERTY(QString label READ label NOTIFY labelChanged)

        Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged)

    public:
        /*
         * Creates a new image loader.
         *
         * @param imageUrl The url to load the image from.
         */
        ImageLoader(const QString &imageUrl, QObject* parent = 0);

        /*
         * Destroys the image loader.
         */
        ~ImageLoader();

        /*
         * Loads the image.
         */
        void load();

    Q_SIGNALS:
        // The change notification signals of the properties
        void imageChanged();
        void labelChanged();
        void loadingChanged();

    private Q_SLOTS:
        /*
         * Response handler for the network operation.
         */
        void onReplyFinished();

        /*
         * Response handler for the image process operation.
         */
        void onImageProcessingFinished(const QImage &image);

    private:
        // The accessor methods of the properties
        QVariant image() const;
        QString label() const;
        bool loading() const;

        // The property values
        bb::cascades::Image m_image;
        QString m_label;
        bool m_loading;

        // The URL of the image that should be loaded
        QString m_imageUrl;

        // The thread context that processes the image
        QPointer<QThread> m_thread;
    };

Inside the constructor only the member variables are initialized.

    ImageLoader::ImageLoader(const QString &imageUrl, QObject* parent)
        : QObject(parent)
        , m_loading(false)
        , m_imageUrl(imageUrl)
    {
    }

Inside the destructor we check whether the worker thread still exists. If that's the case, we wait for it to finish execution.

    ImageLoader::~ImageLoader()
    {
        if (m_thread)
            m_thread->wait();
    }

The load() method is called to start the download operation. Inside this method we first change the value of the 'loading' property to signal that we have started our work. Afterwards the QNetworkAccessManager object is created and a GET request with the image URL is issued. We connect the finished() signal of the returned QNetworkReply to our custom onReplyFinished() slot, so that we get informed when the download is done.

    void ImageLoader::load()
    {
        m_loading = true;
        emit loadingChanged();

        QNetworkAccessManager* netManager = new QNetworkAccessManager(this);

        const QUrl url(m_imageUrl);
        QNetworkRequest request(url);

        QNetworkReply* reply = netManager->get(request);
        connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished()));
    }

Inside the onReplyFinished() slot we check whether an error occurred. If that's the case we fill the 'label' property accordingly and signal that we have finished loading.

If the download was successful, we read the raw image data from the QNetworkReply and store them temporarily. In the next step a new ImageProcessor object is created. This one will convert the raw image data into a QImage object and scale it down to a proper size. However since this operation can take a long time, we want to offload it into the worker thread.

So we create a new QThread instance and move the ImageProcessor object to the worker thread. Moving a QObject based object to a QThread means that the object is associated with this thread and further slot invocations will be executed inside this thread (see http://qt-project.org/doc/qt-4.8/threads-qobject.html)

The QThread object will emit the started() signal once the thread context is set up. We connect this signal against the start() slot of the ImageProcessor object, so that this code will be executed inside the thread context. When the QThread has finished the execution of the thread context, it emits the finished() signal. We connect it against the QThread's own deleteLater() slot, so that it is deleted automatically.

When the ImageProcessor has finished its work, it emits the finished(QImage) signal. We connect this signal against two slots. Once against our custom onImageProcessingFinished() slot, where we use the converted and scaled image, and once against the quit() slot of the QThread to trigger its termination.

In the last step we trigger the start of the QThread object.

    void ImageLoader::onReplyFinished()
    {
        QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());

        QString response;
        if (reply) {
            if (reply->error() == QNetworkReply::NoError) {
                const int available = reply->bytesAvailable();
                if (available > 0) {
                    const QByteArray data(reply->readAll());

                    // Setup the image processing thread
                    ImageProcessor *imageProcessor = new ImageProcessor(data);
                    m_thread = new QThread(this);

                    // Move the image processor to the worker thread
                    imageProcessor->moveToThread(m_thread);

                    // Invoke ImageProcessor's start() slot as soon as the worker thread has started
                    connect(m_thread, SIGNAL(started()), imageProcessor, SLOT(start()));

                    // Delete the worker thread automatically after it has finished
                    connect(m_thread, SIGNAL(finished()), m_thread, SLOT(deleteLater()));

                    /*
                     * Invoke our onProcessingFinished slot after the processing has finished.
                     * Since imageProcessor and 'this' are located in different threads we use 'QueuedConnection' to
                     * allow a cross-thread boundary invocation. In this case the QImage parameter is copied in a thread-safe way
                     * from the worker thread to the main thread.
                     */
                    connect(imageProcessor, SIGNAL(finished(QImage)), this, SLOT(onImageProcessingFinished(QImage)), Qt::QueuedConnection);

                    // Terminate the thread after the processing has finished
                    connect(imageProcessor, SIGNAL(finished(QImage)), m_thread, SLOT(quit()));

                    m_thread->start();
                }
            } else {
                m_label = tr("Error: %1 status: %2").arg(reply->errorString(), reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString());
                emit labelChanged();

                m_loading = false;
                emit loadingChanged();
            }

            reply->deleteLater();
        } else {
            m_label = tr("Download failed. Check internet connection");
            emit labelChanged();

            m_loading = false;
            emit loadingChanged();
        }
    }

Inside the onImageProcessingFinished() slot, we convert the QImage into a bb::Image object, since the ImageView in the UI can only use the latter as input. The bb::Image object is used as value for the 'image' property.

Furthermore we clear the content of the 'label' property and change the 'loading' property to 'false' to signal that we have finished the operation.

    void ImageLoader::onImageProcessingFinished(const QImage &image)
    {
        const QImage swappedImage = image.rgbSwapped();
        const bb::ImageData imageData = bb::ImageData::fromPixels(swappedImage.bits(), bb::PixelFormat::RGBX, swappedImage.width(), swappedImage.height(), swappedImage.bytesPerLine());

        m_image = bb::cascades::Image(imageData);
        emit imageChanged();

        m_label.clear();
        emit labelChanged();

        m_loading = false;
        emit loadingChanged();
    }

The ImageProcessor class

The ImageProcessor class encapsulates the conversion of the raw image data into a QImage and the following scaling. Its API is designed to be used inside a thread by providing a start() slot, which is invoked as a result of the QThread's started() signal, and a finished() signal, which is connected against the QThread's quit() slot to stop the event loop inside the thread.

The raw image data are passed in via the constructor and the scaled down QImage object is passed back as parameter of the finished() signal.

    class ImageProcessor : public QObject
    {
        Q_OBJECT

    public:
        /*
         * Creates a new image processor.
         *
         * @param imageData The raw image data.
         * @param parent The parent object.
         */
        ImageProcessor(const QByteArray &imageData, QObject *parent = 0);

    public Q_SLOTS:
        /*
         * Starts the actual operation.
         */
        void start();

    Q_SIGNALS:
        /*
         * This signal is emitted after the operation has finished.
         *
         * @param image The processed image.
         */
        void finished(const QImage &image);

    private:
        // The raw image data
        QByteArray m_data;
    };

Inside the constructor we just store the raw image data in a member variable, so that we have access to it later on in the start() slot.

    ImageProcessor::ImageProcessor(const QByteArray &imageData, QObject *parent)
        : QObject(parent)
        , m_data(imageData)
    {
    }

The start() slot is the part of the class where the actual work happens. We first convert the raw image data into a QImage object by using the loadFromData() convenience method. QImage supports a wide range of image types and support for custom types can be added by implementing new plugins. In our example however we only load JPEG images, which QImage supports out of the box.

In the second step we scale down the image to 768x500 pixels to make them fit inside the ListView.

In the last step we emit the finished signal to notify that we are done.

    void ImageProcessor::start()
    {
        QImage image;

        image.loadFromData(m_data);

        image = image.scaled(768, 500, Qt::KeepAspectRatioByExpanding);

        // Image processing goes here, example could be adding water mark to the downloaded image

        emit finished(image);
    }

Note: Since signal/slot connections across thread boundaries are no direct method calls (unlike signal/slot connections in the same thread), but are based on event delivery, passing parameters in the signal is thread-safe, because a copy of the parameters is created and the copy is sent to the receiver thread.