【问题标题】:Can I create widgets on a non-GUI thread, then send to the GUI?我可以在非 GUI 线程上创建小部件,然后发送到 GUI 吗?
【发布时间】:2019-10-31 08:48:41
【问题描述】:

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

所以我在另一个线程上创建了一个 GUIUpdater 类来执行周期性操作。我基于这里的解决方案,它显示了如何从另一个线程更新 QLabel:

Qt - updating main window with second thread

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

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

【问题讨论】:

    标签: multithreading qt qwidget qthread


    【解决方案1】:

    您的非 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;
    }
    

    几秒钟后:

    【讨论】:

      【解决方案2】:

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

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

      这意味着可以将接口部分链接到保存数据的单独部分,而不是使用方便的“小部件”来管理界面和数据。事实上,数据源可以是完全虚拟的……只是响应请求它的函数(您必须为该行为编写自定义代码——不过这可能是值得的)。

      因此,虽然您不能以编程方式在非 GUI 线程上构建小部件,但您可以构建类似 QStandardItemModel 的东西。然后,您的 UI 可以使用 QTreeView,而不是使用像 QTreeWidget 这样的小部件。然后,您无需在线程之间传输小部件,而是传输模型,并将其连接到视图...丢弃旧模型。

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-08-30
        • 2013-06-24
        • 1970-01-01
        • 1970-01-01
        • 2021-04-12
        • 1970-01-01
        相关资源
        最近更新 更多