"var" properties behave completely differently from C++ QVariant properties



  • Modifying QVariant properties in javascript silently discards changes.

    For example, given a property defined like this:

    @
    #include <QObject>
    #include <QVariant>

    class MyQMLObject : public QObject
    {
    Q_OBJECT
    Q_PROPERTY(QVariant myProp READ myProp WRITE setMyProp NOTIFY myPropChanged)

    public:
    explicit MyQMLObject(QObject *parent = 0) : QObject(parent) {};

    const QVariant myProp() const { return m_myProp; }
    void setMyProp(const QVariant value) { m_myProp = value; }
    

    signals:
    void myPropChanged();

    private:
    QVariant m_myProp;
    };
    @

    And a qml component:

    @
    import QtQuick 2.0
    import MyQML 1.0

    MyQMLObject {
    myProp: [{foo: true}]
    property var myJSProp: [{foo: true}]

    Component.onCompleted: {
        myProp[0].foo = false;
        // myProp[0].foo is still true
    
        myJSProp[0].foo = false;
        // myJSProp[0].foo is false, as expected
    
        myProp = [{foo: false}]
        // myProp[0].foo is now false
    }
    

    }
    @

    I'd expect the value of myProp to be [{foo: false}], but it is instead [{foo: true}]. The dynamic "var" property myJSProp works as expected.

    I think this is because QVariants are immutable, but it's very unintuitive.



  • Hi,

    Sort of, but not quite.

    This is (one of the reasons) why the "variant" property type is deprecated in Qt5, replaced by the "var" property type. With a QVariant property, what happens is that a temporary is created when you do the lookup; then that temporary is modified; then it is discarded by the garbage collector ;-) That is, write-back semantics for QVariant properties are not implemented (for various reasons).

    If you're interested in what's required for write-back semantics to be implemented, take a look at either what we call "value-types" in the code, or what we call "sequence-types". You'll see that there are a lot of edge cases, and a lot of hairy code, in order to implement it safely without completely ruining performance.

    Cheers,
    Chris.



  • Hey Chris, thanks for the quick reply.

    So one option is to implement write-back semantics for QVariant properties. What about going the other way, and using the JSValue on the C++ side instead (so I can create a "var" on the C++ side). Like:

    @Q_PROPERTY(QJSValue blah READ blah WRITE setBlah)@

    Then editing the structure of the QJSValue becomes the QObject's responsibility.

    OK, I just tried this out and it seems to work. Is this recommended practice over exposing a QVariant? I didn't see this anywhere in docs or examples, but that could be my oversight.

    I see that creating complex QJSValue types (array, object, etc) requires a call to QJSEngine (or QQmlEngine) ::newType. Is there a way to gain access to the current engine instance? I couldn't find anything in the examples. I assume the QJSEngine has ownership of objects created in this manner, so using a temporary engine is probably a bad idea, no?

    cheers,
    -Mark

    edit: Found some answers to my own questions:

    http://qt-project.org/wiki/property-var

    QJSValue is 100% supported:

    bq. When implementing types on the C++ side, one can use the QJSValue class as a property/method parameter to transfer values between C++ and QML/JS without type/data loss.
    Includes JS functions; for example, you can assign a function to a property from QML and call it later from C++ using QJSValue::call().

    I still can't see how to create a new Array JS value from a QObject method. For example, say a method or slot in a C++ object modifies a property by inserting a new Array object into JS object.

    ninja edit:

    I think I figured this one out, too, correct me if I'm wrong:

    @void MyObject::onHandleSomeSignal()
    {
    QQmlContext *thisContext = QQmlEngine::contextForObject(this);
    Q_ASSERT(thisContext);

    QJSValue newWrappedQObject = thisContext->engine()->newObject(new QObject());
    QJsValue newArray = thisContext->engine()->newArray();
    QJsValue newObject = thisContext->engine()->newObject();
    
    newObject.setProperty("myQObject", newWrappedQObject);
    newArray.setProperty(0, newObject);
    m_jsValue.setProperty("myArray", newArray);
    

    }
    @

    Now jsValue property should be [{"myQObject": <QObject instance>}].

    Of course, if the instance of MyObject was not constructed by QML, the assertion will fail.

    Some other discoveries from poring over the docs:

    qjsvalue_cast can convert from a QJSValue to a Qt type, e.g.
    @QObject *obj = qjsvalue_cast<QObject>(myQObject);@

    Going the other way requires having a pointer to the engine:
    @QVector<int> numbers;
    numbers << 1 << 2 << 3;
    QJSValue jsArray = context->engine()->toScriptValue(numbers);
    @

    update: (At this point, I'm just documenting my findings for myself and anyone else who happens along.)

    Converting a QObject pointer to a JSValue requires using

    @context->engine()->newObject(new QObject(ptrToQObject)); @

    Using toScriptValue just returns "undefined".

    I have to say that using QJSValue types feels very rough. Two ways come to mind that would make them much more intuitive:

    1. if QJSValues had a pointer to their engine, setProperty could be overridden to handle QObject*, and creating arrays/objects could also be handled directly from the QJSValue.
    2. an alternate approach could be to wrap it using something similar to QQmlProperty (QQmlJSValueProperty?) that has convenience methods for creating QObject QJSValues, new Arrays and new Objects.

    Also, it's not very clear when a context is available. For example, initializing a QJSValue property to an empty array can't be done in either the constructor or by handling the QQmlParser::componentComplete signal; QQmlEngine::contextForObject(this) returns null in both places. I'm working around that by initializing it in my property getter (if m_prop.isUndefined() my_prop = ... etc). I hope there's some docs on the way for QJSValue!

    Overall: works great!



  • The conversion codepaths for QJSValue are separate to the conversion codepaths for QVariant and "var" properties. It's unfortunate, but the QJSValue stuff was implemented by Kent and the other QtScript developers, parallel with and mostly separate to the effort to implement "var" properties by myself and Aaron. It would certainly be nice if we could unify the conversion codepaths, so that all property values are converted the same way, but until that's done there will unfortunately be friction/dissonance.

    In short, don't assume that "var" means "QJSValue" exactly... they're similar, and can be assigned to each other, but... yeah.

    I agree with your suggestions about improving support for QJSValue properties from C++ - definitely file a suggestion in JIRA about this. The whole "when is a context available" issue is a can of worms (eg, there have been bugs about differences between context availability in delegates vs components vs normal child items, at component completion time, and so forth). It should be improved, but before that can happen, investigation must occur to identity the complete set of "current semantics", imo. In general, I think the context should always be available at component completion time, whenever a parent is available. If that's not the case, I think it's a bug.

    Cheers,
    Chris.




Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.