如何使用 QProgressDialog 的取消按钮 stop/cancel 工人作业

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

我的代码由一个工人 class 和一个对话框 class 组成。 worker class 启动了一个工作(一个很长的工作)。 我的对话框 class 有 2 个按钮,允许启动和停止作业(它们工作正常)。 我想实现一个忙碌的酒吧,表明工作正在进行中。 我在 Worker class 中使用了 QProgressDialog。当我想使用 Qpr​​ogressDialog 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();
}

您发送到工作线程的任何信号都将排队,因此在所有工作都已完成之后,信号的处理为时已晚。

有(至少)三种方法可以避免这个问题:

  1. 在进行工作时,以常规方式中断您的工作,以便处理传入的信号。例如,您可以使用 QTimer::singleShot(0, ...) 来通知自己何时应该恢复工作。然后,此信号将位于队列的末尾,在任何 canceled/stop 工作信号之后。显然,这是破坏性的,会使您的代码复杂化。

  2. 使用您从 GUI 线程设置但从工作线程读取的状态变量。因此,默认为 false 的 bool isCancelled。一旦属实,立即停止工作。

  3. 有一个控制器对象来管理工作人员/作业并使用锁定。这个对象提供了一个isCancelled()方法,可以被worker直接调用。

我以前使用第二种方法,现在在我的代码中使用第三种方法,并且通常将它与进度更新结合起来。每当我发布进度更新时,我也会检查取消标志。原因是我对我的进度更新进行了计时,以便它们对用户来说是流畅的,但不会完全阻止工人工作。

对于第二种方法,在您的情况下,m_TraitementProdCartoWrkr 将有一个您直接调用的 cancel() 方法(而不是通过 signal/slot),因此它将 运行 在调用者的线程,并设置取消标志(你可以将 std::atomic 扔到混合中)。 GUI/worker 之间的其余通信仍将使用信号和槽——因此它们在各自的线程中进行处理。

有关第三种方法的示例,请参阅 here and here. The job registry also manages progress (see here),并将其进一步发送给监视器(即进度条)。

@ypnos,谢谢你的想法。 我为解决问题所做的是修改:

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

Worker 构造器到这一行:

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

现在我可以通过 QProgressDialogcancel 按钮停止作业。

我不明白的是,为什么第一个代码(下面)不起作用?

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

它没有工作,因为在发出信号时选择了 signals/slots 的连接类型,默认情况下是 Qt::AutoConnection,但我在接收器和发射器之间有不同的线程。 (查看更多详情here),因此无法正常工作

然后我必须指定在发出信号时使用哪种连接类型来立即调用插槽,因此,这段代码现在也可以工作(主要区别在于,在这里,我们明确指定连接类型 Qt::DirectConnection):

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

看看使用 High-Level 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;
}

另请阅读:

Threading Basics in Qt
Multithreading Technologies in Qt
Synchronizing Threads
Threads and Objects
The Missing Article About Qt Multithreading in C++
Threads Events QObjects