mirror of https://gitlab.com/pamhyr/pamhyr2
network: Add graphwidget view.
parent
6aea82f22a
commit
04c3f76eef
|
|
@ -50,8 +50,12 @@ class Graph(object):
|
||||||
def node(self, node_name:str):
|
def node(self, node_name:str):
|
||||||
return list(filter(lambda n: n.name == node_name, self._nodes))[0]
|
return list(filter(lambda n: n.name == node_name, self._nodes))[0]
|
||||||
|
|
||||||
def add_node(self):
|
def add_node(self, x:float = 0.0, y:float = 0.0):
|
||||||
node = Node(self._nodes_ids, f"Node {self._nodes_ids}")
|
node = Node(
|
||||||
|
self._nodes_ids,
|
||||||
|
f"Node {self._nodes_ids}",
|
||||||
|
x = x, y = y
|
||||||
|
)
|
||||||
self._nodes.append(node)
|
self._nodes.append(node)
|
||||||
self._nodes_ids += 1
|
self._nodes_ids += 1
|
||||||
return node
|
return node
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
from model.network.Point import Point
|
from model.network.Point import Point
|
||||||
|
|
||||||
class Node(object):
|
class Node(object):
|
||||||
def __init__(self, id:str, name:str):
|
def __init__(self, id:str, name:str,
|
||||||
|
x:float = 0.0, y:float = 0.0):
|
||||||
super(Node, self).__init__()
|
super(Node, self).__init__()
|
||||||
|
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.pos = Point(0, 0)
|
self.pos = Point(x, y)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return f"Node {{id: {self.id}, name: {self.name}}}"
|
return f"Node {{id: {self.id}, name: {self.name}}}"
|
||||||
|
|
@ -20,6 +21,8 @@ class Node(object):
|
||||||
ret = self.name
|
ret = self.name
|
||||||
elif name == "id":
|
elif name == "id":
|
||||||
ret = self.id
|
ret = self.id
|
||||||
|
elif name == "pos":
|
||||||
|
ret = f"({self.pos.x},{self.pos.y})"
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
def Point(object):
|
class Point(object):
|
||||||
def __init__(self, x:float, y:float):
|
def __init__(self, x:float, y:float):
|
||||||
super(Point, self).__init__()
|
super(Point, self).__init__()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from view.ASubWindow import ASubWindow
|
|
||||||
from model.network.Node import Node
|
from model.network.Node import Node
|
||||||
from model.network.Edge import Edge
|
from model.network.Edge import Edge
|
||||||
from model.network.Graph import Graph
|
from model.network.Graph import Graph
|
||||||
|
from view.ASubWindow import ASubWindow
|
||||||
|
from view.network.GraphWidget import GraphWidget
|
||||||
|
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
Qt, QRect, QVariant, QAbstractTableModel, pyqtSlot, pyqtSignal,
|
Qt, QRect, QVariant, QAbstractTableModel, pyqtSlot, pyqtSignal,
|
||||||
)
|
)
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
QTableView, QItemDelegate, QComboBox, QLineEdit,
|
QTableView, QItemDelegate, QComboBox, QLineEdit, QHBoxLayout,
|
||||||
)
|
)
|
||||||
|
|
||||||
class LineEditDelegate(QItemDelegate):
|
class LineEditDelegate(QItemDelegate):
|
||||||
|
|
@ -121,11 +122,20 @@ class NetworkWindow(ASubWindow):
|
||||||
self.ui.setWindowTitle(title)
|
self.ui.setWindowTitle(title)
|
||||||
|
|
||||||
self.graph = Graph()
|
self.graph = Graph()
|
||||||
|
n1 = self.graph.add_node()
|
||||||
|
n2 = self.graph.add_node(50.0,50.0)
|
||||||
|
e1 = self.graph.add_edge(n1,n2)
|
||||||
|
|
||||||
|
# Graph Widget
|
||||||
|
|
||||||
|
self.graph_widget = GraphWidget(self.graph)
|
||||||
|
self.graph_layout = self.find(QHBoxLayout, "horizontalLayout_graph")
|
||||||
|
self.graph_layout.addWidget(self.graph_widget)
|
||||||
|
|
||||||
# Nodes table
|
# Nodes table
|
||||||
|
|
||||||
self.nodes_model = TableModel(
|
self.nodes_model = TableModel(
|
||||||
headers = ["name", "id"],
|
headers = ["name", "id", "pos"],
|
||||||
rows = self.graph.nodes(),
|
rows = self.graph.nodes(),
|
||||||
graph = self.graph,
|
graph = self.graph,
|
||||||
)
|
)
|
||||||
|
|
@ -140,11 +150,11 @@ class NetworkWindow(ASubWindow):
|
||||||
self.reachs_model = TableModel(
|
self.reachs_model = TableModel(
|
||||||
headers = ["name", "node1", "node2"],
|
headers = ["name", "node1", "node2"],
|
||||||
rows = self.graph.edges(),
|
rows = self.graph.edges(),
|
||||||
graph=self.graph,
|
graph = self.graph,
|
||||||
)
|
)
|
||||||
self.delegate_combobox = ComboBoxDelegate(
|
self.delegate_combobox = ComboBoxDelegate(
|
||||||
graph=self.graph,
|
graph = self.graph,
|
||||||
parent=self,
|
parent = self,
|
||||||
)
|
)
|
||||||
table = self.find(QTableView, "tableView_reachs")
|
table = self.find(QTableView, "tableView_reachs")
|
||||||
table.setModel(self.reachs_model)
|
table.setModel(self.reachs_model)
|
||||||
|
|
@ -152,6 +162,7 @@ class NetworkWindow(ASubWindow):
|
||||||
table.setItemDelegateForColumn(2, self.delegate_combobox)
|
table.setItemDelegateForColumn(2, self.delegate_combobox)
|
||||||
#table.resizeColumnsToContents()
|
#table.resizeColumnsToContents()
|
||||||
|
|
||||||
# Connection the two table
|
# Connection
|
||||||
|
|
||||||
self.nodes_model.dataChanged.connect(self.reachs_model.update)
|
self.nodes_model.dataChanged.connect(self.reachs_model.update)
|
||||||
self.reachs_model.dataChanged.connect(self.nodes_model.update)
|
self.reachs_model.dataChanged.connect(self.nodes_model.update)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,284 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
from model.network.Node import Node
|
||||||
|
from model.network.Edge import Edge
|
||||||
|
from model.network.Graph import Graph
|
||||||
|
|
||||||
|
from PyQt5.QtCore import (
|
||||||
|
qAbs, QLineF, QPointF, qrand, QRectF, QSizeF, qsrand,
|
||||||
|
Qt, QTime
|
||||||
|
)
|
||||||
|
from PyQt5.QtGui import (
|
||||||
|
QBrush, QColor, QLinearGradient, QPainter,
|
||||||
|
QPainterPath, QPen, QPolygonF, QRadialGradient,
|
||||||
|
QFont,
|
||||||
|
)
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QApplication, QGraphicsItem, QGraphicsScene,
|
||||||
|
QGraphicsView, QStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
class LandMark(QGraphicsItem):
|
||||||
|
def paint(self, painter, option, widget):
|
||||||
|
painter.setPen(QPen(Qt.black, 0))
|
||||||
|
|
||||||
|
painter.drawLine(-500, -500, 500, 500)
|
||||||
|
painter.drawLine(-500, 500, 500, -500)
|
||||||
|
|
||||||
|
painter.drawLine(-500, -500, 500, -500)
|
||||||
|
painter.drawLine(500, -500, 500, 500)
|
||||||
|
painter.drawLine(500, 500, -500, 500)
|
||||||
|
painter.drawLine(-500, 500, -500, -500)
|
||||||
|
|
||||||
|
class NodeItem(QGraphicsItem):
|
||||||
|
Type = QGraphicsItem.UserType + 1
|
||||||
|
|
||||||
|
def __init__(self, node, graphWidget):
|
||||||
|
super(NodeItem, self).__init__()
|
||||||
|
|
||||||
|
self.node = node
|
||||||
|
self.setPos(QPointF(self.node.pos.x, self.node.pos.y))
|
||||||
|
|
||||||
|
self.graph = graphWidget
|
||||||
|
self.newPos = QPointF()
|
||||||
|
|
||||||
|
self.setFlag(QGraphicsItem.ItemIsMovable)
|
||||||
|
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges)
|
||||||
|
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
||||||
|
self.setZValue(1)
|
||||||
|
|
||||||
|
def type(self):
|
||||||
|
return NodeItem.Type
|
||||||
|
|
||||||
|
def advance(self):
|
||||||
|
if self.newPos == self.pos():
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.setPos(self.newPos)
|
||||||
|
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)
|
||||||
|
|
||||||
|
painter.setFont(QFont("Arial", 20))
|
||||||
|
painter.drawText(QRectF(-10, -10, 20, 20), Qt.AlignCenter, self.node.name)
|
||||||
|
|
||||||
|
def itemChange(self, change, value):
|
||||||
|
if change == QGraphicsItem.ItemPositionHasChanged:
|
||||||
|
self.graph.itemMoved()
|
||||||
|
|
||||||
|
return super(NodeItem, self).itemChange(change, value)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
self.update()
|
||||||
|
super(NodeItem, self).mousePressEvent(event)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
self.update()
|
||||||
|
super(NodeItem, self).mouseReleaseEvent(event)
|
||||||
|
|
||||||
|
class EdgeItem(QGraphicsItem):
|
||||||
|
Type = QGraphicsItem.UserType + 2
|
||||||
|
|
||||||
|
def __init__(self, edge):
|
||||||
|
super(EdgeItem, self).__init__()
|
||||||
|
|
||||||
|
self.arrowSize = 10.0
|
||||||
|
self.sourcePoint = QPointF()
|
||||||
|
self.destPoint = QPointF()
|
||||||
|
|
||||||
|
self.setAcceptedMouseButtons(Qt.NoButton)
|
||||||
|
self.source = edge.node1
|
||||||
|
self.dest = edge.node2
|
||||||
|
self.edge = edge
|
||||||
|
self.adjust()
|
||||||
|
|
||||||
|
def type(self):
|
||||||
|
return Edge.Type
|
||||||
|
|
||||||
|
def sourceNode(self):
|
||||||
|
return self.source
|
||||||
|
|
||||||
|
def setSourceNode(self, node):
|
||||||
|
self.source = node
|
||||||
|
self.adjust()
|
||||||
|
|
||||||
|
def destNode(self):
|
||||||
|
return self.dest
|
||||||
|
|
||||||
|
def setDestNode(self, node):
|
||||||
|
self.dest = 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()
|
||||||
|
|
||||||
|
self.prepareGeometryChange()
|
||||||
|
|
||||||
|
if length > 20.0:
|
||||||
|
edgeOffset = QPointF((line.dx() * 10) / length,
|
||||||
|
(line.dy() * 10) / length)
|
||||||
|
|
||||||
|
self.sourcePoint = line.p1() + edgeOffset
|
||||||
|
self.destPoint = line.p2() - edgeOffset
|
||||||
|
else:
|
||||||
|
self.sourcePoint = line.p1()
|
||||||
|
self.destPoint = line.p1()
|
||||||
|
|
||||||
|
def boundingRect(self):
|
||||||
|
if not self.source or not self.dest:
|
||||||
|
return QRectF()
|
||||||
|
|
||||||
|
penWidth = 1.0
|
||||||
|
extra = (penWidth + self.arrowSize) / 2.0
|
||||||
|
|
||||||
|
return QRectF(self.sourcePoint,
|
||||||
|
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 = QLineF(self.sourcePoint, self.destPoint)
|
||||||
|
|
||||||
|
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 = Edge.TwoPi - angle
|
||||||
|
|
||||||
|
sourceArrowP1 = self.sourcePoint + QPointF(math.sin(angle + Edge.Pi / 3) * self.arrowSize,
|
||||||
|
math.cos(angle + Edge.Pi / 3) * self.arrowSize)
|
||||||
|
sourceArrowP2 = self.sourcePoint + QPointF(math.sin(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize,
|
||||||
|
math.cos(angle + Edge.Pi - Edge.Pi / 3) * self.arrowSize);
|
||||||
|
destArrowP1 = self.destPoint + QPointF(math.sin(angle - Edge.Pi / 3) * self.arrowSize,
|
||||||
|
math.cos(angle - Edge.Pi / 3) * self.arrowSize)
|
||||||
|
destArrowP2 = self.destPoint + QPointF(math.sin(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize,
|
||||||
|
math.cos(angle - Edge.Pi + Edge.Pi / 3) * self.arrowSize)
|
||||||
|
|
||||||
|
painter.setBrush(Qt.black)
|
||||||
|
painter.drawPolygon(QPolygonF([line.p1(), sourceArrowP1, sourceArrowP2]))
|
||||||
|
painter.drawPolygon(QPolygonF([line.p2(), destArrowP1, destArrowP2]))
|
||||||
|
|
||||||
|
|
||||||
|
class GraphWidget(QGraphicsView):
|
||||||
|
def __init__(self, graph):
|
||||||
|
super(GraphWidget, self).__init__()
|
||||||
|
|
||||||
|
self.timerId = 0
|
||||||
|
|
||||||
|
scene = QGraphicsScene(self)
|
||||||
|
scene.setItemIndexMethod(QGraphicsScene.NoIndex)
|
||||||
|
scene.setSceneRect(-500, -500, 500, 500)
|
||||||
|
self.setScene(scene)
|
||||||
|
self.setCacheMode(QGraphicsView.CacheBackground)
|
||||||
|
self.setViewportUpdateMode(QGraphicsView.BoundingRectViewportUpdate)
|
||||||
|
self.setRenderHint(QPainter.Antialiasing)
|
||||||
|
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
|
||||||
|
self.setResizeAnchor(QGraphicsView.AnchorViewCenter)
|
||||||
|
|
||||||
|
scene.addItem(LandMark())
|
||||||
|
|
||||||
|
self.graph = graph
|
||||||
|
for node in self.graph.nodes():
|
||||||
|
scene.addItem(NodeItem(node, self))
|
||||||
|
|
||||||
|
# for edge in self.graph.edges():
|
||||||
|
# scene.addItem(EdgeItem(edge))
|
||||||
|
|
||||||
|
self.scale(0.8, 0.8)
|
||||||
|
self.setMinimumSize(400, 400)
|
||||||
|
|
||||||
|
def itemMoved(self):
|
||||||
|
if not self.timerId:
|
||||||
|
self.timerId = self.startTimer(1000 / 25)
|
||||||
|
|
||||||
|
def keyPressEvent(self, event):
|
||||||
|
key = event.key()
|
||||||
|
|
||||||
|
if key == Qt.Key_Up:
|
||||||
|
self.centerNode.moveBy(0, -20)
|
||||||
|
elif key == Qt.Key_Down:
|
||||||
|
self.centerNode.moveBy(0, 20)
|
||||||
|
elif key == Qt.Key_Left:
|
||||||
|
self.centerNode.moveBy(-20, 0)
|
||||||
|
elif key == Qt.Key_Right:
|
||||||
|
self.centerNode.moveBy(20, 0)
|
||||||
|
elif key == Qt.Key_Plus:
|
||||||
|
self.scaleView(1.2)
|
||||||
|
elif key == Qt.Key_Minus:
|
||||||
|
self.scaleView(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 + qrand() % 300, -150 + qrand() % 300)
|
||||||
|
else:
|
||||||
|
super(GraphWidget, self).keyPressEvent(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.angleDelta().y() / 240.0))
|
||||||
|
|
||||||
|
def scaleView(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)
|
||||||
|
|
@ -62,28 +62,7 @@
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
<layout class="QHBoxLayout" name="horizontalLayout_graph"/>
|
||||||
<item>
|
|
||||||
<widget class="QGraphicsView" name="graphicsView_3">
|
|
||||||
<property name="minimumSize">
|
|
||||||
<size>
|
|
||||||
<width>0</width>
|
|
||||||
<height>400</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QSlider" name="verticalSlider_2">
|
|
||||||
<property name="value">
|
|
||||||
<number>50</number>
|
|
||||||
</property>
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Vertical</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -140,7 +119,20 @@
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTableView" name="tableView_reachs"/>
|
<widget class="QTableView" name="tableView_reachs">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>650</width>
|
||||||
|
<height>150</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>200</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
|
@ -197,7 +189,20 @@
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QTableView" name="tableView_nodes"/>
|
<widget class="QTableView" name="tableView_nodes">
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>150</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>200</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue