Can I create widgets on a non-GUI thread, then send to the GUI?

对着背影说爱祢 提交于 2020-12-12 02:56:29

问题


I have a complex function in my MainWindow class which periodically runs queries and changes attributes and data of the widgets. Since it can take a long time on the main thread, the GUI can appear to freeze.

So I created a GUIUpdater class on another thread to do the periodic operation. I based it on the solution here, which shows how to update a QLabel from another thread:

Qt - updating main window with second thread

But that solution requires defining a connection. With a complex function with multiple widgets, it is difficult to define a connection for each attribute and data of the widgets.

Is there an easier way? For instance: Can I create entirely new widgets making the same API calls as on the main thread in the GUIUpdater thread, and send the whole widget over to the UI using a signal, to be replaced in the UI?


回答1:


Your non-GUI "requests" shall know nothing about widgets.

GUI and "requests" shall be as independent as possible. You cannot create widgets in a non-GUI thread, but you can create your own class that can update GUI widgets inside itself by calls from another thread. Widgets for such classes should be set once from a GUI thread, but then you can apply values to the widgets by "direct" calls. The idea is to have one base class which will do queued method invokes for you. Also I introduced one QObject-based interface class:

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

Then base class to perform queued- and common operations: 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;
}

Your control example:

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

Example of use:

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;
}

After a few seconds:




回答2:


Can I create entirely new widgets making the same API calls as on the main thread in the GUIUpdater thread, and send the whole widget over to the UI using a signal, to be replaced in the UI?

You can't make "widgets" on a non-GUI thread. But a "widget" in Qt's terminology often is made up of two components, a "view" and a "model". (If you haven't already, then read about Qt's model/view separation, and perhaps work through the Model/View tutorial)

This means that instead of using the convenience "widgets" that manage both the interface and the data, it is possible to link up the interface part to a separate part that holds the data. In fact, the data source could be fully virtual...just responding to functions that ask for it (you will have to write custom code for that behavior--might be worth it, though).

So while you can't programmatically build a widget on a non-GUI thread, you can build something like a QStandardItemModel. Then instead of using a widget like QTreeWidget, your UI could use QTreeView. Then instead of transferring the widget between threads you would transfer the model, and connect it to the view...throwing the old model away.

BEWARE, HOWEVER... you do have to transfer the model to the GUI thread to use it with a widget. A data model and a view must have the same thread affinity--I hit that issue a while ago (here is a cache from the discussion on the mailing list, which went missing):

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

Writing your own data model that uses mutexes and semaphores and such is another way to go. But multithreaded programming in C++ is just generally pretty hard to get right. So there's not going to be any super easy answer if what you are doing is genuinely tricky. Just remember that whatever you do, the model and view have to ultimately live on the same thread to work together.



来源:https://stackoverflow.com/questions/58639622/can-i-create-widgets-on-a-non-gui-thread-then-send-to-the-gui

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!