Continuously update a QML LineSeries ChartView with a data model from C++ - c++

I have a ChartView in QML I'm trying to update from a data model in C++, however, I'm running into certain issues where I can't seem to figure out a way to update the LineSeries properly.
My Sample QML Code looks like:
ChartView {
id: dataChartView
animationOptions: ChartView.NoAnimation
theme: ChartView.ChartThemeDark
antialiasing: true
Layout.fillHeight: true
Layout.fillWidth: true
ValueAxis {
id: axisXdata
min: dataManager.xMin
max: dataManager.xMax
}
ValueAxis {
id: axisYdata
min: 0
max: 1
}
LineSeries {
id: dataLineSeries
name: "Angle"
axisX: axisXdata
axisY: axisYdata
}
VXYModelMapper {
id: dataModelMapper
model: dataManager.dataModel[data]
series: dataLineSeries
firstRow: 1
xColumn: 0
yColumn: 1
}
}
The underlying model is a QAbstractTableModel that looks like this:
class DataModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum{
NameRole,
ValueRole
};
explicit DataModel(QObject *parent = nullptr);
QHash<int, QByteArray> roleNames() const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
void setMaxSize(int maxSize);
int maxSize() const;
Q_INVOKABLE void addData(const QPointF& point);
public:
void handleNewData(const QPointF& point);
private:
QVector<QPointF> m_data;
int m_ModelMaxSize;
signals:
void newDataAdded(const QPointF& point);
};
and the relevant addData() function simply just pushes the Point into the m_data vector
void MsclDataModel::addData(const QPointF &point)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_data.push_back(point);
endInsertRows();
}
I then have another class called DataManager which runs a function in another thread and just adds points to the data model
void DataManager::GeneratePoints()
{
setXMin(QDateTime::currentDateTime().toMSecsSinceEpoch()); // for the min and max on the horizontal axis
for(double t=0 ; ; t+=1)
{
double y = (1 + sin(t/10.0)) / 2.0;
// many data models are required
for(const auto& model : m_Models)
{
auto time = QDateTime::currentDateTime().toMSecsSinceEpoch();
setXMax(qMax(m_xMax, time));
model->handleNewData(QPointF(time, y));
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
My issue is with this method and with the number of models I have, the application slows to a crawl and crashes eventually after about a minute or so. I've tried to constrain the addData() function to only show the latest 500 points by using something like this:
void DataModel::addData(const QPointF &point)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
if(m_data.size() == 500)
{
m_data.erase(m_data.begin());
}
m_data.push_back(point);
endInsertRows();
}
However, my output looks something like:
where the data model just stops updating after the 500th point.
I've also tried using a List to pop after max size of the list has been reached but the output is still the same. What am I doing wrong here?

Instead of beginInsertRows() and endInsertRows() I've used beginResetModel() and endResetModel() which works. I assume the range for the beginInsertRows() is wrong and it doesn't send an update anymore, so it stops drawing. You would probably also need to call beginRemoveRows() I would stick with a full reset.
void DataModel::addData(const QPointF &point)
{
beginResetModel();
if (m_data.size() == 500) {
m_data.pop_front();
emit xMinChanged();
}
m_data.push_back(point);
emit xMaxChanged();
endResetModel();
}
Edit: This will also work.
void DataModel::addData(const QPointF &point)
{
if (m_data.size() == 500) {
beginRemoveRows(QModelIndex(), 0, 0);
m_data.pop_front();
endRemoveRows();
emit xMinChanged();
}
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_data.push_back(point);
endInsertRows();
emit xMaxChanged();
}

Related

Qml/Qt c++: How to automatically update items in Qml ListView when the underlying Qt c++ ListModel changes?

Based on an example video from Qt (the ToDo example), I have created a ListView based Qt/Qml application.
Data for the ListView in Qml comes from a c++ class based on QAbstractListModel. The c++ model class is populated with data from a database.
This all works fine.
I have added a pushbutton in the Qml file which invokes a method in the c++ code that fetches data from a remote source (i.e. makes an https request). The https response data is of course asynchronous with the method that makes the http request.
Debug lines in the c++ https response handler confirm that the response is received okay. I save the response to the database.
If I close the application and re-open it, the new data is shown in the Qml list because, once again, on opening the application the c++ model is populated from the db.
But what I really need is that, after saving the data to the db in the https response handler, I also push the new data to the Qml ListView so that I don't have to restart the application to refresh the updated list data.
Unfortunately I don't know how to push the new data from c++ to Qml. I have tried a number of ways (signals from c++, slots in Qml, reading the updated list from c++, etc) but nothing has worked so far.
I know it has to do with the fact that the pushbutton starts an http request in c++ which is not handled synchronously but in a slot function which is the http response handler.
But unfortunately I don't know how to resolve this issue.
I would appreciate some help with this.
Note:
The following example is in fact off of an excellent YouTube Video on Qt model/view by Mitch Curtis Using C++ Models in QML - To-Do List!
But my code is very similar except that I want to add a button which changes the descriptions based on an https response:
The c++ files providing the listdata:
todolist.h and todolist.cpp
=============================================================
#ifndef TODOLIST_H
#define TODOLIST_H
#include <QObject>
#include <QVector>
struct ToDoItem
{
bool done;
QString description;
};
class ToDoList : public QObject
{
Q_OBJECT
public:
explicit ToDoList(QObject *parent = nullptr);
QVector<ToDoItem> items() const;
bool setItemAt(int index, const ToDoItem &item);
signals:
void preItemAppended();
void postItemAppended();
void preItemRemoved(int index);
void postItemRemoved();
public slots:
void appendItem();
void removeCompletedItems();
private:
QVector<ToDoItem> m_Items;
};
=======================================================================
#include "todolist.h"
ToDoList::ToDoList(QObject *parent) : QObject(parent)
{
m_Items.append({ true, QStringLiteral("Wash the car") });
m_Items.append({ false, QStringLiteral("Fix the sink") });
m_Items.append({ true, QStringLiteral("Wash the dishes") });
}
QVector<ToDoItem> ToDoList::items() const
{
return m_Items;
}
bool ToDoList::setItemAt(int index, const ToDoItem &item)
{
if (index <0 || index >= m_Items.size()) {
return false;
}
const ToDoItem &oldItem = m_Items.at(index);
bool nothingChanged = oldItem.done == item.done
&& oldItem.description == item.description;
if(nothingChanged) {
return false;
}
m_Items[index] = item;
return true;
}
void ToDoList::appendItem()
{
emit preItemAppended();
ToDoItem item;
item.done = false;
m_Items.append(item);
emit postItemAppended();
}
void ToDoList::removeCompletedItems()
{
for (int i = 0; i < m_Items.size();) {
if(!m_Items[i].done) {
++i;
continue;
}
//otherwise...
emit preItemRemoved(i);
m_Items.removeAt(i);
emit postItemRemoved();
}
}
###################################################################
The c++ files implementing the listmodel:
todomodel.h and todomodel.cpp
==================================================
#ifndef TODOMODEL_H
#define TODOMODEL_H
#include <QAbstractListModel>
class ToDoList;
class TodoModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(ToDoList *list READ list WRITE setList)
public:
explicit TodoModel(QObject *parent = nullptr);
enum {
DoneRole = Qt::UserRole,
DescriptionRole
};
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
// Editable:
bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual QHash<int, QByteArray> roleNames() const override;
ToDoList *list() const;
void setList(ToDoList *newList);
private:
ToDoList *m_List;
};
#endif // TODOMODEL_H
===================================================================
#include "todomodel.h"
#include "todolist.h"
TodoModel::TodoModel(QObject *parent)
: QAbstractListModel(parent)
, m_List(nullptr)
{
}
int TodoModel::rowCount(const QModelIndex &parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
if (parent.isValid() || !m_List)
return 0;
return m_List->items().size();
}
QVariant TodoModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_List)
return QVariant();
const ToDoItem item = m_List->items().at(index.row());
switch (role) {
case DoneRole:
return QVariant(item.done);
case DescriptionRole:
return QVariant(item.description);
}
return QVariant();
}
bool TodoModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!m_List) {
return false;
}
ToDoItem item = m_List->items().at(index.row());
switch (role) {
case DoneRole:
item.done = value.toBool();
break;
case DescriptionRole:
item.description = value.toByteArray();
break;
}
if (m_List->setItemAt(index.row(), item)) {
emit dataChanged(index, index, QVector<int>() << role);
return true;
}
return false;
}
Qt::ItemFlags TodoModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable;
}
QHash<int, QByteArray> TodoModel::roleNames() const
{
QHash<int, QByteArray> names;
names[DoneRole] = "done";
names[DescriptionRole] = "description";
return names;
}
ToDoList *TodoModel::list() const
{
return m_List;
}
void TodoModel::setList(ToDoList *newList)
{
beginResetModel();
if(m_List) {
m_List->disconnect();
}
m_List = newList;
if(!m_List) {
endResetModel();
return;
}
connect(m_List, &ToDoList::preItemAppended, this, [=]() {
const int index = m_List->items().size();
beginInsertRows(QModelIndex(), index, index);
});
connect(m_List, &ToDoList::postItemAppended, this, [=]() {
endInsertRows();
});
connect(m_List, &ToDoList::preItemRemoved, this, [=](int index) {
beginRemoveRows(QModelIndex(), index, index);
});
connect(m_List, &ToDoList::postItemRemoved, this, [=]() {
endRemoveRows();
});
endResetModel();
}
#########################################################################
The View File: ToDoList.qml
Displays the data provided by the c++ classes
============================================
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import ToDo 1.0
ColumnLayout {
Frame {
Layout.fillWidth: true
ListView {
implicitWidth: 250
implicitHeight: 250
clip: true
anchors.fill: parent
model: TodoModel {
list: toDoList
}
delegate: RowLayout {
width: parent.width
CheckBox {
checked: model.done
onClicked: model.done=checked
}
TextField {
text: model.description
onEditingFinished: model.desciption = text
Layout.fillWidth: true
}
}
}
}
RowLayout {
Button {
text: qsTr("Add new item")
onClicked: toDoList.appendItem()
Layout.fillWidth: true
}
Button {
text: qsTr("Remove Completed Items")
onClicked: toDoList.removeCompletedItems()
Layout.fillWidth: true
}
}
}
Whenever the Model changes we should notify the View. Refer to this link:
QML views are automatically updated when the model changes. Remember
the model must follow the standard rules for model changes and notify
the view when the model has changed by using
QAbstractItemModel::dataChanged(),
QAbstractItemModel::beginInsertRows(), and so on. See the Model
subclassing reference for more information.
Here's how you can enhance your example to achieve a similar result:
On click of Fetch data button, after 3 seconds, the first row's description changes to https.
todolist.h:
signals:
void updateData();
public slots:
void fetchData();
todolist.cpp:
void ToDoList::fetchData()
{
QTimer::singleShot(3000, (QObject*)this, SIGNAL(updateData()));
}
todomodel.cpp:
connect(mList, &ToDoList::postItemRemoved, this, [=]() {
endRemoveRows();
});
connect(mList, &ToDoList::updateData, this, [=]() {
QVariant value = "https";
QModelIndex index = createIndex(0,0);
setData(index, value, DescriptionRole);
});
ToDoList.qml:
Button {
text: qsTr("Remove completed")
onClicked: toDoList.removeCompletedItems()
Layout.fillWidth: true
}
Button {
text: qsTr("Fetch data")
onClicked: toDoList.fetchData()
Layout.fillWidth: true
}
I have now fixed my issue thanks to #ArunKumarB.
The main tip from #ArunKumarB's comment was QModelIndex index = createIndex(0,0);
That is, how to convert a row index to a QModelIndex object.
The rest was mostly plumbing.

QML Chart using QAbstractTableModel doesn't dynamically update

I have a QAbtractTableModel that is accessible to a QML file with chart and series and XYModelMapper. When trying to update the model, nothing shows up on the chart. I've tried different methods(even trying QStandardItemModel before) and I can't seem to get any charts to update once it's finished loading. Is there something I'm doing wrong using either the model or model mapper? I've included simplified version of the files here.
class MyModel : public QAbstractTableModel
{
Q_OBJECT
public:
QVariant data(const QModelIndex& index, int role) const override;
int columnCount(const QModelIndex& parent) const override;
int rowCount(const QModelIndex& parent) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
Qt::ItemFlags flags(const QModelIndex & index) const override;
void clear();
void addData(QDateTime time, float temperature, quint32 brightness, quint8 moisture, quint16 conductivity);
private:
struct data {
QDateTime time;
float value;
};
QList<data> tableData;
};
class Obj : public QObject
{
Q_OBJECT
Q_PROPERTY(QAbstractTableModel* model READ getModel NOTIFY updated)
public:
QAbstractTableModel* getModel() {return myModel;}
Q_INVOKABLE void startUpdate(); //updated emitted here eventually as well as addData called on model.
signals:
void updated();
private:
MyModel *myModel = new MyModel();
};
QVariant MyModel::data(const QModelIndex& index, int role) const
{
if(role == Qt::DisplayRole && tableData.size()-1 >= index.row()) {
if(index.column() == 0) {
return QVariant(tableData[index.row()].time.toMSecsSinceEpoch());
} else if(index.column() == 1) {
return QVariant(tableData[index.row()].value);
} else {
return QVariant();
}
} else {
return QVariant();
}
}
int MyModel::columnCount(const QModelIndex& parent) const
{
return 5;
}
int MyModel::rowCount(const QModelIndex& parent) const
{
return 24;
}
void MyModel::addData(QDateTime time, float value)
{
tableData.append(data{time, value}); //Previously tried dynamic row count with begin/end row inserted.
emit dataChanged(createIndex(0,0),createIndex(23,4));
}
Q_DECL_EXPORT int main(int argc, char *argv[])
{
QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
QQmlApplicationEngine engine;
qmlRegisterType<Obj>("org.eyecreate.testmodel",1,0,"ObjModel");
engine.load(QUrl(QStringLiteral("qrc:///main.qml")));
if (engine.rootObjects().isEmpty()) {
return -1;
}
return app.exec();
}
import QtCharts 2.3 as Charts
import org.eyecreate.testmodel 1.0
ObjModel {
id: myObj
}
Charts.ChartView {
antialiasing: true
legend.visible: false
anchors.fill: parent
Charts.LineSeries {
axisX: Charts.DateTimeAxis {
format: "MMM d yyyy ha"
}
axisY: Charts.ValueAxis {
min: 0
max: 50
}
Charts.VXYModelMapper {
model: myObj.model
xColumn: 0
yColumn: 1
}
}
}
Button {
onClicked: {
myObj.startUpdate();
}
}
EDIT: I found code very similar to mine that shows the same issue here: https://bitbucket.org/johnwoltman/chartmodelmappertest/src/master/
It seems this might be a bug: https://bugreports.qt.io/browse/QTBUG-60336
I found out through trying everything that the data points are in the series correctly, just that the graph needed to be adjusted so the points were in view.
Adjusting the min/max for each axis has allowed the lines to show up.

Custom model in c ++ doesnt update ListView on QML side

I'm building GUI to control my ADC, connected to the STM32 board. Communications are handled by UART. Everything works fine. The problem is to properly handling my custom channel structure as a model object to be able to present data in different ways depending on the delegate object.
I have tried a few different solutions, none of them works. The case looks trivial however is a little different than presented examples on the forum or in tutorials on the web because I'm trying to update the data model from c++ side, not from qml. That forces me to invoke setData() override method to be invoked on c++ side and propagate dataChange() signal into qml. Let's present some code.
That's how the app presents the data models after initialization. This data are provided in SensorList constructor. No matter what I tried, I can not change those values on qml side. Even when I confirmed using debugger that vector of my channels in sensorList is changing when data are received from sensor. This looks like a problem with communication between C++ and QML.
My data handler.
ADS8588h.h
#ifndef CHANNEL_H
#define CHANNEL_H
#include <QObject>
#include <QString>
class AdsChannel {
Q_GADGET
Q_PROPERTY(QString name READ name )
Q_PROPERTY(double value READ value )
public:
AdsChannel(const QString& channelName, const double value)
: mName{channelName}, mValue{value} {}
AdsChannel() = default;
AdsChannel(const AdsChannel& rhs) = default;
AdsChannel& operator=(const AdsChannel& rhs) = default;
void setName(const QString& name) { mName = name;}
void setValue(const double value) { mValue = value; }
const QString name() const { return mName; }
double value() const { return mValue; }
private:
QString mName;
double mValue;
};
Q_DECLARE_METATYPE(AdsChannel);
#endif // CHANNEL_H
SensorList.h which collects list of channels
#ifndef SENSORLIST_H
#define SENSORLIST_H
#include "AdsChannel.h"
class SensorList : public QObject
{
Q_OBJECT
public:
explicit SensorList(QObject *parent = nullptr)
{
mItems.push_back(AdsChannel("CH1",5.0));
mItems.push_back(AdsChannel("CH2",55.0));
mItems.push_back(AdsChannel("CH3",0.0));
mItems.push_back(AdsChannel("CH4",0.0));
mItems.push_back(AdsChannel("CH5",0.0));
mItems.push_back(AdsChannel("CH6",0.0));
mItems.push_back(AdsChannel("CH7",0.0));
mItems.push_back(AdsChannel("CH8",0.0));
}
QVector<AdsChannel> items() const
{
return mItems;
}
bool setItemAt(int index, const AdsChannel &item)
{
if (index < 0 || index >= mItems.size())
return false;
const AdsChannel &oldItem = mItems.at(index);
if (item.name() == oldItem.name() && item.value() == oldItem.value())
return false;
mItems[index] = item;
return true;
}
private:
QVector<AdsChannel> mItems;
};
#endif // SENSORLIST_H
SensorDataModel.h
#ifndef SENSORDATAMODEL_H
#define SENSORDATAMODEL_H
#include <QAbstractListModel>
#include <QStandardItemModel>
#include "AdsChannel.h"
#include "SensorList.h"
class SensorList;
class SensorDataModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(SensorList *list READ list WRITE setList)
public:
explicit SensorDataModel(QObject *parent = nullptr);
enum {
NameRole = Qt::UserRole +1 ,
ValueRole
};
// Basic functionality:
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = ValueRole) const override;
// Editable:
bool setData(const QModelIndex &index, const QVariant &value,
const int role = ValueRole) override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
virtual QHash<int, QByteArray> roleNames() const override;
SensorList *list() const;
void setList(SensorList *list);
private:
SensorList *mList;
};
#endif // SENSORDATAMODEL_H
SensorDataModel.cpp
#include "SensorDataModel.h"
#include "SensorList.h"
SensorDataModel::SensorDataModel(QObject *parent)
: QAbstractListModel(parent)
, mList(new SensorList)
{
}
int SensorDataModel::rowCount(const QModelIndex &parent) const
{
// For list models only the root node (an invalid parent) should return the list's size. For all
// other (valid) parents, rowCount() should return 0 so that it does not become a tree model.
if (parent.isValid() || !mList)
return 0;
return mList->items().size();
}
QVariant SensorDataModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !mList)
return QVariant();
const AdsChannel item = mList->items().at(index.row());
switch (role) {
case NameRole:
return QVariant(item.name());
case ValueRole:
return QVariant(item.value());
}
return QVariant();
}
bool SensorDataModel::setData(const QModelIndex &indexModel, const QVariant &value,const int role)
{
// indexModel is empty as this method is invocable from c++ side. Index which is given in dataChange() signal is calculated
// based on index data which are updated
//When data arrived from a sensor this how this method is invoked.
//mData.setData(QModelIndex(),QVariant::fromValue<AdsChannel>(temp),SensorDataModel::ValueRole);
if (!mList)
return false;
auto newElement = value.value<AdsChannel>();
const auto listElements = mList->items();
AdsChannel item;
int indexFinder = 0;
for(indexFinder ; indexFinder < rowCount(); ++indexFinder)
{
if(newElement.name() == listElements.at(indexFinder).name()) {
item = listElements.at(indexFinder);
break;
}
}
switch (role) {
case NameRole:
item.setName(newElement.name());
break;
case ValueRole:
item.setValue(newElement.value());
break;
}
if (mList->setItemAt(indexFinder, item)) {
emit dataChanged (this->index(indexFinder,0), this->index(indexFinder,0), QVector<int>() << role);
//This signal is emited but never reached on QML side
return true;
}
return false;
}
Qt::ItemFlags SensorDataModel::flags(const QModelIndex &index) const
{
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsEditable;
}
QHash<int, QByteArray> SensorDataModel::roleNames() const
{
QHash<int, QByteArray> names;
names[NameRole] = "channelName";
names[ValueRole] = "value";
return names;
}
SensorList *SensorDataModel::list() const
{
return mList;
}
void SensorDataModel::setList(SensorList *list)
{
// I never use a setList() as my list is initialized throw constructor.
beginResetModel();
if (mList)
mList->disconnect(this);
mList = list;
endResetModel();
}
main.cpp
#include "ads8588h.h"
#include "SensorList.h"
#include "SensorDataModel.h"
int main(int argc, char *argv[]) {
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
ADS8588H driver ;
qmlRegisterType<ADS8588H>("AdsDriver", 1, 0, "AdsDriver"); // Used to some other features
qmlRegisterType<SensorDataModel>("SensorModel", 1, 0, "SensorDataModel");
qmlRegisterUncreatableType<SensorList>("SensorModel", 1, 0, "SensorList",
QStringLiteral("SensorList should not be created in QML"));
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty(QStringLiteral("sensorList"), driver.getSensor()->list());
const QUrl url(QStringLiteral("qrc:/main.qml"));
QObject::connect(
&engine, &QQmlApplicationEngine::objectCreated, &app,
[url](QObject *obj, const QUrl &objUrl) {
if (!obj && url == objUrl) QCoreApplication::exit(-1);
},
Qt::QueuedConnection);
engine.load(url);
return app.exec();
}
QML code using simple TEXT label to eliminate some potential bug using CANVAS, requestPaint() signals and other.
import QtQuick 2.0
import QtQuick.Layouts 1.3
import QtQuick.Controls 2.3
import SensorModel 1.0
Item {
id:root
anchors.fill: parent
ListView {
implicitWidth: root.width * 0.8
implicitHeight: width
x:root.width/2 - width/2
y:170
id: customList
focus: true
spacing: 4
snapMode: ListView.SnapOneItem
highlightRangeMode: ListView.StrictlyEnforceRange
highlightMoveDuration: 250
orientation: ListView.Vertical
boundsBehavior: Flickable.StopAtBounds
model: SensorDataModel{
list: sensorList
onDataChanged: console.log("ss") //<-- This signal never appear
}
delegate: Item{
width: parent.width
height: width * 0.2
// ChannelProgressBar{
// id:licznik
// anchors.fill: parent
// labelSuffix: suffix
// labelChannel: model.channelName
// progressValue: model.value
// }
Row{
spacing: 20
Text{
id:label1
font.pixelSize: 40
text: model.channelName
}
Text{
id:label2
font.pixelSize: 40
text: model.value
}
}
}
}
}
So my question is What im doing wrong that qml did not updated data even when dataChanged() signal is emitted.

Model isn't updated with setContextProperty

I'm very new to Qt and have issues passing my Model to my View.
My view features a bunch of buttons and a Map with some markers whose latitudes/longitudes come from my Model.
Clicking on buttons should update the markers on the map (delete some and/or display new ones).
The problem is : When my model (a QList) gets updated on the C++ side, the QML side doesn't.
(I know this kind of question seems to have already been asked, but after reading the different answers, I can't get a clear view of whether I can get away with a smarter way of calling setContextProperty() or if I have to use things like emit signals and bind properties, which I also can't get a clear view of after reading a little documentation)
The architecture is the following :
A main class with a QApplication instantiation and a MainWindow (MainWindow being a custom QMainWindow class). App gets executed and Window gets shown.
A Mapwidget class (custom QQuickWidget class) with an updateMap() method that :
Reacts to button clicks on the user interface
Updates the Model (the QList)
Uses the setContextProperty() method to pass the updated Model to
the View
The MainWindow class has a Mapwidget attribute
Things I have tried so far :
When making a call to setContextProperty() in the Mapwidget Constructor before calling the setSource() method, the Model is taken into consideration. So the syntax I'm using for passing the Model into the View ought to be correct. The problem seems to be that any call to setContextProperty() afterwards (in this case : in the updateMap() method) isn't passed to the QML File.
Calling the setContextProperty() on different levels (Mapwidget class, MainWindow class), the results are the same, it's never taken into account after the application's first launch.
I have tested the Model and know for a fact that it does get updated inside the updateMap() method, it just seems like the update isn't transfered to the QML File.
QML File :
Item {
width: 1200
height: 1000
visible: true
Plugin {
id: osmPlugin
name: "osm"
}
Map {
id: map
anchors.fill: parent
plugin: osmPlugin
center: QtPositioning.coordinate(45.782074, 4.871263)
zoomLevel: 5
MapItemView {
model : myModel
delegate: MapQuickItem {
coordinate:QtPositioning.coordinate(
model.modelData.lat,model.modelData.lon)
sourceItem: Image {
id:image_1
source: <picturePath>
}
anchorPoint.x: image_1.width / 2
anchorPoint.y: image_1.height / 2
}
}
}
Mapwidget Class :
mapwidget::mapwidget(QWidget *parent) : QQuickWidget(parent)
{
this->setSource(QUrl(QStringLiteral("qrc:/main.qml")));
}
void mapwidget::updateMap(QList<QObject *> &data)
{
/**
DO OPERATIONS TO UPDATE data
Each append has the following form :
data.append(new DataObject(someLatitude, someLongitude))
*/
this->rootContext()->setContextProperty("myModel", QVariant::fromValue(data));
}
In the updateMap() method, the QObjects appended to the list are of a custom Class DataObject :
class DataObject : public QObject
{
Q_OBJECT
Q_PROPERTY(double lat READ lat WRITE setLat)
Q_PROPERTY(double lon READ lon WRITE setLon)
public:
explicit DataObject(QObject *parent = nullptr);
DataObject(double latitude, double longitude, QObject *parent =
nullptr);
void setLat(double latitude);
void setLon(double longitude);
double lat() const;
double lon() const;
double d_lat;
double d_lon;
}
Why can't the View see the updated Model even after a call to setContextProperty() ?
Thank you for your help
The name passed to you through setContextProperty(...) is an alias to the object that you pass, in the case of the binding of model: myModel it is made between the objects, in your case when you pass a new object with the same alias no longer the initial binding is valid since they are different objects, it is something similar to:
T *t = new T;
connect(t, &T::foo_signal, obj, &U::foo_slot);
t = new T;
Although both objects have the same alias (t) it does not imply that the connection persists with the second object.
The solution is to use the same object that notifies the update to QML, and in this case the solution is to implement a custom QAbstractListModel:
CoordinateModel class
// coordinatemodel.h
#ifndef COORDINATEMODEL_H
#define COORDINATEMODEL_H
#include <QAbstractListModel>
#include <QGeoCoordinate>
class CoordinateModel : public QAbstractListModel
{
Q_OBJECT
public:
enum{
PositionRole = Qt::UserRole + 1000
};
explicit CoordinateModel(QObject *parent = nullptr);
void insert(int index, const QGeoCoordinate & coordinate);
void append(const QGeoCoordinate & coordinate);
void clear();
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
QList<QGeoCoordinate> m_coordinates;
};
#endif // COORDINATEMODEL_H
// coordinatemodel.cpp
#include "coordinatemodel.h"
CoordinateModel::CoordinateModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void CoordinateModel::insert(int index, const QGeoCoordinate &coordinate){
int i = index;
if(index < 0) // prepend
i = 0;
else if (index >= rowCount()) // append
i = rowCount();
beginInsertRows(QModelIndex(), i, i);
m_coordinates.insert(i, coordinate);
endInsertRows();
}
void CoordinateModel::append(const QGeoCoordinate &coordinate){
insert(rowCount(), coordinate);
}
void CoordinateModel::clear(){
beginResetModel();
m_coordinates.clear();
endResetModel();
}
int CoordinateModel::rowCount(const QModelIndex &parent) const{
if (parent.isValid())
return 0;
return m_coordinates.count();
}
QVariant CoordinateModel::data(const QModelIndex &index, int role) const{
if (index.row() < 0 || index.row() >= m_coordinates.count())
return QVariant();
if (!index.isValid())
return QVariant();
const QGeoCoordinate &coordinate = m_coordinates[index.row()];
if(role == PositionRole)
return QVariant::fromValue(coordinate);
return QVariant();
}
QHash<int, QByteArray> CoordinateModel::roleNames() const{
QHash<int, QByteArray> roles;
roles[PositionRole] = "position";
return roles;
}
MapWidget class
// mapwidget.h
#ifndef MAPWIDGET_H
#define MAPWIDGET_H
#include <QQuickWidget>
class CoordinateModel;
class MapWidget : public QQuickWidget
{
public:
MapWidget(QWidget *parent=nullptr);
CoordinateModel *model() const;
private:
CoordinateModel *m_model;
};
#endif // MAPWIDGET_H
// mapwidget.cpp
#include "coordinatemodel.h"
#include "mapwidget.h"
#include <QQmlContext>
MapWidget::MapWidget(QWidget *parent):
QQuickWidget(parent),
m_model(new CoordinateModel{this})
{
rootContext()->setContextProperty("myModel", m_model);
setSource(QUrl(QStringLiteral("qrc:/main.qml")));
}
CoordinateModel *MapWidget::model() const
{
return m_model;
}
And then you can use it as:
MapWidget w;
w.model()->append(QGeoCoordinate(45.782074, -6.871263));
w.model()->append(QGeoCoordinate(50.782074, -1.871263));
w.model()->append(QGeoCoordinate(55.782074, 4.871263));
w.model()->append(QGeoCoordinate(45.782074, 4.871263));
w.model()->append(QGeoCoordinate(50.782074, 4.871263));
w.model()->append(QGeoCoordinate(55.782074, 4.871263));
main.qml
import QtQuick 2.12
import QtLocation 5.12
import QtPositioning 5.12
Item {
width: 1200
height: 1000
visible: true
Plugin {
id: osmPlugin
name: "osm"
}
Map {
id: map
anchors.fill: parent
plugin: osmPlugin
center: QtPositioning.coordinate(45.782074, 4.871263)
zoomLevel: 5
MapItemView {
model : myModel
delegate: MapQuickItem {
coordinate: model.position
sourceItem: Image {
id: image_1
source: "http://maps.gstatic.com/mapfiles/ridefinder-images/mm_20_red.png"
}
anchorPoint.x: image_1.width / 2
anchorPoint.y: image_1.height / 2
}
}
}
}
The complete example is here.

Bind CheckBox checked-state in TableView to custom model attribute

I've got a QML-application containing a TableView with two columns. One of them is a CheckBox. Now I created a model derived from QAbstractTableModel. Reading data for the ordinary text-column already works but how do I sync the checked-property for my CheckBox with the model?
Currently I can't even set it checked via model. You find the relevant code below.
tablemodel.cpp
TableModel::TableModel(QObject *parent) :
QAbstractTableModel(parent)
{
list.append("test1");
list.append("test2");
}
int TableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return list.count();
}
int TableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 2;
}
QVariant TableModel::data(const QModelIndex & index, int role) const {
if (index.row() < 0 || index.row() >= list.count())
return QVariant();
if (role == NameRole)
return list[index.row()]
else if (role== EnabledRole){
//list is not QList<QString>, its a custom class saving a String and a boolean for the checked state
return list[index.row()].isEnabled();
}
else {
return QVariant();
}
}
QHash<int, QByteArray> TableModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[EnabledRole] = "enabled";
return roles;
}
tablemodel.hpp
class TableModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum Roles {
NameRole = Qt::UserRole + 1,
EnabledRole
};
explicit TableModel(QObject *parent = 0);
int rowCount(const QModelIndex &parent = QModelIndex()) const;
int columnCount(const QModelIndex & parent = QModelIndex()) const;
Q_INVOKABLE QVariant data (const QModelIndex & index, int role) const;
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<QString> list;
main.qml
TableView {
id: Table
model: TableModel
TableViewColumn {
role: "name"
}
TableViewColumn {
role: "enabled"
delegate: CheckBox {
//how to get the right state from the model
//checked: ??
}
}
}
main.cpp
QQmlApplicationEngine engine;
QQmlContext * context = new QQmlContext(engine.rootContext());
TableModel tableModel;
context->setContextProperty("tableModel",&tableModel);
QQmlComponent component(&engine, QUrl("qrc:///main.qml"));
QQuickWindow * window = qobject_cast<QQuickWindow*>(component.create(context));
window->show();
You can emit signal from qml, when clicked on checkbox; connect this signal to your model slot and do something
main.qml
TableView {
id: table
objectName: "myTable"
signal checkedChanged(int index, bool cheked)
TableViewColumn {
role: "enabled"
delegate: CheckBox {
onClicked: table.checkedChanged(styleData.row, checked);
}
}
main.cpp
QQuickItem *obj = engine.rootObjects().at(0)->findChild<QQuickItem*>(QStringLiteral("myTable"));
QObject::connect(obj,SIGNAL(checkedChanged(int,bool)),tableModel,SLOT(mySlot(int,bool)));