derM
derM

Reputation: 13691

Connect dynamically to signals of dynamically created objects

The title might be a bit confusing, but my intent is pretty simple:

I have a [Repeater/Instantiator] which creates multiple instances of a arbitrary delegate. I want to react to all changes of the properties (only first-level, so no properties of properties) of the instances of the delegate, calling a function

function update(index, propertyName)

This seems to be easy, but I fail. This is my code

TestObj.qml

Repeater {
    onItemAdded: {
        var keys = Object.keys(item)
        console.log(keys)
        for (var k = 0; k < keys.length; k++) {
            if (item[keys[k] + 'Changed'] !== undefined  && keys[k] !== 'objectName') {
                var key = keys[k]
                var str = "function() { console.log('Property, " + key + " of " + index + " changed') }"
                console.log(str)
                item[key + 'Changed'].connect(function() { console.log('Property', key, 'of', index, 'changed') })
            }
        }
    }
}

main.qml:

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQml 2.2
import QtQuick.Controls 1.4 as Ctrl
import '.'
import 'test.js' as Test

ApplicationWindow {
    id: root
    visible: true
    minimumWidth: 500
    minimumHeight: 500
    property var blub: []

    Column {
        spacing: 5
        TestObj {
            model: 5
            delegate: Row {
                spacing: 2
                property int p1: 0
                property int p2: 2
                property int p3: 4

                Button {
                    text: parent.p1
                    onClicked: parent.p1++
                }
                Button {
                    text: parent.p2
                    onClicked: parent.p2 *= 2
                }
                Button {
                    text: parent.p3
                    onClicked: parent.p3 *= parent.p3
                }
            }
        }
    }
}

It succesfully connects something, but I can't get the key properly locked. Whichever property I change, I always get the info, I had changed property p3 in my example.

How can I lock the key, so that I get propertyName when I change the the property with the corresponding name?

Upvotes: 0

Views: 4038

Answers (3)

jimav
jimav

Reputation: 840

The reason "key" is always p3 is that all the created functions reference the same "key" variable (they are closures over "key"), and so when any of them are called, they find the most-recently-stored value in "key" (p3 in the OP's original code).

The code looks like it creates a separate local key var each time through the loop, but that is not the case in Javascript, becase in JS all var declarations are "hoisted" to the top of their containing scope (that's why you can reference a var before declaring it -- the declaration is moved "for you"); unfortunatly, in Javascript there are no local scopes other than function scope. In this case the "var key" declaration gets hoisted to the top of the onItemHandled block (which qml compiles into an anon function).

Possible solution: Create the handler function inside another function, so that the "key" referenced by the handler is a parameter of the wrapper function, which is separately created for each call:

item[key + 'Changed'].connect(
     (function(k,i) {
             return function() { console.log('Property',k,'of',i,'changed') }
                    })(key,index)
);

Upvotes: 0

Jimmy Chen
Jimmy Chen

Reputation: 520

What you want to do is to introspect objects' property changing at runtime. Therefore thanks to Qt's powerful meta-system, it is more intrinsic to do this using the metaobjects in the QObject.

The main idea is to use a custom C++ extension for QML which can analyze and gather all the properties in a QML object, and connect the notifySignals of these properties to our custom slots if any. Below are some code snippets, and the complete demo can be found in my Github repo: https://github.com/cjmdaixi/PropertySpy

  1. define PropertySpy

    class PropertySpy : public QObject
    {
        Q_OBJECT
    public:
        explicit PropertySpy(QObject *parent = 0);
    
        Q_INVOKABLE void spy(QObject * object);
    public slots:
        void onPropertyChanged();
    };
    
  2. the implementation of spy:

    void PropertySpy::spy(QObject *object)
    {
        auto slotIndex = metaObject()->indexOfSlot("onPropertyChanged()");
        if(slotIndex == -1){
            qDebug()<<"The index of onPropertyChanged is invalid!";
            return;
        }
        auto slotMethod = metaObject()->method(slotIndex);
        if(!slotMethod.isValid()){
            qDebug()<<"cannot find onPropertyChanged!";
            return;
        }
        for(auto i = 0; i != object->metaObject()->propertyCount(); ++i){
            auto prop = object->metaObject()->property(i);
            auto sig = prop.notifySignal();
            if(!sig.isValid()) continue;
            connect(object, sig, this, slotMethod);
        }
    }
    
  3. the implementation of onPropertyChanged. Here in the demo, we simply print the property and it's value. Actually you can do whatever you want:

    void PropertySpy::onPropertyChanged()
    {
        auto senderObj = sender();
        auto signalIndex = senderSignalIndex();
        for(auto i = 0; i != senderObj->metaObject()->propertyCount(); ++i){
            auto prop = senderObj->metaObject()->property(i);
            if(prop.notifySignalIndex() == signalIndex){
                qDebug()<<prop.name()<<prop.read(senderObj);
            }
        }
    }
    
  4. register PropertySpy to the qml engine in main.cpp

    qmlRegisterType<PropertySpy> ("PropertySpy", 1, 0, "PropertySpy");
    
  5. use PropertySpy in qml, usually in Component.onCompleted of some qml objects that you are interested. For example, the following codes show how to introspect the changes of MouseArea's properties:

    PropertySpy{
        id: propSpy
    }
    
    Rectangle{
        id: rect
        anchors.centerIn: parent
        color: "red"
        width: 100
        height: 50
        radius: 8
    
        MouseArea{
            anchors.fill: parent
            id: mouseArea
    
            Component.onCompleted: propSpy.spy(mouseArea)
        }
    }
    
  6. Then you can get notified when any property is changed like below: enter image description here

Upvotes: 1

derM
derM

Reputation: 13691

One possible solution is, to create an object that has the function, and then just assign this function

Repeater {
    onItemAdded: {
        var keys = Object.keys(item)
        for (var k = 0; k < keys.length; k++) {
            if (item[keys[k] + 'Changed'] !== undefined  && keys[k] !== 'objectName') {
                var key = keys[k]
                var f = __funproto.createObject(this, { i: index, n: key })
                item[key + 'Changed'].connect(f.fun)
            }
        }
    }
}

Component {
    id: __funproto
    QtObject {
        property int i
        property string n
        function fun() { /*do something with it!*/}
    }
}

But maybe there is a even better solution waiting to be disclosed?

Upvotes: 0

Related Questions