QML - 追踪组件的全局位置

20

我想追踪一个对象的全局位置(或相对于其祖先之一),并将其绑定到另一个物品的位置。

我考虑使用mapFromItem,如下所示:

SomeObject {
  x: ancestor.mapFromItem(trackedObject, trackedObject.x, 0).x
  y: ancestor.mapFromItem(trackedObject, 0, trackedObject.y).y
}
这种方法的问题在于,mapFromItem 只会被计算一次,而且不会随着它的参数更新而更新。此外,映射有时会返回新位置被偏移的值,我无法在代码中跟踪这个偏移(但这不是关键问题)。
我的第二个想法是通过实现一个函数来计算全局位置,该函数会递归地求和偏移量,并在提供的祖先处停止(类似于 calculateOffsetFrom(ancestor))。不过这只是一个函数,就我所知它不会在其中一个祖先位置发生更改时重新计算(除非在该函数中将调用绑定到沿途每个祖先的 onXChanged 信号上,但这似乎是一种不太优雅的解决方案)。
最终,我向我打算跟踪的对象添加了属性,然后将其绑定到它们上:
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
}

但是...是的...这个方法根本无法扩展,并且非常丑陋。

有人有任何想法,如何以更清晰的方式解决这个问题吗?

编辑: 就我所知,在这种情况下我无法使用锚点。 SomeObject 组件是绘制从一个点到另一个点的贝塞尔曲线的自定义组件(它将连接两个 TrackedObjects)。 为此,我需要坐标之间的差异。 如果我没错,锚点没有提供计算它们之间距离的任何方法。

5个回答

12

这是一个难点,但是我在一个项目中使用了以下方法:为了让位于另一个父级中的蓝色矩形随着绿色矩形的移动而保持对齐,同时也要随着黄色矩形(绿色矩形的父级)的移动而保持对齐:

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

关键在于使用mapfromItem和mapToItem将所有坐标带回到第一个共同祖先,为了强制函数重新评估,只需设置一个全局布尔标志,并将其传递给计算函数,在您的地图上移动可移动元素时每次反转。您不必将它放在任何地方,只需将其放在可以移动且位于祖先项内部的项目的父项上即可。

因此,它有效,您的位置始终是正确的,而且它具有相当大的可扩展性,并且并不会添加太多代码。


非常感谢。我没想到可以使用全局标识来强制重新评估。这看起来很不规矩,但确实有效。 - Konrad Madej
1
请注意,您不必在参数中包含globalBit以触发重新评估。JS逗号运算符在这种情况下非常有效:property var pos:globalBit,updatePos(rectGreen,rectBlue) - Daniel Handojo

4
下面的解决方案将会在其父级的“x”或“y”值更改时触发qmlElementToTrack.onPropertyNameXChanged()和qmlElementToTrack.onPropertyNameYChanged()事件。它通过连接到每个父对象的onXChanged()和onYChanged()信号来实现这一点。当其中一个值更改时,它通过遍历qmlElementToTrack的所有父对象重新计算propertyNameX或propertyNameY值。为了使其“相对”定位(而不是“绝对”定位),请向每个while()条件添加current !== qmlElementToStopAt。

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


非常好。一个注意点是:除了连接到onXChangedonYChanged之外,还应该连接到onParentChanged,以便跟踪任何重新父级关系的技巧。 - Russ

1

如果开发一些复杂的图形交互,跟踪特定物品的全局位置似乎是一个重要问题。我想出了一个相对简单优雅的解决方案。下面是我的核心代码:

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

核心思想是:是什么导致了一个项目的全局位置改变?可能是它本身、它的父级或其父级的父级等。因此,向其最远的父级进行遍历,并将每个祖先的x/y更改信号连接到一个函数中,在其中获取项目的全局位置并向外广播。

1
我不知道这是否有帮助,但对于TheBootroo提到的上述黄色矩形、蓝色矩形和绿色矩形问题,我使用以下代码解决了该问题。
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;
                }

                )
    }


}

这个想法是通过计算绿色矩形及其可视祖先的位置,来计算蓝色矩形相对于绿色矩形的位置。

这个解决方案的灵感来自于 -> http://developer.nokia.com/Community/Wiki/How_to_create_a_Context_Menu_with_QML


与上面提到的彩色矩形解决方案类似,您可以将TrackedObject的x和y坐标绑定到JavaScript函数,该函数遍历可视树并返回祖先的坐标。 - Programmer

0

我试图通过将代码移动到自己的ItemPositionTracker.qml文件中来改进@Shubhanga的答案:

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

                    )
    }
}

现在可以将代码添加到任何QML对象中,如下所示:

ItemPositionTracker {
    trackedItem: rectGreen
    movedItem: rectBlue
}

这使得rectBlue跟随rectGreen。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接