TaskTree Image Scaling | Qt TaskTree | Qt 6.11.1
Description: Demonstrates how to run For loop iterations in parallel to asynchronously download and scale images using TaskTree.

- Qt 6.11
- Qt TaskTree
- Qt TaskTree Examples
- TaskTree Image Scaling
- Qt 6.11.1 Reference Documentation

# TaskTree Image Scaling

Demonstrates how to run For loop iterations in parallel to asynchronously download and scale images using TaskTree.

This example shows how to use Qt TaskTree classes to download a collection of images from the network and scale them without blocking the UI. It demonstrates how to use the For element to run subtasks in a parallel loop over QList items, and how to use the Storage element to share data between tasks.

This example provides an alternative implementation for the image scaling example in Qt Concurrent.

#### Application workflow

When the user presses the Add URLs button, the following method runs:

         DownloadDialog dialog(this);
         if (dialog.exec() != QDialog::Accepted)
             return;

         const QList<QUrl> urls = dialog.getUrls();
         initLayout(urls.size());

The method runs `DownloadDialog`. When the user accepts the dialog, the method gets the list of selected URLs, clears the displayed images, and prepares a grid for new images by calling `initLayout(urls.size())`.

For each item in the `urls` list, the app:

- Downloads the image for a given address using QNetworkReplyWrapper and stores the received QByteArray data locally.
- Uses QThreadFunction to convert the QByteArray data to a QImage and scales it to a thumbnail in a separate thread.
- Displays the resulting thumbnail in the grid.

To iterate over the `urls` list, the app uses an `iterator` element of ListIterator type. The QByteArray data received after downloading is stored in the `storage` element of Storage<QByteArray> type, which is used to pass the data to the scale function.

         const ListIterator iterator(urls);
         const Storage<QByteArray> storage;

The `recipe` that describes the workflow looks like this:

         const Group recipe = For (iterator) >> Do {
             finishAllAndSuccess,
             parallel,
             onGroupSetup(onRootSetup),
             Group {
                 storage,
                 QNetworkReplyWrapperTask(onDownloadSetup, onDownloadDone),
                 QThreadFunctionTask<QImage>(onScaleSetup, onScaleDone)
             },
             onGroupDone(onRootDone)
         };
         taskTreeRunner.start(recipe);

Here is a summary of all elements used in the recipe:

Element | Purpose
--- | ---
For | Top level loop iterating over a list of `urls`.
Do | Describes the body of each iteration.
finishAllAndSuccess | Instructs the task tree to continue running despite errors and report success when finished.
parallel | Causes all direct children of the Do body to run in parallel, including all iterations of the top-level For element. This means that all iterations will start simultaneously.
onGroupSetup(onRootSetup) | Invokes `onRootSetup` handler when the first iteration is about to be executed.
onGroupDone(onRootDone) | Invokes `onRootDone` handler when all the iterations are finished.
Group | A subgroup, executed for each iteration. By default, all its children execute in sequence.
`storage` | Instructs the running task tree to instantiate the underlying QByteArray instance when entering the subgroup. This means the running task tree instantiates a separate QByteArray copy for each iteration.
QNetworkReplyWrapperTask | The first asynchronous task that runs in sequence for each iteration. Starts downloading the QByteArray data for a given `url`. When the download is about to start, the `onDownloadSetup` is invoked to setup the input `url` for download. When it's finished, the `onDownloadDone` is invoked. In case of successful execution, the downloaded QByteArray data is stored inside `storage` element.
QThreadFunctionTask<QImage> | The second asynchronous task executed in sequence for each iteration. It's executed only after successful execution of the first task. Executes a function in a separate thread, passing the stored QByteArray data of the downloaded image. When the function is about to start, the `onScaleSetup` is invoked to setup the QByteArray data for scaling. When it's finished, the `onScaleDone` is invoked. In case of successful execution it displays the thumbnail in a grid.

#### Update the status bar

The following shows how all handlers are implemented:

         const auto onRootSetup = [this] {
             statusBar->showMessage(tr("Downloading and Scaling..."));
             cancelButton->setEnabled(true);
         };
         const auto onRootDone = [this](DoneWith result) {
             const QString message = result == DoneWith::Cancel ? tr("Canceled.") : tr("Finished.");
             statusBar->showMessage(message);
             cancelButton->setEnabled(false);
         };

When the first iteration starts, the `onRootSetup` handler updates the status bar message and enables the Cancel button. When the last iteration finishes, the `onRootDone` handler updates the status bar message and disables the Cancel button.

#### Download and scale images

Each iteration runs in parallel with others and consists of two sequential tasks. The first task in each iteration is QNetworkReplyWrapperTask. Here is how its setup and done handlers are implemented:

         const auto onDownloadSetup = [this, iterator](QNetworkReplyWrapper &task) {
             task.setNetworkAccessManager(&qnam);
             task.setRequest(QNetworkRequest(*iterator));
         };
         const auto onDownloadDone = [this, storage, iterator](const QNetworkReplyWrapper &task,
                                                               DoneWith result) {
             const int it = iterator.iteration();
             if (result == DoneWith::Success)
                 *storage = task.reply()->readAll();
             else if (result == DoneWith::Error)
                 labels[it]->setText(tr("Download\nError.\nCode: %1.").arg(task.reply()->error()));
             else
                 labels[it]->setText(tr("Canceled."));
         };

The `onDownloadSetup` handler runs when QNetworkReplyWrapper is about to start. It sets up the network access manager and network request. The handler captures the `iterator`, which gives access to the current address from the `urls` list.

The `onDownloadDone` handler runs when QNetworkReplyWrapper finishes. It captures the `storage` and `iterator`. If the task succeeds, it stores the received QByteArray data in `storage`. The task tree creates a separate QByteArray instance for each iteration. Dereferencing `storage` gives access to the QByteArray instance for the current iteration. If the task fails or is canceled, the handler shows the error message for the label immediately. The index of the current iteration is returned by `iterator.iteration()`.

The second task in each iteration is QThreadFunctionTask<QImage>. Its setup and done handlers are as follows:

         const auto onScaleSetup = [storage](QThreadFunction<QImage> &task) {
             task.setThreadFunctionData(&scale, *storage);
         };
         const auto onScaleDone = [this, iterator](const QThreadFunction<QImage> &task,
                                                   DoneWith result) {
             const int it = iterator.iteration();
             if (result == DoneWith::Success)
                 labels[it]->setPixmap(QPixmap::fromImage(task.result()));
             else if (result == DoneWith::Error)
                 labels[it]->setText(tr("Image\nData\nError."));
             else
                 labels[it]->setText(tr("Canceled."));
         };

Inside `onScaleSetup`, the handler schedules a call to the `scale` function, passing the QByteArray data received from the first task as an argument. This is done by dereferencing the captured `storage`. The `scale` function runs in a separate thread.

The `onScaleDone` handler runs when the `scale` function finishes. `onScaleDone` is called from the main thread. If scaling succeeds, the handler assigns the scaled image to the appropriate label. Otherwise, the handler shows an error message.

The following shows how the `scale` function is implemented:

     static void scale(QPromise<QImage> &promise, const QByteArray &data)
     {
         const auto image = QImage::fromData(data);
         if (image.isNull())
             promise.future().cancel();
         else
             promise.addResult(image.scaled(100, 100, Qt::KeepAspectRatio));
     }

The function takes a QPromise to the result type as an argument. The `promise` is used to return the final result or to issue an error by canceling the promise's future.

#### Running the Example

To run the example from Qt Creator, open the Welcome mode and select the example from Examples. For more information, see Qt Creator: Tutorial: Build and run.

Example project @ code.qt.io