图像缩放

演示如何异步下载和缩放图像。

本示例演示了如何使用QFutureQPromiseQFutureWatcher 类从网络下载图像集合并缩放它们,而不会阻塞用户界面。

应用程序包括以下步骤:

  1. 从用户指定的 URL 列表中下载图片。
  2. 缩放图片。
  3. 以网格布局显示缩放后的图片。

让我们从下载开始:

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

download() 方法接收一个 URL 列表并返回一个QFutureQFuture 存储为每个下载图像接收的字节数组数据。为了将数据存储在QFuture 中,我们创建了一个QPromise 对象,并报告它已开始,以表示下载的开始:

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

与承诺相关的未来将返回给调用者。

我们暂且不谈细节,但要注意的是,promise 对象被包裹在QSharedPointer 中。稍后我们将对此进行解释。

我们使用QNetworkAccessManager 发送网络请求并下载每个 url 的数据:

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

有趣的部分开始了:

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

我们不使用QObject::connect() 方法连接QNetworkReply 的信号,而是使用QtFuture::connect() 方法。它的工作原理与QObject::connect() 类似,但会返回一个QFuture 对象,该对象在QNetworkReply::finished() 信号发出后立即可用。这样,我们就可以像示例中那样,附加继续和失败处理程序。

在通过.then() 附加的继续程序中,我们会检查用户是否要求取消下载。如果是,我们就停止处理请求。通过调用QPromise::finish() 方法,我们会通知用户处理已完成。如果网络请求以错误结束,我们将抛出一个异常。异常将在使用.onFailed() 方法附加的故障处理程序中进行处理。请注意,我们有两个故障处理程序:第一个捕获网络错误,第二个捕获执行过程中抛出的所有其他异常。两个处理程序都会将异常保存在 promise 对象中(由download() 方法的调用者处理),并报告计算已经完成。另外需要注意的是,为了简单起见,如果出现错误,我们会中断所有待处理的下载。

如果请求没有被取消,也没有发生错误,我们将从网络回复中读取数据,并将其添加到 promise 对象的结果列表中:

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

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

如果存储在承诺对象中的结果数等于urls 的待下载数,则没有更多请求需要处理,因此我们也会报告承诺已完成。

如前所述,我们将 promise 封装在QSharedPointer 中。由于连接到每个网络回复的处理程序之间共享 promise 对象,因此我们需要在多个地方同时复制和使用 promise 对象。因此,我们使用了QSharedPointer

download() 方法由Images::process 方法调用。当用户按下"添加 URL "按钮时,它就会被调用:

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

清除上次下载可能遗留的内容后,我们会创建一个对话框,以便用户指定要下载图片的 URL。根据指定的 URL 数量,我们将初始化显示图片的布局并开始下载。我们将保存download() 方法返回的未来值,以便用户在需要时取消下载:

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

接下来,我们附加一个继续程序来处理缩放步骤。稍后再详述:

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

之后,我们附加onCanceled() 和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()));
        })
    ...

如果用户按下了"取消 "按钮,通过.onCanceled() 方法附加的处理程序将被调用:

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

cancel() 方法只是终止所有待处理请求:

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

    downloadFuture.cancel();
    abortDownload();
}

通过.onFailed() 方法附加的处理程序将在前一个步骤中发生错误时被调用。例如,如果在下载步骤中,网络错误被保存在承诺中,那么它就会传播到以QNetworkReply::NetworkError 为参数的处理程序中。

如果downloadFuture 没有被取消,也没有报告任何错误,则会继续执行缩放操作。

由于缩放的计算量可能很大,而且我们不想阻塞主线程,因此我们使用QtConcurrent::run() 在新线程中启动缩放步骤。

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

由于缩放是在单独的线程中启动的,因此用户有可能决定在缩放操作过程中关闭应用程序。为了从容应对这种情况,我们将QtConcurrent::run()返回的QFuture 传递给QFutureWatcher 实例。

观察者的QFutureWatcher::finished 信号连接到Images::scaleFinished 插槽:

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

该槽负责在用户界面中显示缩放后的图像,并处理缩放过程中可能发生的错误:

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

错误报告通过从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;
}

此处的Images::OptionalImages 类型只是std::optional 的类型定义:

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

注意: 我们不能使用.onFailed() 处理程序来处理异步缩放操作产生的错误,因为处理程序需要在用户界面线程中的Images 对象上下文中执行。如果用户在异步计算完成后关闭应用程序,Images 对象将被销毁,而从继续执行中访问其成员将导致崩溃。使用QFutureWatcher 及其信号可以避免这个问题,因为当QFutureWatcher 销毁时,信号将被断开,所以相关槽永远不会在已销毁的上下文中执行。

其余代码都很简单,你可以查看示例项目了解更多细节。

运行示例

运行示例 Qt Creator,打开Welcome 模式,然后从Examples 中选择示例。更多信息,请参阅Qt Creator: 教程:构建并运行

示例项目 @ code.qt.io

© 2025 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.