我可以在非GUI线程上创建小部件,然后发送到GUI吗?

时间:2019-10-31 08:48:41

标签: multithreading qt qwidget qthread

我的MainWindow类中有一个复杂的函数,该函数定期运行查询并更改小部件的属性和数据。由于在主线程上可能要花费很长时间,因此GUI可能会冻结。

因此,我在另一个线程上创建了一个GUIUpdater类来执行定期操作。我基于此处的解决方案,该解决方案展示了如何从另一个线程更新QLabel:

Qt - updating main window with second thread

但是该解决方案需要定义一个连接。对于具有多个小部件的复杂功能,很难为每个小部件的属性和数据定义连接。

有没有更简单的方法?例如:是否可以创建全新的小部件,使其与GUIUpdater线程中的主线程上的API调用相同,并使用信号将整个小部件发送到UI,并在UI中替换?

2 个答案:

答案 0 :(得分:3)

您的非GUI“请求”对小部件一无所知。

GUI和“请求”应尽可能独立。您不能在非GUI线程中创建窗口小部件,但是可以创建自己的类,该类可以通过来自另一个线程的调用来更新其内部的GUI窗口小部件。应该从GUI线程设置一次此类类的窗口小部件,但是您可以通过“直接”调用将值应用于窗口小部件。想法是有一个基类,它将为您执行排队方法调用。我还介绍了一个基于QObject的接口类:

IUiControlSet.h

#include <QObject>
#include <QVariant>

class QWidget;

// Basic interface for an elements set which should be updated
// and requested from some non-GUI thread
class IUiControlSet: public QObject
{
    Q_OBJECT

public:
    virtual void setParentWidget(QWidget* par) = 0;

    virtual void setValues(QVariant var) = 0;

    virtual QVariant values() = 0;

signals:
    void sControlChanged(QVariant var);
};

然后,基类执行排队的和常见的操作: BaseUiControlSet.h

#include "IUiControlSet.h"

#include <QList>

class QDoubleSpinBox;
class QPushButton;

// Abstract class to implement the core queued-based functionality
class BaseUiControlSet : public IUiControlSet
{
    Q_OBJECT

public slots:
    void setParentWidget(QWidget* par) override;

    void setValues(QVariant var) override;

    QVariant values() override;

protected slots:
    virtual void create_child_elements() = 0;

    virtual void set_values_impl(QVariant var) = 0;
    virtual QVariant values_impl() const = 0;

    void on_control_applied();

protected:
    // common elements creation
    QDoubleSpinBox* create_spinbox() const;
    QPushButton* create_applied_button() const;

protected:
    QWidget*        m_parentWidget = nullptr;
};

BaseUiControlSet.cpp

#include "BaseUiControlSet.h"

#include <QDoubleSpinBox>
#include <QPushButton>

#include <QThread>

void BaseUiControlSet::setParentWidget(QWidget* par)
{
    m_parentWidget = par;

    create_child_elements();
}

// The main idea
void BaseUiControlSet::setValues(QVariant var)
{
    QMetaObject::invokeMethod(this, "set_values_impl",
        Qt::QueuedConnection, Q_ARG(QVariant, var));
}

// The main idea
QVariant BaseUiControlSet::values()
{
    QVariant ret;

    QThread* invokeThread = QThread::currentThread();
    QThread* widgetThread = m_parentWidget->thread();

    // Check the threads affinities to avid deadlock while using Qt::BlockingQueuedConnection for the same thread
    if (invokeThread == widgetThread)
    {
        ret = values_impl();
    }
    else
    {
        QMetaObject::invokeMethod(this, "values_impl",
            Qt::BlockingQueuedConnection, 
            Q_RETURN_ARG(QVariant, ret));
    }

    return ret;
}

void BaseUiControlSet::on_control_applied()
{
    QWidget* wgt = qobject_cast<QWidget*>(sender());

    QVariant val = values();

    emit sControlChanged(val);
}

// just simplify code for elements creation
// not necessary
QDoubleSpinBox* BaseUiControlSet::create_spinbox() const
{
    auto berSpinBox = new QDoubleSpinBox(m_parentWidget);


    bool connectOk = connect(berSpinBox, &QDoubleSpinBox::editingFinished,
        this, &BaseUiControlSet::on_control_applied);

    Q_ASSERT(connectOk);

    return berSpinBox;
}

// just simplify code for elements creation
// not necessary
QPushButton* BaseUiControlSet::create_applied_button() const
{
    auto button = new QPushButton(m_parentWidget);

    bool connectOk = connect(button, &QPushButton::clicked,
        this, &BaseUiControlSet::on_control_applied);

    Q_ASSERT(connectOk);

    return button;
}

您的控制示例:

MyControl.h

#include "BaseUiControlSet.h"

// User control example
class MyControl : public BaseUiControlSet
{
    Q_OBJECT

public:
    struct Data
    {
        double a = 0;
        double b = 0;
    };

protected slots:
    void create_child_elements() override;

    void set_values_impl(QVariant var) override;
    QVariant values_impl() const override;

private:
    QDoubleSpinBox*     dspin_A = nullptr;
    QDoubleSpinBox*     dspin_B = nullptr;

    QPushButton*        applyButton = nullptr;
};

Q_DECLARE_METATYPE(MyControl::Data);

MyControl.cpp

#include "MyControl.h"

#include <QWidget>

#include <QDoubleSpinBox>
#include <QPushButton>

#include <QVBoxLayout>

void MyControl::create_child_elements()
{
    dspin_A = create_spinbox();
    dspin_B = create_spinbox();

    applyButton = create_applied_button();
    applyButton->setText("Apply values");

    auto layout = new QVBoxLayout;

    layout->addWidget(dspin_A);
    layout->addWidget(dspin_B);
    layout->addWidget(applyButton);

    m_parentWidget->setLayout(layout);
}

void MyControl::set_values_impl(QVariant var)
{
    Data myData = var.value<MyControl::Data>();

    dspin_A->setValue(myData.a);
    dspin_B->setValue(myData.b);
}

QVariant MyControl::values_impl() const
{
    Data myData;

    myData.a = dspin_A->value();
    myData.b = dspin_B->value();

    return QVariant::fromValue(myData);
}

使用示例:

MainWin.h

#include <QtWidgets/QWidget>
#include "ui_QueuedControls.h"

#include <QVariant>

class MainWin : public QWidget
{
    Q_OBJECT

public:
    MainWin(QWidget *parent = Q_NULLPTR);

private slots:
    void on_my_control_applied(QVariant var);

private:
    Ui::QueuedControlsClass ui;
};

MainWin.cpp

#include "MainWin.h"

#include "MyControl.h"

#include <QtConcurrent> 
#include <QThread>

#include <QDebug>

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

    auto control = new MyControl;

    control->setParentWidget(this);

    connect(control, &IUiControlSet::sControlChanged,
        this, &MainWin::on_my_control_applied);

    // Test: set the GUI spinboxes' values from another thread
    QtConcurrent::run(
        [=]()
    {
        double it = 0;

        while (true)
        {
            it++;

            MyControl::Data myData;

            myData.a = it / 2.;
            myData.b = it * 2.;

            control->setValues(QVariant::fromValue(myData)); // direct call

            QThread::msleep(1000);
        }
    });
}

// will be called when the "Apply values" button pressed,
// or when spinboxes editingFinished event triggered
void MainWin::on_my_control_applied(QVariant var)
{
    MyControl::Data myData = var.value<MyControl::Data>();

    qDebug() << "value a =" << myData.a;
    qDebug() << "value b =" << myData.b;
}

几秒钟后:

enter image description here

答案 1 :(得分:2)

  

我是否可以创建全新的小部件,使其与GUIUpdater线程中的主线程进行相同的API调用,然后使用信号将整个小部件发送到UI,并在UI中进行替换?

您不能在非GUI线程上创建“小部件”。但是Qt术语中的“小部件”通常由两个部分组成,即“视图”和“模型”。 (如果您还没有,请读一下Qt's model/view separation,也许可以通过Model/View tutorial进行学习)

这意味着,可以使用接口部分链接到保存数据的单独部分,而不是使用同时管理界面和数据的便利“小部件”。实际上,数据源可能是完全虚拟的...只是响应要求它的功能(您将不得不为该行为编写自定义代码-可能值得这样做)。

因此,尽管您不能以编程方式在非GUI线程上构建窗口小部件,但是可以构建类似QStandardItemModel的东西。然后,您的UI可以使用QTreeWidget,而不是使用QTreeView之类的小部件。然后,您无需转移线程之间的窗口小部件,而是转移模型并将其连接到视图...将旧模型扔掉。

注意,但是... ,您必须将模型转移到GUI线程才能与小部件一起使用。数据模型和视图必须具有相同的thread affinity-我前一段时间遇到了这个问题(这是邮件列表中讨论的缓存,丢失了):

http://blog.hostilefork.com/qt-model-view-different-threads/

编写使用互斥量和信号量的数据模型是另一种方法。但是,通常很难正确地实现C ++中的多线程编程。因此,如果您正在做的事情确实很棘手,那么不会有任何简单超级答案。只需记住,无论您做什么,模型和视图最终都必须位于同一线程上才能协同工作。