Invoke slot of child of child of QQmlPropertyMap?

Viewed 261

Before marking this as a duplicate of "Can't call slot or Q_INVOKABLE from QML in subclass of QQmlPropertyMap," please read the final response (https://stackoverflow.com/a/18674363/447438).

I am working on a design where I have a class (call it MyBaseClass) that inherits from QQmlPropertyMap. It implements some additional behaviors for my system which are then used by other classes that inerit from MyBaseClass (e.g. ChildClass).

From QML, (QtQuick), I wish to invoke a slot (mySlot()) of ChildClass. When I try, I get the following issues in the Application Output (mentioning MyBaseClass) even though I am only using ChildClass.

qrc:/main.qml:17: TypeError: Property 'mySlot' of object MyBaseClass(0x55f88ef86b50) is not a function
qrc:/main.qml:24: TypeError: Property 'mySlot' of object MyBaseClass(0x55f88ef86b50) is not a function

I also tried implementing sending signals from QML, but I get a similar issue trying to bind to the QML emitted signals. (I am not adding that example.)

For those suggesting I just inherit QQmlPropertyMap into ChildClass (in addition to MyBaseClass to which it already has the proper inheritance and intialization via the protected method), the result is not pretty:

/home/jwerner/Projects/InheritanceTest-simple/ChildClass.h:8: warning: direct base 'QQmlPropertyMap' is inaccessible due to ambiguity:
    class ChildClass -> class MyBaseClass -> class QQmlPropertyMap
    class ChildClass -> class QQmlPropertyMap

My "gut" says that the problem is I am not doing things the "Qt Way," but I can't figure out what it is.

Here is the minimum example code of the problem to demonstrate the problem. (I'm leaving out the .pro file since it is trivial to make.)

MyBaseClass.h

#include <QQmlPropertyMap>

class MyBaseClass : public QQmlPropertyMap
{
    Q_OBJECT
public:
    explicit MyBaseClass(QObject *parent = nullptr)
        : QQmlPropertyMap(this, parent)
    {}

};

ChildClass.h

#include "MyBaseClass.h"
#include <QObject>
#include <QDebug>

class ChildClass : public MyBaseClass
{
    Q_OBJECT
public:
    ChildClass(QObject *parent = nullptr)
        : MyBaseClass(parent) {}

public slots:
    void mySlot() {
        qDebug() << "This is mySlot";
    };
};

main.qml

import QtQuick 2.15
import QtQuick.Window 2.15


Window {
    id: theMainWindow
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    property QtObject   cData: childData;

    onCDataChanged: {
        childData.mySlot();
    }

    Timer { // wait a bit before calling
        repeat: false
        interval: 2000
        running: true
        onTriggered: childData.mySlot();
    }
}

main.cpp

int main(int argc, char *argv[])
{
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#endif

    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;

    auto childData = new ChildClass();

    engine.rootContext()->setContextProperties({
        QQmlContext::PropertyPair{"childData",  QVariant::fromValue(childData)},
    });
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
        &app, [url](QObject *obj, const QUrl &objUrl) {
            if (!obj && url == objUrl)
                QCoreApplication::exit(-1);
        }, Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

2 Answers

Observe that the 2-argument QQmlPropertyMap constructor is a template constructor that needs the type of the [most] derived class (the documentation could be clearer on that, but that's the only way to make it work with automagic registration). The only type it ever sees is that of MyBaseClass.

You need to pass the type of the derived class to that constructor, e.g. by making a protected MyBaseClass constructor templated and giving it the same signature as that of QQmlPropertyMap, and then invoking that from MyChildClass. The alternative of making MyBaseClass a CRTP template class only works if that class doesn't add any properties nor signals nor slots.

Thus:

#include <QQmlPropertyMap>

class MyBaseClass : public QQmlPropertyMap
{
    Q_OBJECT
protected:
    template <typename Derived>
    explicit MyBaseClass(Derived *derived, QObject *parent = nullptr)
        : QQmlPropertyMap(derived, parent)
    {}
};

The constructor should be protected since MyBaseClass won't be usable by itself, I presume. You can always add a single-argument public constructor if you wish:

public:
    MyBaseClass(QObject *parent = nullptr) : QQmlPropertyMap(this, parent) {}

And then:

#include "MyBaseClass.h"
#include <QObject>
#include <QDebug>

class ChildClass /* final, or see below */ : public MyBaseClass
{
    Q_OBJECT
public:
    ChildClass(QObject *parent = nullptr)
        : MyBaseClass(this, parent) {}   // notice the additional argument

protected:
    // if this class is not final, then add:
    template <typename Derived>
    ChildClass(Derived *derived, QObject *parent = nullptr)
         : MyBaseClass(derived, parent) {}

public slots:
    void mySlot() { qDebug() << "This is mySlot"; };
};

Note that either of the following must be present:

  1. Declaration of class ChildClass as a final class, or

  2. The templated protected 2-argument constructor.

It is a design mistake to have neither of the two, or both.

I tested this and it worked, but I can't say I recommend this as a solution. It kind of negates the purpose of having a base class if it has to know about its derived types.

// Forward-declare the derived type so it can be passed to the constructor
class ChildClass;

class MyBaseClass : public QQmlPropertyMap
{
    Q_OBJECT
public:
    explicit MyBaseClass(ChildClass *derivedObj, QObject *parent = nullptr)
        : QQmlPropertyMap(derivedObj, parent)
    {}

};

Related