Updating view based on model update on Qt
-
I am having trouble updating my qtableview when the data wrapped by the model the view uses changes. I am not talking about appending/adding data here, I am specifically talking about modifying existing data. View is only updated when I scroll or select cell(s). In other words, it is only updated when repainting is triggered (this is my guess actually). So, this made me think that calling update() on widget when the data is modified would suffice, but this was not the case.
I am adding data to the model programmatically, user cannot modify or add data in the model.
Here is the minimal, reproducible example:
mymodel.h:
#pragma once #include <QAbstractTableModel> #include <QVector> #include "myobject.h" class MyModel : public QAbstractTableModel { Q_OBJECT private: QVector<MyObject> my_objects_; /// underlying data structure for the model. public: MyModel(QObject * parent = {}); int rowCount(const QModelIndex &) const override; int columnCount(const QModelIndex &) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::EditRole) const override; void add(const MyObject& my_object); void edit(const MyObject& my_object); };
mymodel.cpp:
#include "mymodel.h" #include <QDebug> #include <iostream> MyModel::MyModel(QObject * parent) : QAbstractTableModel{parent} { } int MyModel::rowCount(const QModelIndex &) const { return my_objects_.count(); } int MyModel::columnCount(const QModelIndex &) const { return 4; } QVariant MyModel::data(const QModelIndex &index, int role) const { const auto row = index.row(); if(row == -1) { return {}; } const auto my_object = my_objects_[row]; if(role == Qt::DisplayRole) { switch (index.column()) { case 0: return my_object.id; case 1: return my_object.a; case 2: return my_object.b; case 3: return my_object.c; default: return {}; }; } return {}; } QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole) { return {}; } switch (section) { case 0: return "id"; case 1: return "a"; case 2: return "b"; case 3: return "c"; default: return {}; } } void MyModel::add(const MyObject &my_object) { beginInsertRows({}, my_objects_.count(), my_objects_.count()); my_objects_.push_back(my_object); endInsertRows(); } void MyModel::edit(const MyObject& my_object) { const auto id = my_object.id; for(auto& my_object_ : my_objects_) { if(my_object_.id == id) { my_object_.a = my_object.a; my_object_.b = my_object.b; my_object_.c = my_object.c; /// should I use dataChanged signal here? If so, how? break; } } }
myobject.h:
#pragma once #include <QString> struct MyObject { int id; int a; double b; QString c; };
mainwindow.h:
#pragma once #include <QMainWindow> #include "mymodel.h" QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACE class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); private slots: void onEditClicked(); private: Ui::MainWindow *ui; MyModel my_model_; };
mainwindow.cpp:
#include "mainwindow.h" #include "./ui_mainwindow.h" const static QVector<MyObject> g_my_objects{ {0, 1, 1.1, "object1"}, {1, 11, 1.2, "object2"}, {2, 12, 1.3, "object3"}, {3, 13, 1.4, "object4"}, {4, 14, 1.5, "object5"}, {5, 15, 1.6, "object6"}, {6, 16, 1.7, "object7"}, {7, 17, 1.8, "object8"} }; MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui->setupUi(this); ui->tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); ui->tableView->setModel(&my_model_); for(const auto& g_my_object : g_my_objects) { my_model_.add(g_my_object); } } MainWindow::~MainWindow() { delete ui; } void MainWindow::onEditClicked() { qDebug() << __PRETTY_FUNCTION__; my_model_.edit({2, 22222, 2222.222, "myobject22222"}); update(); /// seems to be doing nothing. }
main.cpp:
#include "mainwindow.h" #include <QApplication> #include <QVector> #include "myobject.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); MainWindow w; w.show(); return a.exec(); }
mainwindow.ui:
<?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> <class>MainWindow</class> <widget class="QMainWindow" name="MainWindow"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>800</width> <height>600</height> </rect> </property> <property name="windowTitle"> <string>MainWindow</string> </property> <widget class="QWidget" name="centralwidget"> <layout class="QVBoxLayout" name="verticalLayout"> <item> <widget class="QPushButton" name="pushButton"> <property name="text"> <string>Edit</string> </property> </widget> </item> <item> <widget class="QTableView" name="tableView"/> </item> </layout> </widget> <widget class="QMenuBar" name="menubar"> <property name="geometry"> <rect> <x>0</x> <y>0</y> <width>800</width> <height>23</height> </rect> </property> </widget> <widget class="QStatusBar" name="statusbar"/> </widget> <resources/> <connections> <connection> <sender>pushButton</sender> <signal>clicked()</signal> <receiver>MainWindow</receiver> <slot>onEditClicked()</slot> <hints> <hint type="sourcelabel"> <x>604</x> <y>41</y> </hint> <hint type="destinationlabel"> <x>951</x> <y>20</y> </hint> </hints> </connection> </connections> <slots> <slot>onEditClicked()</slot> </slots> </ui>
CMakeLists.txt:
cmake_minimum_required(VERSION 3.5) project(model_view_example VERSION 0.1 LANGUAGES CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) set(PROJECT_SOURCES main.cpp mainwindow.cpp mymodel.cpp mainwindow.h mainwindow.ui ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) qt_add_executable(model_view_example MANUAL_FINALIZATION ${PROJECT_SOURCES} ) # Define target properties for Android with Qt 6 as: # set_property(TARGET model_view_example APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR # ${CMAKE_CURRENT_SOURCE_DIR}/android) # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation else() if(ANDROID) add_library(model_view_example SHARED ${PROJECT_SOURCES} ) # Define properties for Android with Qt 5 after find_package() calls as: # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") else() add_executable(model_view_example ${PROJECT_SOURCES} ) endif() endif() target_link_libraries(model_view_example PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(model_view_example PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} MACOSX_BUNDLE TRUE WIN32_EXECUTABLE TRUE ) if(QT_VERSION_MAJOR EQUAL 6) qt_finalize_executable(model_view_example) endif()
So, why the view is not updated when I call update()? Is there something wrong about my implementation?
Here, I clicked on edit button, but the view only got updated with its new value when I clicked on a cell:
I feel like Qt MVC examples do not really map well to how I do it here. I do not know how I can implement this logic in a more Qt-ish way if possible.
Here is the github repo for the minimal project:
-
@ozcanay
You have written asetData()
, but as you say you are not calling that from yourMyModel::edit()
method. That seems to look up some row viamy_object_.id == id
and changes it. But the model does not tell the view this has happened. Where you wrote/// should I use dataChanged signal here? If so, how?
I said, yes, you must do that, passing the correct
index
todataChanged()
for whichever row item you alter. -
@ozcanay
You do not seem to implementMyModel::setData()
if your model is editable? https://doc.qt.io/qt-5/qabstracttablemodel.html#subclassingEditable models need to implement
setData()
Your
MyModel::edit()
changes the model data, but withoutsetData()
oremit dataChanged()
the view does not know anything has changed. Implement that in the model, remove theupdate()
in the view, behaviour better now?[BTW, your
update()
was onMainWindow
. I think you meant to call it onui->tableView
.]/// should I use dataChanged signal here? If so, how?
You must identify the (row,column) in the model for any item you change in your backing model. From that you can generate a
QModelIndex index(row, column)
in the model to pass todataChanged()
.If you're implementing
QAbstractItemModel::setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole)
essentially you should be callingemit dataChanged(index, index)
. -
What I changed with respect to your feedback is as follows:
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role) { qDebug() << __PRETTY_FUNCTION__ << ", row: " << index.row() << ", column:" << index.column(); if (role == Qt::EditRole) { if (!checkIndex(index)) return false; my_objects_[index.row()] = qvariant_cast<MyObject>(value); emit dataChanged(index, index); return true; } return false; }
myobject.h
#pragma once #include <QString> #include <QMetaType> struct MyObject { int id; int a; double b; QString c; }; Q_DECLARE_METATYPE(MyObject)
However, still no improvement on the behavior. Btw, you are right that I meant to call ui->tableView->update(), but that does not work as well.
setData method is not even called when I programmatically edit the model. However, when I make cells editable via:
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const { return Qt::ItemIsEditable | QAbstractTableModel::flags(index); }
then of course setData method is called when I edit a cell because user is editing the model, however I do not want user to edit value in the table directly, it has to be edited by the underlying logic imposed by the code. Specifically, only Model::edit method should be editing the model.
I feel like what I am trying to do should have been straightforward, what am I still missing here?
-
@ozcanay
You have written asetData()
, but as you say you are not calling that from yourMyModel::edit()
method. That seems to look up some row viamy_object_.id == id
and changes it. But the model does not tell the view this has happened. Where you wrote/// should I use dataChanged signal here? If so, how?
I said, yes, you must do that, passing the correct
index
todataChanged()
for whichever row item you alter. -
Now, it seems to be working:
void MyModel::edit(const MyObject& my_object) { const auto id = my_object.id; for(auto i = 0u; i < my_objects_.size(); ++i) { if(my_objects_[i].id == id) { const auto var = QVariant::fromValue(my_object); setData(index(i, 1), var); setData(index(i, 2), var); setData(index(i, 3), var); break; } } }
bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role) { qDebug() << __PRETTY_FUNCTION__ << ", row: " << index.row() << ", column:" << index.column(); if (role == Qt::EditRole) { if (!checkIndex(index)) return false; my_objects_[index.row()] = qvariant_cast<MyObject>(value); emit dataChanged(index, index); return true; } return false; }
The thing I did not know was how to create a QModelIndex in edit() method. QAbtractItemModel::index() was what I was looking for. dataChanged() signal is emitted in setData() method, so I do not have to explicitly emit that signal in edit() method. Thanks for your help. One thing bothering me is 3 separate calls to setData in my code, since I want to update 3 columns. Is there a way to say for example "update all of the values in this row" given the input QVariant.
-
@ozcanay
Good that it works, shows the issue was the rightdataChanged()
emission(s) so that the view knew what was changed in the model.setData()
can only alter a single (row, column) element at a time, and hence emit separatedataChanged()
signals. In your case you do not need to go viasetData()
in your ownedit()
method. Now that you have identified the row number via afor (auto i = 0; ++i)
loop you can revert to your first approach, something like:for(auto i = 0u; i < my_objects_.size(); ++i) { if(my_objects_[i].id == id) { my_object_.a = my_object.a; my_object_.b = my_object.b; my_object_.c = my_object.c; /// should I use dataChanged signal here? If so, how? emit dataChanged(index(i, 1), index(i, 3)); // not sure about your column numbers here break; } }
-
void MyModel::edit(const MyObject& my_object) { const auto id = my_object.id; for(auto i = 0u; i < my_objects_.size(); ++i) { if(my_objects_[i].id == id) { my_objects_[i].a = my_object.a; my_objects_[i].b = my_object.b; my_objects_[i].c = my_object.c; emit dataChanged(index(i, 1), index(i, 3)); // not sure about your column numbers here break; } } }
works like a charm. Then, should I conclude that setData is not needed? I have removed setData method and it still works.
-
@ozcanay
As I think you said earlier,setData()
will for one thing be used if you make your view editable. If you only ever do updates programmatically via youredit()
method there is nothing that needs to call it, no point mapping your changes over tosetData()
if you have internal code to do it directly. The vital thing is theemit dataChanged()
covering the rows/columns you have changed; from that the view knows which cells to refresh. -
@JonB
Yes, the current version of edit() method is basically doing what setData() method is supposed to do in my scenario. As you said, given that users will not be able to edit cells in the table, i.e. the model will not be editable by users, setData() method seems to be redundant. Thanks for elaborating.