Skip to content

Instantly share code, notes, and snippets.

@mottosso
Last active February 6, 2023 13:56
Show Gist options
  • Star 12 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mottosso/d5a95bc89460bf2c88b6 to your computer and use it in GitHub Desktop.
Save mottosso/d5a95bc89460bf2c88b6 to your computer and use it in GitHub Desktop.
Minimal QML SceneGraph 2

untitled

An elaboration of QML SceneGraph 1

Usage

python main.py --amount 100
  • Panning by left-click + drag
  • Scale by rotating the mouse wheel

Differences

  • Connections are also drawn
  • Connections stored in separate model, drawn in separate view
  • Zoom occurs at mouse cursor
  • Zoom is animated
import sys
import random
import contextlib
from PyQt5 import QtQml, QtGui, QtCore
@contextlib.contextmanager
def application():
app = QtGui.QGuiApplication(sys.argv)
yield
app.exec_()
class Model(QtCore.QAbstractListModel):
def __init__(self, items, roles, parent=None):
super(Model, self).__init__(parent)
self._items = items
self._roles = roles
def rowCount(self, parent=None):
return len(self._items)
def data(self, index, role=QtCore.QModelIndex()):
name = self.roleNames()[role]
try:
return self._items[index.row()][name]
except KeyError:
pass
def roleNames(self):
return self._roles
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--amount", type=int, default=100)
amount = parser.parse_args().amount
print("Creating %i nodes" % amount)
with application():
engine = QtQml.QQmlApplicationEngine()
# Generate lots of nodes
scale = 1
nodes = list()
x = 0
y = 0
for i in xrange(amount):
x += random.randint(100, 150)
y += random.randint(-100, 100)
nodes.append({
"name": "Node %s" % i,
"x": x * scale + 500, # Offset nodes towards center
"y": y * scale + 1000
})
connections = list()
for index, node in enumerate(nodes):
if index == 0:
continue
# Next node connected to the previous node
previous = nodes[index - 1]
# Pythagoras a**2 + b**2 = c**2
connections.append({
"x1": node["x"] + 0, # Out position
"y1": node["y"] + 25,
"x2": previous["x"] + 100, # In position
"y2": previous["y"] + 25,
})
print("Nodes: %s" % nodes)
print("Connections: %s" % connections)
sg_model = Model(nodes, roles={
QtCore.Qt.UserRole + 0: "name",
QtCore.Qt.UserRole + 1: "x",
QtCore.Qt.UserRole + 2: "y",
})
con_model = Model(connections, roles={
QtCore.Qt.UserRole + 1: "x1",
QtCore.Qt.UserRole + 2: "y1",
QtCore.Qt.UserRole + 3: "x2",
QtCore.Qt.UserRole + 4: "y2",
})
context = engine.rootContext()
context.setContextProperty("sgModel", sg_model)
context.setContextProperty("conModel", con_model)
engine.load(QtCore.QUrl.fromLocalFile("main.qml"))
import QtQuick 2.4
import QtQuick.Controls 1.2
ApplicationWindow {
title: "QML SceneGraph"
width: 900
height: 500
visible: true
readonly property real transitionDuration: 200 // ms
Item {
id: canvas
/* Encapsulate graph */
width: 20000
height: 20000
/* Move "camera" to center */
x: -500
y: -1000
MouseArea {
anchors.fill: parent
propagateComposedEvents: true
property double factor: 2.0
onWheel: {
var zoomFactor = wheel.angleDelta.y > 0 ? factor : 1 / factor
var realX = wheel.x * transform.xScale
var realY = wheel.y * transform.yScale
canvas.x += (1 - zoomFactor) * realX
canvas.y += (1 - zoomFactor) * realY
transform.xScale *= zoomFactor
transform.yScale *= zoomFactor
}
}
transform: Scale {
id: transform
Behavior on xScale { PropertyAnimation { duration: transitionDuration; easing.type: Easing.InOutCubic } }
Behavior on yScale { PropertyAnimation { duration: transitionDuration; easing.type: Easing.InOutCubic } }
}
Behavior on x { PropertyAnimation { duration: transitionDuration; easing.type: Easing.InOutCubic } }
Behavior on y { PropertyAnimation { duration: transitionDuration; easing.type: Easing.InOutCubic } }
/* Draw connections */
Repeater {
model: conModel
delegate: Item {
x: x1
y: y1
Rectangle {
color: "steelblue"
antialiasing: true
height: 1
width: {
/* Pythagoras theorem */
var a = x2 - x1;
var b = y2 - y1;
var c = Math.sqrt(a*a + b*b)
return c
}
transform: Rotation {
/* Rotate from (0,0) */
angle: Math.atan2(y2 - y1, x2 - x1) * (180 / Math.PI)
}
}
}
}
/* Draw nodes from model */
Repeater {
model: sgModel
delegate: Item {
width: 100
height: 50
x: model.x
y: model.y
Rectangle {
anchors.fill: parent
radius: 5
border.width: 1
border.color: Qt.darker("brown", 1.2)
color: "brown"
}
Text {
anchors.centerIn: parent
text: model.name
color: "#ddd"
}
Rectangle {
/* In plug */
anchors.verticalCenter: parent.verticalCenter
x: -width / 2
height: 10
width: 10
radius: width / 2
color: "steelblue"
}
Rectangle {
/* Out plug */
anchors.verticalCenter: parent.verticalCenter
x: parent.width - width / 2
height: 10
width: 10
radius: width / 2
color: "steelblue"
}
}
}
}
/* Make canvas draggable */
MouseArea {
drag.target: canvas
anchors.fill: parent
acceptedButtons: Qt.LeftButton
propagateComposedEvents: true
}
}
import QtQuick 2.4
import QtQuick.Controls 1.2
ApplicationWindow {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Rectangle {
id: rect
x: 100
y: 100
width: 300
height: 300
Rectangle {
x: 50
y: 50
width: 50
height: 50
color: "blue"
}
Rectangle {
x: 100
y: 100
width: 50
height: 50
color: "red"
}
transform: Scale {
id: tform
}
MouseArea {
anchors.fill: parent
property double factor: 2.0
onWheel:
{
if(wheel.angleDelta.y > 0) // zoom in
var zoomFactor = factor
else // zoom out
zoomFactor = 1/factor
var realX = wheel.x * tform.xScale
var realY = wheel.y * tform.yScale
rect.x += (1-zoomFactor)*realX
rect.y += (1-zoomFactor)*realY
tform.xScale *=zoomFactor
tform.yScale *=zoomFactor
}
}
}
}
@dakuhn
Copy link

dakuhn commented Dec 3, 2019

Hi mottosso,
thank you so much for providing your code snippets!
The "Minimal QML SceneGraph 2" example is exactly what I'm looking for.
What license agreement does your code have?
Thanks in advance!
Many greetings from Germany!
Daniel

@mottosso
Copy link
Author

Hi @dakuhn, good to hear you found use for it, happy to make it available under public domain. It's all yours!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment