QML - tracking global position of a component

14,349

Solution 1

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.

Solution 2

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;
};

Solution 3

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

Solution 4

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.

Share:
14,349

Related videos on Youtube

Konrad Madej
Author by

Konrad Madej

Updated on July 01, 2020

Comments

  • Konrad Madej
    Konrad Madej about 4 years

    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.

  • Konrad Madej
    Konrad Madej almost 11 years
    Thanks a lot. Wouldn't have thought have thought of using that global flag to force re-evaluation. This seems dirty but it does the trick.
  • Programmer
    Programmer almost 11 years
    Similar to the colored rect solution mentioned above, you can bind the x and y coordinates of TrackedObject to javascript functions which traverse the visual tree and return the coordinates of the ancestors.
  • Russ
    Russ over 7 years
    Very nice. One note: in addition to connecting to onXChanged and onYChanged it should also probably connect to onParentChanged so that any re-parenting trickery is also tracked.
  • Daniel Handojo
    Daniel Handojo over 3 years
    Note that you don't have to include globalBit in the args to trigger reevaluation. The JS comma operator works well for this situation: property var pos: globalBit, updatePos(rectGreen, rectBlue)