Search code examples
pythonqtqmlpyside2

Pyside2 QQmlPropertyMap's slot not accessible from QML; Python version of old C++ bug?


I've encountered what seems very much like a modern, Python version of a bug that was fixed years ago in (C++) Qt: Can't call slot or Q_INVOKABLE from QML in subclass of QQmlPropertyMap

I tested the example in the question on my system (Qt 5.15.0, modifying syntax as needed) and verified that it is indeed fixed; the object's slot is correctly accessed, whether it inherits from QObject or QQmlPropertyMap.

However, still using Qt 5.15.0, working from PySide2 seems to replicate the same issue:

main.py:

from PySide2.QtCore import QObject, Property, Slot
from PySide2.QtGui import QGuiApplication
from PySide2.QtQml import QQmlApplicationEngine, QQmlPropertyMap

class MyObj(QObject):
    @Property(str)
    def field(self):
        return 'Field from QObject'
    
    @Slot()
    def test_func(self):
        print('QObject signal worked')

class MyMap(QQmlPropertyMap):
    def __init__(self):
        super().__init__()
        self.insert('field', 'Field from QQmlPropertyMap')
    
    @Slot()
    def test_func(self):
        print('QQmlPropertyMap signal worked')

app = QGuiApplication()
engine = QQmlApplicationEngine()

qobj = MyObj()
engine.rootContext().setContextProperty('qobj', qobj)

qmap = MyMap()
engine.rootContext().setContextProperty('qmap', qmap)

engine.load('main.qml')
app.exec_()

main.qml:

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    visible: true
    width: 200
    height: 200

    Text {
        text: qobj.field + '\n' + qmap.field
        anchors.centerIn: parent
    }
    
    MouseArea {
        anchors.fill: parent
        onClicked: {
            qobj.test_func();
            qmap.test_func();
        }
    }
}

Both fields display properly in the generated window, but clicking on it produces this console output:

QObject slot worked
file:///Users/charles/Projects/qt/map-signal-test/main.qml:20: TypeError: Property 'test_func' of object QQmlPropertyMap(0x7fb954f5f460) is not a function

I don't know much about how PySide2 is implemented. Is this the same bug, unfixed or resurfacing? Or is it something else? In either event, is there a workaround?

EDIT: In response to eyllanesc's answer below. While your posted code reproduces the error in C++, this somewhat different class definition (derived from the old question linked above) works correcty:

class MyMap: public QQmlPropertyMap {
    Q_OBJECT
public:
    MyMap(QObject* parent = 0): QQmlPropertyMap(this, parent) {}
public slots:
    Q_INVOKABLE void test_func() {
        qDebug() << "QQmlPropertyMap signal worked";
    }
};

I don't know enough about C++ to understand the implication there.


Solution

  • Unfortunately it is a Qt bug, check the comments in QTBUG-29836. I have verified it with the following MRE with Qt 5.15:

    #include <QGuiApplication>
    #include <QQmlApplicationEngine>
    #include <QQmlContext>
    #include <QQmlPropertyMap>
    
    class MyMap: public QQmlPropertyMap{
        Q_OBJECT
    public:
        using QQmlPropertyMap::QQmlPropertyMap;
        Q_INVOKABLE void test_func(){
            qDebug() << "QQmlPropertyMap signal worked";
        }
    };
    int main(int argc, char *argv[])
    {
        QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    
        QGuiApplication app(argc, argv);
    
        QQmlApplicationEngine engine;
    
        MyMap qmap;
        engine.rootContext()->setContextProperty("qmap", &qmap);
    
        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();
    }
    
    #include "main.moc"
    
    import QtQuick 2.15
    import QtQuick.Window 2.15
    
    Window {
        visible: true
        width: 640
        height: 480
        title: qsTr("Hello World")
        MouseArea {
            anchors.fill: parent
            onClicked: {
                qmap.test_func()
            }
        }
    }
    

    Update:

    It seems that the workaround of that bug is to use the protected constructor that uses the QMetaObject of the derived class, unlike the default constructor that only accesses the QMetaObject of QQmlProperty, therefore the elements like the Q_SLOTS, Q_INVOKABLES, etc of the new class are mapped. Unfortunately that constructor is not accessible from Python.