garbart
garbart

Reputation: 485

QML. How to change MapPolyline path from C++?

Here is a QML map:

Map {
        anchors.fill: parent
        plugin: osmMapPlugin
        center: QtPositioning.coordinate(56.006355, 92.860984)
        zoomLevel: 14

        MapPolyline {
            line.width: 3
            line.color: 'red'
            path: [
                { latitude: -27, longitude: 153.0 },
                { latitude: -27, longitude: 154.1 },
                { latitude: -28, longitude: 153.5 },
                { latitude: -29, longitude: 153.5 }
            ]
        }
    }

How to change path from C++/qt? I tried this:

QML:

Map {
        anchors.fill: parent
        plugin: osmMapPlugin
        center: QtPositioning.coordinate(56.006355, 92.860984)
        zoomLevel: 14

        MapPolyline {
            line.width: 3
            line.color: 'red'

            path: map_path
        }
    }

C++:

map = new QQuickWidget();
map->setSource(QUrl("qrc:map.qml"));

QQmlContext *qml_map = map->rootContext();
QGeoPath *path = new QGeoPath();
path->addCoordinate(*(new QGeoCoordinate(56.0831528053, 92.8405031454)));
path->addCoordinate(*(new QGeoCoordinate(56.1, 93)));
qml_map->setContextProperty("map_path", path);

But I got a exception:

calling a private constructor of class 'QVariant':
    qml_map->setContextProperty("map_path", path);

and

attempted to use of deleted function:
    qml_map->setContextProperty("map_path", path);

UPD: @hi-im-frogatto suggested that it was necessary to do so:

qml_map->setContextProperty("map_path", QVariant::fromValue(path));

It helped, the error no longer occurs. But the line is not drawn. Got error:

qrc:map.qml:30: ReferenceError: map_path is not defined
qrc:map.qml:21:5: QML Map: Plugin is a write-once property, and cannot be set again.

Upvotes: 4

Views: 3458

Answers (3)

Marcus Harrison
Marcus Harrison

Reputation: 869

Based on the answer I found here - How to set QML MapPolyline Path Property - I wrote a simple test-case which is correct, works and, most importantly, is simpler than the accepted answer on this question.

The principle is simple - we can wrap our QGeoCoordinates in QVariants. We can add the QVariants to a QVariantList, and pass that directly to the QML MapPolyline.path property.

Even better, if we do this with a C++ class' Q_PROPERTY value, we don't even have to have QML logic to read or re-read the property when it changes.

Finally, with this method you can call setContextProperty after the QQmlWidget has loaded its source and begun displaying, which is useful if, for example, your QQmlWidget is part of a Qt Designer form file instead of added explicitly in C++.

Lets take a look at how this looks in practice - I'm going to create a class called RouteProvider which provides the path:

#ifndef ROUTEPROVIDER_H
#define ROUTEPROVIDER_H

#include <QObject>
#include <qqml.h>
#include <QMetaClassInfo>
#include <QGeoPath>

class RouteProvider : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QVariantList path READ path NOTIFY pathUpdated)
    QML_ELEMENT

public:
    explicit RouteProvider(QObject *parent = nullptr);

    QVariantList path();

signals:
    void pathUpdated();

private:
    QVariantList m_path;
};

#endif // ROUTEPROVIDER_H

To pass this class as a context property, the following needs to be true:

  • The class must be a subclass of QObject
    • This might actually be something you can skip using Q_GADGET and Q_REGISTER_METATYPE but that way lies madness
  • the class must use the QML_ELEMENT macro in the private part of its declaration

Note that your m_path and class functions can, in theory, be anything and do anything. The important aspects are:

  • The path function should return the QVariantList of QVariant::fromValue(QGeoCoordinate) values
  • Functions that change the path should emit the pathUpdated signal.

Looking at the .cpp file for this example is very simple:

#include "routeprovider.h"

RouteProvider::RouteProvider(QObject *parent) : QObject(parent)
{
    m_path.append(QVariant::fromValue(QGeoCoordinate(51.50648, -0.12927)));
    m_path.append(QVariant::fromValue(QGeoCoordinate(51.50826, -0.12599)));
}

QVariantList RouteProvider::path()
{
    return m_path;
}

For the sake of this demonstration, I'm just creating a line between two coordinates and returning the path.

I'm going to create a QMainWindow with a QQmlWidget as its central widget, then set an instance of this class as a context property.

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include "routeprovider.h"

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    RouteProvider m_provider;
};
#endif // MAINWINDOW_H

To demonstrate this method still works even after the QML file has been loaded, I'm loading the QML file first, then setting the context property:

#include "mainwindow.h"
#include <QQuickWidget>
#include <QQmlContext>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    auto centralWidget = new QQuickWidget(this);
    centralWidget->setSource(QUrl{"qrc:/CentralMap.qml"});
    centralWidget->rootContext()->setContextProperty("routeProvider", &m_provider);
    centralWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);

    this->setCentralWidget(centralWidget);
    this->resize(400, 400);
}

MainWindow::~MainWindow()
{
}

And here's the simplest kind of QML file which still demonstrates this in action:

import QtQuick 2.0
import QtPositioning 5.12
import QtLocation 5.15

Item {
    Plugin {
        id: mapPlugin
        name: "osm"
    }

    Map {
        anchors.fill: parent
        zoomLevel: 14
        plugin: mapPlugin

        MapPolyline {
            id: line
            path: routeProvider.path
            line.width: 15
            line.color: 'red'
        }
    }
}

Place it in a .qrc file to make it available to the program at run-time.

Finally, a default-generated main function starts our application:

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

Result:

Resulting application window showing map of London with red line

Upvotes: 1

eyllanesc
eyllanesc

Reputation: 243897

As it says @HiI'mFrogatto you should use QVariant::fromValue(), but it can not be used directly on the .qml side:

QGeoPath geopath;
geopath.addCoordinate(QGeoCoordinate(56.006355, 92.860984));
geopath.addCoordinate(QGeoCoordinate(56.1, 93));
geopath.addCoordinate(QGeoCoordinate(56.1, 92.777));


QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("geopath", QVariant::fromValue(geopath));

what you have to do is access each element through a loop:

Map {
    anchors.fill: parent
    plugin: osmMapPlugin
    center: QtPositioning.coordinate(56.006355, 92.860984)
    zoomLevel: 10

    MapPolyline {
        id: pl
        line.width: 3
        line.color: 'red'
    }

    Component.onCompleted: {
        var lines = []
        for(var i=0; i < geopath.size(); i++){
            lines[i] = geopath.coordinateAt(i)
        }
        pl.path = lines
    }
}

But with this case we can not update the values of the QGeoPath, it is appropriate to implement a class that inherits from QObject and has as property to QGeoPath, since you can manipulate it from C++ or from QML.

main.cpp

#include <QGeoPath>
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QTimer>

class PathController: public QObject{
    Q_OBJECT
    Q_PROPERTY(QGeoPath geopath READ geoPath WRITE setGeoPath NOTIFY geopathChanged)
public:
    PathController(QObject *parent=0):QObject(parent){}
    void test(){
        mGeoPath.addCoordinate(QGeoCoordinate(56.006355, 92.860984));
        mGeoPath.addCoordinate(QGeoCoordinate(56.1, 93));
        mGeoPath.addCoordinate(QGeoCoordinate(56.1, 92.777));
        QTimer *timer = new QTimer(this);

        QObject::connect(timer, &QTimer::timeout, [this](){
            mGeoPath.translate(0.001, -0.01);
            emit geopathChanged();
        });
        timer->start(1000);

    }
    QGeoPath geoPath() const{return mGeoPath;}
    void setGeoPath(const QGeoPath &geoPath){
        if(geoPath == mGeoPath)
            return;
        mGeoPath = geoPath;
        emit geopathChanged();
    }

signals:
    void geopathChanged();
private:
    QGeoPath mGeoPath;
};

int main(int argc, char *argv[])
{
#if defined(Q_OS_WIN)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QGuiApplication app(argc, argv);

    PathController controller;
    controller.test();

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("pathController", &controller);

    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;
    return app.exec();
}

#include "main.moc"

main.qml

import QtQuick 2.9
import QtQuick.Window 2.2

import QtLocation 5.6
import QtPositioning 5.6

Window {
    visible: true
    width: 640
    height: 480

    Plugin {
        id: osmMapPlugin
        name: "osm"
    }

    Map {
        anchors.fill: parent
        plugin: osmMapPlugin
        center: QtPositioning.coordinate(56.006355, 92.860984)
        zoomLevel: 10

        MapPolyline {
            id: pl
            line.width: 10
            line.color: 'red'
        }
    }

    function loadPath(){
        var lines = []
        for(var i=0; i < pathController.geopath.size(); i++){
            lines[i] = pathController.geopath.coordinateAt(i)
        }
        return lines;
    }
    Connections{
        target: pathController
        onGeopathChanged: pl.path = loadPath()
    }

    Component.onCompleted: pl.path = loadPath()
}

In this link you will find the complete example.

Upvotes: 6

frogatto
frogatto

Reputation: 29285

QQmlContext::setContextProperty accepts a QVariant and there's no any implicit conversion from QGeoPath to QVariant. However using the following method you can do this:

qml_map->setContextProperty("map_path", QVariant::fromValue(path));

More info: http://doc.qt.io/archives/qt-5.5/positioning-cpp-qml.html

Upvotes: 3

Related Questions