1

Is it possible to dynamically load QML from memory instead of using a file? I've seen a lot of code that always needs a URL, like this:

QGuiApplication app(argc, argv);
QQmlEngine* engine = new QQmlEngine;
QQmlComponent component(engine, QUrl(QStringLiteral("qrc:/main.qml")));

// ignoring error checking (make sure component is ready, compiles w/o errors, etc)

QObject* object = component.create();
app.exec();

In the example above, main.qml will be loaded, and the magic of QML will ensure any types get resolved. In particular, Qt considers the name of the file to be the QML type, which is made available to any other QML file within the same directory. So if main.qml used the type Simple, it would look for the file Simple.qml and use that as the type definition (http://doc.qt.io/qt-5/qtqml-documents-definetypes.html).

Now I'm going to get dangerous: I want to use in-memory QML instead of using a file. This is how it might work (please ignore bad design, memory leaks, etc. for the moment):

QByteArray SimpleQml()
{
    return QByteArray("import QtQuick 2.7; Text {") +
        "text: \"Hello, World!\"}";
}

QByteArray TextExampleQml()
{
    return QByteArray("import QtQuick 2.7; Simple{") +
        "color: \"Red\"}";
}

QObject* createObjectFromMemory(QByteArray qmlData, QQmlEngine* engine, QQmlComponent* componentOut)
{
    componentOut = new QQmlComponent(engine);

    // note this line: load data from memory (no URL specified)
    componentOut->setData(qmlData, QUrl());

    if (componentOut->status() != QQmlComponent::Status::Ready)
    {
        qDebug() << "Component status is not ready";

        foreach(QQmlError err, componentOut->errors())
        {
            qDebug() << "Description: " << err.description();
            qDebug() << err.toString();
        }

        qDebug() << componentOut->errorString();

        return nullptr;
    }
    else
    {
        return component.create();
    }
}

int main(int argc, char* argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    QQmlComponent* component = nullptr;

    // works great: shows a small window with the text "Hello, World!"
    QObject* object = createObjectFromMemory(SimpleQml(), view.engine(), component);

    // problem: Simple is not a type
    // QObject* object = createObjectFromMemory(TextExampleQml(), view.engine(), component);

    // setContent() is marked as an internal Qt method (but it's public)
    // see source for qmlscene for why I'm using it here
    view.setContent(QUrl(), component, object);

    view.show();

    app.exec();

    // don't forget to clean up (if you're not doing a demo snippet of code)
    //delete object;
    //delete component;
}

So SimpleQml() simply is a Text object with "Hello, World!" as the text. When createObjectFromMemory() is called with SimpleQml() as the data source, a small window appears with the text "Hello, World!".

I'm using QQuickView::setContent(), which is a function that is marked as internal in the Qt source, but it's used in qmlscene, and I'm trying to achieve a similar effect here (https://code.woboq.org/qt5/qtdeclarative/tools/qmlscene/main.cpp.html#main).

Obviously, the problem is when I try to call createObjectFromMemory() with TextExampleQml() as the data source. The QML engine has no idea that Simple should be a type--how can I tell QML about this type? I thought that qmlRegisterType (http://doc.qt.io/qt-5/qqmlengine.html#qmlRegisterType) might be a good candidate, but I could not find a way to register a type that is coming from a chunk of QML sitting in memory. I'd like to say, "Hey, I have this chunk of QML code, and it's type is 'Simple'."

Let's assume I could easily predict all type dependencies and would be able to know the proper order to load QML memory chunks. Can I register the QML code returned by SimpleQml() as the type Simple (keeping it in memory, and not saving it to a file)? If anything, I'd like to understand how the QML engine does this under the hood.

I realize I'm breaking a golden rule: don't reach into QML from C++ (reach into C++ from QML instead). So if anyone has a JavaScript solution that's cleaner, I'm open to suggestions, but please remember I'm trying to load from memory, not from file(s).

Update: As noted in the comments, there is a JavaScript function called createQmlObject() that creates a QML object from a string, which is extremely similar to what I'm doing in C++ above. However, this suffers the same problem--the type that createQmlObject creates is...well, it's not a type, it's an object. What I'd like is a way to register a string of in-memory QML as a QML type. So my question still stands.

Matthew Kraus
  • 6,660
  • 5
  • 24
  • 31
  • 1
    You have two ways of dynamic object creation in JavaScript, mentioned [here](http://doc.qt.io/qt-5/qtqml-javascript-dynamicobjectcreation.html). However, I don't know if you will encounter the same issues using them as you are here. I suspect the QML engine can't give your loaded component a type name because it's "anonymous", in the sense that it doesn't come from a URL. The type name for components declared in QML is taken from the file name. If you passed a URL to `setData()` it would probably work. – Mitch Jan 19 '17 at 07:42
  • I guess that your application design has some problem if you create/load components from c++. There are easy and clear ways to do the same in QML. – folibis Jan 19 '17 at 07:47
  • @Mitch thanks for the link to dynamic object creation in JavaScript. I updated my question to include this possibility. However, it still doesn't seem to solve the challenge of registering a type from a chunk of in-memory QML source code. Certainly the QML engine has to do this under the hood...maybe the API is only intended for the designers of Qt, but I think there are good uses for this. – Matthew Kraus Jan 20 '17 at 01:42

1 Answers1

11

One option would be to provide a custom QNetworkAccessManager to your QML engine and implement handling of a custom URL scheme via createRequest().

E.g. your implementation of createRequest() would check if the passed URL has your scheme, lets say `memory", if yes, it takes the rest of the URL to decide which function to call or which data to deliver.

For anything else it just calls the base implementation.

Your main QML file could then be loaded like this

QQuickView view;
view.setSource(QUrl("memory://main.qml"));

All URLs in QML are relative to the current file if not specified otherwise to a lookup for Simple should look for memory://Simple.qml

Kevin Krammer
  • 5,159
  • 2
  • 9
  • 22
  • Thanks for this! I'm going to mark this as the best answer because it's the best of both worlds: it gives fast (memory-speed) access to the QML data (which is a file in memory), and because it's a file, it works well with how QML does types (by creating a type based on the file name). Bravo! – Matthew Kraus Feb 01 '17 at 19:18