aboutsummaryrefslogtreecommitdiffstats
path: root/examples/widgets/graphicsview/elasticnodes.py
diff options
context:
space:
mode:
Diffstat (limited to 'examples/widgets/graphicsview/elasticnodes.py')
-rwxr-xr-xexamples/widgets/graphicsview/elasticnodes.py414
1 files changed, 414 insertions, 0 deletions
diff --git a/examples/widgets/graphicsview/elasticnodes.py b/examples/widgets/graphicsview/elasticnodes.py
new file mode 100755
index 000000000..8da21334b
--- /dev/null
+++ b/examples/widgets/graphicsview/elasticnodes.py
@@ -0,0 +1,414 @@
+#!/usr/bin/env python
+
+#############################################################################
+##
+## Copyright (C) 2013 Riverbank Computing Limited.
+## Copyright (C) 2016 The Qt Company Ltd.
+## Contact: http://www.qt.io/licensing/
+##
+## This file is part of the PySide examples of the Qt Toolkit.
+##
+## $QT_BEGIN_LICENSE:BSD$
+## You may use this file under the terms of the BSD license as follows:
+##
+## "Redistribution and use in source and binary forms, with or without
+## modification, are permitted provided that the following conditions are
+## met:
+## * Redistributions of source code must retain the above copyright
+## notice, this list of conditions and the following disclaimer.
+## * Redistributions in binary form must reproduce the above copyright
+## notice, this list of conditions and the following disclaimer in
+## the documentation and/or other materials provided with the
+## distribution.
+## * Neither the name of The Qt Company Ltd nor the names of its
+## contributors may be used to endorse or promote products derived
+## from this software without specific prior written permission.
+##
+##
+## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+## "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+## LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+## A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+## OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+## SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+## LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+## DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+## THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+## (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+## OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+##
+## $QT_END_LICENSE$
+##
+#############################################################################
+
+import sys
+import weakref
+import math
+from PySide2 import QtCore, QtGui, QtWidgets
+
+
+class Edge(QtWidgets.QGraphicsItem):
+ Pi = math.pi
+ TwoPi = 2.0 * Pi
+
+ Type = QtWidgets.QGraphicsItem.UserType + 2
+
+ def __init__(self, sourceNode, destNode):
+ QtWidgets.QGraphicsItem.__init__(self)
+
+ self.arrowSize = 10.0
+ self.sourcePoint = QtCore.QPointF()
+ self.destPoint = QtCore.QPointF()
+ self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
+ self.source = weakref.ref(sourceNode)
+ self.dest = weakref.ref(destNode)
+ self.source().addEdge(self)
+ self.dest().addEdge(self)
+ self.adjust()
+
+ def type(self):
+ return Edge.Type
+
+ def sourceNode(self):
+ return self.source()
+
+ def setSourceNode(self, node):
+ self.source = weakref.ref(node)
+ self.adjust()
+
+ def destNode(self):
+ return self.dest()
+
+ def setDestNode(self, node):
+ self.dest = weakref.ref(node)
+ self.adjust()
+
+ def adjust(self):
+ if not self.source() or not self.dest():
+ return
+
+ line = QtCore.QLineF(self.mapFromItem(self.source(), 0, 0), self.mapFromItem(self.dest(), 0, 0))
+ length = line.length()
+
+ if length == 0.0:
+ return
+
+ edgeOffset = QtCore.QPointF((line.dx() * 10) / length, (line.dy() * 10) / length)
+
+ self.prepareGeometryChange()
+ self.sourcePoint = line.p1() + edgeOffset
+ self.destPoint = line.p2() - edgeOffset
+
+ def boundingRect(self):
+ if not self.source() or not self.dest():
+ return QtCore.QRectF()
+
+ penWidth = 1
+ extra = (penWidth + self.arrowSize) / 2.0
+
+ return QtCore.QRectF(self.sourcePoint,
+ QtCore.QSizeF(self.destPoint.x() - self.sourcePoint.x(),
+ self.destPoint.y() - self.sourcePoint.y())).normalized().adjusted(-extra, -extra, extra, extra)
+
+ def paint(self, painter, option, widget):
+ if not self.source() or not self.dest():
+ return
+
+ # Draw the line itself.
+ line = QtCore.QLineF(self.sourcePoint, self.destPoint)
+
+ if line.length() == 0.0:
+ return
+
+ painter.setPen(QtGui.QPen(QtCore.Qt.black, 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
+ painter.drawLine(line)
+
+ # Draw the arrows if there's enough room.
+ angle = math.acos(line.dx() / line.length())
+ if line.dy() >= 0:
+ angle = Edge.TwoPi - angle
+
+ sourceArrowP1 = self.sourcePoint + QtCore.QPointF(math.sin(angle + Edge.Pi / 3) * self.arrowSize,
+ math.cos(angle + Edge.Pi / 3) * self.arrowSize)
+ sourceArrowP2 = self.sourcePoint + QtCore.QPointF(math.sin(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize,
+ math.cos(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize);
+ destArrowP1 = self.destPoint + QtCore.QPointF(math.sin(angle - Edge.Pi / 3) * self.arrowSize,
+ math.cos(angle - Edge.Pi / 3) * self.arrowSize)
+ destArrowP2 = self.destPoint + QtCore.QPointF(math.sin(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize,
+ math.cos(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize)
+
+ painter.setBrush(QtCore.Qt.black)
+ painter.drawPolygon(QtGui.QPolygonF([line.p1(), sourceArrowP1, sourceArrowP2]))
+ painter.drawPolygon(QtGui.QPolygonF([line.p2(), destArrowP1, destArrowP2]))
+
+
+class Node(QtWidgets.QGraphicsItem):
+ Type = QtWidgets.QGraphicsItem.UserType + 1
+
+ def __init__(self, graphWidget):
+ QtWidgets.QGraphicsItem.__init__(self)
+
+ self.graph = weakref.ref(graphWidget)
+ self.edgeList = []
+ self.newPos = QtCore.QPointF()
+ self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
+ self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges)
+ self.setCacheMode(self.DeviceCoordinateCache)
+ self.setZValue(-1)
+
+ def type(self):
+ return Node.Type
+
+ def addEdge(self, edge):
+ self.edgeList.append(weakref.ref(edge))
+ edge.adjust()
+
+ def edges(self):
+ return self.edgeList
+
+ def calculateForces(self):
+ if not self.scene() or self.scene().mouseGrabberItem() is self:
+ self.newPos = self.pos()
+ return
+
+ # Sum up all forces pushing this item away.
+ xvel = 0.0
+ yvel = 0.0
+ for item in self.scene().items():
+ if not isinstance(item, Node):
+ continue
+
+ line = QtCore.QLineF(self.mapFromItem(item, 0, 0), QtCore.QPointF(0, 0))
+ dx = line.dx()
+ dy = line.dy()
+ l = 2.0 * (dx * dx + dy * dy)
+ if l > 0:
+ xvel += (dx * 150.0) / l
+ yvel += (dy * 150.0) / l
+
+ # Now subtract all forces pulling items together.
+ weight = (len(self.edgeList) + 1) * 10.0
+ for edge in self.edgeList:
+ if edge().sourceNode() is self:
+ pos = self.mapFromItem(edge().destNode(), 0, 0)
+ else:
+ pos = self.mapFromItem(edge().sourceNode(), 0, 0)
+ xvel += pos.x() / weight
+ yvel += pos.y() / weight
+
+ if QtCore.qAbs(xvel) < 0.1 and QtCore.qAbs(yvel) < 0.1:
+ xvel = yvel = 0.0
+
+ sceneRect = self.scene().sceneRect()
+ self.newPos = self.pos() + QtCore.QPointF(xvel, yvel)
+ self.newPos.setX(min(max(self.newPos.x(), sceneRect.left() + 10), sceneRect.right() - 10))
+ self.newPos.setY(min(max(self.newPos.y(), sceneRect.top() + 10), sceneRect.bottom() - 10))
+
+ def advance(self):
+ if self.newPos == self.pos():
+ return False
+
+ self.setPos(self.newPos)
+ return True
+
+ def boundingRect(self):
+ adjust = 2.0
+ return QtCore.QRectF(-10 - adjust, -10 - adjust,
+ 23 + adjust, 23 + adjust)
+
+ def shape(self):
+ path = QtGui.QPainterPath()
+ path.addEllipse(-10, -10, 20, 20)
+ return path
+
+ def paint(self, painter, option, widget):
+ painter.setPen(QtCore.Qt.NoPen)
+ painter.setBrush(QtCore.Qt.darkGray)
+ painter.drawEllipse(-7, -7, 20, 20)
+
+ gradient = QtGui.QRadialGradient(-3, -3, 10)
+ if option.state & QtWidgets.QStyle.State_Sunken:
+ gradient.setCenter(3, 3)
+ gradient.setFocalPoint(3, 3)
+ gradient.setColorAt(1, QtGui.QColor(QtCore.Qt.yellow).lighter(120))
+ gradient.setColorAt(0, QtGui.QColor(QtCore.Qt.darkYellow).lighter(120))
+ else:
+ gradient.setColorAt(0, QtCore.Qt.yellow)
+ gradient.setColorAt(1, QtCore.Qt.darkYellow)
+
+ painter.setBrush(QtGui.QBrush(gradient))
+ painter.setPen(QtGui.QPen(QtCore.Qt.black, 0))
+ painter.drawEllipse(-10, -10, 20, 20)
+
+ def itemChange(self, change, value):
+ if change == QtWidgets.QGraphicsItem.ItemPositionChange:
+ for edge in self.edgeList:
+ edge().adjust()
+ self.graph().itemMoved()
+
+ return QtWidgets.QGraphicsItem.itemChange(self, change, value)
+
+ def mousePressEvent(self, event):
+ self.update()
+ QtWidgets.QGraphicsItem.mousePressEvent(self, event)
+
+ def mouseReleaseEvent(self, event):
+ self.update()
+ QtWidgets.QGraphicsItem.mouseReleaseEvent(self, event)
+
+
+class GraphWidget(QtWidgets.QGraphicsView):
+ def __init__(self):
+ QtWidgets.QGraphicsView.__init__(self)
+
+ self.timerId = 0
+
+ scene = QtWidgets.QGraphicsScene(self)
+ scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
+ scene.setSceneRect(-200, -200, 400, 400)
+ self.setScene(scene)
+ self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground)
+ self.setRenderHint(QtGui.QPainter.Antialiasing)
+ self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse)
+ self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter)
+
+ node1 = Node(self)
+ node2 = Node(self)
+ node3 = Node(self)
+ node4 = Node(self)
+ self.centerNode = Node(self)
+ node6 = Node(self)
+ node7 = Node(self)
+ node8 = Node(self)
+ node9 = Node(self)
+ scene.addItem(node1)
+ scene.addItem(node2)
+ scene.addItem(node3)
+ scene.addItem(node4)
+ scene.addItem(self.centerNode)
+ scene.addItem(node6)
+ scene.addItem(node7)
+ scene.addItem(node8)
+ scene.addItem(node9)
+ scene.addItem(Edge(node1, node2))
+ scene.addItem(Edge(node2, node3))
+ scene.addItem(Edge(node2, self.centerNode))
+ scene.addItem(Edge(node3, node6))
+ scene.addItem(Edge(node4, node1))
+ scene.addItem(Edge(node4, self.centerNode))
+ scene.addItem(Edge(self.centerNode, node6))
+ scene.addItem(Edge(self.centerNode, node8))
+ scene.addItem(Edge(node6, node9))
+ scene.addItem(Edge(node7, node4))
+ scene.addItem(Edge(node8, node7))
+ scene.addItem(Edge(node9, node8))
+
+ node1.setPos(-50, -50)
+ node2.setPos(0, -50)
+ node3.setPos(50, -50)
+ node4.setPos(-50, 0)
+ self.centerNode.setPos(0, 0)
+ node6.setPos(50, 0)
+ node7.setPos(-50, 50)
+ node8.setPos(0, 50)
+ node9.setPos(50, 50)
+
+ self.scale(0.8, 0.8)
+ self.setMinimumSize(400, 400)
+ self.setWindowTitle(self.tr("Elastic Nodes"))
+
+ def itemMoved(self):
+ if not self.timerId:
+ self.timerId = self.startTimer(1000 / 25)
+
+ def keyPressEvent(self, event):
+ key = event.key()
+
+ if key == QtCore.Qt.Key_Up:
+ self.centerNode.moveBy(0, -20)
+ elif key == QtCore.Qt.Key_Down:
+ self.centerNode.moveBy(0, 20)
+ elif key == QtCore.Qt.Key_Left:
+ self.centerNode.moveBy(-20, 0)
+ elif key == QtCore.Qt.Key_Right:
+ self.centerNode.moveBy(20, 0)
+ elif key == QtCore.Qt.Key_Plus:
+ self.scaleView(1.2)
+ elif key == QtCore.Qt.Key_Minus:
+ self.scaleView(1 / 1.2)
+ elif key == QtCore.Qt.Key_Space or key == QtCore.Qt.Key_Enter:
+ for item in self.scene().items():
+ if isinstance(item, Node):
+ item.setPos(-150 + QtCore.qrand() % 300, -150 + QtCore.qrand() % 300)
+ else:
+ QtWidgets.QGraphicsView.keyPressEvent(self, event)
+
+
+ def timerEvent(self, event):
+ nodes = [item for item in self.scene().items() if isinstance(item, Node)]
+
+ for node in nodes:
+ node.calculateForces()
+
+ itemsMoved = False
+ for node in nodes:
+ if node.advance():
+ itemsMoved = True
+
+ if not itemsMoved:
+ self.killTimer(self.timerId)
+ self.timerId = 0
+
+ def wheelEvent(self, event):
+ self.scaleView(math.pow(2.0, -event.delta() / 240.0))
+
+ def drawBackground(self, painter, rect):
+ # Shadow.
+ sceneRect = self.sceneRect()
+ rightShadow = QtCore.QRectF(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height())
+ bottomShadow = QtCore.QRectF(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5)
+ if rightShadow.intersects(rect) or rightShadow.contains(rect):
+ painter.fillRect(rightShadow, QtCore.Qt.darkGray)
+ if bottomShadow.intersects(rect) or bottomShadow.contains(rect):
+ painter.fillRect(bottomShadow, QtCore.Qt.darkGray)
+
+ # Fill.
+ gradient = QtGui.QLinearGradient(sceneRect.topLeft(), sceneRect.bottomRight())
+ gradient.setColorAt(0, QtCore.Qt.white)
+ gradient.setColorAt(1, QtCore.Qt.lightGray)
+ painter.fillRect(rect.intersected(sceneRect), QtGui.QBrush(gradient))
+ painter.setBrush(QtCore.Qt.NoBrush)
+ painter.drawRect(sceneRect)
+
+ # Text.
+ textRect = QtCore.QRectF(sceneRect.left() + 4, sceneRect.top() + 4,
+ sceneRect.width() - 4, sceneRect.height() - 4)
+ message = self.tr("Click and drag the nodes around, and zoom with the "
+ "mouse wheel or the '+' and '-' keys")
+
+ font = painter.font()
+ font.setBold(True)
+ font.setPointSize(14)
+ painter.setFont(font)
+ painter.setPen(QtCore.Qt.lightGray)
+ painter.drawText(textRect.translated(2, 2), message)
+ painter.setPen(QtCore.Qt.black)
+ painter.drawText(textRect, message)
+
+ def scaleView(self, scaleFactor):
+ factor = self.matrix().scale(scaleFactor, scaleFactor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width()
+
+ if factor < 0.07 or factor > 100:
+ return
+
+ self.scale(scaleFactor, scaleFactor)
+
+
+if __name__ == "__main__":
+ app = QtWidgets.QApplication(sys.argv)
+ QtCore.qsrand(QtCore.QTime(0,0,0).secsTo(QtCore.QTime.currentTime()))
+
+ widget = GraphWidget()
+ widget.show()
+
+ sys.exit(app.exec_())