Как остановить / отменить рабочее задание с помощью кнопки отмены QProgressDialog

Мой код состоит из рабочего класса и диалогового класса. Рабочий класс запускает работу (очень долгую работу). В моем диалоговом классе есть 2 кнопки, которые позволяют запускать и останавливать задание (они работают правильно). Я хотел бы реализовать полосу занятости, показывающую, что работа выполняется. Я использовал QProgressDialog в классе Worker. Когда я хочу остановить задание с помощью QprogressDialog cancel кнопка, я не могу поймать сигнал &QProgressDialog::canceled. Я пробовал это (вставил конструктор Worker):

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

без какого-либо эффекта.

Вы можете увидеть полный код компиляции ниже.

Как я могу остановить задание, нажав кнопку отмены QprogressDialog?

Вот мой полный код для воспроизведения поведения, если это необходимо.

//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();
}

3 ответа

Решение

Любые сигналы, которые вы отправляете рабочему потоку, будут помещены в очередь, поэтому сигнал будет обработан слишком поздно, после того как вся работа уже будет выполнена.

Есть (как минимум) три способа избежать этой проблемы:

  1. Регулярно выполняя работу, прерывайте ее, чтобы можно было обработать входящие сигналы. Например, вы можете использовать QTimer::singleShot(0, ...)чтобы подать себе сигнал, когда следует возобновить работу. Этот сигнал будет в конце очереди после любых сигналов отмены / остановки работы. Очевидно, это разрушительно и усложняет ваш код.

  2. Используйте переменную состояния, которую вы устанавливаете из потока графического интерфейса пользователя, но читаете из рабочего потока. Итак, bool isCancelledэто по умолчанию false. Как только это станет правдой, прекратите работу.

  3. Имейте объект контроллера, который управляет рабочим / заданиями и использует блокировку. Этот объект предоставляет isCancelled() метод, который будет вызываться непосредственно работником.

Раньше я использовал второй подход, сейчас я использую третий подход в своем коде и обычно комбинирую его с обновлениями прогресса. Каждый раз, когда я выпускаю обновление, я также проверяю флаг отмены. Причина в том, что я рассчитываю обновления своего прогресса так, чтобы они были гладкими для пользователя, но не полностью удерживали работника от выполнения работы.

Для второго подхода в вашем случае m_TraitementProdCartoWrkr будет иметь метод cancel(), который вы вызываете напрямую (не через сигнал / слот), поэтому он будет запускаться в потоке вызывающего и установить флаг отмены (вы можете бросить std::atomicв смесь). Остальная часть связи между графическим интерфейсом пользователя и рабочим по-прежнему будет использовать сигналы и слоты, поэтому они обрабатываются в соответствующих потоках.

Пример третьего подхода см. Здесь и здесь. Реестр заданий также управляет прогрессом (см. Здесь) и сигнализирует об этом мониторам (например, индикаторам выполнения).

Посмотрите, как легко вы можете переписать свой код, используя высокоуровневый QtConcurrent API:

MyDialog.h

       #include <QtWidgets/QDialog>
#include "ui_MyDialog.h"

class MyDialog : public QDialog
{
    Q_OBJECT

public:
    MyDialog(QWidget *parent = nullptr);
    ~MyDialog();

    void triggerWork();
    void stopWork();

signals:
    void sigChangeValue(int val);

private:
    Ui::MyDialogClass ui;
};

MyDialog.cpp

       #include "MyDialog.h"

#include <QtConcurrent/QtConcurrent>
#include <QThread>
#include <atomic>
#include <QProgressDialog>

// Thread-safe flag to stop the thread. No mutex protection is needed 
std::atomic<bool> gStop = false;

MyDialog::MyDialog(QWidget *parent)
    : QDialog(parent)
{
    ui.setupUi(this);

    auto progress = new QProgressDialog;

    connect(this, &MyDialog::sigChangeValue, 
        progress, &QProgressDialog::setValue);

    connect(progress, &QProgressDialog::canceled, 
        this, [this]()
        {
            stopWork();
        }
    );

    // To simplify the example, start the work here:
    triggerWork();
}

MyDialog::~MyDialog()
{ 
    stopWork();
}

void MyDialog::triggerWork()
{
    // Run the code in another thread using High-Level QtConcurrent API
    QtConcurrent::run([this]()
        {
            for(int i = 0; i < 100 && !gStop; i++)
            {
                this->sigChangeValue(i); // signal emition is always thread-safe

                qDebug() << "running... i =" << i;

                QThread::msleep(100);
            }

            qDebug() << "stopped";
        });
}

void MyDialog::stopWork()
{
    gStop = true;
}

Читайте также:

Основы
многопоточности в Qt Технологии многопоточности в Qt
Синхронизация потоков
Потоки и объекты
Пропущенная статья о многопоточности Qt в C++
События потоков QObjects

@ypnos, благодарю за идеи. Что я сделал для решения проблемы, так это изменил:

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

из Worker конструктор в эту строку:

    QObject::connect(progress, &QProgressDialog::canceled, [&]() {
                                                                  this->stopWork();
                                                                 });

Теперь я могу остановить работу cancel кнопка QProgressDialog.

Я не мог понять, почему первый код (ниже) не работал?

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

Это не сработало, потому что тип подключения signals/slots выбираются, когда сигнал испускается и по умолчанию Qt::AutoConnection, но у меня другой поток между приемником и эмиттером. (подробнее см. здесь), поэтому он не мог работать

Затем мне нужно указать, какой тип подключения использовать для немедленного вызова слота при передаче сигнала, таким образом, этот код также работает сейчас (основное отличие в том, что здесь мы явно указываем тип подключения Qt::DirectConnection):

    QObject::connect(progress, &QProgressDialog::canceled, this, &Worker::stopWork, Qt::DirectConnection);
Другие вопросы по тегам