Как остановить / отменить рабочее задание с помощью кнопки отмены 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 ответа
Любые сигналы, которые вы отправляете рабочему потоку, будут помещены в очередь, поэтому сигнал будет обработан слишком поздно, после того как вся работа уже будет выполнена.
Есть (как минимум) три способа избежать этой проблемы:
Регулярно выполняя работу, прерывайте ее, чтобы можно было обработать входящие сигналы. Например, вы можете использовать
QTimer::singleShot(0, ...)
чтобы подать себе сигнал, когда следует возобновить работу. Этот сигнал будет в конце очереди после любых сигналов отмены / остановки работы. Очевидно, это разрушительно и усложняет ваш код.Используйте переменную состояния, которую вы устанавливаете из потока графического интерфейса пользователя, но читаете из рабочего потока. Итак,
bool isCancelled
это по умолчанию false. Как только это станет правдой, прекратите работу.Имейте объект контроллера, который управляет рабочим / заданиями и использует блокировку. Этот объект предоставляет
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);