图像缩放
演示了如何异步下载和缩放图像。
本示例显示了如何使用 QFuture,QPromise 和 QFutureWatcher 类从网络中下载图像集合并缩放它们,而不阻塞 UI。
应用程序包括以下步骤
- 从用户指定的 URL 列表中下载图像。
- 缩放图像。
- 以网格布局显示缩放后的图像。
让我们从下载开始
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 中运行示例,请打开 欢迎 模式并从 示例 中选择示例。有关更多信息,请访问 构建和运行示例。
© 2024 The Qt Company Ltd。本文件中的文档贡献均为其各自所有者的版权。所提供的文档受GNU自由文档许可证第1.3版的条款约束,该许可证由自由软件基金会发布。Qt及相应标志是芬兰及/或其他国家/地区的The Qt Company Ltd的商标。所有其他商标均为其各自所有者的财产。