图像缩放

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

本示例显示了如何使用 QFutureQPromiseQFutureWatcher 类从网络中下载图像集合并缩放它们,而不阻塞 UI。

应用程序包括以下步骤

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

让我们从下载开始

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

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

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

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

在不深入了解的情况下,让我们注意承诺对象被封装在一个 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();
    });
}
    ...

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

在通过 .then() 附加的继续处理程序中,我们检查用户是否请求取消下载。如果是这种情况,我们停止处理请求。通过调用 QPromise::finish() 方法,我们通知用户处理已完成。如果网络请求以错误结束,我们抛出一个异常。异常将被使用 .onFailed() 方法附加的失败处理程序处理。请注意,我们有两个失败处理程序:第一个捕获网络错误,第二个处理执行期间抛出的所有其他异常。两个处理程序都将异常保存到承诺对象中(由 download() 方法的调用者处理)并报告计算已完成。请注意,为了简化,在发生错误的情况下,我们会中断所有挂起的下载。

如果请求未取消且没有错误发生,我们读取网络回复中的数据并将其添加到承诺对象的结果列表中

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

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

如果存储在承诺对象内部的结果数量与要下载的 url 数量相等,则没有更多请求要处理,因此我们还将承诺完成。

如前所述,我们将承诺包裹在QSharedPointer中。由于承诺对象在连接到每个网络回复的处理程序之间共享,我们需要在多个地方同时复制并使用承诺对象。因此,这里使用QSharedPointer

Images::process方法调用download()方法。当用户按下“添加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);

此槽负责在UI中显示缩放后的图像,并处理缩放过程中可能发生的错误

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的typedef

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

注意:我们无法使用.onFailed()处理器处理异步缩放操作中的错误,因为处理器需要在UI线程中Images对象的上下文中执行。如果用户在异步计算完成时关闭应用程序,则Images对象将被销毁,并且从续页中访问其成员将导致崩溃。使用QFutureWatcher及其信号可以使我们避免这个问题,因为当QFutureWatcher被销毁时,信号会被断开连接,因此相关的插槽将永远不会在销毁的上下文中执行。

其余的代码很简单,您可以查看示例项目以获取更多详细信息。

运行示例

要从 Qt Creator 中运行示例,请打开 欢迎 模式并从 示例 中选择示例。有关更多信息,请访问 构建和运行示例

示例项目 @ code.qt.io

© 2024 The Qt Company Ltd。本文件中的文档贡献均为其各自所有者的版权。所提供的文档受GNU自由文档许可证第1.3版的条款约束,该许可证由自由软件基金会发布。Qt及相应标志是芬兰及/或其他国家/地区的The Qt Company Ltd的商标。所有其他商标均为其各自所有者的财产。