Files:
The Image Loader example demonstrates how to offload work intensive tasks into seperate work threads, and than report show their results asynchronously.
In this example we'll learn how to use QNetworkAccessManager and QtConcurrent 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.
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 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 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 associated with the QFuture 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(); 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 status watcher QFutureWatcher<QImage> m_watcher; };
Inside the constructor only the member variables are initialized.
ImageLoader::ImageLoader(const QString &imageUrl, QObject* parent) : QObject(parent) , m_loading(false) , m_imageUrl(imageUrl) { }
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); bool ok = connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished())); Q_ASSERT(ok); Q_UNUSED(ok); }
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 QFuture instance by calling QtConcurrent::run(), where we pass the ImageProcessor reference and its member function that is to be executed in a separate thread. Afterwards, we assign this QFuture to the QFutureWatcher in order to monitor thread status indirectly throught the QFuture, that is associated with the thread context, using signals and slots.
The QFutureWatcher object will emit the started() signal once it starts watching the QFuture. When the QFuture has finished, meaning the execution of the thread context is done, it emits the finished() signal which causes the connected method onImageProcessingFinished() to be executed.
When the ImageProcessor has finished its work, it returns the QImage instance. Which in turn causes a finished() signal to be emited by the QFutureWatcher, which in turn executes the onImageProcessingFinished method where we use the converted and scaled image.
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); QFuture<QImage> future = QtConcurrent::run(imageProcessor, &ImageProcessor::start); // Invoke our onProcessingFinished slot after the processing has finished. bool ok = connect(&m_watcher, SIGNAL(finished()), this, SLOT(onImageProcessingFinished())); Q_ASSERT(ok); Q_UNUSED(ok); // starts watching the given future m_watcher.setFuture(future); } } 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() { QImage swappedImage = m_watcher.future().result().rgbSwapped(); if(swappedImage.format() != QImage::Format_RGB32) { swappedImage = swappedImage.convertToFormat(QImage::Format_RGB32); } 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 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 QtConcurrent::run() method.
The raw image data are passed in via the constructor and the scaled down QImage object is passed back as the return value of the start() slot.
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. */ QImage start(); 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 return the processed QImage to notify that we are done.
QImage 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 return image; }