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
}
}
}
}
}
[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!