diff options
Diffstat (limited to 'examples/widgets/graphicsview/elasticnodes/elasticnodes.py')
-rw-r--r-- | examples/widgets/graphicsview/elasticnodes/elasticnodes.py | 391 |
1 files changed, 391 insertions, 0 deletions
diff --git a/examples/widgets/graphicsview/elasticnodes/elasticnodes.py b/examples/widgets/graphicsview/elasticnodes/elasticnodes.py new file mode 100644 index 000000000..90cb49626 --- /dev/null +++ b/examples/widgets/graphicsview/elasticnodes/elasticnodes.py @@ -0,0 +1,391 @@ +# Copyright (C) 2013 Riverbank Computing Limited. +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys +import weakref +import math + +from PySide6.QtCore import (QLineF, QPointF, QRandomGenerator, QRectF, QSizeF, + Qt, qAbs) +from PySide6.QtGui import (QColor, QBrush, QLinearGradient, QPainter, QPainterPath, QPen, + QPolygonF, QRadialGradient) +from PySide6.QtWidgets import (QApplication, QGraphicsItem, QGraphicsScene, + QGraphicsView, QStyle) + + +def random(boundary): + return QRandomGenerator.global_().bounded(boundary) + + +class Edge(QGraphicsItem): + + def __init__(self, sourceNode, destNode): + super().__init__() + + self._arrow_size = 10.0 + self._source_point = QPointF() + self._dest_point = QPointF() + self.setAcceptedMouseButtons(Qt.NoButton) + self.source = weakref.ref(sourceNode) + self.dest = weakref.ref(destNode) + self.source().add_edge(self) + self.dest().add_edge(self) + self.adjust() + + def item_type(self): + return QGraphicsItem.UserType + 2 + + def source_node(self): + return self.source() + + def set_source_node(self, node): + self.source = weakref.ref(node) + self.adjust() + + def dest_node(self): + return self.dest() + + def set_dest_node(self, node): + self.dest = weakref.ref(node) + self.adjust() + + def adjust(self): + if not self.source() or not self.dest(): + return + + line = QLineF(self.mapFromItem(self.source(), 0, 0), + self.mapFromItem(self.dest(), 0, 0)) + length = line.length() + + if length == 0.0: + return + + edge_offset = QPointF((line.dx() * 10) / length, (line.dy() * 10) / length) + + self.prepareGeometryChange() + self._source_point = line.p1() + edge_offset + self._dest_point = line.p2() - edge_offset + + def boundingRect(self): + if not self.source() or not self.dest(): + return QRectF() + + pen_width = 1 + extra = (pen_width + self._arrow_size) / 2.0 + + width = self._dest_point.x() - self._source_point.x() + height = self._dest_point.y() - self._source_point.y() + rect = QRectF(self._source_point, QSizeF(width, height)) + return rect.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 = QLineF(self._source_point, self._dest_point) + + if line.length() == 0.0: + return + + painter.setPen(QPen(Qt.black, 1, Qt.SolidLine, Qt.RoundCap, 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 = 2 * math.pi - angle + + arrow_head1 = QPointF(math.sin(angle + math.pi / 3) * self._arrow_size, + math.cos(angle + math.pi / 3) * self._arrow_size) + source_arrow_p1 = self._source_point + arrow_head1 + arrow_head2 = QPointF(math.sin(angle + math.pi - math.pi / 3) * self._arrow_size, + math.cos(angle + math.pi - math.pi / 3) * self._arrow_size) + source_arrow_p2 = self._source_point + arrow_head2 + + arrow_head1 = QPointF(math.sin(angle - math.pi / 3) * self._arrow_size, + math.cos(angle - math.pi / 3) * self._arrow_size) + dest_arrow_p1 = self._dest_point + arrow_head1 + arrow_head2 = QPointF(math.sin(angle - math.pi + math.pi / 3) * self._arrow_size, + math.cos(angle - math.pi + math.pi / 3) * self._arrow_size) + dest_arrow_p2 = self._dest_point + arrow_head2 + + painter.setBrush(Qt.black) + painter.drawPolygon(QPolygonF([line.p1(), source_arrow_p1, source_arrow_p2])) + painter.drawPolygon(QPolygonF([line.p2(), dest_arrow_p1, dest_arrow_p2])) + + +class Node(QGraphicsItem): + + def __init__(self, graphWidget): + super().__init__() + + self.graph = weakref.ref(graphWidget) + self._edge_list = [] + self._new_pos = QPointF() + self.setFlag(QGraphicsItem.ItemIsMovable) + self.setFlag(QGraphicsItem.ItemSendsGeometryChanges) + self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) + self.setZValue(-1) + + def item_type(self): + return QGraphicsItem.UserType + 1 + + def add_edge(self, edge): + self._edge_list.append(weakref.ref(edge)) + edge.adjust() + + def edges(self): + return self._edge_list + + def calculate_forces(self): + if not self.scene() or self.scene().mouseGrabberItem() is self: + self._new_pos = 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 = QLineF(self.mapFromItem(item, 0, 0), QPointF(0, 0)) + dx = line.dx() + dy = line.dy() + l = 2.0 * (dx * dx + dy * dy) # noqa: E741 + if l > 0: + xvel += (dx * 150.0) / l + yvel += (dy * 150.0) / l + + # Now subtract all forces pulling items together. + weight = (len(self._edge_list) + 1) * 10.0 + for edge in self._edge_list: + if edge().source_node() is self: + pos = self.mapFromItem(edge().dest_node(), 0, 0) + else: + pos = self.mapFromItem(edge().source_node(), 0, 0) + xvel += pos.x() / weight + yvel += pos.y() / weight + + if qAbs(xvel) < 0.1 and qAbs(yvel) < 0.1: + xvel = yvel = 0.0 + + scene_rect = self.scene().sceneRect() + self._new_pos = self.pos() + QPointF(xvel, yvel) + self._new_pos.setX(min(max(self._new_pos.x(), scene_rect.left() + 10), + scene_rect.right() - 10)) + self._new_pos.setY(min(max(self._new_pos.y(), scene_rect.top() + 10), + scene_rect.bottom() - 10)) + + def advance(self): + if self._new_pos == self.pos(): + return False + + self.setPos(self._new_pos) + return True + + def boundingRect(self): + adjust = 2.0 + return QRectF(-10 - adjust, -10 - adjust, + 23 + adjust, 23 + adjust) + + def shape(self): + path = QPainterPath() + path.addEllipse(-10, -10, 20, 20) + return path + + def paint(self, painter, option, widget): + painter.setPen(Qt.NoPen) + painter.setBrush(Qt.darkGray) + painter.drawEllipse(-7, -7, 20, 20) + + gradient = QRadialGradient(-3, -3, 10) + if option.state & QStyle.State_Sunken: + gradient.setCenter(3, 3) + gradient.setFocalPoint(3, 3) + gradient.setColorAt(1, QColor(Qt.yellow).lighter(120)) + gradient.setColorAt(0, QColor(Qt.darkYellow).lighter(120)) + else: + gradient.setColorAt(0, Qt.yellow) + gradient.setColorAt(1, Qt.darkYellow) + + painter.setBrush(QBrush(gradient)) + painter.setPen(QPen(Qt.black, 0)) + painter.drawEllipse(-10, -10, 20, 20) + + def itemChange(self, change, value): + if change == QGraphicsItem.ItemPositionChange: + for edge in self._edge_list: + edge().adjust() + self.graph().item_moved() + + return QGraphicsItem.itemChange(self, change, value) + + def mousePressEvent(self, event): + self.update() + QGraphicsItem.mousePressEvent(self, event) + + def mouseReleaseEvent(self, event): + self.update() + QGraphicsItem.mouseReleaseEvent(self, event) + + +class GraphWidget(QGraphicsView): + def __init__(self): + super().__init__() + + self._timer_id = 0 + + scene = QGraphicsScene(self) + scene.setItemIndexMethod(QGraphicsScene.NoIndex) + scene.setSceneRect(-200, -200, 400, 400) + self.setScene(scene) + self.setCacheMode(QGraphicsView.CacheBackground) + self.setRenderHint(QPainter.Antialiasing) + self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(QGraphicsView.AnchorViewCenter) + + node1 = Node(self) + node2 = Node(self) + node3 = Node(self) + node4 = Node(self) + self._center_node = 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._center_node) + 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._center_node)) + scene.addItem(Edge(node3, node6)) + scene.addItem(Edge(node4, node1)) + scene.addItem(Edge(node4, self._center_node)) + scene.addItem(Edge(self._center_node, node6)) + scene.addItem(Edge(self._center_node, 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._center_node.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 item_moved(self): + if not self._timer_id: + self._timer_id = self.startTimer(1000 / 25) + + def keyPressEvent(self, event): + key = event.key() + + if key == Qt.Key_Up: + self._center_node.moveBy(0, -20) + elif key == Qt.Key_Down: + self._center_node.moveBy(0, 20) + elif key == Qt.Key_Left: + self._center_node.moveBy(-20, 0) + elif key == Qt.Key_Right: + self._center_node.moveBy(20, 0) + elif key == Qt.Key_Plus: + self.scale_view(1.2) + elif key == Qt.Key_Minus: + self.scale_view(1 / 1.2) + elif key == Qt.Key_Space or key == Qt.Key_Enter: + for item in self.scene().items(): + if isinstance(item, Node): + item.setPos(-150 + random(300), -150 + random(300)) + else: + 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.calculate_forces() + + items_moved = False + for node in nodes: + if node.advance(): + items_moved = True + + if not items_moved: + self.killTimer(self._timer_id) + self._timer_id = 0 + + def wheelEvent(self, event): + delta = event.angleDelta().y() + self.scale_view(math.pow(2.0, -delta / 240.0)) + + def draw_background(self, painter, rect): + # Shadow. + scene_rect = self.sceneRect() + right_shadow = QRectF(scene_rect.right(), scene_rect.top() + 5, + 5, scene_rect.height()) + bottom_shadow = QRectF(scene_rect.left() + 5, scene_rect.bottom(), + scene_rect.width(), 5) + if right_shadow.intersects(rect) or right_shadow.contains(rect): + painter.fillRect(right_shadow, Qt.darkGray) + if bottom_shadow.intersects(rect) or bottom_shadow.contains(rect): + painter.fillRect(bottom_shadow, Qt.darkGray) + + # Fill. + gradient = QLinearGradient(scene_rect.topLeft(), scene_rect.bottomRight()) + gradient.setColorAt(0, Qt.white) + gradient.setColorAt(1, Qt.lightGray) + painter.fillRect(rect.intersected(scene_rect), QBrush(gradient)) + painter.setBrush(Qt.NoBrush) + painter.drawRect(scene_rect) + + # Text. + text_rect = QRectF(scene_rect.left() + 4, scene_rect.top() + 4, + scene_rect.width() - 4, scene_rect.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(Qt.lightGray) + painter.drawText(text_rect.translated(2, 2), message) + painter.setPen(Qt.black) + painter.drawText(text_rect, message) + + def scale_view(self, scaleFactor): + factor = self.transform().scale(scaleFactor, scaleFactor).mapRect( + QRectF(0, 0, 1, 1)).width() + + if factor < 0.07 or factor > 100: + return + + self.scale(scaleFactor, scaleFactor) + + +if __name__ == "__main__": + app = QApplication(sys.argv) + + widget = GraphWidget() + widget.show() + + sys.exit(app.exec()) |