2
votes

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 :

  1. A main class with a QApplication instantiation and a MainWindow (MainWindow being a custom QMainWindow class). App gets executed and Window gets shown.

  2. 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
  3. 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

1

1 Answers

0
votes

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
            }
        }
    }
}

enter image description here

The complete example is here.