1

I am trying to develop a modified version of the example "qmlscatter", available in Qt examples. My intention is to plot a flight trajectory in the 3D environment, by modifying the data points of the "Data.qml" file. The actual coordinates of the flight path are stored in three different QVectors QVector<double> "cogPosX_", "cogPosY_" and "cogPosZ_", each index represents a latter time step.

As the forums mention, I need to use the ".setContextProperty" function to update the data values in the QML file. However, I can't find really find out how to get it working. I guess the main reason is that I am not referencing to the right "dataModel" id. Here you have my code:

datapointobject.h // This should define a generic Object that contains X Y and Z coordinates for each data point.

#ifndef DATAPOINTOBJECT_H
#define DATAPOINTOBJECT_H
#include <QWidget>
class DataObject : public QObject
{
    Q_OBJECT

    Q_PROPERTY(double x READ x WRITE setX)
    Q_PROPERTY(double y READ y WRITE setY)
    Q_PROPERTY(double z READ z WRITE setZ)
public:
    DataObject(QObject* parent =0);//Constructor
    DataObject(const double & x, const double & y, const double & z, QObject * parent=0);
    ~DataObject();//Destructor
    double x() const;
    double y() const;
    double z() const;
    void setX(const double &x);
    void setY(const double &y);
    void setZ(const double &z);
signals:

    void xChanged();
    void yChanged();
    void zChanged();
private:
    double m_x;
    double m_y;
    double m_z;
};
#endif // DATAPOINTOBJECT_H

datapointobject.cpp // This defines the constructors and functions.

#include "datapointobject.h"
#include<QDebug>

DataObject::DataObject(QObject *parent)//Constructor
    : QObject(parent)
{
}
DataObject::DataObject(const double &x, const double &y, const double &z, QObject *parent)//Constructor
    :QObject(parent), m_x(x), m_y(y), m_z(z)
{

}

DataObject::~DataObject(){//Destructor

}
double DataObject::x() const
{
    return m_x;
}
double DataObject::y() const
{
    return m_y;
}
double DataObject::z() const
{
    return m_z;
}
void DataObject::setX(const double &x){
    if (x != m_x) {
        m_x = x;
        emit xChanged();
    }
}
void DataObject::setY(const double &y){
    if (y != m_y) {
        m_y = y;
        emit yChanged();
    }
}
void DataObject::setZ(const double &z){
    if (z != m_z) {
        m_z = z;
        emit zChanged();
    }
}

threeDviewer.h // this header defines the "Flightviewer" class, which creates a QQuickView instance, the responsible for plotting the 3D Scatter.

#ifndef FLIGHTVIEWER_H
#define FLIGHTVIEWER_H
#include <QtWidgets>
#include <QtGui/QGuiApplication>
#include <QtCore/QDir>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>
#include "flight.h"
#include <QtQml>

class Flightviewer: public QWidget
{
  Q_OBJECT

public:
  Flightviewer(Flight displayedFlight, QString directory, QWidget *parent = 0);//Constructor
  virtual ~Flightviewer();//Destructor
  QQuickView viewer;

  void showWindow();

  void readFlightTrajectory(Flight flight);
};


#endif // FLIGHTVIEWER_H

threeDviewer.cpp //This file configures the QQuickView viewer instance. It is the responsible for importing the flight data to the QML data file.

#include <QtGui/QGuiApplication>
#include <QtCore/QDir>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>

#include "window.h"
#include <QWidget>
#include "threeDviewer.h"
#include "datapointobject.h"
Flightviewer::Flightviewer(Flight displayedFlight, QString directory, QWidget*parent){

    // The following are needed to make examples run without having to install the module
    // in desktop environments.
#ifdef Q_OS_WIN
    QString extraImportPath(QStringLiteral("%1/../../../../%2"));
#else
    QString extraImportPath(QStringLiteral("%1/../../../%2"));
#endif
    qmlRegisterType<DataObject>();
    readFlightTrajectory(displayedFlight);
    viewer.setVisible(false);//Open only after clicked.
    viewer.engine()->addImportPath(extraImportPath.arg(QGuiApplication::applicationDirPath(),
                                  QString::fromLatin1("qml")));
    //! [4]
    QObject::connect(viewer.engine(), &QQmlEngine::quit, &viewer, &QWindow::close);
    //! [4]

    viewer.setTitle(QStringLiteral("3D Flight Trajectory Visualization"));

    //! [3]
    viewer.setSource(QUrl("qrc:/qmlscatter/qml/qmlscatter/main.qml"));
    //! [3]

    viewer.setResizeMode(QQuickView::SizeRootObjectToView);

    //! [2]
    //! [2]
}
Flightviewer::~Flightviewer(){

}

void Flightviewer::showWindow(){
   viewer.showMaximized();
}
void Flightviewer::readFlightTrajectory(Flight flight){

   QList<QObject*> dataList;
   for (int i=0; i<flight.cogPosX_.size();i++){
       dataList.append(new DataObject(flight.cogPosX_.at(i),flight.cogPosY_.at(i),flight.cogPosZ_.at(i)));
   }
   QQmlContext * ctxt = viewer.rootContext();
   ctxt->setContextProperty("dataModel", QVariant::fromValue(dataList));
   viewer.update();
}

The 'QQuickViewer viewer' is initialized in an outer function, with the following command:

void Form::run3D(){
    threeDviewer= new Flightviewer(displayedFlights_[flightIndex],directory_);//initiate viewer

    threeDviewer->showWindow();//Show viewer

}

The data in the qml file is defined exactly as in the "qmlscatter" example:

Data.qml:

import QtQuick 2.1
Item {

property alias model: dataModel

ListModel {
    id: dataModel
    //ListElement{ xPos: -1000.0; yPos: 500.0; zPos: -5.0 }
} 
}

The data is accessed by the main.qml file, which defines the Scatter3D Item:

main.qml

    //! [0]
import QtQuick 2.1
import QtQuick.Layouts 1.0
import QtDataVisualization 1.0
import "."
//! [0]

//! [1]
Rectangle {
    id: mainView
    //! [1]
    width: 500
    height: 500

    //! [4]
    Data {
        id: seriesData
    }
    //! [4]

    //! [13]
    Theme3D {
        id: themeIsabelle
        type: Theme3D.ThemeIsabelle
        font.family: "Lucida Handwriting"
        font.pointSize: 40
    }
    //! [13]

    Theme3D {
        id: themeArmyBlue
        type: Theme3D.ThemeArmyBlue
    }

    //! [8]
    //! [9]
    Item {
        id: dataView
        anchors.bottom: parent.bottom
        //! [9]
        width: parent.width
        height: parent.height - buttonLayout.height
        //! [8]

        //! [2]
        Scatter3D {
            id: scatterGraph
            width: dataView.width
            height: dataView.height
            //! [2]
            //! [3]
            theme: themeIsabelle
            shadowQuality: AbstractGraph3D.ShadowQualitySoftLow
            //! [3]
            //! [6]
            axisX.segmentCount: 3
            axisX.subSegmentCount: 2
            axisX.labelFormat: "%.2f"
            axisZ.segmentCount: 2
            axisZ.subSegmentCount: 2
            axisZ.labelFormat: "%.2f"
            axisY.segmentCount: 2
            axisY.subSegmentCount: 2
            axisY.labelFormat: "%.2f"
            //! [6]
            //! [5]
            Scatter3DSeries {
                id: scatterSeries
                //! [5]
                //! [10]
                itemLabelFormat: "Series 1: X:@xLabel Y:@yLabel Z:@zLabel"
                //! [10]

                //! [11]
                ItemModelScatterDataProxy {
                    itemModel: seriesData.model
                    xPosRole: "xPos"
                    yPosRole: "yPos"
                    zPosRole: "zPos"
                }
                //! [11]
            }
        }
    }

    RowLayout {
        id: buttonLayout
        Layout.minimumHeight: cameraToggle.height
        width: parent.width
        anchors.left: parent.left
        spacing: 0
        //! [7]
        NewButton {
            id: shadowToggle
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: scatterGraph.shadowsSupported ? "Hide Shadows" : "Shadows not supported"
            enabled: scatterGraph.shadowsSupported
            onClicked: {
                if (scatterGraph.shadowQuality === AbstractGraph3D.ShadowQualityNone) {
                    scatterGraph.shadowQuality = AbstractGraph3D.ShadowQualitySoftLow;
                    text = "Hide Shadows";
                } else {
                    scatterGraph.shadowQuality = AbstractGraph3D.ShadowQualityNone;
                    text = "Show Shadows";
                }
            }
        }
        //! [7]

        NewButton {
            id: smoothToggle
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: "Use Smooth for Series One"
            onClicked: {
                if (scatterSeries.meshSmooth === false) {
                    text = "Use Flat for Series One";
                    scatterSeries.meshSmooth = true;
                } else {
                    text = "Use Smooth for Series One"
                    scatterSeries.meshSmooth = false;
                }
            }
        }

        NewButton {
            id: cameraToggle
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: "Change Camera Placement"
            onClicked: {
                if (scatterGraph.scene.activeCamera.cameraPreset === Camera3D.CameraPresetFront) {
                    scatterGraph.scene.activeCamera.cameraPreset =
                            Camera3D.CameraPresetIsometricRightHigh;
                } else {
                    scatterGraph.scene.activeCamera.cameraPreset = Camera3D.CameraPresetFront;
                }
            }
        }

        NewButton {
            id: themeToggle
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: "Change Theme"
            onClicked: {
                if (scatterGraph.theme.type === Theme3D.ThemeArmyBlue) {
                    scatterGraph.theme = themeIsabelle
                } else {
                    scatterGraph.theme = themeArmyBlue
                }
                if (scatterGraph.theme.backgroundEnabled === true) {
                    backgroundToggle.text = "Hide Background";
                } else {
                    backgroundToggle.text = "Show Background";
                }
            }
        }

        NewButton {
            id: backgroundToggle
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: "Hide Background"
            onClicked: {
                if (scatterGraph.theme.backgroundEnabled === true) {
                    scatterGraph.theme.backgroundEnabled = false;
                    text = "Show Background";
                } else {
                    scatterGraph.theme.backgroundEnabled = true;
                    text = "Hide Background";
                }
            }
        }

        NewButton {
            id: exitButton
            Layout.fillHeight: true
            Layout.fillWidth: true
            text: "Quit"
            onClicked: Qt.quit(0);
        }
    }
}

I would be very grateful if you can give me some hint on how to program this properly. Thanks in beforehand!

  • I guess you should provide real DataModel, derived from `QAbstractItemModel` not a list of your data objects.(see [here](https://doc.qt.io/qt-5/qtdatavisualization-data-handling.html#item-models-and-data-mapping)) This will help you to interact with the model ie. data change notifiers etc. Also you have to register your `DataObject` using `qmlRegisterType`. QML knows nothing about types you've defined in C++. – folibis Aug 28 '17 at 13:55

1 Answers1

2

Here is simple example of providing data to Scatter3D from C++:

Firs of all we define a model class derived from QAbstractListModel. In this case we must provide implementations of only 3 methods.

FlightModel.h

#ifndef FLIGHTMODEL_H
#define FLIGHTMODEL_H

#include <QAbstractListModel>
#include <QList>
#include <QVector3D>

class FlightModel : public QAbstractListModel
{
    Q_OBJECT
public:
    FlightModel(QObject *parent = Q_NULLPTR);
    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<QVector3D> m_innerData;
};

#endif // FLIGHTMODEL_H

FlightModel.cpp

#include "flightmodel.h"

FlightModel::FlightModel(QObject *parent) : QAbstractListModel(parent)
{
    qsrand(time(NULL));
    int count = 100 + qrand() % 100;
    for(int i = 0;i < count;i ++) {
        double x = (double)(qrand() % 1000);
        double y = (double)(qrand() % 1000);
        double z = (double)(qrand() % 1000);
        m_innerData.append(QVector3D(x, y, z));
    }
}

int FlightModel::rowCount(const QModelIndex &parent) const
{
    return m_innerData.count();
}

QVariant FlightModel::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();

    QVector3D point = m_innerData[index.row()];
    switch(role) {
        case Qt::UserRole + 1: return point.x(); break;
        case Qt::UserRole + 2: return point.y(); break;
        case Qt::UserRole + 3: return point.z(); break;
    }
    return QVariant();
}

QHash<int, QByteArray> FlightModel::roleNames() const
{
    QHash<int, QByteArray> roles;
       roles[Qt::UserRole + 1] = "x";
       roles[Qt::UserRole + 2] = "y";
       roles[Qt::UserRole + 3] = "z";
       return roles;
}

The constructor creates array of 100-200 random 3D points as our data source.

To use the model in QML we should use either qmlRegisterType or setContextProperty. In this case I use first one.

QQmlApplicationEngine engine;
qmlRegisterType<FlightModel>("mytest", 1, 0, "FlightModel");
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

And so now we can use the model in QML:

main.qml

import QtQuick 2.9
import QtQuick.Window 2.0
import QtDataVisualization 1.2
import mytest 1.0

Window {
    id: window
    width: 600
    height: 400
    visible: true

    FlightModel {
        id: dataModel
    }

    Scatter3D {
        anchors.fill: parent
        Scatter3DSeries {
            ItemModelScatterDataProxy {
                itemModel: dataModel

                xPosRole: "x"
                yPosRole: "y"
                zPosRole: "z"
            }
        }
    }
}
folibis
  • 12,048
  • 6
  • 54
  • 97
  • Thanks folibis! I have tried to integrate your solution in my program, but I am having a problem with a compilation error: C2280: 'QQmlPrivate::QQmlElement::QQmlElement(void)': attempting to reference a deleted function with [ T=FlightModel ] Could you help me on that, please? – Miquel Bosch Bruguera Aug 30 '17 at 17:10
  • So, I have found that the problem lies on the "redefinition" that I have done for the FlightModel constructor, which had to import the Flight displayedFlight, in order to load the trajectory points. However, adding an input to the constructor is not valid for the qmlRegisterType, since its constructor is not as simple. Is there any way I can load the flight data to the m_innerData outside the class? I have tried defining the variable as static, but it doesn't seem to work....Thanks – Miquel Bosch Bruguera Aug 30 '17 at 18:44
  • Usually you have to define some data source/provider for you model. Read about MVC in Qt. Also there are lots of examples across the Internet. – folibis Aug 30 '17 at 19:11