I'm currently working in a project with Qt QML and I have a type that has a property (holds a JavaScript Object), and its reference changes constantly.
What I want to do is to detect the changes in this property and compare with a cached one.
I took a look into Qt Property Binding, but the callback function is executed before the change occurs and the property gets the value of the return of the callback. I want something to be executed after that (the property change), because I need to have the property dataset updated before emitting the signals (in the code the signals are emitted with kind of reflection thing, in the forEach callback).
What I've done so far:
Item {
id: root
property var dataset
Component.onCompleted: {
dataset = Qt.binding(function(){
return MediaWatcherHelper.formatDataset(internals.metadata);
// I want this code to execute after the binding occurs, but I obviously can't execute things after the return.
MediaWatcherHelper.detectChanges(root.dataset, internals.cache)
.forEach(it => root[`${it}Change`](dataset[it]));
internals.cache = root.dataset; // And update the cache
});
}
signal trackIdChange(string trackId)
signal albumChange(string album)
signal artistChange(var artist)
signal titleChange(string title)
signal urlChange(url url)
}
You always can keep the previous value and update it accordingly.
property int myCurrentValue: 10
property int myLastValue: myCurrentValue
onMyCurrentValueChanged: {
console.log("current: " + myCurrentValue + ", last:" + myLastValue)
myLastValue = myCurrentValue;
}
Related
I found an interesting article on how to impement QObject with dynamic properties (see C++ class DynamicObject). The code from the article works fine, the properties of DynamicObject are get and set successfully from both C++ and QML, but the only thing I cannot figure out is how to fire dynamic signals.
I tried to fire "nameChanged()" signal with the following code:
bool DynamicObject::emitDynamicSignal(char *signal, void **arguments)
{
QByteArray theSignal = QMetaObject::normalizedSignature(signal);
int signalId = metaObject()->indexOfSignal(theSignal);
if (signalId >= 0)
{
QMetaObject::activate(this, metaObject(), signalId, arguments);
return true;
}
return false;
}
myDynamicObject->emitDynamicSignal("nameChanged()", nullptr);
the index of the signal is found and signalId is assigned to 5, but the signal is not fired. But if I do, for example,
myDynamicObject->setProperty("name", "Botanik");
the property is changed and the signal is fired successfully.
What is wrong in my code? What should I pass as 'arguments' parameter of QMetaObject::activate ?
EDIT1:
The full source code is temporarily available here.
A signal is also a method. You can invoke it from the meta object.
So, replace your line QMetaObject::activate(...) by:
metaObject()->method(signalId).invoke(this);
And let Qt handles the call to activate().
There is also an issue in DynamicObject::qt_metacall(): you are handling only QMetaObject::ReadProperty and QMetaObject::WriteProperty calls.
You have to add QMetaObject::InvokeMetaMethod if you want to emit your signal.
Take MenuItem as an example, normally in QML, specifying the handler for the triggered signal is simple:
MenuItem {
onTriggered: {
console.log("Hey");
}
}
Now if I want to do the same thing, but instead to a dynamically created MenuItem, e.g. via Menu.addItem(), then what is the syntax like to connect and specify the signal handler?
I didn't expect this to work, but here is a working solution:
function onTriggered() {
console.log("Hey");
}
var newItem = myMenu.addItem("Item 1");
newItem.triggered.connect(onTriggered);
Nevertheless is there a better way? Above I defined a custom function that happened to be named onTriggered, but it can be named anything, right? So this code piece doesn't make use of the built-in handler, that's why I'm wondering if there's a neater solution?
More importantly, later on I've noticed further problems with this approach: in a for loop, if there is a temporary variable used by the handler, things don't work any more:
for (var i = 0; i < myArray.length; i ++) {
var info = myArray[i];
var newItem = myMenu.addItem("Item " + i);
newItem.triggered.connect(function() {
console.log(info);
});
}
Here you'll see that console prints the last info in myArray for all added menu items when triggered. How can I properly set up independent handlers for each individual menu item?
In addition to the comments, you can easily make it "easier":
Menu {
id: myMenu
function add(text, handler) {
var newItem = addItem(text)
newItem.triggered.connect(handler)
}
}
And there you have it, problem solved, now you can simply myMeny.add("Item 1", onTriggered)
As for the result you get in the loop and functor, that's because of JS's scoping rules. Check the linked answer for details how to work around that.
So this code piece doesn't make use of the built-in handler
Don't think of onSignal as a handler, it is just a hook to attach a handler. Think of it as the declarative connection syntax. Sure, you can also use the Connection element in declarative, but it only makes sense when the situation actually merits it.
I think this confusion stems from some other language / framework which does generate handler methods for you. A onSignal is different from function onSignal() { expression } - the latter is a handler function, the former is handler hook, which just connects the signal to the bound expression.eval(). The Qt documentation too refers to onSignal as a handler, which IMO is technically and conceptually wrong, since the handler is the code which gets executed, the handler is whatever you bind to onSignal.
So you can rest easy, the code you are worried about does not result in any sort of redundancy or inefficiency and doesn't leave anything unused and is in fact the correct way to do things in QML.
All that being said, you can have "built in handlers", but it is a very different thing:
// SomeItem.qml
Item {
signal someSignal
onSomeSignal: console.log("I am a built in handler")
}
// main.qml
SomeItem {
onSomeSignal: console.log("I am another handler")
Component.onCompleted: {
someSignal.connect(function(){console.log("Yet another handler")})
someSignal()
}
}
And the output in the console will say:
qml: I am a built in handler
qml: I am another handler
qml: Yet another handler
As you see, it not really a handler, but a connection hook. There is no shadowing, no "replacing / not using the built in handler", there is just a signal with 3 connections to the evaluation of three expressions.
Using signal.connect() with a named function does come with one advantage, you can later signal.disconnect(namedFunction) if you need to remove a built in or another handler. I am not sure if you can do this if you use onSignal: expr since you don't have a way to reference that anonymous expression. Note that if you use onSignal: namedFunction() this will not work, you will not be able to signal.disconnect(namedFunction) because the signal is not directly connected to that function, but to an anonymous expression invoking it.
Is it possible to raise notify-method of a var/variant/object/ (etc.) variables automatically during updating?
Suppose I have:
property var objects: {'obj1': 'unnamed', 'obj2': 'unnamed'}
Next I have binding in, for example, text:
Text {
text: objects.obj1
onTextChanged: objects.obj1 = text
}
In onTextChanged I want to raise a notify signal of objects variable to update it everywhere.
Hm, if I am not mistaken, QML generates a onObjectsChanged signal handler for objects but it is not emitted when you change objects internally, and due to QML brilliant design, you cannot emit objectsChanged() manually, it is expected to automatically emit, except that it doesn't. It only emits when the property is reassigned to another object.
You cannot create a signal for the JS object, since that requires a QObject derived class to get signals and therefore notifications and bindings.
You can force to emit objectsChanged() by reassigning the objects property a new object with the new value for obj1 and the old value of obj2, this will force the second text element to update and show the new value. It is not exactly elegant, but if you really need to use the JS object, it is a valid solution. Otherwise you will have to use a QtObject element and QML properties for obj1/2
property var objects: {'obj1': 'unnamed', 'obj2': 'unnamed'}
Column {
spacing: 30
TextEdit {
text: objects.obj1
onTextChanged: {
objects = {'obj1': text, 'obj2': objects.obj2}
}
}
Text {
text: objects.obj1
}
}
Another possible solution would be to simply not rely on notifications from objects and use a proxy property as a controller for it.
property var objects: {'obj1': 'unnamed', 'obj2': 'unnamed'}
property string obj1: objects.obj1
onObj1Changed: objects.obj1 = obj1
This way you don't use objects at all, except for storing data into it, use the obj1 property instead, and every time it is changed it will write the changed into objects without reassigning the entire objects as in the first solution.
But unless you really need the JS objects, I'd recommend scrapping it and using a more QML friendly data representation..
I am continuously getting data for my application as it runs, but I am having a bit of trouble displaying the data once I have read it in and stored it in a map.
When I try to display the data in the QML, it simply displays zero, despite the fact that I can see it updating in the application output.
I access the value in QML using property bindings (I was under the impression that these led headingSensor to be updated whenever carData.headingSensor changed?):
property int headingSensor: carData.headingSensor
Text { text: "Heading: " + headingSensor }
In my data class I have:
Q_PROPERTY(int headingSensor READ getHeadingSensor NOTIFY headingSensorChanged)
int headingSensor;
In the c++ implementation I originally had:
int data::getHeadingSensor(){
return data.value(heading)[headingSensorReading];
}
Where it returns the value in the map which is being updated with the incoming information.
This I realized, probably doesn’t work, because the property is dependent upon the headingSensor variable, which is itself not being updated despite the correct value being returned. So, I thought if I changed it to update the headingSensor value and return that it might work.
So in my data aquisition logic I wrote a method to update the variables as well.
data.insert(key, value);
updateVariables();
}
}
}
void data::updateVariables(){
headingSensor = data.value(heading)[headingSensorReading];
}
int data::getHeadingSensor(){
return headingSensor;
}
While this led to the headingSensor variable being updated in addition to the value in the map, the correct value is still not displayed in the QML display. It simply displays 0 (its default value when it is initially displayed since it has not gotten a value from incoming data yet).
So, I am wondering, how can I get the value of sensorHeading displayed in the QML to update as the value of it and/or the value in the map changes in C++? Do I need to do something like:
Connections {
target: carData
onSensorHeadingChanged: updateValues
}
EDIT:
Trying something like this, the onSensorHeadingChanged never fires. I am not sure why, since the value of sensorHeading clearly changes as I watch it in the application output
Connections{
target: carData
onHeadingSensorChanged: console.log("It's noting the change!")
}
It is the responsibility of the C++ element writer to emit headingSensorChanged() in order to cause the binding to be updated.
This tutorial is a good place to start when implementing a C++ element.
In your case you need to do something like this:
void data::updateVariables(){
int sensorReading = data.value(heading)[headingSensorReading];
if (headingSensor != sensorReading) {
headingSensor = sensorReading;
emit headingSensorChanged();
}
}
Note that we don't emit the change notifier unless there really is a change. This prevents needless JS evaluations, and also removes the possibility of binding loops.
I have a QTableView, populated with a QStandardItemModel.
I update the model frequently over network and the model is also updated by user directly via the QTableView.
Now I like to call a method when the user is changing some data, so i did:
connect(model, SIGNAL(itemChanged(QStandardItem*)), this, SLOT(dataChanged(QStandardItem*)));
The Problem now is, that my dataChanged method is called, also when the item is updated over the network.
model->setData(index, new_val);
Is there another signal which is only emitted if, the user is changing something inside the QTableview ???
No, AFAIK there is no such signal but you there is a way to hack it.
When editing an item from the QTableView the activated signal will have been emited. The idea is to catch this signal and connect it to a slot that will store the last manually changed item.
connect(view, SIGNAL(activated(QModelIndex), this, SLOT(manuallyActivated(QModelIndex)));
void manuallyActivated(QModelIndex index)
{
// This variable should be in your header file...
lastManuallyModifiedIndex = index;
}
Now simply modify your dataChanged slot to check if the item that changed corresponds to the last modified item.
void dataChanged(QStandardItem* item)
{
// If it is invalid simply ignore it...
if (lastManuallyModifiedIndex.isValid() == false)
return;
// only if it is modified manually we process it
if (item->index() == lastManuallyModifiedIndex)
{
// make last modified index invalid
lastManuallyModifiedIndex = QModelIndex();
doSomething();
}
}
You could block the table signals when an update comes in from your network.
QObject::blockSignals(bool block)
or you could listen for click and edit event in succession.