import sys, os from abc import abstractmethod from enum import Enum from typing import List, Dict, Tuple import networkx as nx from PyQt5.QtCore import QPointF, QRectF, Qt, pyqtSignal from PyQt5.QtGui import QFont, QPainterPath, QPen, QPainter, QMouseEvent from PyQt5.QtWidgets import QGraphicsItem, QGraphicsView, QApplication, QGraphicsScene from PyQt5.QtWidgets import QSplitter, QHBoxLayout sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) from graph.DataType import Point, Line class PresentNodeType(Enum): PresentNode = 0, ConnectionNode = 1, class GraphNode: @abstractmethod def node_type(self) -> PresentNodeType: raise NotImplementedError("node_type") @abstractmethod def highlight(self, flag: bool): raise NotImplementedError("highlight") @abstractmethod def is_highlighted(self) -> bool: raise NotImplementedError("is_highlighted") @abstractmethod def boundingRect(self) -> QRectF: raise NotImplementedError("boundingRect") @abstractmethod def pos(self) -> QPointF: raise NotImplementedError("pos") @abstractmethod def relayout_exec(self, ppu: float = 1): raise NotImplementedError("relayout_exec") class PresentNode(QGraphicsItem, GraphNode): def __init__(self, name: str, font: QFont, prim_x:float, prim_y:float, parent): QGraphicsItem.__init__(self, parent) self.node_name = name self.__is_highlight_mark = False self.__font_bind = font self.__sibling_list = [] self.__prim_pos: Tuple[float, float] = (prim_x, prim_y) self.setZValue(10) pass def sibling_append(self, node: 'PresentNode'): if node not in self.__sibling_list: self.__sibling_list.append(node) pass pass def sibling_nodes(self) -> List['PresentNode']: return self.__sibling_list def node_type(self) -> PresentNodeType: return PresentNodeType.PresentNode def highlight(self, flag: bool): self.__is_highlight_mark = flag pass def is_highlighted(self) -> bool: return self.__is_highlight_mark pass def boundingRect(self) -> QRectF: width_x = self.__font_bind.pixelSize() * (len(self.node_name)+1) height_x = self.__font_bind.pixelSize() return QRectF(0, 0, width_x + 10, height_x + 10) pass def paint(self, painter, option, widget = ...): outline = self.boundingRect() path_icon = QPainterPath() path_icon.lineTo(outline.height()/2 - 5, outline.height() -5) path_icon.lineTo(outline.height()/2, outline.height()/2) path_icon.lineTo(outline.height() - 5, outline.height()/2 - 5) path_icon.lineTo(0, 0) painter.save() painter.setRenderHint(QPainter.Antialiasing) #painter.drawRect(outline) if self.__is_highlight_mark: brush = Qt.red painter.setPen(Qt.red) else: brush = Qt.black painter.setPen(Qt.black) pass painter.fillPath(path_icon, brush) painter.translate(outline.height(), 5) painter.drawText(outline, self.node_name) painter.restore() pass def relayout_exec(self, ppu: float = 1): pos_x = ppu * self.__prim_pos[0] pos_y = ppu * self.__prim_pos[1] self.setPos(pos_x, pos_y) self.update() pass class ConnectionNode(QGraphicsItem, GraphNode): def __init__(self, p0: GraphNode, p1: GraphNode, parent): QGraphicsItem.__init__(self, parent) self.__highlight_mark = False self.__point0 = p0 self.__point1 = p1 self.__outline = QRectF() self.setZValue(1) pass def node_type(self) -> PresentNodeType: return PresentNodeType.ConnectionNode def highlight(self, flag: bool): self.__highlight_mark = flag pass def is_highlighted(self) -> bool: return self.__highlight_mark pass def relayout_exec(self, ppi: float = 1): start_pos = self.__point0.pos() end_pos = self.__point1.pos() start_x = min(start_pos.x(), end_pos.x()) start_y = min(start_pos.y(), end_pos.y()) end_x = max(start_pos.x(), end_pos.x()) end_y = max(start_pos.y(), end_pos.y()) self.setPos(QPointF(start_x, start_y)) self.__outline = QRectF(0, 0, end_x - start_x, end_y - start_y) self.update() pass def boundingRect(self): return self.__outline pass def paint(self, painter, option, widget = ...): start_pos = self.__point0.pos() end_pos = self.__point1.pos() outline = self.boundingRect() painter.save() painter.setRenderHint(QPainter.Antialiasing) if self.__highlight_mark: pen = QPen(Qt.red) else: pen = QPen(Qt.lightGray) pen.setWidthF(3) painter.setPen(pen) if start_pos.y() < end_pos.y(): if start_pos.x() < end_pos.x(): painter.drawLine(outline.topLeft(), outline.bottomRight()) else: painter.drawLine(outline.topRight(), outline.bottomLeft()) else: if start_pos.x() < end_pos.x(): painter.drawLine(outline.bottomLeft(), outline.topRight()) else: painter.drawLine(outline.topLeft(), outline.bottomRight()) painter.restore() pass class UDGPresent(QGraphicsView): node_clicked = pyqtSignal(str) def __init__(self, parent): QGraphicsView.__init__(self, parent) self.pixel_per_unit = 5000 self.__highlight_nodes: List[GraphNode] = [] self.node_set: Dict[str, GraphNode] = {} self.__layout_graph = nx.Graph() self.__scene_bind = QGraphicsScene(self) self.setScene(self.__scene_bind) font = QFont() font.setPixelSize(20) self.setFont(font) pass def rebuild_from_edges(self, line_set: List[Line]): self.node_set.clear() for line in line_set: start_node = line.points()[0] self.__layout_graph.add_node(start_node.point_name) end_node = line.points()[1] self.__layout_graph.add_node(end_node.point_name) self.__layout_graph.add_edge(start_node.point_name, end_node.point_name) pass pos_map = nx.spring_layout(self.__layout_graph) for node_name in pos_map: node_prim_pos = pos_map[node_name] targetx_node = PresentNode(node_name, self.font(), node_prim_pos[0], node_prim_pos[1], None) self.node_set[node_name] = targetx_node self.scene().addItem(targetx_node) targetx_node.relayout_exec(self.pixel_per_unit) pass for edge in nx.edges(self.__layout_graph): edge_start = edge[0] edge_end = edge[1] node_one: PresentNode = self.node_set[edge_start] node_two: PresentNode = self.node_set[edge_end] node_one.sibling_append(node_two) node_two.sibling_append(node_one) connection = ConnectionNode(node_one, node_two, None) self.scene().addItem(connection) connection.relayout_exec(self.pixel_per_unit) self.node_set[f"conn::{edge_start}&{edge_end}"] = connection pass pass def mousePressEvent(self, event: QMouseEvent): QGraphicsView.mousePressEvent(self, event) if event.button() == Qt.MouseButton.LeftButton: item: GraphNode = self.itemAt(event.pos()) if item is not None and item.node_type() == PresentNodeType.PresentNode: vnode: PresentNode = item self.node_clicked.emit(vnode.node_name) print(vnode.node_name) pass pass pass def refresh_with_ppu(self, ppu: float): self.pixel_per_unit = ppu for node in self.node_set.values(): if node.node_type() == PresentNodeType.PresentNode: node.relayout_exec(ppu) pass pass for node in self.node_set.values(): if node.node_type() == PresentNodeType.ConnectionNode: node.relayout_exec(ppu) pass pass self.scene().update() self.update() pass def update_scene_rect(self): minx = 2*64 miny = 2*64 maxx = -2**64 maxy = -2**64 for item in self.node_set.values(): minx = min(item.pos().x(), minx) miny = min(item.pos().y(), miny) maxx = max(item.pos().x() + item.boundingRect().width(), maxx) maxy = max(item.pos().y() + item.boundingRect().height(), maxy) pass self.scene().setSceneRect(minx, miny, maxx - minx, maxy - miny) pass def highlight_sibling_nodes(self, center_node: str): for node in self.__highlight_nodes: node.highlight(False) pass self.__highlight_nodes.clear() target_node: PresentNode = self.node_set[center_node] if target_node is None: return target_node.highlight(True) self.__highlight_nodes.append(target_node) for sib_node in target_node.sibling_nodes(): sib_node.highlight(True) self.__highlight_nodes.append(sib_node) sib_name = sib_node.node_name constr1 = f"conn::{center_node}&{sib_name}" constr2 = f"conn::{sib_name}&{center_node}" if constr1 in self.node_set: self.node_set[constr1].highlight(True) self.__highlight_nodes.append(self.node_set[constr1]) elif constr2 in self.node_set: self.node_set[constr2].highlight(True) self.__highlight_nodes.append(self.node_set[constr2]) pass pass self.scene().update() self.update() pass if __name__ == "__main__": app = QApplication(sys.argv) view = UDGPresent(None) view.show() list_in = [ Line(Point('a中古'), Point('b')), Line(Point('a中古'), Point('c')), Line(Point('a中古'), Point('d')), ] view.rebuild_from_edges(list_in) app.exec()