Search code examples
qtqmlqtquick2

Access repeater delegate items by model property


I am trying to draw a graph structure in Qt using QML. I want to draw lines representing edges between vertices. The vertices are created in a repeater with a given vertexID, x, and y. The edges have two vertexIDs that should be connected.

The vertices can be dragged/moved and the endpoints of the edges should follow. Since the edges depend on the vertices, I am loading them after the vertices are completed.

However, I am not sure how to access the vertex delegates by their vertex ID and bind their positions to the edges. How can I accomplish this? Is there any way of doing it without looping through every repeater item? Is there a better approach in general to this kind of dependency in QML?

Here is sample code of what I am trying to achieve:

import QtQuick 6

Rectangle {
    id: window
    width: 800
    height: 600
    color: "black"

    Repeater {
        id: vertices
        function getVert(vID) {
            // How to return vertex with given vID and bind it to edge?
        }
        model: ListModel {
            ListElement {
                vID: 3; x: 100; y: 200
            }
            ListElement {
                vID: 4; x: 600; y: 500
            }
            ListElement {
                vID: 7; x: 500; y: 300
            }
            ListElement {
                vID: 9; x: 400; y: 200
            }
        }
        delegate: Rectangle {
            id: vertex
            x: model.x
            y: model.y
            width: 10
            height: 10
            radius: 5
            color: "white"

            function centerPos() {
                return Qt.point(x + width / 2.0, y + height / 2.0)
            }

            Drag.active: vertexMouse.drag.active
            MouseArea {
                id: vertexMouse
                anchors.fill: parent
                drag.target: vertex
            }
        }
    }

    Repeater {
        id: edges
        model: ListModel {
            ListElement {
                vID1: 3
                vID2: 7
            }
            ListElement {
                vID1: 3
                vID2: 4
            }
        }
        delegate: Item {
            Loader {
                id: edgeLoader
            }
            Component {
                id: edge
                Canvas {
                    x: 0
                    y: 0
                    width: window.width
                    height: window.height

                    property var pos1: vertices.getVert(vID1).centerPos()
                    onPos1Changed: requestPaint()

                    property var pos2: vertices.getVert(vID2).centerPos()
                    onPos2Changed: requestPaint()

                    onPaint: {
                        var ctx = getContext("2d")
                        ctx.clearRect(x, y, width, height)

                        ctx.lineWidth = 4.0
                        ctx.strokeStyle = "white"

                        ctx.beginPath()
                        ctx.moveTo(pos1.x, pos1.y)
                        ctx.lineTo(pos2.x, pos2.y)
                        ctx.stroke()
                    }
                }
            }

            Connections {
                target: vertices
                Component.onCompleted: {
                    edgeLoader.sourceComponent = edge
                }
            }
        }
    }
}

Solution

  • [REWRITE]

    No need for Canvas, you can use Shape, ShapePath, and Line.

    We create a lookup table to help look up vID in the ListModel.

    The (x, y) coordinates from the verts ListModel are used to draw both the vertices and edges so that any changes that happen to the verts ListModel will automatically update both vertices and edges.

    I made use of DragHandler to reduce the code needed for UI action.

    import QtQuick
    import QtQuick.Controls
    import QtQuick.Shapes
    Page {
        ListModel {
            id: verts
            ListElement { vID: 3; x: 100; y: 200 }
            ListElement { vID: 4; x: 600; y: 500 }
            ListElement { vID: 7; x: 500; y: 300 }
            ListElement { vID: 9; x: 400; y: 200 }
        }
        ListModel {
            id: edges
            ListElement { vID1: 3; vID2: 7 }
            ListElement { vID1: 3; vID2: 4 }
        }
        property var lookup: {
            let _lookup = [ ];
            for (let i = 0; i < verts.count; i++)
              _lookup[verts.get(i).vID] = i;
            return _lookup;
        }
        Repeater {
            model: verts
            Rectangle {
                x: model.x - 5
                y: model.y - 5
                width: 10
                height: 10
                color: tapHandler.pressed  && "orange"
                    || dragHandler.active && "green"
                    || "red"
                TapHandler { id: tapHandler }
                DragHandler { id: dragHandler }
                onXChanged: model.x = x + 5
                onYChanged: model.y = y + 5
            }
        }
        Repeater {
            model: edges
            Item {
                property var s: verts.get(lookup[vID1])
                property var e: verts.get(lookup[vID2])
                Shape {
                    ShapePath {
                        strokeColor: "blue"
                        startX: s.x; startY: s.y
                        PathLine { x: e.x; y: e.y }
                    }
                }
            }
        }
    }
    

    You can Try it Online!