Reputation: 13691
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
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
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
define PropertySpy
class PropertySpy : public QObject
{
Q_OBJECT
public:
explicit PropertySpy(QObject *parent = 0);
Q_INVOKABLE void spy(QObject * object);
public slots:
void onPropertyChanged();
};
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);
}
}
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);
}
}
}
register PropertySpy to the qml engine in main.cpp
qmlRegisterType<PropertySpy> ("PropertySpy", 1, 0, "PropertySpy");
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)
}
}
Then you can get notified when any property is changed like below:
Upvotes: 1
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