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 thesubmit
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 connectingvalueChanged
tosubmit
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 connectvalueChanged
tosubmit
. It works, and I have no problem that the box value updates if I change the back-end model. I leave thesubmitPolicy
on default (which I think isAutoSubmit
).Which sounds like your situation, which does not work for you? If it makes a difference, note that for
QSpinBox
es I always connectvalueChanged
withQt::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, doesQt::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 theconnection
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 thevalueChanged
(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 calledDirectConnection
you must not cause the value to change yourself? But the code does work if it's runQueuedConnection
.