forked from qt-creator/qt-creator
TaskTree: Add a new example showing input / output data exchange
This example shows how to separate the business logic of the recipe from the GUI and how to write a reusable recipe. It shows how to feed the recipe with the initial data and how to retrieve the final result from the recipe using onStorageSetup() and onStorageDone() handlers. Change-Id: I04c0c0c9bd6cf25ac4e91317e527ad12832e9143 Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
@@ -13,6 +13,7 @@ Project {
|
||||
"shootout/shootout.qbs",
|
||||
"spinner/spinner.qbs",
|
||||
"subdirfilecontainer/subdirfilecontainer.qbs",
|
||||
"tasking/dataexchange/dataexchange.qbs",
|
||||
"tasking/demo/demo.qbs",
|
||||
"tasking/imagescaling/imagescaling.qbs",
|
||||
"terminal/terminal.qbs",
|
||||
|
@@ -1,2 +1,3 @@
|
||||
add_subdirectory(dataexchange)
|
||||
add_subdirectory(demo)
|
||||
add_subdirectory(imagescaling)
|
||||
|
10
tests/manual/tasking/dataexchange/CMakeLists.txt
Normal file
10
tests/manual/tasking/dataexchange/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
add_qtc_test(tst_tasking_dataexchange
|
||||
MANUALTEST
|
||||
DEPENDS Tasking Qt::Concurrent Qt::Network Qt::Widgets
|
||||
SOURCES
|
||||
main.cpp
|
||||
recipe.cpp
|
||||
recipe.h
|
||||
viewer.cpp
|
||||
viewer.h
|
||||
)
|
15
tests/manual/tasking/dataexchange/dataexchange.qbs
Normal file
15
tests/manual/tasking/dataexchange/dataexchange.qbs
Normal file
@@ -0,0 +1,15 @@
|
||||
QtcManualTest {
|
||||
name: "Tasking dataexchange"
|
||||
type: ["application"]
|
||||
|
||||
Depends { name: "Qt"; submodules: ["concurrent", "network", "widgets"] }
|
||||
Depends { name: "Tasking" }
|
||||
|
||||
files: [
|
||||
"main.cpp",
|
||||
"recipe.cpp",
|
||||
"recipe.h",
|
||||
"viewer.cpp",
|
||||
"viewer.h",
|
||||
]
|
||||
}
|
17
tests/manual/tasking/dataexchange/main.cpp
Normal file
17
tests/manual/tasking/dataexchange/main.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "viewer.h"
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
app.setOrganizationName("QtProject");
|
||||
app.setApplicationName("Data Exchange");
|
||||
|
||||
Viewer viewer;
|
||||
viewer.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
86
tests/manual/tasking/dataexchange/recipe.cpp
Normal file
86
tests/manual/tasking/dataexchange/recipe.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "recipe.h"
|
||||
|
||||
#include <tasking/concurrentcall.h>
|
||||
#include <tasking/networkquery.h>
|
||||
|
||||
using namespace Tasking;
|
||||
|
||||
static void readImage(QPromise<QImage> &promise, const QByteArray &data)
|
||||
{
|
||||
const auto image = QImage::fromData(data);
|
||||
if (image.isNull())
|
||||
promise.future().cancel();
|
||||
else
|
||||
promise.addResult(image);
|
||||
}
|
||||
|
||||
static void scaleImage(QPromise<QImage> &promise, const QImage &inputImage, const QSize &size)
|
||||
{
|
||||
promise.addResult(inputImage.scaled(size, Qt::KeepAspectRatio));
|
||||
}
|
||||
|
||||
class InternalData
|
||||
{
|
||||
public:
|
||||
QByteArray dataSource;
|
||||
QImage imageSource;
|
||||
};
|
||||
|
||||
static int sizeForIndex(int index) { return (index + 1) * s_sizeInterval; }
|
||||
|
||||
Group recipe(const Tasking::TreeStorage<ExternalData> &externalStorage)
|
||||
{
|
||||
TreeStorage<InternalData> internalStorage;
|
||||
|
||||
const auto onDownloadSetup = [externalStorage](NetworkQuery &query) {
|
||||
query.setNetworkAccessManager(externalStorage->inputNam);
|
||||
query.setRequest(QNetworkRequest(externalStorage->inputUrl));
|
||||
};
|
||||
const auto onDownloadDone = [internalStorage, externalStorage](const NetworkQuery &query,
|
||||
DoneWith doneWith) {
|
||||
if (doneWith == DoneWith::Success) {
|
||||
internalStorage->dataSource = query.reply()->readAll();
|
||||
} else {
|
||||
externalStorage->outputError
|
||||
= QString("Download Error. Code: %1.").arg(query.reply()->error());
|
||||
}
|
||||
};
|
||||
|
||||
const auto onReadSetup = [internalStorage](ConcurrentCall<QImage> &data) {
|
||||
data.setConcurrentCallData(&readImage, internalStorage->dataSource);
|
||||
};
|
||||
const auto onReadDone = [internalStorage, externalStorage](const ConcurrentCall<QImage> &data,
|
||||
DoneWith doneWith) {
|
||||
if (doneWith == DoneWith::Success)
|
||||
internalStorage->imageSource = data.result();
|
||||
else
|
||||
externalStorage->outputError = "Image Data Error.";
|
||||
};
|
||||
|
||||
QList<GroupItem> parallelTasks;
|
||||
parallelTasks.reserve(s_imageCount + 1); // +1 for parallelLimit
|
||||
parallelTasks.append(parallelLimit(QThread::idealThreadCount() - 1));
|
||||
|
||||
for (int i = 0; i < s_imageCount; ++i) {
|
||||
const int s = sizeForIndex(i);
|
||||
const auto onScaleSetup = [internalStorage, s](ConcurrentCall<QImage> &data) {
|
||||
data.setConcurrentCallData(&scaleImage, internalStorage->imageSource, QSize(s, s));
|
||||
};
|
||||
const auto onScaleDone = [externalStorage, s](const ConcurrentCall<QImage> &data) {
|
||||
externalStorage->outputImages.insert(s, data.result());
|
||||
};
|
||||
parallelTasks.append(ConcurrentCallTask<QImage>(onScaleSetup, onScaleDone));
|
||||
}
|
||||
|
||||
const QList<GroupItem> recipe {
|
||||
Storage(externalStorage),
|
||||
Storage(internalStorage),
|
||||
NetworkQueryTask(onDownloadSetup, onDownloadDone),
|
||||
ConcurrentCallTask<QImage>(onReadSetup, onReadDone),
|
||||
Group { parallelTasks }
|
||||
};
|
||||
return recipe;
|
||||
}
|
36
tests/manual/tasking/dataexchange/recipe.h
Normal file
36
tests/manual/tasking/dataexchange/recipe.h
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef RECIPE_H
|
||||
#define RECIPE_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QMap>
|
||||
#include <QUrl>
|
||||
|
||||
#include <optional>
|
||||
|
||||
namespace Tasking {
|
||||
class Group;
|
||||
template <typename T>
|
||||
class TreeStorage;
|
||||
}
|
||||
|
||||
static const int s_sizeInterval = 10;
|
||||
static const int s_imageCount = 100;
|
||||
static const int s_maxSize = s_sizeInterval * s_imageCount;
|
||||
|
||||
class QNetworkAccessManager;
|
||||
|
||||
class ExternalData
|
||||
{
|
||||
public:
|
||||
QNetworkAccessManager *inputNam = nullptr;
|
||||
QUrl inputUrl;
|
||||
QMap<int, QImage> outputImages;
|
||||
std::optional<QString> outputError;
|
||||
};
|
||||
|
||||
Tasking::Group recipe(const Tasking::TreeStorage<ExternalData> &externalStorage);
|
||||
|
||||
#endif // RECIPE_H
|
94
tests/manual/tasking/dataexchange/viewer.cpp
Normal file
94
tests/manual/tasking/dataexchange/viewer.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#include "viewer.h"
|
||||
#include "recipe.h"
|
||||
|
||||
#include <tasking/concurrentcall.h>
|
||||
#include <tasking/networkquery.h>
|
||||
|
||||
using namespace Tasking;
|
||||
|
||||
Viewer::Viewer(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_recipe(recipe(m_storage))
|
||||
{
|
||||
setWindowTitle(tr("Data Exchange"));
|
||||
|
||||
QLabel *urlLabel = new QLabel(tr("Url:"));
|
||||
m_lineEdit = new QLineEdit("https://media.licdn.com/dms/image/D4D22AQFj3ksh5rmnrg/"
|
||||
"feedshare-shrink_800/0/1697023188446?e=1701302400&v=beta"
|
||||
"&t=6dy5dmhzgONaLu139A6XmFSGqDohiezq1fH-q2mmu3w");
|
||||
QPushButton *startButton = new QPushButton(tr("Start"));
|
||||
QPushButton *stopButton = new QPushButton(tr("Stop"));
|
||||
QPushButton *resetButton = new QPushButton(tr("Reset"));
|
||||
m_progressBar = new QProgressBar;
|
||||
m_listWidget = new QListWidget;
|
||||
m_statusBar = new QStatusBar;
|
||||
|
||||
QBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
QBoxLayout *subLayout1 = new QHBoxLayout;
|
||||
QBoxLayout *subLayout2 = new QHBoxLayout;
|
||||
subLayout1->addWidget(urlLabel);
|
||||
subLayout1->addWidget(m_lineEdit);
|
||||
subLayout2->addWidget(startButton);
|
||||
subLayout2->addWidget(stopButton);
|
||||
subLayout2->addWidget(resetButton);
|
||||
subLayout2->addWidget(m_progressBar);
|
||||
mainLayout->addLayout(subLayout1);
|
||||
mainLayout->addLayout(subLayout2);
|
||||
mainLayout->addWidget(m_listWidget);
|
||||
mainLayout->addWidget(m_statusBar);
|
||||
|
||||
m_listWidget->setIconSize(QSize(s_maxSize, s_maxSize));
|
||||
|
||||
const auto reset = [this] {
|
||||
m_listWidget->clear();
|
||||
m_statusBar->clearMessage();
|
||||
m_progressBar->setValue(0);
|
||||
};
|
||||
|
||||
connect(startButton, &QAbstractButton::clicked, this, [this, reset] {
|
||||
reset();
|
||||
const auto setInput = [this](ExternalData &data) {
|
||||
data.inputNam = &m_nam;
|
||||
data.inputUrl = m_lineEdit->text();
|
||||
m_statusBar->showMessage(tr("Executing recipe..."));
|
||||
};
|
||||
|
||||
const auto getOutput = [this](const ExternalData &data) {
|
||||
if (data.outputError) {
|
||||
m_statusBar->showMessage(*data.outputError);
|
||||
return;
|
||||
}
|
||||
m_statusBar->showMessage(tr("Recipe executed successfully."));
|
||||
for (auto it = data.outputImages.begin(); it != data.outputImages.end(); ++it) {
|
||||
m_listWidget->addItem(new QListWidgetItem(QPixmap::fromImage(it.value()),
|
||||
QString("%1x%1").arg(it.key())));
|
||||
}
|
||||
};
|
||||
|
||||
const auto onDone = [this] { m_taskTree.release()->deleteLater(); };
|
||||
|
||||
m_taskTree.reset(new TaskTree(m_recipe));
|
||||
m_taskTree->onStorageSetup(m_storage, setInput);
|
||||
m_taskTree->onStorageDone(m_storage, getOutput);
|
||||
m_progressBar->setMaximum(m_taskTree->progressMaximum());
|
||||
QObject::connect(m_taskTree.get(), &TaskTree::progressValueChanged,
|
||||
m_progressBar, &QProgressBar::setValue);
|
||||
QObject::connect(m_taskTree.get(), &TaskTree::done, this, onDone);
|
||||
m_taskTree->start();
|
||||
});
|
||||
|
||||
connect(stopButton, &QAbstractButton::clicked, this, [this] {
|
||||
if (m_taskTree) {
|
||||
m_statusBar->showMessage(tr("Recipe stopped by user."));
|
||||
m_taskTree.reset();
|
||||
}
|
||||
});
|
||||
|
||||
connect(resetButton, &QAbstractButton::clicked, this, [this, reset] {
|
||||
m_taskTree.reset();
|
||||
reset();
|
||||
});
|
||||
}
|
33
tests/manual/tasking/dataexchange/viewer.h
Normal file
33
tests/manual/tasking/dataexchange/viewer.h
Normal file
@@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2023 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
|
||||
|
||||
#ifndef VIEWER_H
|
||||
#define VIEWER_H
|
||||
|
||||
#include "recipe.h"
|
||||
|
||||
#include <tasking/tasktree.h>
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QtWidgets>
|
||||
|
||||
class Viewer : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
Viewer(QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QLineEdit *m_lineEdit = nullptr;
|
||||
QProgressBar *m_progressBar = nullptr;
|
||||
QListWidget *m_listWidget = nullptr;
|
||||
QStatusBar *m_statusBar = nullptr;
|
||||
|
||||
QNetworkAccessManager m_nam;
|
||||
const Tasking::TreeStorage<ExternalData> m_storage;
|
||||
const Tasking::Group m_recipe;
|
||||
std::unique_ptr<Tasking::TaskTree> m_taskTree;
|
||||
};
|
||||
|
||||
#endif // VIEWER_H
|
Reference in New Issue
Block a user