图像缩放

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

主要使用QFuture,QPromise和QFutureWatcher来从网络下载图像集合并缩放他们,而不阻塞用户界面。

用到了链式调用,监视器。

应用程序包括以下步骤:

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

模态对话框

先来看一下点击添加url按钮后的dialog对话窗口

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
DownloadDialog::DownloadDialog(QWidget *parent)
: QWidget(parent)
, ui(new Ui::DownloadDialog)
{
ui->setupUi(this);
ui->urlLineEdit->setPlaceholderText(tr("Enter the URL of an image to download"));
QObject::connect(ui->addUrlButton,&QPushButton::clicked,this,[this](){
const auto text = ui->urlLineEdit->text();
if(!text.isEmpty()){
ui->urlListWidget->addItem(text);
ui->urlLineEdit->clear();
}
});
QObject::connect(ui->urlListWidget,&QListWidget::itemSelectionChanged,this,[this](){
bool isnull = ui->urlListWidget->selectedItems().empty();
ui->removeUrlButton->setEnabled(!isnull);
});
// QObject::connect(ui->clearUrlsButton,&QPushButton::clicked,ui->urlListWidget,&QListWidget::clear);
// QObject::connect(ui->removeUrlButton,&QPushButton::clicked,this,[this](){
// // 1. ui->urlListWidget->selectedItems():获取列表中所有选中项,返回 QList<QListWidgetItem*>
// // 2. qDeleteAll(...):遍历这个列表,对每个 QListWidgetItem* 调用 delete(释放堆内存)
// // 方式:先删内存,再移除列表项(推荐)
// QList<QListWidgetItem*> selectedItems = ui->urlListWidget->selectedItems();
// qDeleteAll(selectedItems); // 释放堆内存
// // 从列表中移除所有选中项
// for (QListWidgetItem* item : selectedItems) {
// ui->urlListWidget->removeItemWidget(item); // 移除项的控件(若有)
// ui->urlListWidget->takeItem(ui->urlListWidget->row(item)); // 从列表中移除项
// }
// });
上面已经qdeleteall后还访问了item这个野指针导致报错,修改为下述代码
QObject::connect(ui->removeUrlButton,&QPushButton::clicked,this,[this](){
QList<QListWidgetItem*> selectedItems = ui->urlListWidget->selectedItems();
// 第一步:先从列表中移除所有选中项
for (QListWidgetItem* item : selectedItems) {
ui->urlListWidget->takeItem(ui->urlListWidget->row(item));
}
// 第二步:释放已移除项的内存(此时item已不属于列表,可安全delete)
qDeleteAll(selectedItems);
ui->urlListWidget->clearSelection();
});

connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
// Cancel 按钮绑定 reject()
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);

}

DownloadDialog::~DownloadDialog()
{
delete ui;
}

QList<QUrl> DownloadDialog::getUrls() const
{
QList<QUrl> list;
for(int i = 0;i<ui->urlListWidget->count();++i){
list.push_back(ui->urlListWidget->item(i)->text());
}
return list;
}

该窗口主要就两个任务,将输入的url存入listwidget并显示,以及将listwidget里面的url存到数据容器list里面方便主窗口调用以取数据。其他连接信号都是一些按键的信号。

要注意的是用到了qt语法糖qDeleteAll以删除容器中指向堆内存的对象,但不会清空容器本身,承担手动循环delete的效果。

只删除对象,不会从容器中移除元素(即保留空指针),需要手动clear、removeitem清空容器

clear() 清空容器(移除元素),但不释放内存;qDeleteAll 释放内存,但不清空容器

对话框的右下角ok与cancel按钮用的是buttonbox而非两个独立按钮组件,注意信号的绑定。

主窗口

我们希望点击主窗口add按钮后,出现模态对话框。

这里需要讲解一下模态对话框代码部分

if (downloadDialog->exec() == QDialog::Accepted) 这行代码有两个作用

  1. exec()QDialog(及子类,如 QFileDialog/ 自定义对话框)的核心方法,作用是:将对话框设置为模态(Modal):弹出后阻塞调用线程(通常是主线程),用户无法操作程序其他窗口,只能先处理这个对话框;
  2. == QDialog::Accepted``QDialog::Accepted 是 Qt 预定义的枚举值(本质是 int 类型,值为 1),代表「用户确认了对话框操作」(比如点击了 “确定”“下载”“保存” 等按钮);对应的反向枚举值是 QDialog::Rejected(值为 0),代表「用户取消了操作」(比如点击了 “取消” 按钮、关闭对话框右上角的 ×)。
异步批量下载图片

再讲解一下如何对拿到手的urls进行图片下载,也就是我们的download函数

角色 核心作用
QPromise<T> 生产者 异步任务的 “结果写入者”:在任务执行过程中,写入结果、异常、取消状态,控制任务完成;
QFuture<T> 消费者 异步任务的 “结果读取者”:获取 QPromise 写入的结果、异常、状态,监听任务完成(不能写入,只能读);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
QFuture<QByteArray> MainWindow::download(const QList<QUrl> &urls)
{
//创建promise对象
QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
promise->start();//标记任务开始,准备接受结果/异常

//遍历所有url,逐个发送下载请求
for(auto url:urls){
//发送get请求,智能指针管理reply防止泄露
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;
}
//再检查下载是否出错,若出错抛个异常先,交给后面onfailed处理
if(reply->error() !=QNetworkReply::NoError){
if(!promise->future().isFinished()){
throw reply->error();
}
}
//下载成功,将当前响应的二进制数据写入promise
promise->addResult(reply->readAll());
//当最后一个任务下载完成时,标记任务结束
if(promise->future().resultCount()==urls.count())
promise->finish();//所有结果已经写入,任务已完成
})
.onFailed([promise](QNetworkReply::NetworkError error){
//捕获网络错误,设置promise的错误信息
const auto ex =std::make_exception_ptr(
std::runtime_error("Unknow error occurred while downloading."));
promise->setException(ex);
promise->finish();
});
}
return promise->future();
}

核心逻辑:

  1. 为每个 URL 发送网络 GET 请求(QNetworkAccessManager::get);
  2. 异步监听每个请求的完成 / 错误状态;
  3. 所有请求完成后,通过 QPromise 汇总下载的二进制数据(QByteArray);
  4. 返回 QFuture,让调用方可以获取下载结果、监听下载状态。

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

清空旧网格布局的旧图片并初始化新的网格布局

img

内存管理(重中之重)

  • 清理旧布局时,必须:
  1. takeAt 取出布局项(否则布局仍持有项的引用,删除会崩溃);第一次写的是itemAt仅返回布局中第 0 个项的指针,不会从布局中移除它;导致报错
  2. 解除 widget 的父对象(setParent(nullptr));
  3. 分别删除 widget 和布局项;
  4. 清空 labels 列表(避免保存已删除的野指针);
  • 新创建的 QLabel 会自动将 gridLayout 的父窗口作为父对象(Qt 布局的特性),无需手动设置父对象,关闭窗口时会自动释放。

  • 通过 qSqrt(count) + 1 计算行列数,让布局尽量接近正方形,比 “单行 / 单列” 展示更美观;

  • 固定 QLabel 大小,避免不同尺寸的图片导致布局错乱。

  • labels 列表保存所有标签引用,后续只需遍历 labels,就能给每个标签设置对应的图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void MainWindow::initLayout(qsizetype count)
{
//1. 先清除旧的images
QLayoutItem* child;
//循环取出第一个布局项
while((child = ui->gridLayout->takeAt(0)) != nullptr){
// 1. 取出布局项对应的widget(这里是QLabel),解除父对象关联
child->widget()->setParent(nullptr);
// 2. 删除widget(释放QLabel的内存)
delete child->widget();
// 3. 删除布局项本身(释放QLayoutItem的内存)
delete child;
}
labels.clear();// 清空保存QLabel引用的列表(避免野指针)

//2. 为新图片组初始化布局
//根据图片数量count,计算网格的行列数dim,让布局近似正方形
const auto dim = int(qSqrt(qreal(count)))+1;//计算 count 的平方根(比如 count=8,平方根≈2.828);

//3.生成新的图片标签并添加到布局
for(int i = 0;i<dim;++i){
for(int j = 0;j<dim;++j){
// 1. 创建新的QLabel(用于显示单张图片)
QLabel *imageLabel = new QLabel;
// 2. 设置标签固定大小(100×100像素,避免图片拉伸变形)
imageLabel->setFixedSize(100, 100);
// 3. 将标签添加到网格布局的(i,j)位置(第i行第j列)
ui->gridLayout->addWidget(imageLabel, i, j);
// 4. 将标签指针保存到labels列表(后续可通过labels访问/设置图片)
labels.append(imageLabel);
}
}
}
核心入口函数process

串联了「用户交互→批量下载→图片缩放→状态更新」全流程,核心逻辑:

  1. 清理旧状态(重置网络响应列表、禁用按钮);
  2. 弹出下载确认对话框,获取用户选择的图片 URL 列表;
  3. 初始化图片展示布局(根据 URL 数量生成对应网格);
  4. 异步批量下载图片(调用之前的 download 函数);
  5. 监听下载任务的状态(完成 / 取消 / 失败),执行对应逻辑【这里是用的链式调用来监视】:
  • 下载成功:启动图片缩放异步任务【对这些异步任务用的是watcher来监视】;
  • 下载取消 / 失败:更新状态提示、终止请求;
  1. 任务结束后重置按钮状态,恢复用户操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
用户点击“开始处理” → 调用process()

清理旧状态 → 弹出下载对话框 → 用户确认

获取URL列表 → 初始化布局 → 启动异步下载(downloadFuture)

┌───────────── 下载中 ─────────────┐
│ 分支1:下载成功 → then(缩放图片) │
│ 分支2:下载取消 → onCanceled(提示)│
│ 分支3:下载失败 → onFailed(处理错误)│
└───────────── 最终then ────────────┘

重置按钮状态(禁用取消、启用添加URL)
void MainWindow::process()
{
replies.clear();
ui->pushButtonAddUrls->setEnabled(false);
if(downloadDialog->exec() == QDialog::Accepted){
//// 仅当用户点击“确认”按钮时,执行这里的代码
const auto urls = downloadDialog->getUrls();//获取url列表
if(urls.empty())
return;
ui->pushButtonCancel->setEnabled(true);
initLayout(urls.size());//初始化图片label的布局,这部分要求数量灵活所以是不能靠designer设计的
downloadFuture = download(urls);//先看这个异步下载任务
statusBar->showMessage(tr("Downloading..."));

////重点 监听downloadFuture的状态(Qt6链式调用,异步非阻塞)
downloadFuture
// 1 下载成功完成后执行:准备缩放图片
.then([this](auto){
ui->pushButtonCancel->setEnabled(false);
updateStatus(tr("Scaling..."));
///启动图片缩放任务。
/// QConcurrent::run将目标函数放置到线程池异步执行
/// 拿监视者监听缩放异步任务的状态。
scalingWatcher.setFuture(QtConcurrent::run(MainWindow::scaled,downloadFuture.results()));
})
//下载被取消时
.onCanceled([this]{
updateStatus(tr("Download has been canceled."));
})
//下载出错时
.onFailed([this](QNetworkReply::NetworkError error){
updateStatus(tr("Download finished with error: %1").arg(error));
// 终止所有挂起的网络请求(避免无效下载)
abortDownload();
})
// 下载触发其他异常时执行:更新状态栏(显示异常信息)
.onFailed([this](const std::exception &ex) {
updateStatus(tr(ex.what()));
})
// 无论下载成功/失败/取消,最终都会执行:重置按钮状态
.then([this]() {
ui->pushButtonCancel->setEnabled(false);
ui->pushButtonAddUrls->setEnabled(true);
});
}
}

对于缩放图片异步任务的监视器,信号绑定在主窗口构造函数

缩放函数

亮点在返回的是std::optional<QList>

  • 返回 **std::nullopt**:不是 “没有图片数据”,而是 “有数据但解码失败(图片无效)”—— 明确表示 “缩放任务失败”;
  • 返回空的 **QList<QImage>**(若 data 为空列表):表示 “没有图片数据需要处理”—— 是合法的 “空结果”;
  • 返回非空 **QList<QImage>**:表示 “所有图片都解码 + 缩放成功”。
1
2
3
4
5
6
7
8
9
10
11
12
MainWindow::OptionalImages MainWindow::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;
}
  • 遍历过程中只要有一张图片解码失败,立即返回 std::nullopt,不再处理后续图片 —— 符合 “批量任务中‘一票否决’” 的常见逻辑(比如下载 10 张图,有 1 张损坏则整个缩放任务失败);
异步缩放任务监视器的槽函数scaledFinished()

演示

img

img

质数计算器

如何使用QFutureWatcher 类和filteredReduced函数创建一个交互式非阻塞 QtWidgets 应用程序。 Qt Concurrent.通过该示例,用户可以创建一个可调整大小的整数QList 。程序将检查列表中的质数,并显示找到的质数总数。

先看ui

img

再看头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef PRIMECOUNTER_H
#define PRIMECOUNTER_H
#include<QtConcurrent>
#include <QDialog>
namespace Ui {
class PrimeCounter;
}
class PrimeCounter : public QDialog
{
Q_OBJECT
using Element = unsigned long long;
public:
explicit PrimeCounter(QWidget *parent = nullptr);
~PrimeCounter();
private:
static bool filterFunction(const Element &ele);
static void reduceFunction(Element &out,const Element &value);
void fileElementList(unsigned int count);
Ui::PrimeCounter* setupUi();//用在dialog初始化列表赋值
private slots:
void start();
void finish();
private:
Ui::PrimeCounter *ui;
QList<Element> elementList;
QFutureWatcher<Element> watcher;
QtConcurrent::ReduceOptions currentReduceOpt;
QElapsedTimer timer;
QThreadPool pool;//防止全局线程池阻塞,所以用自己的线程池,算是一个亮点
unsigned int stepSize;//数字最大范围(100000)
};

ReduceOptions主要是控制mappedReduced或者filterReduced的reduce归约阶段行为,有多种枚举值,且可以并列。比如UnorderReduced|SequentialReduce。这是两个默认,谁最先mapped/filter就去reduce,且同一线程归约;而ParallelReduce是指归约操作也多线程执行(需保证归约函数线程安全,如加锁),仅适合归约逻辑耗时的场景;

start函数与按钮的点击信号连接,但这里需要注意的是该按钮为一键两用按钮, 不是普通按钮,而是被设置为 setCheckable(true)(可选中 / 切换),所以在start函数内部需要通过if (ui->pushButton->isChecked())决定按下走什么逻辑,抬起走什么逻辑

也没什么复杂的,定时器用来异步任务开始前start,结束后timer.elasped得到任务耗时然后显示ui上。

点击开始按钮后,根据用户选择归约模式与范围,初始化元素列表,并发对每一个元素进行质数检测。是质素则归约函数的首个输出参数out值就加1.

源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
#include "primecounter.h"
#include "ui_primecounter.h"

PrimeCounter::PrimeCounter(QWidget *parent)
: QDialog(parent),stepSize(100000)
, ui(setupUi())
{
QObject::connect(ui->pushButton,&QPushButton::clicked,this,[this]{start();});
//这里传引用,因为局部/成员变量建立在栈上,需要取地址
//如果QFutureWatcher<Element>* watcher = new QFutureWatcher;则是堆对象,无需&
QObject::connect(&watcher,&QFutureWatcher<Element>::progressRangeChanged,
ui->progressBar,&QProgressBar::setRange);///异步任务初始化 / 变更进度范围时触发
QObject::connect(&watcher,&QFutureWatcher<Element>::finished,
this,[this]{finish();});
QObject::connect(&watcher,&QFutureWatcher<Element>::progressValueChanged,
ui->progressBar, &QProgressBar::setValue);///异步任务执行过程中进度更新时触发

}

PrimeCounter::~PrimeCounter()
{
watcher.cancel();
delete ui;
}

bool PrimeCounter::filterFunction(const Element &ele)
{
if(ele<=1)
return false;
for(Element i = 2;i*i<=ele;++i)
if(ele % i ==0)
return false;
return true;
}
void PrimeCounter::reduceFunction(Element &out, const Element &value)
{
// Count the amount of primes.
Q_UNUSED(value);
++out;
}
void PrimeCounter::finish(){
//撤销的时候同样触发监视器的finish信号
if(watcher.isCanceled())
return;
//获取 timer 计时器从「启动(start())到当前时刻」的流逝时间(毫秒级)
//用于统计代码 / 任务的执行耗时(比如你场景中异步计算任务的总耗时)
auto elapsedTime = timer.elapsed();
ui->progressBar->setValue(0);
ui->comboBox->setEnabled(true);
ui->pushButton->setChecked(false);
ui->pushButton->setText(tr("Start"));
ui->labelFilter->setText(
tr("Filter '%1' took %2 ms to calculate").arg(ui->comboBox->currentText())
.arg(elapsedTime));
ui->labelResult->setText(
tr("Found %1 primes in the range of elements").arg(watcher.result()));
}
Ui::PrimeCounter* PrimeCounter::setupUi(){
Ui::PrimeCounter *setupUI = new Ui::PrimeCounter;
setupUI->setupUi(this);
setModal(true);
// ===== 修复 slider 初始值:手动触发 valueChanged 信号 =====
connect(setupUI->horizontalSlider, &QSlider::valueChanged,
this, [setupUI, this] (const int &pos) {
setupUI->labelResult->setText("");
setupUI->labelSize->setText(tr("Elements in list: %1").arg(pos * stepSize));
});
setupUI->horizontalSlider->setValue(30);
// 手动触发 valueChanged 信号(关键!代码设置值不自动发信号)
emit setupUI->horizontalSlider->valueChanged(30);

// ===== 修复 comboBox 初始化:显式赋值 + 验证 =====
// 1. 插入选项(保留原有逻辑)
setupUI->comboBox->insertItem(0, tr("Unordered Reduce"), QtConcurrent::UnorderedReduce);
setupUI->comboBox->insertItem(1, tr("Ordered Reduce"), QtConcurrent::OrderedReduce);
// 2. 强制设置默认选中项(避免控件初始化延迟)
setupUI->comboBox->setCurrentIndex(0);
// 3. 直接初始化 currentReduceOpt(不依赖lambda,最可靠)
currentReduceOpt = QtConcurrent::UnorderedReduce;
// 4. 手动更新label(确保UI显示和变量一致)
setupUI->labelFilter->setText(tr("Selected Reduce Option: %1")
.arg(setupUI->comboBox->currentText()));

// 5. 保留原有lambda连接(处理后续手动切换)
auto comboBoxChange = [this, setupUI](int pos) {
currentReduceOpt = setupUI->comboBox->itemData(pos).value<QtConcurrent::ReduceOptions>();
setupUI->labelFilter->setText(tr("Selected Reduce Option: %1")
.arg(setupUI->comboBox->currentText()));
};
connect(setupUI->comboBox, &QComboBox::currentIndexChanged, this, comboBoxChange);

return setupUI;
}
void PrimeCounter::fillElementList(unsigned int count)
{
auto prevSize = elementList.size();
if(prevSize == count)
return;
//计算补充数据的“起始值”:
// - 若列表为空 → 从1开始;
// - 若列表已有数据 → 从最后一个元素+1开始(保证连续)
auto startVal = elementList.empty()?1:elementList.back()+1;
//扩容列表到目标长度count:
// - 若count > prevSize → 列表尾部新增 (count - prevSize) 个元素(初始值随机/默认);
// - 若count < prevSize → 列表截断到count长度(但结合step2,count<prevSize时不会走到这)
elementList.resize(count);
// 仅对“新增的部分”填充连续递增的整数:
// - elementList.begin() + prevSize → 新增部分的起始迭代器;
// - elementList.end() → 新增部分的结束迭代器;
// - std::iota → 从startVal开始,给区间内的元素依次赋值(+1递增)
if (elementList.begin() + prevSize < elementList.end())
std::iota(elementList.begin() + prevSize, elementList.end(), startVal);

}

void PrimeCounter::start()
{
if(ui->pushButton->isChecked()){
ui->comboBox->setEnabled(false);
ui->pushButton->setText(tr("Cancel"));
ui->labelResult->setText(tr("Calculating..."));
ui->labelFilter->setText(tr("Selected Reduce Option:%1").arg(ui->comboBox->currentText()));
fillElementList(ui->horizontalSlider->value()*stepSize);//
timer.start();


watcher.setFuture(
QtConcurrent::filteredReduced(
&pool,
elementList,
filterFunction,
reduceFunction,
currentReduceOpt|QtConcurrent::SequentialReduce));//

}else {
watcher.cancel();
ui->progressBar->setValue(0);
ui->comboBox->setEnabled(true);
ui->labelResult->setText(tr(""));
ui->pushButton->setText(tr("Start"));
ui->labelFilter->setText(tr("Operation Canceled"));
}
}

踩坑:列表初始化要看成员变量的声明顺序

而非列表书写顺序!!!!

如下原先ui声明在stepsize前面,但是setUpUi()里面用到了stepsize变量,因列表初始化顺序由类中声明顺序决定,所以此时stepsize还没有被声明,导致初始化出来的ui全是问题。

1
2
3
4
5
6
7
8
9
.h
private:
Ui::PrimeCounter *ui;
/.../
unsigned int stepSize;

PrimeCounter::PrimeCounter(QWidget *parent)
: QDialog(parent),stepSize(100000)
, ui(setupUi())

将ui声明放置最后(一般建议都这么做),程序正常运行

字数统计

演示如何使用mappedReduced来计算文件集合中的字数