Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Two-way binding with QDataWidgetMapper



  • I am building an application with Qt Widgets where I adjust some properties of a model in real-time with slider controls. I also want to update the controls if the model updates on its own.

    I am using a QStandardItemModel as the model, and QDataWidgetMapper to map widgets to the model. The problem is that QDataWidgetMapper only submits changes done by touching the controls when a widget loses focus or I press enter. Changes to the model reflect to the controls correctly.

    If I connect valueChanged signals from the controls to the submit slot of the QDataWidgetMapper, touching the controls changes the model immediately like expected. However, if I change the model in the background, the controls do not update anymore. It is as if manually connecting valueChanged to submit breaks the connection between the model and the controls.

    Is it possible to have both directions work at the same time? Effectively I want two-way binding between the controls and the model.

    Here is a very minimal code example, where I have a QStandardItemModel with one row and one column. I connect a QSlider, a QSpinBox and a QTableView to the model. There is also a QPushButton that sets the model data to a certain value. Ideally pressing it would also reset all the other widgets to reflect the model.

    #include "MainWindow.h"
    #include "ui_MainWindow.h"
    #include <QStandardItemModel>
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent)
        , ui(new Ui::MainWindow)
        , m_model(new QStandardItemModel(1, 1, this))
        , m_mapper(new QDataWidgetMapper(this))
    {
        populateModel();
    
        ui->setupUi(this);
        ui->tableView->setModel(m_model);
        initializeMapper();
    
        /* Reset model when button is pushed */
        connect(ui->pushButton, &QPushButton::clicked, [this]() {
            m_model->setData(m_model->index(0, 0), 10);
        });
    }
    
    void MainWindow::populateModel()
    {
        m_model->setData(m_model->index(0, 0), 10);
        m_model->setHeaderData(0, Qt::Orientation::Horizontal, "Column header");
    }
    
    void MainWindow::initializeMapper()
    {
        m_mapper->setModel(m_model);
        m_mapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
        m_mapper->addMapping(ui->spinBox, 0);
        m_mapper->addMapping(ui->horizontalSlider, 0);
        m_mapper->toFirst();
    
        /* The two following lines cause updates in the model to not show up in the control widgets */
        connect(ui->horizontalSlider, &QSlider::valueChanged, m_mapper, &QDataWidgetMapper::submit);
        connect(ui->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), m_mapper, &QDataWidgetMapper::submit);
    }
    
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    

    If I comment out the two connect lines, updates in the model show up correctly in the controls.



  • @naavis
    I don't know whether my situation is the same/will help you, but....

    I want my model to update immediately on QSpinBox::valueChanged. I too connect valueChanged to submit. It works, and I have no problem that the box value updates if I change the back-end model. I leave the submitPolicy on default (which I think is AutoSubmit).

    Which sounds like your situation, which does not work for you? If it makes a difference, note that for QSpinBoxes I always connect valueChanged with Qt::QueuedConnection, else my like is miserable:

    template <typename Context, typename Method>
        QMetaObject::Connection connectSpinBoxValueChanged(QSpinBox *spin, Context slotObject, Method method)
        {
            // connect `spin->valueChanged(int i)` signal to slot
            // see https://forum.qt.io/topic/113606/qspinbox-valuechanged-with-debugger-breakpoint-brain-damaged and https://bugreports.qt.io/browse/QTBUG-14259
            // for why `Qt::QueuedConnection` is specified here
            return QObject::connect(spin, QOverload<int>::of(&QSpinBox::valueChanged), slotObject, method, Qt::QueuedConnection);
        }
    

    Now, I might have the dimmest recollection that you might need to do this. But for the reason of: I'm thinking that updating inside the slot gets mashed when within the QDataWidgetMapper code, which is doing its own model/view updating.... Anyway, does Qt::QueuedConnection change your behaviour?



  • @JonB said in Two-way binding with QDataWidgetMapper:

    Anyway, does Qt::QueuedConnection change your behaviour?

    Wow, it indeed fixed the problem!

    It works just as expected after adding the Qt::QueuedConnection parameter to the connection calls, like this:

    connect(ui->horizontalSlider, &QSlider::valueChanged, m_mapper, &QDataWidgetMapper::submit, Qt::QueuedConnection);
    connect(ui->spinBox, QOverload<int>::of(&QSpinBox::valueChanged), m_mapper, &QDataWidgetMapper::submit, Qt::QueuedConnection);
    

    Thanks a ton! I have been struggling with this a long time. If someone knows more details about why this happens, it would be interesting to hear.



  • Now that I found some good keywords to Google with, I found another thread where you had mentioned the same solution: https://forum.qt.io/topic/119814/auto-update-model-in-qwidgetmapper/3

    Thanks again!



  • @naavis
    Yes, thanks for digging it out. I recall it was tough figuring at the time, because using a debugger with breakpoints in the valueChanged (or anything it calls) would alter behaviour from when no breakpoint. To do with Qt internal timer for spinboxes.

    Anyway, the issue with valueChanged: isn't it something like if called DirectConnection you must not cause the value to change yourself? But the code does work if it's run QueuedConnection.


Log in to reply