Sur cette page

Redimensionnement des images

Cet exemple montre comment télécharger et redimensionner des images de manière asynchrone.

Cet exemple montre comment utiliser les classes ` QFuture`, ` QPromise` et ` QFutureWatcher ` pour télécharger un ensemble d’images depuis le réseau et les redimensionner, sans bloquer l’interface utilisateur.

Images téléchargées présentées sous forme de grille, avec les boutons « Ajouter des URL » et « Annuler » ainsi que le statut « Terminé »

L'application comprend les étapes suivantes :

  1. Télécharger les images à partir de la liste d'URL spécifiée par l'utilisateur.
  2. Redimensionner les images.
  3. Afficher les images redimensionnées dans une disposition en grille.

Commençons par le téléchargement :

QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{

La méthode ` download() ` prend une liste d'URL et renvoie un ` QFuture`. Le ` QFuture ` stocke les données du tableau d'octets reçues pour chaque image téléchargée. Pour stocker les données dans le ` QFuture`, nous créons un objet ` QPromise ` et signalons qu'il a démarré pour indiquer le début du téléchargement :

    QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
    promise->start();
    ...
    return promise->future();
}

Le « future » associé à la « promise » est renvoyé à l’appelant.

Sans entrer dans les détails pour l’instant, notons que l’objet `promise` est encapsulé dans un ` QSharedPointer`. Cela sera expliqué plus tard.

Nous utilisons ` QNetworkAccessManager ` pour envoyer des requêtes réseau et télécharger des données pour chaque URL :

for (const auto &url : urls) {
    QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
    replies.push_back(reply);

Et c’est là que les choses deviennent intéressantes :

    QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
        if (promise->isCanceled()) {
            if (!promise->future().isFinished())
                promise->finish();
            return;
        }

        if (reply->error() != QNetworkReply::NoError) {
            if (!promise->future().isFinished())
                throw reply->error();
        }
        promise->addResult(reply->readAll());

        // Report finished on the last download
        if (promise->future().resultCount() == urls.size())
            promise->finish();
    }).onFailed([promise] (QNetworkReply::NetworkError error) {
        promise->setException(std::make_exception_ptr(error));
        promise->finish();
    }).onFailed([promise] {
        const auto ex = std::make_exception_ptr(
                    std::runtime_error("Unknown error occurred while downloading."));
        promise->setException(ex);
        promise->finish();
    });
}
    ...

Au lieu de nous abonner aux signaux de ` QNetworkReply` à l’aide de la méthode ` QObject::connect()`, nous utilisons ` QtFuture::connect()`. Elle fonctionne de manière similaire à ` QObject::connect()`, mais renvoie un objet ` QFuture `, qui devient disponible dès que le signal ` QNetworkReply::finished()` est émis. Cela nous permet d’associer des continuations et des gestionnaires d’échec, comme le montre l’exemple.

Dans la continuation associée via ` .then()` (), nous vérifions si l’utilisateur a demandé d’annuler le téléchargement. Si tel est le cas, nous arrêtons le traitement de la requête. En appelant la méthode ` QPromise::finish()` (), nous informons l’utilisateur que le traitement est terminé. Si la requête réseau s’est soldée par une erreur, nous levons une exception. L’exception sera gérée dans le gestionnaire d’échecs associé à l’aide de la méthode .onFailed(). Notez que nous disposons de deux gestionnaires d’échecs : le premier intercepte les erreurs réseau, le second toutes les autres exceptions levées pendant l’exécution. Les deux gestionnaires enregistrent l’exception dans l’objet promise (qui sera gérée par l’appelant de la méthode download() ) et signalent que le calcul est terminé. Notez également que, par souci de simplicité, en cas d’erreur, nous interrompons tous les téléchargements en cours.

Si la requête n’a pas été annulée et qu’aucune erreur ne s’est produite, nous lisons les données issues de la réponse réseau et les ajoutons à la liste des résultats de l’objet `promise` :

    ...
    promise->addResult(reply->readAll());

    // Report finished on the last download
    if (promise->future().resultCount() == urls.size())
        promise->finish();
    ...

Si le nombre de résultats stockés dans l’objet `promise` est égal au nombre d’ urls à télécharger, il n’y a plus de requêtes à traiter ; nous signalons donc également que la `promise` est terminée.

Comme mentionné précédemment, nous avons encapsulé la promesse dans un ` QSharedPointer`. Étant donné que l’objet `Promise` est partagé entre les gestionnaires associés à chaque réponse réseau, nous devons copier et utiliser cet objet à plusieurs endroits simultanément. C’est pourquoi nous utilisons un ` QSharedPointer `.

La méthode ` download() ` est appelée depuis la méthode ` Images::process `. Elle est invoquée lorsque l’utilisateur clique sur le bouton « Ajouter des URL »:

    ...
    connect(addUrlsButton, &QPushButton::clicked, this, &Images::process);
    ...

Après avoir effacé les éventuels résidus du téléchargement précédent, nous créons une boîte de dialogue afin que l’utilisateur puisse spécifier les URL des images à télécharger. En fonction du nombre d’URL spécifiées, nous initialisons la mise en page dans laquelle les images seront affichées et lançons le téléchargement. Le « future » renvoyé par la méthode ` download() ` est enregistré, afin que l’utilisateur puisse annuler le téléchargement si nécessaire :

void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        statusBar->showMessage(tr("Downloading..."));
    ...

Ensuite, nous associons une continuation pour gérer l’étape de redimensionnement. Nous y reviendrons plus tard :

downloadFuture
        .then([this](auto) {
            cancelButton->setEnabled(false);
            updateStatus(tr("Scaling..."));
            scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                       downloadFuture.results()));
        })
    ...

Nous associons ensuite les gestionnaires ` onCanceled()` et ` onFailed()` :

        .onCanceled([this] {
            updateStatus(tr("Download has been canceled."));
        })
        .onFailed([this](QNetworkReply::NetworkError error) {
            updateStatus(tr("Download finished with error: %1").arg(error));
            // Abort all pending requests
            abortDownload();
        })
        .onFailed([this](const std::exception &ex) {
            updateStatus(tr(ex.what()));
        })
    ...

Le gestionnaire associé via la méthode ` .onCanceled()` sera appelé si l'utilisateur a cliqué sur le bouton « Annuler »:

    ...
    connect(cancelButton, &QPushButton::clicked, this, &Images::cancel);
    ...

La méthode cancel() se contente d'annuler toutes les requêtes en attente :

void Images::cancel()
{
    statusBar->showMessage(tr("Canceling..."));

    downloadFuture.cancel();
    abortDownload();
}

Les gestionnaires associés via la méthode .onFailed() seront appelés si une erreur s’est produite au cours d’une des étapes précédentes. Par exemple, si une erreur réseau a été enregistrée dans la promesse lors de l’étape de téléchargement, elle sera propagée au gestionnaire qui prend QNetworkReply::NetworkError en argument.

Si la méthode ` downloadFuture ` n’est pas annulée et n’a signalé aucune erreur, la suite du calcul d’échelle est exécutée.

Comme la mise à l'échelle peut être gourmande en ressources de calcul et que nous ne voulons pas bloquer le thread principal, nous utilisons QtConcurrent::run() pour lancer l'étape de mise à l'échelle dans un nouveau thread.

scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                           downloadFuture.results()));

La mise à l'échelle étant lancée dans un thread distinct, l'utilisateur peut éventuellement décider de fermer l'application pendant que l'opération de mise à l'échelle est en cours. Pour gérer ces situations de manière élégante, nous transmettons l'QFuture renvoyé par QtConcurrent::run() à l'instance QFutureWatcher.

Le signal ` QFutureWatcher::finished ` du `watcher` est connecté au slot ` Images::scaleFinished ` :

    connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished,
            this, &Images::scaleFinished);

Ce slot est chargé d’afficher les images redimensionnées dans l’interface utilisateur, ainsi que de gérer les erreurs pouvant survenir lors du redimensionnement :

void Images::scaleFinished()
{
    const OptionalImages result = scalingWatcher.result();
    if (result.has_value()) {
        const auto scaled = result.value();
        showImages(scaled);
        updateStatus(tr("Finished"));
    } else {
        updateStatus(tr("Failed to extract image data."));
    }
    addUrlsButton->setEnabled(true);
}

Le signalement des erreurs est implémenté en renvoyant un `optional` depuis la méthode ` Images::scaled() ` :

Images::OptionalImages Images::scaled(const QList<QByteArray> &data)
{
    QList<QImage> scaled;
    for (const auto &imgData : data) {
        QImage image;
        image.loadFromData(imgData);
        if (image.isNull())
            return std::nullopt;

        scaled.push_back(image.scaled(100, 100, Qt::KeepAspectRatio));
    }

    return scaled;
}

Le type ` Images::OptionalImages ` est ici simplement une définition de type (`typedef`) pour ` std::optional` :

using OptionalImages = std::optional<QList<QImage>>;

Remarque : nous ne pouvons pas gérer les erreurs issues de l’opération de mise à l’échelle asynchrone à l’aide du gestionnaire ` .onFailed()`, car ce dernier doit être exécuté dans le contexte de l’objet ` Images ` au sein du thread de l’interface utilisateur. Si l’utilisateur ferme l’application pendant le calcul asynchrone, l’objet ` Images ` sera détruit, et l’accès à ses membres depuis la continuation entraînera un plantage. L'utilisation d’ QFutureWatcher et de ses signaux permet d’éviter ce problème, car les signaux sont déconnectés lorsque l’ QFutureWatcher est détruit ; ainsi, les slots associés ne seront jamais exécutés dans un contexte détruit.

Le reste du code est simple ; vous pouvez consulter le projet d’exemple pour plus de détails.

Exécution de l'exemple

Vous pouvez exécuter l’exemple à partir de :

Exemple de projet sur code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.