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.
Related
I have class, which reads data from file to 2D array.
So i need to display that array in qml TableView.
I have QVector<QVector> table; to display it as data in my TableModel.
The OperatingFiles object creates in main.cpp it contains functions to encode/decode passwords and save them to file. Functions for this object is also called from qml code
So what i want is to make "table = passwordsDecoded" somewhere but i don't know how to do it.
OperatingFiles.h :
class OperatingFiles : public QObject
{
Q_OBJECT
public:
OperatingFiles();
public slots:
QVector<QVector<QString>> getVector(); // returns passwordsDecoded
private:
QVector<QVector<QString>> passwordsDecoded;
};
#endif // OPERATINGFILES_H
TableModel.h:
class TableModel : public QAbstractTableModel
{
Q_OBJECT
QVector<QVector<QString>> table;
public:
explicit TableModel(QObject *parent = nullptr);
int rowCount(const QModelIndex & = QModelIndex()) const override;
int columnCount(const QModelIndex & = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
};
TableModel.cpp:
#include "tablemodel.h"
TableModel::TableModel(QObject *parent)
: QAbstractTableModel{parent}
{
QVector<QString> mini; // this only for test that it really appears in TableView (it appears)
mini.append("rstrst");
mini.append("rstrst");
mini.append("rstrst");
table.append(mini);
table.append(mini);
table.append(mini);
}
int TableModel::rowCount(const QModelIndex &) const
{
return table.size();
}
int TableModel::columnCount(const QModelIndex &) const
{
return table.at(0).size();
}
QVariant TableModel::data(const QModelIndex &index, int role) const
{
switch (role) {
case Qt::DisplayRole:
return table.at(index.row()).at(index.column());
default:
break;
}
return QVariant();
}
QHash<int, QByteArray> TableModel::roleNames() const
{
return { {Qt::DisplayRole, "display"} };
}
qml:
TableView {
anchors.fill: parent
columnSpacing: 1
rowSpacing: 1
clip: true
model: TableModel{}
delegate: Rectangle {
implicitWidth: 100
implicitHeight: 50
border.width: 0
Text {
text: display
anchors.centerIn: parent
}
}
}
main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "controlbuttons.h"
#include "operatingfiles.h"
#include "tablemodel.h"
int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif
QGuiApplication app(argc, argv);
qmlRegisterType<TableModel>("TableModel", 0,1,"TableModel");
QQmlApplicationEngine engine;
ControlButtons *appManager = new ControlButtons(&app);
engine.rootContext()->setContextProperty("appManager", appManager);
OperatingFiles *fileOperator = new OperatingFiles();
engine.rootContext() -> setContextProperty("fileOperator", fileOperator);
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();
}
I tried to make functions in TableModel that could fill "table" with data from my object which was called directly from qml ("OnClicked:"), tried make constructor which will get object with 2D array.
I seen ton of vids and read docs but literally no idea how to do it.
So whole chain is: choose file✓ -> read file✓ -> decode✓ -> fill 2D array✓ -> send to model (Somehow) -> appear it in UI✓
Maybe it could be done if i make my 2D array global so i could access to it from anywhere but it not a solution.
Thanks!
The easiest modification would be to add a Q_INVOKABLE to your TableModel which allows to set the table
class TableModel : public QAbstractTableModel
{
...
Q_INVOKABLE void setTable(const QVariant& value)
{ //inline for briefity
beginResetModel();
table = value.value<QVector<QVector<QString>>>();
endResetModel();
}
}
Then from QML you can do <id of TableModel>.setTable(fileOperator.getVector())
Another option would be a Q_PROPERTY with a similar setter method. Then you would use <id of TableModel>.table = fileOperator.getVector()
I'm not sure about how you want your structure to look like, but if you don't need TableModel to be instantiatable from QML you could make it available as contextProperty the same way as you did with the fileOperator. Then you will have more control on the TableModel, for instance, you can call a fill function from C++ side when done.
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.
I am building an application to create and edit gpx files on a map. I want to be able to render a line and set of markers from a single model.
Each GPX point has a co-ordinate, elevation and a time. I am aiming to create a model that can be used to show it on the map and also show the elevation profile.
I have modified the answer to this question QT QmlMap PolyLine not updated properly when underlying model changes in order to base my model on a structure of GPX points.
main.c
#include "mainwindow.h"
#include <QApplication>
#include <QQmlApplicationEngine>
#include "mapmarker.h"
#include <QQmlContext>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
//Added for model registration
QQmlApplicationEngine engine;
qmlRegisterType<MarkerModel>("MarkerModel", 1, 0, "MarkerModel");
engine.load(QUrl(QStringLiteral("qrc:/mainWindow.qml")));
return app.exec();
}
mainWindow.qml
import QtQuick 2.3
import QtQuick.Window 2.3
import QtLocation 5.9
import MarkerModel 1.0
ApplicationWindow {
id: appWindow
width: 1512
height: 1512
visible: true
x:100
y:100
MarkerModel{
id: markerModel
}
Plugin {
id: mapPlugin
name: "osm"
}
Map {
id: mapView
anchors.fill: parent
plugin: mapPlugin
MapItemView {
model: markerModel
delegate: routeMarkers
}
MapPolyline {
line.width: 5
path: markerModel.path
}
Component {
id: routeMarkers
MapCircle {
radius: 10
center: positionRole
}
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = parent.toCoordinate(Qt.point(mouse.x,mouse.y))
markerModel.addMarker(coord)
}
}
}
}
mapmarker.h
#ifndef MAPMARKER_H
#define MAPMARKER_H
#include <QAbstractListModel>
#include <QGeoCoordinate>
#include <QDebug>
#include <QDate>
struct gpxCoordinate {
QGeoCoordinate latlon;
float ele;
QDateTime time;
};
class MarkerModel : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(QVariantList path READ path NOTIFY pathChanged)
public:
enum MarkerRoles{positionRole = Qt::UserRole, pathRole};
MarkerModel(QObject *parent=nullptr): QAbstractListModel(parent)
{
connect(this, &QAbstractListModel::rowsInserted, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::rowsRemoved, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::dataChanged, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::modelReset, this, &MarkerModel::pathChanged);
connect(this, &QAbstractListModel::rowsMoved, this, &MarkerModel::pathChanged);
}
Q_INVOKABLE void addMarker(const QGeoCoordinate &coordinate, float elevation = 0, QDateTime dateTime = QDateTime::currentDateTime()) {
gpxCoordinate item;
item.latlon = coordinate;
item.ele = elevation;
item.time = dateTime;
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_coordinates.append(item);
endInsertRows();
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
if(parent.isValid()) return 0;
return m_coordinates.count();
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if(row + count > m_coordinates.count() || row < 0)
return false;
beginRemoveRows(parent, row, row+count-1);
for(int i = 0; i < count; ++i)
m_coordinates.removeAt(row + i);
endRemoveRows();
return true;
}
bool removeRow(int row, const QModelIndex &parent = QModelIndex()) {
return removeRows(row, 1, parent);
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (index.row() < 0 || index.row() >= m_coordinates.count())
return QVariant();
if(role == Qt::DisplayRole)
return QVariant::fromValue(index.row());
else if(role == MarkerModel::positionRole){
return QVariant::fromValue(m_coordinates[index.row()].latlon);
}
return QVariant();
}
QHash<int, QByteArray> roleNames() const override{
QHash<int, QByteArray> roles;
roles[positionRole] = "positionRole";
return roles;
}
QVariantList path() const{
QVariantList path;
for(const gpxCoordinate & coord: m_coordinates){
path << QVariant::fromValue(coord.latlon);
}
return path;
}
signals:
void pathChanged();
private:
QVector<gpxCoordinate> m_coordinates;
};
#endif // MARKERMODEL_H
I think I have made a really basic mistake somewhere as I can click on the map and draw a polyline but the MapCircles are not rendering. I see the error:-
Unable to assign [undefined] to QGeoCoordinate
When I first click on the map.
Have I misunderstood how models/roles work in Qt QML?
I have tracked this down to a build issue. I had some additional paths in my .pro file and was including some libraries that were not being used (spatialite) removing these fixed the issue completely.
I will leave the question up as it may be useful to anyone wanting to do something similar.
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.
I've figured out how to bind a model derived from QAbstractListModel to a QML view.
But the next thing I tired does not work. If a new Item is added to the model the QML view will not update. Why is that?
DataObject.h
class DataObject {
public:
DataObject(const QString &firstName,
const QString &lastName):
first(firstName),
last(lastName) {}
QString first;
QString last;
};
SimpleListModel.h
class SimpleListModel : public QAbstractListModel
{
Q_OBJECT
enum /*class*/ Roles {
FIRST_NAME = Qt::UserRole,
LAST_NAME
};
public:
SimpleListModel(QObject *parent=0);
QVariant data(const QModelIndex &index, int role) const;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const;
QHash<int, QByteArray> roleNames() const;
void addName(QString firstName, QString lastName);
private:
Q_DISABLE_COPY(SimpleListModel);
QList<DataObject*> m_items;
};
SimpleListModel.cpp
SimpleListModel::SimpleListModel(QObject *parent) :
QAbstractListModel(parent)
{
DataObject *first = new DataObject(QString("Firstname01"), QString("Lastname01"));
DataObject *second = new DataObject(QString("Firstname02"), QString("Lastname02"));
DataObject *third = new DataObject(QString("Firstname03"), QString("Lastname03"));
m_items.append(first);
m_items.append(second);
m_items.append(third);
}
QHash<int, QByteArray> SimpleListModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[/*Roles::*/FIRST_NAME] = "firstName";
roles[/*Roles::*/LAST_NAME] = "lastName";
return roles;
}
void SimpleListModel::addName(QString firstName, QString lastName)
{
DataObject *dataObject = new DataObject(firstName, lastName);
m_items.append(dataObject);
emit dataChanged(this->index(m_items.size()), this->index(m_items.size()));
}
int SimpleListModel::rowCount(const QModelIndex &) const
{
return m_items.size();
}
QVariant SimpleListModel::data(const QModelIndex &index, int role) const
{
//--- Return Null variant if index is invalid
if(!index.isValid())
return QVariant();
//--- Check bounds
if(index.row() > (m_items.size() - 1))
return QVariant();
DataObject *dobj = m_items.at(index.row());
switch (role)
{
case /*Roles::*/FIRST_NAME:
return QVariant::fromValue(dobj->first);
case /*Roles::*/LAST_NAME:
return QVariant::fromValue(dobj->last);
default:
return QVariant();
}
}
AppCore.h
class AppCore : public QObject
{
Q_OBJECT
Q_PROPERTY(SimpleListModel *simpleListModel READ simpleListModel CONSTANT)
public:
explicit AppCore(QObject *parent = 0);
SimpleListModel *simpleListModel() const;
public slots:
void addName();
private:
SimpleListModel *m_SimpleListModel;
};
AppCore.cpp
AppCore::AppCore(QObject *parent) :
QObject(parent)
{
m_SimpleListModel = new SimpleListModel(this);
}
SimpleListModel *AppCore::simpleListModel() const
{
return m_SimpleListModel;
}
void AppCore::addName()
{
m_SimpleListModel->addName("FirstnameNEW", "LastnameNEW");
}
main.cpp
int main(int argc, char *argv[])
{
QGuiApplication a(argc, argv);
QQuickView *view = new QQuickView();
AppCore *appCore = new AppCore();
qRegisterMetaType<SimpleListModel *>("SimpleListModel");
view->engine()->rootContext()->setContextProperty("appCore", appCore);
view->setSource(QUrl::fromLocalFile("main.qml"));
view->show();
return a.exec();
}
main.qml
// ...
ListView {
id: myListView
anchors.fill: parent
delegate: myDelegate
model: appCore.simpleListModel
}
MouseArea {
anchors.fill: parent
onClicked: {
appCore.addName()
console.log('rowCount: ' + appCore.simpleListModel.rowCount())
}
}
//...
you should call beginInsertRows and endInsertRows instead of emitting the signal
void SimpleListModel::addName(QString firstName, QString lastName)
{
DataObject *dataObject = new DataObject(firstName, lastName);
// tell QT what you will be doing
beginInsertRows(ModelIndex(),m_items.size(),m_items.size());
// do it
m_items.append(dataObject);
// tell QT you are done
endInsertRows();
}
these 2 functions emit all needed signals
You're ignoring the semantics of a QAbstractItemModel. There are two kinds of signals that a model must emit:
data change signals: They must be emitted after the data was changed. A data change is a change of value of an existing item. Other changes to the model are not called data changes - the terminology here has a specific meaning.
structure change signals: They must be emitted before and after any structural change. A structural change is addition or removal of any of the items. The beginXxxYyy and endXxxYyy helper functions emit those signals.