I have an application that has potentially long-running tasks and also possibly thousands or millions or results.
This specific application (code below) isn't of any worth, but it is aimed to provide a general use case of the need to maintain a responsive UI amid 'thousands' of results.
To be clear, I am aware that one should reduce the number of times the UI is polled. My question is regarding design principles that can be applied to this (and other similar) scenarios in keeping a responsive UI.
My first thought is to use a QTimer and process all 'results' every e.g. 200ms, an example which can be found here but needs adation.
What methods are available and which are preferred to keep a responsive UI?
A simple example of I am trying to explain is as follows. I have a UI that:
generates a list of integers,
passes it into a mapped function to pow(x,2) the value, and
measure the progress
When running this app, click the 'start' button will run the application, but due to the frequency of results being processed by the QueuedConnection: QFutureWatcher::resultReadyAt, the UI cannot respond to any user clicks, thus attempting to 'pause' or 'stop' (cancel) is futile.
Wrapper for QtConcurrent::mapped()
function passing in lambda (for a member function)
#include <functional>
template <typename ResultType>
class MappedFutureWrapper
{
public:
using result_type = ResultType;
MappedFutureWrapper<ResultType>(){}
MappedFutureWrapper<ResultType>(std::function<ResultType (ResultType)> function): function(function){ }
MappedFutureWrapper& operator =(const MappedFutureWrapper &wrapper) {
function = wrapper.function;
return *this;
}
ResultType operator()(ResultType i) {
return function(i);
}
private:
std::function<ResultType(ResultType)> function;
};
MainWindow.h UI
class MainWindow : public QMainWindow {
Q_OBJECT
public:
struct IntStream {
int value;
};
MappedFutureWrapper<IntStream> wrapper;
QVector<IntStream> intList;
int count = 0;
int entries = 50000000;
MainWindow(QWidget* parent = nullptr);
static IntStream doubleValue(IntStream &i);
~MainWindow();
private:
Ui::MainWindow* ui;
QFutureWatcher<IntStream> futureWatcher;
QFuture<IntStream> future;
//...
}
MainWindow implementation
MainWindow::MainWindow(QWidget* parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
qDebug() << "Launching";
intList = QVector<IntStream>();
for (int i = 0; i < entries; i++) {
int localQrand = qrand();
IntStream s;
s.value = localQrand;
intList.append(s);
}
ui->progressBar->setValue(0);
}
MainWindow::IntStream MainWindow::doubleValue(MainWindow::IntStream &i)
{
i.value *= i.value;
return i;
}
void MainWindow::on_thread1Start_clicked()
{
qDebug() << "Starting";
// Create wrapper with member function
wrapper = MappedFutureWrapper<IntStream>([this](IntStream i){
return this->doubleValue(i);
});
// Process 'result', need to acquire manually
connect(&futureWatcher, &QFutureWatcher<IntStream>::resultReadyAt, [this](int index){
auto p = ((++count * 1.0) / entries * 1.0) * 100;
int progress = static_cast<int>(p);
if(this->ui->progressBar->value() != progress) {
qDebug() << "Progress = " << progress;
this->ui->progressBar->setValue(progress);
}
});
// On future finished
connect(&futureWatcher, &QFutureWatcher<IntStream>::finished, this, [](){
qDebug() << "done";
});
// Start mapped function
future = QtConcurrent::mapped(intList, wrapper);
futureWatcher.setFuture(future);
}
void MainWindow::on_thread1PauseResume_clicked()
{
future.togglePaused();
if(future.isPaused()) {
qDebug() << "Paused";
} else {
qDebug() << "Running";
}
}
void MainWindow::on_thread1Stop_clicked()
{
future.cancel();
qDebug() << "Canceled";
if(future.isFinished()){
qDebug() << "Finished";
} else {
qDebug() << "Not finished";
}
}
MainWindow::~MainWindow()
{
delete ui;
}
Explanation of why the UI is 'not responding'.
The UI loads w/o performing any action other than printing "Launching". When the method on_thread1Start_clicked()
is invoked, it started the future, in addition to adding the following connection:
connect(&futureWatcher, &QFutureWatcher<IntStream>::resultReadyAt, [this](int index){
auto p = ((++count * 1.0) / entries * 1.0) * 100;
int progress = static_cast<int>(p);
if(this->ui->progressBar->value() != progress) {
qDebug() << "Progress = " << progress;
this->ui->progressBar->setValue(progress);
}
});
This connection listens for a result from the future, and acts upon it (this connect function runs on the UI thread). Since I am emulating a massive amount of 'ui updates', shown by int entries = 50000000;
, each time a result is processed, the QFutureWatcher<IntStream>::resultReadyAt
is invoked.
While this is running for +/- 2s, the UI does not respond to the 'pause' or 'stop' clicks linked to on_thread1PauseResume_clicked()
and on_thread1Stop_clicked
respectively.