Auf dieser Seite

Bildskalierung

Veranschaulicht, wie Bilder asynchron heruntergeladen und skaliert werden.

Dieses Beispiel zeigt, wie man die Klassen „ QFuture “, „ QPromise “ und „ QFutureWatcher “ verwendet, um eine Sammlung von Bildern aus dem Netzwerk herunterzuladen und zu skalieren, ohne die Benutzeroberfläche zu blockieren.

Heruntergeladene Bilder in einem Raster mit den Schaltflächen „URLs hinzufügen“, „Abbrechen“ und dem Status „Fertig“

Die Anwendung umfasst die folgenden Schritte:

  1. Bilder aus der vom Benutzer angegebenen URL-Liste herunterladen.
  2. Skalieren der Bilder.
  3. Anzeige der skalierten Bilder in einem Rasterlayout.

Beginnen wir mit dem Herunterladen:

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

Die Methode „ download() “ nimmt eine Liste von URLs entgegen und gibt ein „ QFuture “ zurück. Das „ QFuture “ speichert die für jedes heruntergeladene Bild empfangenen Byte-Array-Daten. Um die Daten im „ QFuture “ zu speichern, erstellen wir ein „ QPromise “-Objekt und melden dessen Start, um den Beginn des Downloads anzuzeigen:

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

Das mit dem Promise verknüpfte Future wird an den Aufrufer zurückgegeben.

Ohne vorerst ins Detail zu gehen, sei angemerkt, dass das Promise-Objekt in ein „ QSharedPointer “ eingekapselt ist. Dies wird später erläutert.

Wir verwenden ` QNetworkAccessManager `, um Netzwerkanfragen zu senden und Daten für jede URL herunterzuladen:

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

Und hier beginnt der interessante Teil:

    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();
    });
}
    ...

Anstatt uns mit der Methode ` QObject::connect()` an die Signale von ` QNetworkReply` anzubinden, verwenden wir ` QtFuture::connect()`. Diese funktioniert ähnlich wie ` QObject::connect()`, gibt jedoch ein ` QFuture `-Objekt zurück, das verfügbar wird, sobald das Signal ` QNetworkReply::finished()` ausgelöst wird. Dies ermöglicht es uns, Fortsetzungen und Fehlerbehandler anzuhängen, wie es im Beispiel geschehen ist.

In der über ` .then()` angehängten Fortsetzung prüfen wir, ob der Benutzer den Download abbrechen wollte. Ist dies der Fall, brechen wir die Bearbeitung der Anfrage ab. Durch den Aufruf der Methode ` QPromise::finish()` benachrichtigen wir den Benutzer, dass die Bearbeitung abgeschlossen ist. Falls die Netzwerkanfrage mit einem Fehler beendet wurde, lösen wir eine Ausnahme aus. Die Ausnahme wird in dem Fehlerbehandler abgefangen, der über die Methode ` .onFailed()` angehängt wurde. Beachten Sie, dass wir zwei Fehlerbehandler haben: Der erste fängt die Netzwerkfehler ab, der zweite alle anderen Ausnahmen, die während der Ausführung ausgelöst werden. Beide Handler speichern die Ausnahme im Promise-Objekt (zur weiteren Behandlung durch den Aufrufer der Methode ` download() `) und melden, dass die Berechnung abgeschlossen ist. Beachten Sie außerdem, dass wir der Einfachheit halber im Fehlerfall alle ausstehenden Downloads abbrechen.

Wenn die Anfrage nicht abgebrochen wurde und kein Fehler aufgetreten ist, lesen wir die Daten aus der Netzwerkantwort aus und fügen sie der Ergebnisliste des Promise-Objekts hinzu:

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

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

Wenn die Anzahl der im Promise-Objekt gespeicherten Ergebnisse der Anzahl der herunterzuladenden „ urls“ entspricht, gibt es keine weiteren Anfragen mehr zu verarbeiten, sodass wir ebenfalls melden, dass das Promise abgeschlossen ist.

Wie bereits erwähnt, haben wir das Promise in ein ` QSharedPointer` verpackt. Da das Promise-Objekt von den mit den einzelnen Netzwerkantworten verbundenen Handlern gemeinsam genutzt wird, müssen wir das Promise-Objekt an mehreren Stellen gleichzeitig kopieren und verwenden. Daher wird ein ` QSharedPointer ` verwendet.

Die Methode ` download() ` wird aus der Methode ` Images::process ` aufgerufen. Sie wird ausgelöst, wenn der Benutzer auf die Schaltfläche „URLs hinzufügen“ klickt:

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

Nachdem wir eventuelle Überbleibsel aus früheren Downloads gelöscht haben, erstellen wir einen Dialog, in dem der Benutzer die URLs der herunterzuladenden Bilder angeben kann. Basierend auf der angegebenen Anzahl an URLs initialisieren wir das Layout, in dem die Bilder angezeigt werden sollen, und starten den Download. Das von der Methode ` download() ` zurückgegebene `Future` wird gespeichert, damit der Benutzer den Download bei Bedarf abbrechen kann:

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..."));
    ...

Als Nächstes fügen wir eine Fortsetzung hinzu, um den Skalierungsschritt zu verarbeiten. Mehr dazu später:

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

Danach fügen wir die Handler „ onCanceled()“ und „ onFailed()“ hinzu:

        .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()));
        })
    ...

Der über die Methode ` .onCanceled()` angehängte Handler wird aufgerufen, wenn der Benutzer die Schaltfläche „Abbrechen“ gedrückt hat:

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

Die Methode ` cancel() ` bricht einfach alle ausstehenden Anfragen ab:

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

    downloadFuture.cancel();
    abortDownload();
}

Die über die Methode ` .onFailed()` angehängten Handler werden aufgerufen, falls während eines der vorherigen Schritte ein Fehler aufgetreten ist. Wenn beispielsweise während des Download-Schritts ein Netzwerkfehler im Promise gespeichert wurde, wird dieser an den Handler weitergeleitet, der ` QNetworkReply::NetworkError ` als Argument entgegennimmt.

Wurde das ` downloadFuture ` nicht abgebrochen und hat es keinen Fehler gemeldet, wird die Fortsetzung der Skalierung ausgeführt.

Da die Skalierung rechenintensiv sein kann und wir den Hauptthread nicht blockieren wollen, verwenden wir `QtConcurrent::run()`, um den Skalierungsschritt in einem neuen Thread zu starten.

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

Da die Skalierung in einem separaten Thread gestartet wird, kann der Benutzer möglicherweise beschließen, die Anwendung zu schließen, während der Skalierungsvorgang noch läuft. Um solche Situationen elegant zu handhaben, übergeben wir das von QtConcurrent::run() zurückgegebene „ QFuture “ an die Instanz „ QFutureWatcher “.

Das Signal „ QFutureWatcher::finished “ des Watchers ist mit dem Slot „ Images::scaleFinished “ verbunden:

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

Dieser Slot ist dafür zuständig, die skalierten Bilder in der Benutzeroberfläche anzuzeigen und Fehler zu behandeln, die möglicherweise während der Skalierung auftreten können:

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);
}

Die Fehlermeldung wird durch die Rückgabe eines „optional“ aus der Methode „ Images::scaled() “ implementiert:

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;
}

Der Typ ` Images::OptionalImages ` ist hier lediglich eine Typdefinition für ` std::optional`:

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

Hinweis: Wir können die Fehler aus dem asynchronen Skalierungsvorgang nicht mithilfe des Handlers ` .onFailed()` behandeln, da der Handler im Kontext des ` Images `-Objekts im UI-Thread ausgeführt werden muss. Wenn der Benutzer die Anwendung schließt, während die asynchrone Berechnung noch läuft, wird das ` Images `-Objekt zerstört, und der Zugriff auf seine Mitglieder aus der Fortsetzung heraus führt zu einem Absturz. Durch die Verwendung von „ QFutureWatcher “ und dessen Signalen lässt sich das Problem vermeiden, da die Signale getrennt werden, wenn das „ QFutureWatcher “ zerstört wird, sodass die zugehörigen Slots niemals in einem zerstörten Kontext ausgeführt werden.

Der Rest des Codes ist unkompliziert; weitere Details finden Sie im Beispielprojekt.

Ausführen des Beispiels

Sie können das Beispiel wie folgt ausführen:

Beispielprojekt @ 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.