Использование Controller и QT Worker в рабочем примере GUI - PullRequest
0 голосов
/ 13 января 2019

Я создал минимальный пример графического интерфейса пользователя QT для обновления виджетов из рабочего потока на основе рекомендуемого подхода в документации QThread 5.12 .

Как описано в документации QThread 5.12 , класс Worker (с потенциально длинным void doWork (const QString & параметр) метод имеет вид:

class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork(const QString &parameter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        emit resultReady(result);
    }

signals:
    void resultReady(const QString &result);
};

и соответствующий класс контроллеров:

class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller() {
        Worker *worker = new Worker;
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
public slots:
    void handleResults(const QString &);
signals:
    void operate(const QString &);
};

В отличие от подклассов из QThread, подход, показанный в документации, показывает рекомендуемый способ, который использует контроллер и рабочий, который расширяет QObject вместо расширения QThread и переопределяет метод QThread::run, однако это делает не показывает, как их следует использовать в контексте реального примера.

Мне нужно использовать рабочий поток QT, который обновляет виджеты в графическом интерфейсе с помощью таймера.

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

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

Мой рабочий код состоит из следующих файлов.

controller.h

#pragma once

// SYSTEM INCLUDES
#include <QObject>
#include <QThread>

// APPLICATION INCLUDES
#include "Worker.h"

// DEFINES
// EXTERNAL FUNCTIONS
// EXTERNAL VARIABLES
// CONSTANTS
// STRUCTS
// TYPEDEFS
// FORWARD DECLARATIONS

class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller(/*MainWindow* mainWindow*/) {
        auto worker = new Worker;
        worker->moveToThread(&workerThread);
        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork);
        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
public slots:
    void handleResults(const QString &) {
        // how do I update the mainWindow from here
    }
signals:
    void operate(int);
};

Worker.h

#pragma once

// SYSTEM INCLUDES
#include <QTimer>
#include <QObject>
#include <QEventLoop>

// APPLICATION INCLUDES
#include "Worker.h"

// DEFINES
// EXTERNAL FUNCTIONS
// EXTERNAL VARIABLES
// CONSTANTS
// STRUCTS
// TYPEDEFS
// FORWARD DECLARATIONS

class Worker : public QObject
{
    Q_OBJECT

public slots:
    void doWork(int count)  {
        QString result = "finished";
        // Event loop allocated in workerThread
        // (non-main) thread affinity (as moveToThread)
        // this is important as otherwise it would occur
        // on the main thread.
        QEventLoop loop;
        for (auto i=0; i< count; i++) {
            // wait 1000 ms doing nothing...
            QTimer::singleShot(1000, &loop, SLOT(quit()));
            // process any signals emitted above
            loop.exec();

            emit progressUpdate(i);
        }
        emit resultReady(result);
    }
signals:
    void progressUpdate(int secondsLeft);
    void resultReady(const QString &result);
};

MainWindow.h - Мне нужно было добавить Controller член здесь. Я также добавил слот updateValue здесь, где я хочу обновить графический интерфейс. К сожалению, я не знаю, как заставить контроллер или рабочий connect получить сигнал из потока, чтобы обновить этот слот.

#pragma once

// SYSTEM INCLUDES
#include <memory>
#include <QMainWindow>

// APPLICATION INCLUDES
// DEFINES
// EXTERNAL FUNCTIONS
// EXTERNAL VARIABLES
// CONSTANTS
// STRUCTS
// TYPEDEFS
// FORWARD DECLARATIONS
namespace Ui {
class MainWindow;
}

class Controller;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_pushButton_clicked();
    void updateValue(int secsLeft);

private:
    Ui::MainWindow *ui;
    std::unique_ptr<Controller> mpController;
};

MainWindow.cpp -

#include <QThread>

#include "MainWindow.h"
#include "ui_MainWindow.h"
#include "Controller.h"

MainWindow::MainWindow(QWidget *parent) 
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
    , mpController(std::make_unique<Controller>())
{
    ui->setupUi(this);
}

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

void MainWindow::on_pushButton_clicked()
{
    emit mpController->operate(100);
}

void MainWindow::updateValue(int secsLeft)
{
    ui->secondsLeft->setText(QString::number(secsLeft));
}

и, наконец, main.cpp

#include "MainWindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

Мне в основном нужна помощь и объяснение того, как я должен использовать контроллер / рабочий QT Thread, интегрированный в мой графический интерфейс.

1 Ответ

0 голосов
/ 14 января 2019

Я постараюсь ответить на все вопросы, которые вы решаете в своем вопросе:

  1. Я не знаю, как заставить контроллер или рабочий подключить сигнал из потока для обновления этого слота.

Вы поняли это почти правильно.

Ваш работник находится в цикле событий вашего контроллера:

+--GUI-thread--+ (main event loop)
| MainWindow,  |
| Controller --o-----> +--QThread--+ (own event loop in ::exec())
+--------------+       | Worker    |
                       +-----------+

Связь между Контроллером и Рабочим должна происходить через соединения сигнальных слотов. Между сигналами MainWindow и Controller помехи сводятся к минимуму.

Вы можете представить контроллер как своего рода ретранслятор: команды из MainWindow передаются через контроллер в рабочий. Результаты от Worker передаются через Контроллер всем, кто заинтересован.

Для этого вы можете просто определить сигналы в контроллере:

class Controller : public QObject
{
    //...
signals:
    void SignalForwardResult(int result);
};

и затем вместо

    connect(worker, &Worker::resultReady, this, &Controller::handleResults);

используйте новый сигнал:

    connect(worker, &Worker::resultReady, this, &Controller::SignalForwardResult);
    // Yes, you can connect a signal to another signal the same way you would connect to a slot.

и в вашем MainWindow конструкторе:

//...
ui->setupUi(this);
connect(mpController, &Controller::SignalForwardResult, this, &MainWindow::displayResult);

Аналогично для Worker::progressUpdate() -> Controller::SignalForwardProgress() -> MainWindow::updateValue().


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

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

  • Вы запускаете задачу, отправляя ее в рабочую функцию ::doWork().
  • Задание заканчивается само по себе, когда долгая работа закончена. Вы получаете уведомление через сигнал рабочего resultReady.
  • Отмена задания возможна только путем вмешательства
    • Если у вас действительно есть QTimer, вы можете использовать слот cancel(), потому что он будет вызываться в цикле событий потока до следующего тайм-аута.
    • Если у вас длительные вычисления, вам нужно поделиться токеном, который вы прочитали из своего метода расчета и установили в потоке GUI. Для этого я обычно использую общий указатель QAtomicInt, но обычно достаточно и общего bool.

Обратите внимание, что пока метод выполняется в потоке, цикл событий этого потока блокируется и не получит никаких сигналов, пока метод не будет завершен.

НЕ используйте QCoreApplication::processEvents(), кроме случаев, когда вы действительно знаете, что делаете. (И ожидайте, что вы этого не сделаете!)


  1. как правильно интегрировать таймер в мой рабочий поток

Вы не должны.

  • Я полагаю, вы используете фоновый поток, потому что нужно сделать так много работы, или вам нужно блокировать ожидание так долго, чтобы оно блокировало графический интерфейс, верно? (Если нет, рассмотрите возможность не использовать темы, избавит вас от головной боли.)

  • Если вам нужен таймер, сделайте его членом Worker и установите для его parentObject экземпляр Worker. Таким образом, оба всегда будут иметь одинаковое сродство потоков. Затем подключите его к слоту, например Worker::timeoutSlot(). Там вы можете испустить сигнал финиша.

...