1
votes

I am trying to write a plugin that contains some QML files and some C++ classes that provide lower-level functionalities and communicate with another application. They are used by the QML components. I want to be able to manage the life time of these C++ objects from QML (i.e. they should be created when the QML file is loaded and destroyed when QML is destroyed), while still being able to mock the C++ objects.

I tried a few different approaches so far. Ideally, the result will be that I can use qmlscene on the QML file I want to edit and have a dummydata folder next to that file which contains the mock for the instantiated C++ class. If I try that by using qmlRegisterType in a plugin class that inherits from QQmlExtensionPlugin (similar to the example in https://qmlbook.github.io/ch17-extensions/extensions.html), and I pass the resulting library to qmlscene, the QML file will not use the mock, but instantiate a C++ object instead. This means that sometimes, I need to start up a fair bit of logic to get some mocked data into my QML file.

It seems like the example in the "QML Book" suggests to completely design the QML component with a mock before introducing any C++ to QML. Is there a way to do that more sustainable? I guess, I could avoid using qmlRegisterType for a C++ class that I want to mock for a while, by commenting out the according line, but I would like to not have to do that.

The other approach I tried was using QQMLContext::setContextProperty from a central C++ controller class. That enables me to pass the C++ object to QML from C++ and also use the dummydata, however the object's lifetime will not be managed by the QML component, but from C++. Also, each class should potentially be instantiated multiple times and connecting signals properly is pretty error-prone. This is what I found so far:

auto proxy = std::make_shared<Proxy>();
//make `proxy` object known in QML realm
_qmlEngine.rootContext()->setContextProperty("proxy", proxy.get());

connect(&_qmlEngine, &QQmlApplicationEngine::objectCreated,
        [&proxy](QObject *object, const QUrl &url) {
            if (url == QUrl("qrc:/imports/Common/TestWindow.qml")) {

                // make sure the proxy is not destroyed when leaving scope of this function
                connect(qobject_cast<QQuickWindow *>(object),
                        &QWindow::visibilityChanged, // as a dirty workaround for missing QWindow::closing signal
                        [proxy]() mutable { proxy.reset(); }); // delete proxy when closing TestWindow
            }
        });

_qmlEngine.load(QUrl("qrc:/imports/Common/TestWindow.qml"));

Is there a "comfortable" way to mock data instantiated in QML and originally coming from C++, or is there at least a good way to attach the life time of such a C++ object to the life time of the QML object?

1

1 Answers

0
votes

The way I solved this issue is as follows:

The actual production application will use a C++ plugin, containing only C++ files and no QML.

For mocking, there is a QML module with the same name as the C++ plugin, containing QML files which provide the same interface as the equivalent C++ classes. This module is passed to qmlscene in addition to the general QML includes.

If the C++ class header looks like this:

class Proxy : public QObject
{
  Q_OBJECT

  public:
    Q_PROPERTY(int foo)
    Q_INVOKABLE void start();

  signals:
    void started();
}

And this class is made available to QML like this:

qmlRegisterType<Proxy>("Logic", 1, 0, "Proxy");

The QML mock (in file Proxy.qml) can look like this:

import QtQml 2.12

QtObject {
  signal started()
  property var foo: 42
  function start() { console.log("start") }
}

And be importable in QML with a qmldir file that looks like this:

module Logic

Proxy 1.0 Proxy.qml

The final call to qmlscene would be

qmlscene [path/to/prototype/qml] -I [path/to/folder/containing/proxy/mock/]