文章目录
- 使用 PyQtGraph 实现图形连接器:支持动态拖动和箭头连线
- 引言
- 实现功能的关键点
- 代码实现
- 功能演示
- 实现过程中的经验教训
- 结语
使用 PyQtGraph 实现图形连接器:支持动态拖动和箭头连线
引言
在这篇博客中,使用 PyQtGraph 和 PyQt6 创建一个类似 Visio 的图形编辑器,支持拖动图形、添加矩形和椭圆,以及通过箭头动态连线的功能。
功能总结:
- 支持添加矩形和椭圆图形。
- 图形可通过鼠标拖动,并动态更新连接的箭头。
- 支持 “移动模式” 和 “连线模式” 的切换。
- 箭头连线具有真实的起点和终点交点计算。
实现功能的关键点
在实现中,我们利用 PyQtGraph 提供的 GraphicsScene
和 GraphicsView
作为画布,并结合 PyQt5 的 QGraphicsItem
类自定义了以下组件:
DraggableRect
和DraggableEllipse
:分别实现了可拖动的矩形和椭圆。ArrowLine
:支持动态位置更新的箭头连线。- 模式管理:通过按钮切换图形的移动和连线模式。
代码实现
以下是完整代码:
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets, QtGui, QtCore
import math# 可拖动的矩形类
class DraggableRect(QtWidgets.QGraphicsRectItem):def __init__(self, x, y, w, h, color):super().__init__(x, y, w, h)self.setBrush(QtGui.QBrush(color))self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)self.connections = []self.move_enabled = Truedef itemChange(self, change, value):if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange:if self.move_enabled:for arrow in self.connections:arrow.update_position()else:return self.pos()return super().itemChange(change, value)def set_movable(self, movable):self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, movable)self.move_enabled = movable# 可拖动的椭圆类
class DraggableEllipse(QtWidgets.QGraphicsEllipseItem):def __init__(self, x, y, w, h, color):super().__init__(x, y, w, h)self.setBrush(QtGui.QBrush(color))self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges)self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable)self.connections = []self.move_enabled = Truedef itemChange(self, change, value):if change == QtWidgets.QGraphicsItem.GraphicsItemChange.ItemPositionChange:if self.move_enabled:for arrow in self.connections:arrow.update_position()else:return self.pos()return super().itemChange(change, value)def set_movable(self, movable):self.setFlag(QtWidgets.QGraphicsItem.GraphicsItemFlag.ItemIsMovable, movable)self.move_enabled = movable# 箭头连线类
class ArrowLine(QtWidgets.QGraphicsLineItem):def __init__(self, start_item, end_item):super().__init__()self.start_item = start_itemself.end_item = end_itemself.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0), 2))self.arrow_head = QtWidgets.QGraphicsPolygonItem()self.arrow_head.setBrush(QtGui.QBrush(QtGui.QColor(0, 0, 0)))self.arrow_head.setZValue(1)self.update_position()start_item.scene().addItem(self.arrow_head)start_item.connections.append(self)end_item.connections.append(self)def update_position(self):start_pos = self._get_intersection(self.start_item, self.end_item)end_pos = self._get_intersection(self.end_item, self.start_item)self.setLine(start_pos.x(), start_pos.y(), end_pos.x(), end_pos.y())arrow_size = 15dx = end_pos.x() - start_pos.x()dy = end_pos.y() - start_pos.y()angle = math.atan2(dy, dx)p1 = QtCore.QPointF(end_pos.x() - arrow_size * math.cos(angle - math.radians(30)),end_pos.y() - arrow_size * math.sin(angle - math.radians(30)))p2 = QtCore.QPointF(end_pos.x() - arrow_size * math.cos(angle + math.radians(30)),end_pos.y() - arrow_size * math.sin(angle + math.radians(30)))polygon = QtGui.QPolygonF([QtCore.QPointF(end_pos), p1, p2])self.arrow_head.setPolygon(polygon)def _get_intersection(self, item1, item2):rect1 = item1.sceneBoundingRect()rect2 = item2.sceneBoundingRect()line = QtCore.QLineF(rect1.center(), rect2.center())edges = [QtCore.QLineF(rect1.topLeft(), rect1.topRight()),QtCore.QLineF(rect1.topRight(), rect1.bottomRight()),QtCore.QLineF(rect1.bottomRight(), rect1.bottomLeft()),QtCore.QLineF(rect1.bottomLeft(), rect1.topLeft()),]for edge_line in edges:intersection_type, intersection_point = line.intersects(edge_line)if intersection_type == QtCore.QLineF.IntersectionType.BoundedIntersection:return intersection_pointreturn rect1.center()# 自定义场景
class GraphicsScene(pg.GraphicsScene):def __init__(self):super().__init__()self.setBackgroundBrush(QtGui.QColor(240, 240, 240))self.mode = "move"self.start_item = Nonedef set_mode(self, mode):self.mode = modeprint(f"切换到 {mode} 模式")def mousePressEvent(self, event):if self.mode == "line":clicked_item = self.itemAt(event.scenePos(), QtGui.QTransform())if isinstance(clicked_item, (DraggableRect, DraggableEllipse)):self.start_item = clicked_itemprint(f"开始连线:{clicked_item}")super().mousePressEvent(event)def mouseReleaseEvent(self, event):if self.mode == "line" and self.start_item:released_item = self.itemAt(event.scenePos(), QtGui.QTransform())if isinstance(released_item, (DraggableRect, DraggableEllipse)) and released_item != self.start_item:print(f"完成连线:从 {self.start_item} 到 {released_item}")arrow = ArrowLine(self.start_item, released_item)self.addItem(arrow)else:print("未释放在有效图形上,取消连线")self.start_item = Nonesuper().mouseReleaseEvent(event)# 主窗口
class MainWindow(QtWidgets.QWidget):def __init__(self):super().__init__()self.setWindowTitle("基于 PyQtGraph 的 Visio 风格")self.setGeometry(100, 100, 1200, 800)self.scene = GraphicsScene()self.view = QtWidgets.QGraphicsView(self.scene)self.view.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)self.scene.setSceneRect(0, 0, 1000, 800)self.top_bar = QtWidgets.QHBoxLayout()self.add_tool_button("矩形", self.add_rectangle)self.add_tool_button("椭圆", self.add_ellipse)self.add_tool_button("移动模式", self.enable_move_mode)self.add_tool_button("连线模式", self.enable_line_mode)main_layout = QtWidgets.QVBoxLayout()main_layout.addLayout(self.top_bar)main_layout.addWidget(self.view)self.setLayout(main_layout)self.enable_move_mode()def add_tool_button(self, name, callback):button = QtWidgets.QPushButton(name)button.setFixedHeight(30)button.clicked.connect(callback)self.top_bar.addWidget(button)def add_rectangle(self):rect = DraggableRect(-50, -50, 100, 100, QtGui.QColor(100, 200, 255, 150))rect.setPos(200, 200)rect.set_movable(self.scene.mode == "move")self.scene.addItem(rect)print("添加矩形到场景")def add_ellipse(self):ellipse = DraggableEllipse(-50, -50, 100, 100, QtGui.QColor(200, 100, 255, 150))ellipse.setPos(400, 200)ellipse.set_movable(self.scene.mode == "move")self.scene.addItem(ellipse)print("添加椭圆到场景")def enable_move_mode(self):self.scene.set_mode("move")for item in self.scene.items():if isinstance(item, (DraggableRect, DraggableEllipse)):item.set_movable(True)def enable_line_mode(self):self.scene.set_mode("line")for item in self.scene.items():if isinstance(item, (DraggableRect, DraggableEllipse)):item.set_movable(False)if __name__ == "__main__":app = pg.mkQApp("PyQtGraph Visio 风格")window = MainWindow()window.show()pg.exec()
功能演示
- 添加图形:点击顶部按钮,可以添加矩形或椭圆到画布中。
- 移动模式:点击“移动模式”按钮后,可以拖动图形,箭头位置会自动更新。
- 连线模式:点击“连线模式”后,可以通过拖动鼠标在图形之间创建带箭头的连线。
实现过程中的经验教训
-
自定义场景和图形:
- 使用
QGraphicsScene
和QGraphicsItem
实现复杂的拖拽和交互功能。 - 在图形移动时,动态更新所有连接的箭头。
- 使用
-
箭头绘制:
- 箭头由多边形组成,需通过起点和终点计算位置及角度。
- 使用
QLineF.intersects
精确计算图形之间的交点。
-
模式管理:
- 设置 “移动模式” 和 “连线模式” 的切换,避免误操作。
结语
本文展示了如何使用 PyQtGraph 和 PyQt6 创建一个简化版的图形工具。