Dynamically change from/to values used in animations - qt

Say I've got the following code:
Rectangle {
id: rect
property int minVal : 0
property int maxVal : 10
property int duration: 1000
SequentialAnimation {
loops: Animation.infinite
NumberAnimation {
target: rect; property: "x"
from: minVal; to: maxVal
duration: duration
}
NumberAnimation {
target: rect; property: "x"
from: maxVal; to: minVal
duration: duration
}
}
}
Then later, I hook up this object's minVal, maxVal, and duration properties to sliders, so that I can play around with this effect.
Is there any way to force the animation to use the new values for min/maxVal?
Currently, it seems that the minVal/maxVal values present when the animations were initialised get "baked" into the animations, with no way to force these to get recalculated/rebaked later
Previously I've been able to get away with throwing away the entire QML canvas everytime one of these changes was needed. But in this case, it's not really practical, so I was wondering if there were other alternatives?

I could not find a way to 'bake' in those values. For me, at least, this works as I expected:
Button {
id: but
property int count: 0
property int minX: 0
property int maxX: 1000
text: 'start' + x
onCountChanged: {
minX += 10
maxX -= 10
}
onClicked: ani.start()
}
SequentialAnimation {
id: ani
NumberAnimation {
target: but
property: 'x'
from: but.minX
to: but.maxX
}
ScriptAction {
script: { but.count += 1 }
}
}
I tried as much as possible to avoid bindings that could be baked in somehow.
Have I missed your problem?

Related

Unexpected binding in QML with Q_GADGET

I have a gadget:
class ZScoreParams
{
Q_GADGET
Q_PROPERTY(quint64 lag MEMBER lag);
Q_PROPERTY(qreal threshold MEMBER threshold);
Q_PROPERTY(qreal influence MEMBER influence);
public:
std::size_t lag;
double threshold;
double influence;
};
Q_DECLARE_METATYPE(ZScoreParams)
and a property of an object of type QVariant:
Q_PROPERTY(QVariant zscoreParams READ zscoreParams WRITE setZscoreParams NOTIFY zscoreParamsChanged)
its getter returns either QVariant{} that is null in QML or QVariant::fromValue(gadget_val), the setter has the same logic.
Can't figure out how do I create a dialog with three SpinBox-es for editing my ZScoreParams. What is the right way to create it?
I attempted this ZParamsEdit.qml:
GridLayout
{
property var params
property var refreshChart: function () {}
signal zparamsChanged()
Component.onCompleted:
{
refreshChart = function ()
{
params.lag = lagEdit.value
params.threshold = thresholdEdit.value
params.influence = influenceEdit.realValue
logTable.debug("qml", JSON.stringify(params))
zparamsChanged()
}
}
Layout.margins: 5
columns: 2
flow: GridLayout.LeftToRight
Label {
text: qsTr("Lag:")
Layout.alignment: Qt.AlignRight
}
SpinBox {
id: lagEdit
value: params.lag
from: 100
to: 1000000
stepSize: 100
editable: true
Layout.alignment: Qt.AlignLeft
onValueChanged: refreshChart()
}
Label {
text: qsTr("Threshold:")
Layout.alignment: Qt.AlignRight
}
SpinBox {
id: thresholdEdit
value: params.threshold
from: 0
to: 100
stepSize: 1
editable: true
Layout.alignment: Qt.AlignLeft
onValueChanged: refreshChart()
}
Label {
text: qsTr("Influence:")
Layout.alignment: Qt.AlignRight
}
RealEdit
{
Layout.alignment: Qt.AlignLeft
id: influenceEdit
decimals: 2
realValue: params.influence
realFrom: 0
realTo: 1.0
realStepSize: 0.1
onValueChanged: refreshChart()
}
}
and then in main.qml:
Dialog
{
id: zdlg
ZParamsEdit
{
id: zedit
}
}
IconButton {
icon.source: "images/analyze.svg"
onClicked: {
zedit.params = market.zscoreParams
zdlg.open()
}
}
and got an effect that I did not expect. Each time an edit box changes, market.zscoreParams property setter is called.
How to make ZParamsEdit store a copy of my gadget and set my property value only once (with the gadget as a whole) when 'OK' button is pressed in the dialog?
Also tried
JSON.parse(JSON.stringify(market.zscoreParams))
but got also something unexpected.
EDIT1:
I probably found an ugly solution with an explicitly defined function that creates a copy of the gadget:
Q_INVOKABLE QVariant cloneZScoreParams(QVariant v)
{
std::optional<ZScoreParams> val;
util::FromVariant(v, val);
return util::ToVariant(val);
}
and then
globalSettings.cloneZScoreParams(market.zscoreParams)
in QML.
Is there something better in QT?
If I understand your problem correctly, you just need some sort of local property/properties to store the copy of your data, and then you need some function that writes those copies back to data source.
Here's how I would change your code:
GridLayout
{
property var params
// Copy the data into local properties
property lag: params.lag
property threshold: params.threshold
property influence: params.influence
// Only update the local properties when spinboxes change
function refreshChart()
{
lag = lagEdit.value
threshold = thresholdEdit.value
influence = influenceEdit.realValue
zparamsChanged()
}
// Call this when OK button is clicked
function save()
{
params.lag = lag
params.threshold = threshold
params.influence = influence
}
signal zparamsChanged()
...
SpinBox {
id: lagEdit
value: lag
...
onValueChanged: refreshChart()
}
SpinBox {
id: thresholdEdit
value: threshold
...
onValueChanged: refreshChart()
}
RealEdit
{
id: influenceEdit
realValue: influence
...
onValueChanged: refreshChart()
}
}
UPDATE:
To avoid calling setter 3 times, you can provide an invokable function in your C++ class:
Q_INVOKABLE void save(quint64 l, qreal t, qreal i)
{
lag = l;
threshold = t;
influence = i;
}
Now the save() function would look like this:
function save()
{
params.save(lag, threshold, influence)
}

Qt Progress Bar Animation When Duration is Longer Than Update

I am working on customizing a Qt5 Quick 2 QML progress bar. When progress updates are very few and large, the progress bar jumps in a blocky way. To fix this, I thought I would add a simple Behavior to the value animation so it smoothly moves to the next value. This works great except when the animation duration is larger that the period between updates. Then the behavior is that the updates move very slowly, and when they are stopped they seem to speed up and try and finish.
The following code increments the progress bar so that it repeats once per second. When the Behavior duration is less than the timer interval, it works, but when the duration is longer, it fails.
I would like a value set to stop the prior executing behavior animation and move on to the next, not simultaneously overlap somehow.
Timer
{
interval: 200; running:true; repeat:true
onTriggered:
{
if(mybar.doUpdate)
mybar.value = (mybar.value + 0.2 ) % 1
}
}
ProgressBar
{
id: mybar
value: .5
property bool doUpdate: true
Behavior on value
{
NumberAnimation
{
duration: 1000
easing.type: Easing.InOutQuad
}
}
MouseArea{
anchors.fill:parent
onClicked:
{
parent.doUpdate = !parent.doUpdate
console.log((!parent.doUpdate ? "Stop" : "Go") + " Now!")
}
}
}
I'm not positive I understand your expected behavior, but I think there's two issues. First, you need to use a go-between value that doesn't animate so you can refer to it later. Then you need a way to turn off the animation so you can jump to a value immediately. Something like this should work:
// This holds the current progress value without animating
property real tempVal: 0.5
// This controls whether or not to animate
property bool doAnimate: true
Timer
{
interval: 200; running:true; repeat:true
onTriggered:
{
if(mybar.doUpdate)
{
// Turn off the animation
doAnimate = false;
// Reset the progress bar to the current expected value
mybar.value = Qt.binding(function() { return tempVal });
// Turn on the animation again
doAnimate = true;
// Animate up to this new value
tempVal = (tempVal + 0.2 ) % 1
}
}
}
ProgressBar
{
id: mybar
// Bind the progress bar to our secondary value
value: tempVal
property bool doUpdate: true
Behavior on value
{
// Control animation with this flag
enabled: doAnimate
NumberAnimation
{
duration: 1000
easing.type: Easing.InOutQuad
}
}
MouseArea{
anchors.fill:parent
onClicked:
{
parent.doUpdate = !parent.doUpdate
console.log((!parent.doUpdate ? "Stop" : "Go") + " Now!")
}
}
}

In MouseArea.onEntered, detect if the cause is only that the *MouseArea* moved and came to be under the cursor

In MouseArea.onEntered, can I detect if the cause for the event firing is only that the MouseArea moved and came to be under the cursor, rather than the other way round?
I thought of doing something like this: (pseudocode)
MouseArea {
// ...
property bool positionDirty = false
Connections {
target: window
onAfterRendering: {
positionDirty = false;
}
}
onMouseAreaPosChanged: {
positionDirty = true;
}
onEntered: {
if(positionDirty) {
positionDirty = false;
return;
}
// handle event here
}
}
But this makes the assumption that entered will be fired after mouseAreaPosChanged, and before window.afterRendering. And I'm not confident in that assumption.
Also, it doesn't work when an ancestor of the MouseArea moves, or when the MouseArea is positioned/sized via anchoring.
Assumptions:
This only affects the edge case, that both, the cursor and the MouseArea are moving.
My Assumption here is, that the movement of the cursor is handled before the movement of the MouseArea. I don't have any definite proof for this. Only my test with the solution below, suggests that.
Solution
The first challenge is to detect movement of the MouseArea. It might be that it moves, without its own x and y-values changing, e.g. if its parent is moving.
To solve this, I'll introduce two properties globalX and globalX. Then I use the trick from this answer on how to track a gobal position of an object.
Now I'll have two signals to handle: globalXChanged and globalYChanged.
According to my assumption, they are fired, after the mouseXChanged and mouseYChanged. I will use a flag isEntered to make sure, I only handle one of them, by setting it to true, if the first of them is fired.
I will use the cursor position on a globalMouseArea to determine, whether the cursor is within bounds of the MouseArea. This requires, the cursor is not in some other MouseArea at that time, or at least I know of it
-> With this I already succeeded in detecting the entrance.
The second challenge is to detect the exit. Here we have 4 cases to distinguish:
Cursor enters and leaves the MouseArea because of it's movement.
Cursor enters and leaves the MouseArea because of the movement of the MouseArea
Cursor enters because the MouseArea moves, and leaves, because the cursor moves
Cursor enters because it moves, and leaves as the MouseArea moves away.
The first would be easy to handle. After it enters we handle entered and when it leaves we handle exited. But after the fix, mentioned by Mitch we can't rely on this anymore.
So we will not set hoverEnabled: true and map the position of the cursor to the targetMouseArea whenever either the cursor moves, or the targetMouseArea moves, and act accordingly.
import QtQuick 2.7
import QtQuick.Controls 2.0
ApplicationWindow {
id: root
visible: true
width: 400; height: 450
MouseArea {
id: globalMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: ani.restart()
}
Rectangle {
x: 300
y: 300
width: 50
height: 50
color: 'green'
}
Rectangle {
id: rect
width: 50
height: 50
color: 'red'
Text {
text: targetMouseArea.isEntered.toString()
}
MouseArea {
id: targetMouseArea
anchors.fill: parent
signal enteredBySelfMovement
signal enteredByMouseMovement
onEnteredByMouseMovement: console.log('Cause: Mouse')
onEnteredBySelfMovement: console.log('Cause: Self')
property point globalPos: {
var c = Qt.point(0, 0)
var itm = this
for (; itm.parent !== null; itm = itm.parent) {
c.x += itm.x
c.y += itm.y
}
return c
}
property bool isEntered: false
function checkCollision(sig) {
if ((globalPos.y < globalMouseArea.mouseY)
&& (globalPos.y + height > globalMouseArea.mouseY)
&& (globalPos.x < globalMouseArea.mouseX)
&& (globalPos.x + width > globalMouseArea.mouseX)) {
if (!isEntered) {
isEntered = true
sig()
}
}
else if (isEntered && !containsMouse) {
console.log(isEntered = false)
}
}
onGlobalPosChanged: {
checkCollision(enteredBySelfMovement)
}
Connections {
target: globalMouseArea
onPositionChanged: {
targetMouseArea.checkCollision(targetMouseArea.enteredByMouseMovement)
}
}
}
}
NumberAnimation {
id: ani
target: rect
properties: 'x,y'
from: 0
to: 300
running: true
duration: 10000
}
}
Problems left: When we clicked within the targetMouseArea, as long as a button is pressed, we won't detect the leaving.
You can check whether mouseX, mouseY were changed since last event.
property int previousMouseX = mouseX; // or use other values to init
property int previousMouseY = mouseY; // e.g., 0, x, parent.x,
// or set it from extern
onEntered() {
if (mouseX != previousMouseX || mouseY != previousMouseY) {
// TODO do something
previousMouseX = mouseX;
previousMouseY = mouseY;
}
}
In case mouseX, mouseY are relative to the mouse area 0,0 you can use mapFromItem(null, 0, 0) to get the absolute values.

How to omit the "Binding loop detected for property" warning?

I decided to rewrite this question. I tried to make it clear and short but it didn't work out.
What I am trying to achieve is an object I can put on anything (Rectangle, Image and so on) and make it respond to touch gestures. Unfortunately I ran into a problem I cannot solve. I couldn't find help so I try here.
Here is simple main.qml:
import QtQuick 2.5
import QtQuick.Controls 1.3
import QtQuick.Window 2.2
import QtQuick.Dialogs 1.2
ApplicationWindow {
title: qsTr("Touch surface test")
width: 640
height: 480
visible: true
Rectangle {
width: 200
height: 60
color: "blue"
MyTouchSurface {
id: myTouchSurface
anchors.fill: parent
}
}
}
and here is MyTouchSurface.qml
import QtQuick 2.5
MultiPointTouchArea {
id: myTouchSurface
// this is object I want to respond to touch gestures
property Item target: parent
// this is object I want to keep target in. I won't let target get out of it
property Item container: parent.parent
property double targetX: target.x
property double targetY: target.y
property double centerX: target.width / 2
property double centerY: target.height / 2
property double lastTouchX
property double lastTouchY
property double lastXDrag
property double lastYDrag
// here I calculate received touches and move target
onPressed: {
lastTouchX = touchPoints[0].sceneX
lastTouchY = touchPoints[0].sceneY
if (slidingAnimation.running)
slidingAnimation.stop()
}
onTouchUpdated: {
if (touchPoints.length) {
target.x += touchPoints[0].sceneX - lastTouchX
target.y += touchPoints[0].sceneY - lastTouchY
lastXDrag = touchPoints[0].sceneX - lastTouchX
lastYDrag = touchPoints[0].sceneY - lastTouchY
lastTouchX = touchPoints[0].sceneX
lastTouchY = touchPoints[0].sceneY
}
else
startSliding()
}
// when lifting fingers off screen I want to slide target across it's container
function startSliding() {
slidingAnimation.toX = target.x + lastXDrag * 10
slidingAnimation.toY = target.y + lastYDrag * 10
slidingAnimation.restart()
}
// This is animation responsible for sliding the target
ParallelAnimation {
id: slidingAnimation
property double toX
property double toY
property int animationDuration: 250
NumberAnimation {
target: myTouchSurface.target
property: "x"
to: slidingAnimation.toX
duration: slidingAnimation.animationDuration
easing.type: Easing.OutQuad
}
NumberAnimation {
target: myTouchSurface.target
property: "y"
to: slidingAnimation.toY
duration: slidingAnimation.animationDuration
easing.type: Easing.OutQuad
}
}
// this is how I keep target object within container
onTargetXChanged: {
if (container != null) {
if (target.x + centerX < 0)
target.x = -centerX
else if (target.x + centerX > container.width)
target.x = container.width - centerX
}
}
onTargetYChanged: {
if (container != null) {
if (target.y + centerY < 0)
target.y = -centerY
else if (target.y + centerY > container.height)
target.y = container.height - centerY
}
}
}
I want to have all calculating and functions within MyTouchSurface so it is easy to apply on other object and to use it in another project.
The problem I have is I don't know how to prevent target object from moving out of container. Well, the code here is working very nice but it also generates very ugly errors about binding loop.
What is going on is:
I check target's x and y when they changes
If the x or y is out of specified area I move them back
I receive signal "x changed" or "y changed" again
I receive error about loop because my function caused itself to execute again
So I am asking: Is there any other simple way to prevent object from flying off screen and not receive binding loop errors at the same time?
I know I can use Timer to check target's x and y from time to time, and bring them back.
I know I can change the easing and duration of the animation so it stops on the container bounds itself but looks like it was stopped.
Seriously. Is there no simple way?
Things that won't work:
stopping animation after object was detected outside container (if it would move really fast it will stop out of the screen )
stopping animation and then changing target's x or y
Thanks everyone helping me so far. It is nice to know I am not alone here :)
The solution is simply use Connections:
import QtQuick 2.5
MultiPointTouchArea {
id: myTouchSurface
// this is object I want to respond to touch gestures
property Item target: parent
// this is object I want to keep target in. I won't let target get out of it
property Item container: parent.parent
property double targetX: target.x
property double targetY: target.y
property double centerX: target.width / 2
property double centerY: target.height / 2
property double lastTouchX
property double lastTouchY
property double lastXDrag
property double lastYDrag
// here I calculate received touches and move target
onPressed: {
lastTouchX = touchPoints[0].sceneX
lastTouchY = touchPoints[0].sceneY
if (slidingAnimation.running)
slidingAnimation.stop()
}
onTouchUpdated: {
if (touchPoints.length) {
target.x += touchPoints[0].sceneX - lastTouchX
target.y += touchPoints[0].sceneY - lastTouchY
lastXDrag = touchPoints[0].sceneX - lastTouchX
lastYDrag = touchPoints[0].sceneY - lastTouchY
lastTouchX = touchPoints[0].sceneX
lastTouchY = touchPoints[0].sceneY
}
else
startSliding()
}
// when lifting fingers off screen I want to slide target across it's container
function startSliding() {
slidingAnimation.toX = target.x + lastXDrag * 10
slidingAnimation.toY = target.y + lastYDrag * 10
slidingAnimation.restart()
}
// This is animation responsible for sliding the target
ParallelAnimation {
id: slidingAnimation
property double toX
property double toY
property int animationDuration: 250
NumberAnimation {
target: myTouchSurface.target
property: "x"
to: slidingAnimation.toX
duration: slidingAnimation.animationDuration
easing.type: Easing.OutQuad
}
NumberAnimation {
target: myTouchSurface.target
property: "y"
to: slidingAnimation.toY
duration: slidingAnimation.animationDuration
easing.type: Easing.OutQuad
}
}
Connections{
target:myTouchSurface.target
onXChanged:{
if (container != null) {
if (target.x + centerX < 0)
target.x = -centerX
else if (target.x + centerX > container.width)
target.x = container.width - centerX
}
}
onYChanged:{
if (container != null) {
if (target.y + centerY < 0)
target.y = -centerY
else if (target.y + centerY > container.height)
target.y = container.height - centerY
}
}
}
// this is how I keep target object within container
// onTargetXChanged: {
// }
// onTargetYChanged: {
// }
}
I think your problem is that you update either targetX or target.x (resp. for y) inside of the property update handler (onTargetXChanged()).
This might be picked up as a biding loop by the QML engine as if you change target.x from onTargetXChanged() the signal onTargetXChanged() will trigger again as it's bound to it in property double targetX: target.x.
You can try something like this:
TouchArea {
property Item target // target is the Image object being moved
property double targetX: target.x
property double targetY: target.y
onTargetXChanged: {
/* if object is out of specified area move x respectively */
performPropertyUpdate(args)
}
onTargetYChanged: {
/* if object is out of specified area move y respectively */
performPropertyUpdate(args)
}
function performPropertyUpdate(args) {
/* actual setting of target.x or target.y */
}
}
To solve the issue myX should be an alias of x,
property alias myX: rectangle2.x
You're getting this problem cause myX is changing the value of x and x is changing the value of myX: a loop.

QML - tracking global position of a component

I would like to track a global position of an object (or relative to one of it's ancestors) and bind it to some other item's position.
I was thinking about using mapFromItem as follows:
SomeObject {
x: ancestor.mapFromItem(trackedObject, trackedObject.x, 0).x
y: ancestor.mapFromItem(trackedObject, 0, trackedObject.y).y
}
The problem with this approach is that the mapFromItem is evaluated once and doesn't update as one of it's arguments gets updated. Moreover the mapping sometimes returns the new position altered by an offset I'm unable to track in the code (but that's not the matter at hand).
My second idea was to calculate the global position by implementing a function that would recursively sum the offsets, stopping at the provided ancestor (something like calculateOffsetFrom(ancestor)). Still this is just a function and as far as I'm concerned it won't get re-evaluated as one of the ancestors position changes (unless, in that function, I'll bind calling it to the onXChanged signal for each one of the ancestors along the way, which seems like a dirty solution).
So in the end I've added properties to the object I intend to track and then I bind to them:
TrackedObject {
property real offsetX: x + parent.x + parent.parent.x + parent.parent.parent.x ...
property real offsetY: y + parent.y + parent.parent.y + parent.parent.parent.y ...
}
SomeObject {
x: trackedObject.globalX
y: trackedObject.globalY
}
But well... yeah... this one doesn't scale at all and is as ugly as it gets.
Does anyone have any idea how this problem might be solved in a cleaner way?
Edit:
As far as I'm concerned I can't use anchors in this case. The SomeObject component is a custom component drawing a bezier curve from one point to another (it will connect two TrackedObjects). For that I need the difference between the coordinates. If I'm correct anchors don't provide any way of calculating the distance between them.
This is a hard point, but here is the hack i used in one of my projects : to make blue rect which is in another parent than green rect move, to stay aligned with it, when green rect moves but also when yellow rect (green rect parent) moves :
import QtQuick 2.0;
Rectangle {
id: window;
width: 800;
height: 480;
property bool globalBit : true;
function updatePos (item_orig, item_dest, bit) {
var pos_abs = window.mapFromItem (item_orig.parent, item_orig.x, item_orig.y);
return window.mapToItem (item_dest.parent, pos_abs.x, pos_abs.y);
}
Rectangle {
id: rectYellow;
width: 400;
height: 300;
x: 300;
y: 200;
color: "yellow";
onXChanged: { globalBit = !globalBit; }
onYChanged: { globalBit = !globalBit; }
MouseArea {
drag {
target: rectYellow;
minimumX: 0;
minimumY: 0;
maximumX: (rectYellow.parent.width - rectYellow.width);
maximumY: (rectYellow.parent.height - rectYellow.height);
}
anchors.fill: parent;
}
Rectangle {
id: rectGreen;
x: 100;
y: 100;
width: 50;
height: 50;
color: "green";
MouseArea {
drag {
target: rectGreen;
minimumX: 0;
minimumY: 0;
maximumX: (rectGreen.parent.width - rectGreen.width);
maximumY: (rectGreen.parent.height - rectGreen.height);
}
anchors.fill: parent;
}
}
}
Rectangle {
id: rectBlue;
x: pos.x + 50;
y: pos.y + 50;
width: 50;
height: 50;
color: "blue";
property var pos : updatePos (rectGreen, rectBlue, globalBit);
}
}
The trick is to bring all coordinates back to the first common ancestor, using both mapfromItem and mapToItem, and to force the function to be re-evaluated, just put a global boolean flag that you pass to the computing function, and that you invert each time a movable element on your map moves... You don't have to put it every where, just on parents of items that can move and are inside the ancestor item.
So it works, your positions will always be right, and it's quite scalable and doesn't add much code.
The solution below will trigger the qmlElementToTrack.onPropertyNameXChanged() and qmlElementToTrack.onPropertyNameYChanged() events each time one of its parents 'x' or 'y' values change.
It does this by attaching to each parent's onXChanged() and onYChanged() signals. When one of those values changes, it recalculates the propertyNameX or propertyNameY values by traversing all of qmlElementToTrack's parents.
To make it 'relative' positioned (instead of 'absolute'), add current !== qmlElementToStopAt to each while() condition.
ElementToTrack {
id: qmlElementToTrack
property real propertyNameX: 0
property real propertyNameY: 0
}
setPositionChangedToParents(qmlElementToTrack);
/**
Connect to each parent's 'onXChanged' and 'onYChanged' signals.
*/
setPositionChangedToParents = function(current) {
while (current && current.parent) {
current.onXChanged.connect(calculatePropertyNameX);
current.onYChanged.connect(calculatePropertyNameY);
current = current.parent;
}
};
/**
Disconnects the signals set to all parents.
*/
removePositionChangedFromParents = function(current) {
while (current && current.parent) {
current.onXChanged.disconnect(calculatePropertyNameX);
current.onYChanged.disconnect(calculatePropertyNameY);
current = current.parent;
}
};
/**
When any parent's 'x' changes, recalculate the 'x' value for the 'property name'.
*/
calculatePropertyNameX = function() {
var calculatedX, current;
calculatedX = 0;
current = qmlElementToTrack;
while (current && current.parent) {
calculatedX += current.x;
current = current.parent;
}
propertyNameX = calculatedX;
};
/**
When any parent's 'y' changes, recalculate the 'y' value for the 'property name'.
*/
calculatePropertyNameY = function() {
var calculatedY, current;
calculatedY = 0;
current = qmlElementToTrack;
while (current && current.parent) {
calculatedY += current.y;
current = current.parent;
}
propertyNameY = calculatedY;
};
I don't know whether this will help, but for the above yellow rect,blue rect and green rect problem mentioned by TheBootroo, I used the below code to solve the problem
import QtQuick 2.0;
Rectangle {
id: window;
width: 800;
height: 480;
Rectangle {
id: rectYellow;
width: 400;
height: 300;
x: 300;
y: 200;
color: "yellow";
MouseArea {
drag {
target: rectYellow;
minimumX: 0;
minimumY: 0;
maximumX: (rectYellow.parent.width - rectYellow.width);
maximumY: (rectYellow.parent.height - rectYellow.height);
}
anchors.fill: parent;
}
Rectangle {
id: rectGreen;
x: 100;
y: 100;
width: 50;
height: 50;
color: "green";
MouseArea {
drag {
target: rectGreen;
minimumX: 0;
minimumY: 0;
maximumX: (rectGreen.parent.width - rectGreen.width);
maximumY: (rectGreen.parent.height - rectGreen.height);
}
anchors.fill: parent;
}
}
}
Rectangle {
id: rectBlue;
//Need to acheive the below behvior(commented)
//x: window.x+rectYellow.x+rectGreen.x+50
//y: window.y + rectYellow.y +rectGreen.y+50
width: 50;
height: 50;
color: "blue";
}
Component.onCompleted: {
rectBlue.x =Qt.binding(
function()
{
//Returns window.x+rectYellow.x+rectGreen.x+rectGreen.width
var docRoot = null;
var x=rectGreen.x;
if(!docRoot)
{
docRoot = rectGreen.parent;
x+=docRoot.x;
while(docRoot.parent)
{
docRoot = docRoot.parent;
x+=docRoot.x
}
}
return x+rectGreen.width;
}
)
rectBlue.y = Qt.binding(
function()
{
//Returns window.y+rectYellow.y+rectGreen.y+rectGreen.height
var docRoot = null;
var y=rectGreen.y
if(!docRoot)
{
docRoot = rectGreen.parent;
y+=docRoot.y;
while(docRoot.parent)
{
docRoot = docRoot.parent;
y+=docRoot.y
}
}
return y+rectGreen.height;
}
)
}
}
The idea is to calculate the position of blue rectangle relative to the green rectangle, by
calculating the position of green rectangle and its visual ancestors.
The inspiration behind this solution is -> http://developer.nokia.com/Community/Wiki/How_to_create_a_Context_Menu_with_QML
Tracking certain Item's global positions seems like an important problem if developing some complex graphics interaction. I came up with a relatively simple & graceful solution. Here is my core codes:
Item{
id: globalRoot
signal globalPositionChanged(Item item, real newX, real newY);
function tracking(item){
var obj = item;
var objN;
function onGlobalXYChanged(){
var pt = mapFromItem(item, item.x, item.y);
globalRoot.globalPositionChanged(item, pt.x, pt.y);
}
do{
objN = obj.objectName;
obj.xChanged.connect(onGlobalXYChanged);
obj.yChanged.connect(onGlobalXYChanged);
obj = obj.parent;
}while(objN !== "furthestAncestorObjectName");
}
}
The core idea is: what essentially makes an Item's global position change? It maybe itself, its parent or its parent's parent etc. So make a traverse back to its furthest parent and connect each of its ancestor's x/y change signal to a function, within which we get the item's global position and broadcast outside.
I have tried to improve on #Shubhanga's answer a bit by moving the code into its own ItemPositionTracker.qml file:
import QtQuick 2.3
Item {
id: root
property Item trackedItem
property Item movedItem
Component.onCompleted: {
movedItem.x =
Qt.binding(
function()
{
if (trackedItem === null) return 0;
var docRoot = trackedItem;
var x = trackedItem.x;
while(docRoot.parent)
{
docRoot = docRoot.parent;
x += docRoot.x
}
return x;
}
)
movedItem.y =
Qt.binding(
function()
{
if (trackedItem === null) return 0;
var docRoot = trackedItem;
var y = trackedItem.y
while(docRoot.parent)
{
docRoot = docRoot.parent;
y += docRoot.y
}
return y;
}
)
}
}
The code can now be added to any QML object like this:
ItemPositionTracker {
trackedItem: rectGreen
movedItem: rectBlue
}
Which makes rectBlue follow rectGreen.

Resources