Могу ли я создать виджеты в потоке без графического интерфейса, а затем отправить в графический интерфейс? - PullRequest
2 голосов
/ 31 октября 2019

У меня есть сложная функция в моем классе MainWindow, которая периодически выполняет запросы и изменяет атрибуты и данные виджетов. Поскольку это может занять много времени в главном потоке, графический интерфейс может зависнуть.

Поэтому я создал класс GUIUpdater в другом потоке для выполнения периодической операции. Я основал его на решении, приведенном здесь, которое показывает, как обновить QLabel из другого потока:

Qt - обновление главного окна вторым потоком

Но это решение требует определениясвязь. С помощью сложной функции с несколькими виджетами сложно определить соединение для каждого атрибута и данных виджетов.

Есть ли более простой способ? Например: Могу ли я создать совершенно новые виджеты, выполняющие те же вызовы API, что и в основном потоке в потоке GUIUpdater, и отправить весь виджет в пользовательский интерфейс с помощью сигнала для замены в пользовательском интерфейсе?

Ответы [ 2 ]

3 голосов
/ 31 октября 2019

Ваши "запросы" без 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

2 голосов
/ 31 октября 2019

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

Вы не можете создавать "виджеты" в потоке без GUI. Но «виджет» в терминологии Qt часто состоит из двух компонентов: «вид» и «модель». (Если вы еще этого не сделали, прочитайте о разделении модели / вида Qt и, возможно, изучите учебник Модель / представление )

Это означает, что вместо использования удобных «виджетов», которые управляют как интерфейсом, так и данными, можно связать интерфейсную часть с отдельной частью, которая содержит данные. Фактически, источник данных может быть полностью виртуальным ... просто реагировать на функции, которые его запрашивают (вам придется написать собственный код для такого поведения - хотя, возможно, это того стоит).

Так что покавы не можете программно создать виджет в потоке без GUI, вы можете создать что-то вроде QStandardItemModel . Тогда вместо использования виджета, подобного QTreeWidget , ваш пользовательский интерфейс может использовать QTreeView . Затем вместо переноса виджета между потоками вы передадите модель и подключите ее к представлению ... выбрасывая старую модель.

BEWARE, HOWEVER ... у вас естьперенести модель в поток графического интерфейса, чтобы использовать ее с виджетом. Модель данных и представление должны иметь одинаковое сходство потоков - я затронул эту проблему некоторое время назад (вот кэш из обсуждения в списке рассылки, который пропал):

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

Создание собственной модели данных, в которой используются мьютексы и семафоры, и это еще один способ. Но многопоточное программирование на C ++, как правило, довольно сложно сделать правильно. Так что не будет никакого супер простого ответа, если то, что вы делаете, действительно сложно. Просто помните, что что бы вы ни делали, модель и представление должны в конечном итоге жить в одном потоке, чтобы работать вместе.

...