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.
Related
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 have a C++ list model, where I can read data properly
model.h
class Animal
{
public:
Animal(const QString &type, const QString &size);
//![0]
QString type() const;
QString size() const;
private:
QString m_type;
QString m_size;
//![1]
};
class AnimalModel : public QAbstractListModel
{
Q_OBJECT
public:
enum AnimalRoles {
TypeRole = Qt::UserRole + 1,
SizeRole
};
AnimalModel(QObject *parent = 0);
//![1]
void addAnimal(const Animal &animal);
int rowCount(const QModelIndex & parent = QModelIndex()) const;
QVariant data(const QModelIndex & index, int role = Qt::DisplayRole) const;
protected:
QHash<int, QByteArray> roleNames() const;
private:
QList<Animal> m_animals;
//![2]
};
model.cpp
#include "model.h"
Animal::Animal(const QString &type, const QString &size)
: m_type(type), m_size(size)
{
}
QString Animal::type() const
{
return m_type;
}
QString Animal::size() const
{
return m_size;
}
AnimalModel::AnimalModel(QObject *parent)
: QAbstractListModel(parent)
{
}
void AnimalModel::addAnimal(const Animal &animal)
{
beginInsertRows(QModelIndex(), rowCount(), rowCount());
m_animals << animal;
endInsertRows();
}
int AnimalModel::rowCount(const QModelIndex & parent) const {
Q_UNUSED(parent);
return m_animals.count();
}
QVariant AnimalModel::data(const QModelIndex & index, int role) const {
if (index.row() < 0 || index.row() >= m_animals.count())
return QVariant();
const Animal &animal = m_animals[index.row()];
if (role == TypeRole)
return animal.type();
else if (role == SizeRole)
return animal.size();
return QVariant();
}
//![0]
QHash<int, QByteArray> AnimalModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[TypeRole] = "type";
roles[SizeRole] = "size";
return roles;
}
//![0]
main.cpp
#include "model.h"
#include <QGuiApplication>
#include <qqmlengine.h>
#include <qqmlcontext.h>
#include <qqml.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
//![0]
int main(int argc, char ** argv)
{
QGuiApplication app(argc, argv);
AnimalModel model;
model.addAnimal(Animal("Wolf", "Medium"));
model.addAnimal(Animal("Polar bear", "Large"));
model.addAnimal(Animal("Quoll", "Small"));
QQuickView view;
view.setResizeMode(QQuickView::SizeRootObjectToView);
view.setInitialProperties({{"model", QVariant::fromValue(&model)}});
//![0]
view.setSource(QUrl("qrc:view.qml"));
view.show();
return app.exec();
}
If I want to add a parent component to my listview, model is not accessible anymore because its visibility it's only from parent object. How can I expose the model then?
import QtQuick 2.0
import QtQuick.Window 2.15
//![0]
Window{
id: root
// how to catch here the model?
visible: true
width: 640
height: 480
ListView {
anchors.fill: parent
model: root.model // does not work
delegate: Text {
required property string type
required property string size
text: "Animal: " + type + ", " + size
}
}
}
Ok, that's really funny bug.
First of all, according to the Qt docs the property definition is:
[default] [required] [readonly] property <propertyType> <propertyName>
as you see the keyword property is mandatory.
The another issue is setInitialProperties says: Note: You can only use this function to initialize top-level properties.. So moving the ListView from the root item (and the model property too) makes this property inaccessible.
and one more issue is that the ListView already has a property named model, the attempt to redefine is incorrect. So you have to add some root item with the property model and that will solve the issue. Sure you will need to bind the ListView.model to this property
And the small example :)
Item {
id: root
width: 600
height: 400
property var model
ListView {
model: root.model
delegate: Text {
text: "Animal: " + type + ", " + size
}
}
}
Window can't be a root item since you use QQuickView.
Create an alias property in the root element with the same name of view.setInitialProperties allow to everything to work
main.cpp
view.setInitialProperties({{"mylistmodel", QVariant::fromValue(&model)}});
view.qml
import QtQuick 2.0
import QtQuick.Window 2.15
Window{
id: root
property alias mylistmodel : listView.model
visible: true
width: 640
height: 480
ListView {
id: listView
anchors.fill: parent
required model
delegate: Text {
required property string type
required property string size
text: "Animal: " + type + ", " + size
}
}
}
I'm trying to use QML TreeView Model. The example from Qt doesn't include how to create the model. I read this post and tried to use the code from #Tarod but the result is not what I expected.
main.cpp
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "animalmodel.h"
#include <qqmlcontext.h>
#include <qqml.h>
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
AnimalModel model;
model.addAnimal("wolf", "Medium");
model.addAnimal("Bear", "Large");
QQmlApplicationEngine engine;
QQmlContext *ctxt = engine.rootContext();
ctxt->setContextProperty("myModel", &model);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
animalmodel.h
#ifndef ANIMALMODEL_H
#define ANIMALMODEL_H
#include <QStandardItemModel>
class AnimalModel : public QStandardItemModel
{
Q_OBJECT //The Q_OBJECT macro must appear in the private section of a class definition that declares its own signals and slots or that uses other services provided by Qt's meta-object system.
public:
enum AnimalRoles {
TypeRole = Qt::UserRole + 1,
SizeRole
};
AnimalModel(QObject *parent = 0);
Q_INVOKABLE void addAnimal(const QString &type, const QString &size);
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
protected:
QHash<int, QByteArray> roleNames() const;
};
#endif // ANIMALMODEL_H
animalmodel.cpp
#include "animalmodel.h"
AnimalModel::AnimalModel(QObject *parent)
: QStandardItemModel(parent)
{
}
void AnimalModel::addAnimal(const QString &type, const QString &size)
{
QStandardItem* entry = new QStandardItem();
entry->setData(type, TypeRole);
auto childEntry = new QStandardItem();
childEntry->setData(size, SizeRole);
entry->appendRow(childEntry);
appendRow(entry);
}
QVariant AnimalModel::data(const QModelIndex & index, int role) const {
QStandardItem *myItem = itemFromIndex(index);
if (role == TypeRole)
return myItem->data(TypeRole);
else if (role == SizeRole) {
if (myItem->child(0) != 0)
{
return myItem->child(0)->data(SizeRole);
}
}
return QVariant();
}
QHash<int, QByteArray> AnimalModel::roleNames() const {
QHash<int, QByteArray> roles;
roles[TypeRole] = "type";
roles[SizeRole] = "size";
return roles;
}
main.qml
import QtQuick 2.6
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
menuBar: MenuBar {
Menu {
title: qsTr("&File")
MenuItem {
text: qsTr("&Open")
onTriggered: messageDialog.show(qsTr("Open Action Triggered"));
}
MenuItem {
text: qsTr("&Exit")
onTriggered: Qt.quit();
}
}
}
TreeView {
anchors.fill: parent
model: myModel
TableViewColumn {
title: "Name"
role: "type"
width: 300
}
TableViewColumn {
title: "Size"
role: "size"
width: 300
}
}
}
What I got is something like this:
Result
What I want to have is the animal size as a child of animal type.
Model sub-classing is one of the worst minefields in Qt. The advice is always to have it go through the model test (https://wiki.qt.io/Model_Test) to see if everything was implemented correctly.
On the other hand, in 90% of the cases you do not need to subclass a model at all as the default models provided by Qt work quite well. What I'd do is just use QStandardItemModel using, on the C++ side, only the QAbstractItemModel interface (i.e. force yourself to use QAbstractItemModel* model = new QStandardItemModel(/*parent*/);) this way, if in the future you feel like you really need to reimplement the model (for efficiency) you'll just need to change 1 line in your existing code.
In your case:
void AnimalModel::addAnimal(const QString &type, const QString &size)
{
if(columnCount()==0) insertColumn(0); // make sure there is at least 1 column
insertRow(rowCount()); // add a row to the root
const QModelIndex addedIdx = index(rowCount()-1,0);
setData(addedIdx, type, TypeRole); // set the root data
insertRow(rowCount(addedIdx),addedIdx ); // add 1 row ...
insertColumn(0,addedIdx ); // ... and 1 column to the added root row
setData(index(0,0,addedIdx), size, SizeRole); // set the data to the child
}
I have got the following scrollview with listview inside:
ScrollView{
anchors.fill: parent
ListView{
id: lvCommitsBranch
model: git.getCommitsBranch();
clip: true
delegate: Rectangle {
height: 100
width: parent.width
Text {
anchors.left: parent.left
font.bold: true
text:model.author
id:txtName
}
Text{
anchors.left: parent.left
anchors.top:txtName.bottom
font.pixelSize: 10
text:model.email
id: txtEmail
}
Text {
anchors.left: parent.left
anchors.top:txtEmail.bottom
text: model.message + ' ' + model.hash
id: txtMsg
}
MouseArea{
anchors.fill: parent
onClicked: {
lvCommitsBranch.currentIndex = index;
console.log('Msg: ' + model.message);
console.log('Hash: ' + model.hash);
}
acceptedButtons: Qt.LeftButton | Qt.RightButton
}
}
}
}
The issue is that when I scroll some items disappear (each time randomly and sometimes I have to scroll fast but not always).
When I click on the items that have not disappeared, I get undefined on all the model's properties. When Mousearea's onclick is triggered it prints the following:
qml: Msg: undefined
qml: Hash: undefined
I get the model info from a method (QAbstractListModel) that is returned from my git custom component.
This is my QAbstractListModel:
header:
class CommitsBranch : public QAbstractListModel
{
Q_OBJECT
public:
enum Roles {
AuthorRole,
EMailRole,
MsgRole,
DateRole,
HashRole
};
explicit CommitsBranch(QObject *parent = 0);
CommitsBranch(Repository *repo);
public:
virtual int rowCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
protected:
// return the roles mapping to be used by QML
virtual QHash<int, QByteArray> roleNames() const override;
private:
QList<Commit> m_data;
QHash<int, QByteArray> m_roleNames;
};
Cpp:
CommitsBranch::CommitsBranch(QObject *parent)
: QAbstractListModel(parent)
{
}
CommitsBranch::CommitsBranch(Repository *repo)
{
m_roleNames[AuthorRole] = "author";
m_roleNames[EMailRole] = "email";
m_roleNames[MsgRole] = "message";
m_roleNames[DateRole] = "date";
m_roleNames[HashRole] = "hash";
/*
here we append the m_data (QList) Items using libgit2 methods
*/
}
int CommitsBranch::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.count();
}
QVariant CommitsBranch::data(const QModelIndex &index, int role) const
{
// this function returns the required data
}
QHash<int, QByteArray> CommitsBranch::roleNames() const
{
return m_roleNames;
}
And git is just a class that inherits from QObject and it has the following method:
Q_INVOKABLE QObject* getCommitsBranch();
QObject *Git::getCommitsBranch()
{
CommitsBranch* files = new CommitsBranch(repo.data());
return files;
}
I get the same behavior without the scrollview.
EDIT:
If I take a repository with a lot of commits (more lines to the listview), even increasing the cacheBuffer won't help, if I scroll a bit fast all the items will disappear.
The problem here is that, by default, if you return a QObject* it will transfer the ownership to QML.
http://doc.qt.io/qt-5/qtqml-cppintegration-data.html#data-ownership
The exception to this rule is when a QObject is returned from an
explicit C++ method call: in this case, the QML engine assumes
ownership of the object, unless the ownership of the object has
explicitly been set to remain with C++ by invoking
QQmlEngine::setObjectOwnership() with QQmlEngine::CppOwnership
specified.
You have to set the returned QObject* ownership manually, so it doesn't get destroyed by the QML engine :
QObject *Git::getCommitsBranch()
{
CommitsBranch* files = new CommitsBranch(repo.data());
QQmlEngine::setObjectOwnership(files, QQmlEngine::CppOwnership)
return files;
}
Note that you will have a memory leak as your CommitsBranch object will never be deleted. But at least your QML items should not disappear anymore !
EDIT: As suggested you can do something like this to avoid the memory leak :
// CommitsBranch Constructor
CommitsBranch::CommitsBranch(Repository *repo, QObject *parent) :
QAbstractListModel(parent) { /*stuff*/ }
QObject *Git::getCommitsBranch()
{
// Setting ownership is not necessary if you pass the parent to the QAbstractListModel
CommitsBranch* commits = new CommitsBranch(repo.data(), this);
return files;
}
I have some model class that inherits QAbstractListModel:
VehiclesModel.h:
class VehiclesModel : public QAbstractListModel {
Q_OBJECT
public:
enum Roles {
ImagePathRole = Qt::UserRole + 1, // QString
NameRole // QString
};
virtual int rowCount(const QModelIndex & parent = QModelIndex()) const override { ... }
virtual QVariant data(const QModelIndex & index, int role) const override { ... }
virtual QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles = QAbstractListModel::roleNames();
roles[ImagePathRole] = "imagePath";
roles[NameRole] = "name";
return roles;
}
};
main.cpp:
#include "VehiclesModel.h"
int main(int argc, char * argv[]) {
QGuiApplication app(argc, argv);
VehiclesModel vehiclesModel;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("vehiclesModel", &vehiclesModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
And ComboBox that displays this model:
main.qml:
ComboBox {
id: control
model: vehiclesModel
delegate: ItemDelegate {
contentItem: RowLayout {
Image {
source: imagePath
}
Label {
text: name
}
}
highlighted: control.highlightedIndex == index
}
contentItem: RowLayout {
Image {
source: ??imagePath??
}
Label {
text: ??name??
}
}
}
I want to customize the ComboBox to show vehicle image and name. I can access to model data from ItemDelegate but how to access to model data outside the ItemDelegate? For example I want to access current index data (ImagePathRole and NameRole) to display vehicle image and name in contentItem.
Is it possible to do it without calling QAbstractListModel methods directly (i.e. index() and data() methods) and making them Q_INVOKABLE?
Not in any sort of a decent built-in way at the present time, unfortunately, this is something I've found to be lacking for quite a while, and I've considered implementing something for this in the QML models functionality, but I haven't yet had the time to do so.
For the time being, you can either do it yourself (like you're discussing), at the cost of type-safety and so on, or (the way I've typically tackled this before), you can create a QObject subclass to represent a single item in the model (ItemDataThing or whatever you choose to call it); provide it with a source model & index, properties, and let it represent a single instance of data from the model.
Something like:
class ImageDataThing : public QObject
{
Q_OBJECT
Q_PROPERTY(QString imagePath READ imagePath NOTIFY imagePathChanged)
Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged)
Q_PROPERTY(int index READ index WRITE setIndex NOTIFY indexChanged)
public:
QString imagePath() const;
QAbstractItemModel *model() const;
void setModel(const QAbstractItemModel *newModel);
int index() const;
void setIndex(int newIndex);
signals:
void imagePathChanged(const QString &imagePath);
void modelChanged(QAbstractItemModel *model);
void indexChanged(int indexChanged);
};
... and in your implementation, whenever the model is set, hook the change signals (e.g. rowsInserted, rowsRemoved, ...) to alter the stored index (if provided) to keep it mapped to the correct place in the model.
In the model data getters (here, imagePath for instance), access the model instance (using the index) to grab the data out, and return it.
This has the obvious disadvantage of being a lot of boilerplate, but on the other hand, it's easy-to-write code if you are familiar with models, type-safe, and one could autogenerate it fairly easily.
You could create your own function to get data from the model, like the one I'm currently using,
VehiclesModel.h:
public slots:
int size() const; // to access from QML javascript
QVariant getData(int index, int role); // to access from QML javascript
VehiclesModel.cpp:
int VehiclesModel::size() const {
return m_list.size();
}
QVariant VehiclesModel::getData(int index, int role) {
if (index < 0 || index >= m_list.count())
return QVariant();
switch (role) {
case ImagePathRole:
return ...
break;
default:
break;
}
}
Shameless plug for my SortFilterProxyModel library.
The problem you are asking is actually a head-scratching scenario. I've found a way to do it somewhat correctly but it's kinda complicated and involves an external library. In my solution we filter the source model to only expose the element corresponding to the current index of the combo box and instantiate a delegate for this element and use it as the contentItem of the ComboBox.
This has the advantage of not having to modify your model and keeping in synch with your model changes.
import SortFilterProxyModel 0.2 // from https://github.com/oKcerG/SortFilterProxyModel
import QtQml 2.2
/*
...
*/
ComboBox {
id: control
model: vehiclesModel
delegate: ItemDelegate {
contentItem: RowLayout {
Image {
source: imagePath
}
Label {
text: name
}
}
highlighted: control.highlightedIndex == index
}
contentItem: { currentIndex; return selectedInstantiator.object; } // use currentIndex to force the binding reevaluation. When the model changes, the instantiator doesn't notify object has changed
Instantiator {
id: selectedInstantiator
model: SortFilterProxyModel {
sourceModel: control.model
filters: IndexFilter {
minimumIndex: control.currentIndex
maximumIndex: control.currentIndex
}
}
delegate: RowLayout {
Image {
source: imagePath
}
Label {
text: name
}
}
}
}
I strongly suggest to look at the Qt QML Tricks library made by Thomas Boutroue:
https://gitlab.com/qt-qml-libraries-4-me/qt-qml-tricks-ng
More specific the QQmlObjectListModel (from the Qt QML Models) could do the trick for you.
Expanding with using the Qt Super-Macros, it reduces overhead writing setters/getters!
These macros basically expand to a Q_PROPERTY, resulting in accessibility from QML, and add definition of a setter, getter and private variable.
Usage in your specific case this could look something like this, quickly written down, not validated (check using the correct index for referencing the model):
VehicleItem.h:
#include <QObject>
#include "QQmlVarPropertyHelpers.h" // Include library Qt Super-Macros
class VehicleItem : public QObject {
Q_OBJECT
QML_WRITABLE_VAR_PROPERTY(QString, imagePath)
QML_WRITABLE_VAR_PROPERTY(QString, name)
public:
explicit VehicleItem(QString imagePath, QString name, QObject* parent=0)
: QObject (parent)
, m_imagePath (imagePath)
, m_name (name)
{}
};
VehiclesModel.h:
#include <QObject>
#include "QQmlObjectListModel.h" // Include library Qt QML Models
#include "VehicleItem.h"
class VehiclesModel : public QObject {
Q_OBJECT
QML_OBJMODEL_PROPERTY(VehicleItem, modelList)
public:
explicit VehiclesModel(QObject *parent = 0);
};
VehiclesModel.c:
#include "VehiclesModel.h"
VehiclesModel::VehiclesModel(QObject *parent) :
QObject(parent), m_modelList(new QQmlObjectListModel<VehicleItem>())
{}
main.c (remains the same):
#include "VehiclesModel.h"
int main(int argc, char * argv[]) {
QGuiApplication app(argc, argv);
VehiclesModel vehiclesModel;
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("vehiclesModel", &vehiclesModel);
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
main.qml:
ComboBox {
id: control
model: vehiclesModel
delegate: ItemDelegate {
contentItem: RowLayout {
Image {
source: imagePath
}
Label {
text: name
}
}
highlighted: control.highlightedIndex == index
}
contentItem: RowLayout {
Image {
source: vehiclesModel.modelList.get(index).imagePath
}
Label {
text: vehiclesModel.modelList.get(index).name
}
}
}
As modelList (and also imagePath and name) is expanded by the macro to a Q_PROPERTY, it is accessible from QML side.
For the ins-and-outs of this library, be sure to check Thomas Boutroue's lightning talk at the QtWS2015: https://www.youtube.com/watch?v=96XAaH97XYo