How to pass C++ collections to QML
-
Hello everyone,
I've spent the past few days reading a lot about QML and I'm still not seeing clearly the best way of using collections of C++ objects in my QML files (or even, how to do it at all!). I've got 10+ years experience with C++, but I'm basically completely new to QML.
As anyone who has read my previous post (which has still not got any replies :'( ) will have seen, I'm building an application that uses the QML Map type to display airports and navigation data to the user.
In my C++ model I have an AirportManager, which currently holds a QHash object of every airport in the world, parsed from a local database. The task I have set myself for this evening, is to get the C++ 'Airport' classes to appear on the map (I'll implement rudimentary BSP sorting tomorrow).
In it's simplest form, I'd just like to have the following sequence:
- User Scrolls the map in QML
- Map sends out signal with new Lat and Lon coordinates
- Airport manager class (C++) receives the signal and updates it's position of interest (then BSP filters the maps etc.)
- Airport manager sends out signal with a QList of airports to render on the map
- QML Map receives the signal, and renders the list of objects
I've tried creating a Q_PROPERTY(QList<Airport> airports READ getAirports) but this implies that I need to tell the QML to specifically find the airport manager and get the list.
Is there a way to rely purely on signals and slots? Ideally I'd like to abstract my C++ implementation from the QML implementation.I know that my QML map needs to have a MapItemView, which in my implementation has a model of type ListModel and a delegate of type Component. Within the delegate I added a MapQuickItem but I don't know how to make this refer to the coordinates from the model items?
Please excuse my ignorance, I'm still really trying hard to get my head around the way to pass info between QML and C++. Literally any response that teaches me more about how this works is more than welcome.
Thanks, Love you all
Catch_0x16
-
QML understands QObject pointers, you can send these via signals and slots, no problem.
If you add your manager to QML context, like this:
engine.rootContext()->setContextProperty(QStringLiteral("airportManager"), airportManager);
you can then use it in Connections component like so:
Connections { target: airportManager onSomeSignal: { // Handle your QObjects here } }
You'll likely need to create new QML components and fill their data with data from these airport objects (because otherwise QML engine would have to take ownership of these objects, which is a big can of worms).
Some docs on this:
- https://doc.qt.io/qt-5/qtqml-cppintegration-topic.html
- https://doc.qt.io/qt-5/qtqml-cppintegration-overview.html
There are many ways to achieve QML-C++ connectivity so it's a bit hard for me to judge which approach is best in your case.
-
Thank you so much for your informative reply. I've looked all over and come across many ways of passing data, which I think confused me a little. But your advice of setting the context property is I think what I wanted to do. That way the C++ has no idea of what is going on in the QML, but I can reference the slots and properties of the C++ object via QML - perfect!
Thanks again.
-
@sierdzio I wonder if you might be able to help me debug...
my QML file, which looks like this:
AirportMap { id: airportMap Connections { target: AirportManager onNewAirportDrawListReady: { console.log("Called the event") var drawList = AirportManager.getAirportDrawList airportMap.onNewAirportData(drawList) console.log("Finished the event") } } }
never actually triggers the onNewAirportDrawListReady function.
My AirportManager class looks like:
#ifndef AIRPORTMANAGER_H #define AIRPORTMANAGER_H #include <QObject> #include <QFile> #include <QHash> #include <QPoint> #include "airport.h" class AirportManager : public QObject { Q_OBJECT Q_PROPERTY(QList<QWeakPointer<Airport>> airportDrawList READ getAirportDrawList) public: explicit AirportManager(QObject *parent = nullptr); QList<QWeakPointer<Airport>> getAirportDrawList() const; signals: void newMapFocusPosition(double lat, double lon); void newAirportDrawListReady(); public slots: void onNewMapFocusPosition(double lat, double lon); private: QList<QWeakPointer<Airport>> mAirportDrawList; QHash<QString,QSharedPointer<Airport>> mAllAirports; QPointF mFocusPosition; }; #endif // AIRPORTMANAGER_H
And my main looks like:
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QQmlContext> #include "airportmanager.h" int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); // Register types called by QML qmlRegisterType<Airport>("com.gtinteractive.airport", 1,0,"Airport"); AirportManager mgr; QQmlApplicationEngine engine; engine.rootContext()->setContextProperty(QStringLiteral("AirportManager"), &mgr); engine.load(QUrl(QStringLiteral("qrc:/QML/main.qml"))); if (engine.rootObjects().isEmpty()) return -1; return app.exec(); }
Any ideas why the events aren't triggering? (I'm definitely emitting the event, I've triple checked).
-
- do you get any warnings at runtime? Look for any messages suggesting that there is some error with connection.
- are you sure you have only one AirportManager object? Sounds silly I know, but I've seen people emit signals from one instance and wondering why slot is not invoked when it is connected to a different object
- this is probably not the issue, but try renaming the context property to something starting with lower-case character (like
airportManager
). QML engine has some special handling for strings starting with a capital letter, perhaps you've hit some corner case
Code looks good.
-
Thanks for your help again @sierdzio . I managed to work out that though I was emitting the signal... I hadn't started the application loop yet, so none of the signals/slots were working - doh!
I'm now faced with the issue that no matter what I do, the data is always 'undefined' in the QML.
Here's my QML, thanks to @sierdzio it's definitely receiving the event (thanks again mate).:
function onNewAirportData(airportsArray) { largeAirportsModel.clear() for (var i = 0; i < airportsArray.length; i++) { //largeAirportsModel.append({coordinates: airport.coordinates, imageUrl: airport.imageUrl, name: airport.name, ident: airport.ident}) console.log("airportsArray length: " + airportsArray.length) console.log("coords = " + airportsArray[i].coordinates) console.log("imageUrl = " + airportsArray[i].imageUrl) console.log("name = " + airportsArray[i].name) console.log("ident = " + airportsArray[i].ident) } }
Here's the output from that function (repeated 200 times):
qml: airportsArray length: 200 qml: coords = undefined qml: imageUrl = undefined qml: name = undefined qml: ident = undefined
And here is the C++ code that compiles the QVariants:
QPointer<AirportMapDrawData> Airport::getMapDrawData() const { return QPointer<AirportMapDrawData>(new AirportMapDrawData(mIdent, mName , getImageURL(), mId, CoordinateType(mLat, mLon))); } void AirportManager::onNewMapFocusPosition(double lat, double lon) { mFocusPosition = QPointF(lat, lon); // TEMP mAirportDrawList.clear(); for (int i = 0; i < 200; ++i) { QPointer<AirportMapDrawData> ptr = (mAllAirports.begin() + i).value()->getMapDrawData(); mAirportDrawList.push_back(QVariant::fromValue(ptr)); } emit newAirportDrawListReady(); }
I've inserted a breakpoint on the line before the AirportMapDrawData object is pushed into the airport list and the data is definitely there.
Here is the header for the AirportMapDrawData object:
typedef QPair<double,double> CoordinateType; Q_DECLARE_METATYPE(CoordinateType); class AirportMapDrawData : public QObject { Q_OBJECT Q_PROPERTY(CoordinateType coordinates READ getPosition) Q_PROPERTY(QString ident READ getIdent) Q_PROPERTY(QString name READ getName) Q_PROPERTY(QUrl imageUrl READ getImageURL) Q_PROPERTY(int id READ getID) public: explicit AirportMapDrawData(const QString& ident, const QString& name, const QUrl& imageUrl, int id, const CoordinateType& position, QObject *parent = nullptr); CoordinateType getPosition() const; QString getIdent() const; QString getName() const; QUrl getImageURL() const; int getID() const; private: QString mIdent; QString mName; QUrl mImageUrl; int mId; CoordinateType mPosition; };
And it is currently being registered like so:
qmlRegisterUncreatableType<AirportMapDrawData>("com.gtinteractive.airportmapdata", 1,0,"AirportMapData","This is a POD class from C++");
Any ideas why the data is null when it enters QML?
-
QObjects do not need to be registered via
qmlRegisterUncreatableType
, although it should not hurt.To keep things simple (you can complicate again it once it starts working :-)), try sending a QObjectList and not a custom type. Your AirportMapDrawData should append to QObjectList without any problems.
In QML, what is the output of:
console.log("airportsArray length: " + airportsArray.length) console.log("current objecth: " + airportsArray[i])
Is it also
undefined
, or does it print some object info?Also, what is
airportsArray
in QML? How do you create it? -
Hey @sierdzio thanks again for the replies.
I've changed the QVariantList object to a QObjectList but now I'm getting:
QMetaProperty::read: Unable to handle unregistered datatype 'QObjectList' for property 'AirportManager::airportDrawList' qrc:/QML/AirportMap.qml:37: TypeError: Cannot read property 'length' of undefined
during run time, from the QML engine (presumably).
-
Also, apologies, I did not answer your questions:
In QML, what is the output of: console.log("airportsArray length: " + airportsArray.length) console.log("current objecth: " + airportsArray[i]) Is it also undefined, or does it print some object info? Also, what is airportsArray in QML? How do you create it?
I'm creating the list via this code in the QML:
Connections { target: airportManager onNewAirportDrawListReady: { var drawList = airportManager.airportDrawList airportMap.onNewAirportData(drawList) } }
Since I changed the Q_PROPERTY of AirportManager to return Q_PROPERTY(QList<QPointer<AirportMapDrawData>> airportDrawList READ getAirportDrawList) (I got errors when I set it to QObjectList) I get the following outputs from QML:
qml: airportsArray length: undefined qml: current object: undefined
-
@Catch_0x16 said in How to pass C++ collections to QML:
qml: airportsArray length: undefined
That line used to work, so something is now more broken than it was ;-)
Unable to handle unregistered datatype 'QObjectList'
Bummer. Perhaps
QList<QObject*>
is registered. Anyway, one more thought: perhaps remove the QPointer<> from property definition, return types etc.? I'm not sure if QML engine handles QPointers well and it's not needed here anyway.