Search code examples
c++qt5qthreadworkerqprogressdialog

How to stop/cancel a worker job using the cancel button of a QProgressDialog


My code is composed of a worker class and a dialog class. The worker class launches a job (a very long job). My dialog class has 2 buttons that allows for launching and stopping the job (they work correctly). I would like to implement a busy bar showing that a job is underway. I have used a QProgressDialog in the Worker class. When I would like to stop the job using the QprogressDialog cancel button, I can't catch the signal &QProgressDialog::canceled. I tried, this (put in the Worker constructor):

QObject::connect(progress, &QProgressDialog::canceled, this, &Worker::stopWork);

without any effect.

You can see the complete compiling code below.

How I can stop the job by clicking on the QprogressDialog cancel button?

Here below is my complete code to reproduce the behavior if necessary.

//worker.h

#ifndef WORKER_H
#define WORKER_H
#include <QObject>
#include <QProgressDialog>
class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr);
    virtual ~Worker();
    QProgressDialog * getProgress() const;
    void setProgress(QProgressDialog *value);
signals:
    void sigAnnuler(bool);
    // pour dire que le travail est fini
    void sigFinished();
    // mise à jour du progression bar
    void sigChangeValue(int);
public slots:
    void doWork();
    void stopWork();
private:
    bool workStopped = false;
    QProgressDialog* progress = nullptr;
};
#endif // WORKER_H

// worker.cpp

#include "worker.h"
#include <QtConcurrent>
#include <QThread>
#include <functional>
// Worker.cpp
Worker::Worker(QObject* parent/*=nullptr*/)
{
    //progress = new QProgressDialog("Test", "Test", 0, 0);
    QProgressDialog* progress = new QProgressDialog("do Work", "Annuler", 0, 0);
    progress->setMinimumDuration(0);
    QObject::connect(this, &Worker::sigChangeValue, progress, &QProgressDialog::setValue);
    QObject::connect(this, &Worker::sigFinished, progress, &QProgressDialog::close);
    QObject::connect(this, &Worker::sigAnnuler, progress, &QProgressDialog::cancel);
    QObject::connect(progress, &QProgressDialog::canceled, this, &Worker::stopWork);
}
Worker::~Worker()
{
    //delete timer;
    delete progress;
}
void Worker::doWork()
{
    emit sigChangeValue(0);

    for (int i=0; i< 100; i++)
    {

       qDebug()<<"work " << i;
       emit sigChangeValue(0);
       QThread::msleep(100);

       if (workStopped)
       {
           qDebug()<< "Cancel work";
           break;
       }
       
    }
    emit sigFinished();
}
void Worker::stopWork()
{
    workStopped = true;
}
QProgressDialog *Worker::getProgress() const
{
    return progress;
}
void Worker::setProgress(QProgressDialog *value)
{
    progress = value;
}

// mydialog.h

#ifndef MYDIALOG_H
#define MYDIALOG_H

#include <QDialog>
#include "worker.h"

namespace Ui {
class MyDialog;
}

class MyDialog : public QDialog
{
    Q_OBJECT

public:
    explicit MyDialog(QWidget *parent = 0);
    ~MyDialog();
    void triggerWork();
    void StopWork();
private:
    Ui::MyDialog *ui;
    QThread* m_ThreadWorker = nullptr;
    Worker* m_TraitementProdCartoWrkr = nullptr;
};

#endif // MYDIALOG_H
#include "mydialog.h"
#include "ui_mydialog.h"
#include <QProgressDialog>
#include <QThread>

MyDialog::MyDialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::MyDialog)
{
    ui->setupUi(this);
    m_TraitementProdCartoWrkr = new Worker(this);
    connect(ui->OK, &QPushButton::clicked, this, &MyDialog::triggerWork);
    connect(ui->Cancel, &QPushButton::clicked, this, &MyDialog::StopWork);
}
MyDialog::~MyDialog()
{
    delete ui;
}
void MyDialog::triggerWork()
{
    m_ThreadWorker = new QThread;
    QProgressDialog* progress = m_TraitementProdCartoWrkr->getProgress();
    m_TraitementProdCartoWrkr->moveToThread(m_ThreadWorker);
    QObject::connect(m_ThreadWorker, &QThread::started, m_TraitementProdCartoWrkr, &Worker::doWork);
    m_ThreadWorker->start();
}

void MyDialog::StopWork()
{
    m_TraitementProdCartoWrkr->stopWork();
}

// main.cpp

#include "mydialog.h"
#include "ui_mydialog.h"
#include <QProgressDialog>
#include <QThread>

MyDialog::MyDialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::MyDialog)
{
    ui->setupUi(this);
    m_TraitementProdCartoWrkr = new Worker(this);
    connect(ui->OK, &QPushButton::clicked, this, &MyDialog::triggerWork);
    connect(ui->Cancel, &QPushButton::clicked, this, &MyDialog::StopWork);
}

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

void MyDialog::triggerWork()
{
    m_ThreadWorker = new QThread;

    QProgressDialog* progress = m_TraitementProdCartoWrkr->getProgress();

    m_TraitementProdCartoWrkr->moveToThread(m_ThreadWorker);
    QObject::connect(m_ThreadWorker, &QThread::started, m_TraitementProdCartoWrkr, &Worker::doWork);
    //QObject::connect(m_ThreadWorker, &QThread::started, progress, &QProgressDialog::exec);

    //QObject::connect(progress, &QProgressDialog::canceled, m_TraitementProdCartoWrkr, &Worker::sigAnnuler);

    m_ThreadWorker->start();
}

void MyDialog::StopWork()
{
    m_TraitementProdCartoWrkr->stopWork();
}

Solution

  • Any signals you send to the worker thread will be queued, so the signal will be processed too late, after all the work has already been done.

    There is (at least) three ways to avoid this problem:

    1. While doing the work, in a regular fashion, interrupt your work so incoming signals can be processed. For example, you could use QTimer::singleShot(0, ...) to signal yourself when work should be resumed. This signal will then be at the end of the queue, after any canceled/stop work signals. Obviously this is disruptive and complicates your code.

    2. Use a state variable that you set from the GUI thread, but read from the worker thread. So, a bool isCancelled that defaults to false. As soon as it is true, stop the work.

    3. Have a controller object that manages the worker / jobs and uses locking. This object provides an isCancelled() method to be called directly by worker.

    I previously used the second approach, nowadays use the third approach in my code and typically combine it with the progress updates. Whenever I issue a progress update, I also check for canceled flag. The reasoning is that I time my progress updates so that they are smooth to the user, but not exhaustively holding of the worker from doing work.

    For the second approach, in your case, m_TraitementProdCartoWrkr would have a cancel() method that you call directly (not through signal/slot), so it will run in caller's thread, and set the canceled flag (you may throw std::atomic into the mix). The rest of the communication between GUI/worker would still use signals & slots -- so they are processed in their respective threads.

    For an example for the third approach, see here and here. The job registry also manages progress (see here), and signals it further to monitors (i.e., progress bars).