diff options
Diffstat (limited to 'examples/widgets/graphicsview/elasticnodes.py')
-rwxr-xr-x | examples/widgets/graphicsview/elasticnodes.py | 414 |
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_()) |