Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make Q_INVOKABLE method that takes variable number of arguments from QML

Tags:

qt

qml

As everyone knows, in JavaScript all functions can take any number of arguments. In Qt you can make QObject, whose methods accessible from QML, having marked it by Q_INVOKABLE. For example:

class myObj: public QObject
{
    Q_OBJECT

//...

public slots:
    Q_INVOKABLE QJSValue myFunction(QJSValue value);

//...

};

And then you can call it from JS:

(function(){
    //like this:
    var result = myObj.myFunction("test");
    //but also like this:
    var result2 = myObj.myFunction(1,2,3,4,5); //,6,7,8, ..., 9998, 9999
})();

So, how to handle variable number of parameters on C++ side? On JS side we have "arguments" object. Is there any analog for Q_INVOKABLE methods?

like image 745
Vladimir Liubimov Avatar asked Sep 19 '18 13:09

Vladimir Liubimov


2 Answers

It seems to be impossible to do by normal way. For now I've ended up with workaround. QJSValue can contain any JS type. Including array. So method could seems like that:

QJSValue myObject::test(QJSValue arg1, QJSValue arg2, QJSValue rest)
{
    qDebug() << "arg1 = " << arg1.toString();
    qDebug() << "arg2 = " << arg2.toString();
    auto args = toJSValueList(rest);
    qDebug() << "args = [";
    for(auto arg : args) {
        qDebug() << "  " << arg.toString() << ",";
    }
    qDebug() << "]";
    return (arg1.isUndefined() ? 0 : 1) + (arg2.isUndefined() ? 0 : 1) + args.length();
}

Arguments arg1 and arg2 are required. Rest of arguments can be passed to method as third argument - an array. Method toJSValueList is just a helper that converts QJSValue, contained array to QJSValueList.

QJSValueList myObject::toJSValueList(QJSValue arg)
{
    QJSValueList list;
    auto length = arg.property("length");
    if(length.isNumber()){
        for(int i = 0, intLength = length.toInt(); i < intLength; ++i){
            list << arg.property(static_cast<quint32>(i));
        }
    } else if(!arg.isUndefined()){
        list << arg;
    }
    return list;
}

This is enough to make things work. But if someone actually need to pass any number of arguments direct to function (not through array), it can be done with a little hack.

class myObject: public QObject, public QmlSingletonProvider<QmlTimer>
{
    Q_OBJECT
public:
    //...
    // declare property of type QJSValue which will actually be a function 
    Q_PROPERTY(QJSValue variadic_test READ variadic_test)

public slots:
    Q_INVOKABLE QJSValue test(QJSValue arg1, QJSValue arg2, QJSValue rest);

private:
    QJSValueList toJSValueList(QJSValue arg);
    QJSValue variadic_test(); // getter for property
    QJSValue variadic_test_fn; // stored JS function
};


// variadic wrapper for  method test()
QJSValue myObject::variadic_test()
{
    if(variadic_test_fn.isCallable())
        return variadic_test_fn;

    auto engine = qjsEngine(this);
    if(!engine) return QJSValue();

    variadic_test_fn = engine->evaluate("function(){ return this.test( arguments[0], arguments[1], Array.prototype.slice.call( arguments, 2 ) ); }");

    return variadic_test_fn;
}

QJSValue can also be a function. So you can add an readonly Q_PROPERTY with type of QJSValue and assign JS function to it on first property access. This function will be a wrapper, that just gather all its arguments to array and pass it to your actual method. From JS you can see and call both functions: the actual one and the variadic wrapper. In the example I've passed first two arguments as is ("undefined" will be passed, if number of arguments is less than 2.), since method "test" waiting for at least 2 arguments. And rest of arguments gathered to an array by "Array.prototype.slice.call( arguments, 2 )". You can use "function(){ return this.test( Array.prototype.slice.call( arguments ) ); }" if you don't have required parameters. Of course your C++ method should be ready for possible "undefined" values. And type of each argument need to be checked by hand. It's seems more like "JS code", even though it written on C++.

like image 96
Vladimir Liubimov Avatar answered Oct 20 '22 17:10

Vladimir Liubimov


It's looks like this is impossible for every QObject, but you can wrap your singleton C++ backend SLOT with custom JS script, which will join together multiple arguments to single array. This can be done through the qmlRegisterSingletonType method callback, which provides access to the QJSEngine instance.

QJSValue ApiProvider::initSingletonType(QQmlEngine *qmlEngine, QJSEngine *jsEngine) {
    qDebug() << "ApiProvider initSingletonType";
    Q_UNUSED(qmlEngine);
    QJSValue instance=jsEngine->newQObject(new ApiProvider(jsEngine));
    QJSValue restProvider=jsEngine->evaluate(
        "(function (instance) {" \
        "   instance.createElement=function(type,...child){" \
        "       return instance.createElementInternal(type,child);" \
        "   }; " \
        "   return instance;" \
        "})"
    );
    if (restProvider.isError()) {
        qCritical() << restProvider.toString();
        return QJSValue();
    } else {
        QJSValue result=restProvider.call({instance});
        if (result.isError()) {
            qCritical() << result.toString();
            return QJSValue();
        } else {
            return instance;
        }
    }
}

...


qmlRegisterSingletonType(
    "com.tripolskypetr.quitejs",
    1, 0,
    "Api",
    ApiProvider::initSingletonType
);

This sample implements a conditional jsx factory, where each element can have an arbitrary number of descendants in rest parameters. The source code is ready to production.

enter image description here

like image 31
Petr Tripolsky Avatar answered Oct 20 '22 16:10

Petr Tripolsky