低代码软件搭建自学第2.5天——箭头连线和动态更新

文章目录

  • 使用 PyQtGraph 实现图形连接器:支持动态拖动和箭头连线
    • 引言
    • 实现功能的关键点
    • 代码实现
    • 功能演示
    • 实现过程中的经验教训
    • 结语


使用 PyQtGraph 实现图形连接器:支持动态拖动和箭头连线


引言

在这篇博客中,使用 PyQtGraphPyQt6 创建一个类似 Visio 的图形编辑器,支持拖动图形、添加矩形和椭圆,以及通过箭头动态连线的功能。

功能总结:

  1. 支持添加矩形和椭圆图形。
  2. 图形可通过鼠标拖动,并动态更新连接的箭头。
  3. 支持 “移动模式” 和 “连线模式” 的切换。
  4. 箭头连线具有真实的起点和终点交点计算。

实现功能的关键点

在实现中,我们利用 PyQtGraph 提供的 GraphicsSceneGraphicsView 作为画布,并结合 PyQt5 的 QGraphicsItem 类自定义了以下组件:

  1. DraggableRectDraggableEllipse:分别实现了可拖动的矩形和椭圆。
  2. ArrowLine:支持动态位置更新的箭头连线。
  3. 模式管理:通过按钮切换图形的移动和连线模式。

代码实现

以下是完整代码:

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()

功能演示

  1. 添加图形:点击顶部按钮,可以添加矩形或椭圆到画布中。
  2. 移动模式:点击“移动模式”按钮后,可以拖动图形,箭头位置会自动更新。
  3. 连线模式:点击“连线模式”后,可以通过拖动鼠标在图形之间创建带箭头的连线。
    在这里插入图片描述

实现过程中的经验教训

  1. 自定义场景和图形

    • 使用 QGraphicsSceneQGraphicsItem 实现复杂的拖拽和交互功能。
    • 在图形移动时,动态更新所有连接的箭头。
  2. 箭头绘制

    • 箭头由多边形组成,需通过起点和终点计算位置及角度。
    • 使用 QLineF.intersects 精确计算图形之间的交点。
  3. 模式管理

    • 设置 “移动模式” 和 “连线模式” 的切换,避免误操作。

结语

本文展示了如何使用 PyQtGraph 和 PyQt6 创建一个简化版的图形工具。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/64696.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CoinShares预测2025年加密市场前景看涨

原文来源:CoinShares预测2025年加密市场前景看涨 - 币热网 - 区块链数字货币新闻消息资讯 欧洲加密投资公司CoinShares发布了2025年的市场预测,概述了可能塑造行业的关键趋势。 报告指出,在即将上任的川普政府下,监管环境将更加…

微服务??

1、微服务架构的定义是什么? 微服务架构是一种将应用程序拆分为多个小型、独立服务的架构风格,每个服务专注于完成特定功能,通过轻量级通信协议(如HTTP/REST、gRPC)进行协作。 2、微服务和单体架构有哪些主要区别&am…

ChromeOS 131 版本更新

ChromeOS 131 版本更新 1. ChromeOS Flex 自动注册 在 ChromeOS 131 中,ChromeOS Flex 的自动注册功能现已允许大规模部署 ChromeOS Flex 设备。与 ChromeOS 零接触注册类似,自动注册将通过组织管理员创建的注册令牌嵌入到 ChromeOS Flex 镜像中。这将…

【LeetCode】394、字符串解码

【LeetCode】394、字符串解码 文章目录 一、递归: 嵌套类问题1.1 递归: 嵌套类问题 二、多语言解法 一、递归: 嵌套类问题 1.1 递归: 嵌套类问题 // go func decodeString(s string) string {// 如果遇到 嵌套括号的情况, 则递归// 可能连续多位数字, 则 通过 cur cur * 10 …

【漏洞复现】英飞达医学影像存档与通信系统 WebUserLogin.asmx 信息泄露漏洞

🏘️个人主页: 点燃银河尽头的篝火(●’◡’●) 如果文章有帮到你的话记得点赞👍+收藏💗支持一下哦 一、漏洞概述 1.1 组件描述 英飞达医学影像存档与通信系统主要用于医院影像科室,负责将日常生成的各种医学影像(如核磁共振、CT、超声、X光、红外、显微镜等设备的图…

Java 中 ConcurrentHashMap 和 HashMap 能存 null 吗?深挖原理和使用场景

前言 当你使用 HashMap 或 ConcurrentHashMap 时,可能会冒出一个经典问题:它们能存储 null 键或 null 值吗? 初学者可能觉得无所谓,试一下不就知道了,但在真实项目中,这个问题可能导致严重的 bug。今天我们…

0101多级nginx代理websocket配置-nginx-web服务器

1. 前言 项目一些信息需要通过站内信主动推动给用户,使用websocket。web服务器选用nginx,但是域名是以前通过阿里云申请的,解析ip也是阿里云的服务器,甲方不希望更换域名。新的系统需要部署在内网服务器,简单拓扑图如…

python安装包的几种方法

要安装一个Python包,通常有几种方法,具体取决于包的来源和类型。以下是几种常见的安装方法: 1. 使用pip安装(推荐) 如果你想要安装的包已经在PyPI(Python Package Index)上注册,你…

Everything实现,快速搜索文件

最近编写NTFS文件实时搜索工具, 类似 Everything 这样, 翻阅了很多博客, 结果大致如下: 1.分析比较肤浅, 采用USN日志枚举来获取文件记录 速度一言难尽, 因为日志枚举的是全盘所有文件的所有日志, 记录比文件记录还多, 速度当然很慢, 还有的甚至于是 使用 DeviceIoControl 函数…

R 基础运算

R 基础运算 R 是一种广泛使用的统计编程语言,它提供了强大的数据操作和分析功能。基础运算在 R 中非常重要,因为它们是进行更复杂计算和数据分析的基础。本文将详细介绍 R 中的基础运算,包括算术运算、逻辑运算、向量化和矩阵运算。 一、算…

京东大数据治理探索与实践 | 京东零售技术实践

01背景和方案 在当今的数据驱动时代,数据作为关键生产要素之一,其在商业活动中的战略价值愈加凸显,京东也不例外。 作为国内领先的电商平台,京东在数据基础设施上的投入极为巨大,涵盖数万台服务器、数 EB 级存储、数百…

nodejs搭配express网站开发后端接口设计需要注意事项

nodejs搭配express网站开发后端接口设计需要注意事项!为了回避一些常见的误区,今天和大家汇总一下,最近我遇到的一些错误信息,虽然都是小问题,但是还是需要分享一下,以免大家再次犯错。 1:第一个…

解决vscode ssh远程连接服务器一直卡在下载 vscode server问题

目录 方法1:使用科学上网 方法2:手动下载 方法3 在使用vscode使用ssh远程连接服务器时,一直卡在下载"vscode 服务器"阶段,但MobaXterm可以正常连接服务器,大概率是网络问题,解决方法如下: 方…

Spring Boot实现OAuth2.0登录实战

一、前言 最近在研究Springboot Vue 的前后端分离框架,刚开始做登录功能,做着做着觉得普通账户密码登录太简单了,决定再加上 GitHub授权 和 人脸识别等多种快捷登录方式。 而GitHub授权登录正好用到了OAuth2.0中最复杂的授权码模式&#xf…

电脑开机提示error loading operating system怎么修复?

前一天电脑还能正常运行,但今天启动时却显示“Error loading operating system”(加载操作系统错误)。我已经仔细检查了硬盘、接线、内存、CPU和电源,确认这些硬件都没有问题。硬盘在其他电脑上可以正常使用,说明不是硬…

Java web的发展历史

目录 前言: 一.Model I和Model II 1.Model I开发模式 ​编辑 2.Model II开发模式 二. MVC模式 前言: 该篇文章主要介绍了Java web的发展历史,以及MVC相关内容 一.Model I和Model II 1.Model I开发模式 Model1的开发模式是&#xff…

Flink DataStream API 编程指南

(对于Flink的开发,建议使用Java,Scala的支持未来会被移除) DataStream是什么 DataStream API得名于DataStream这个Java类,可以将它们视为可以包含重复项的不可变数据集合。该数据可以是有限的,也可以是无限的,用于处理它们的API是相同的。 DataStream在用法上和普通的…

Intel-ECI之Codesys PLC + Ethercat 远端IO + Codesys IDE编程

目录 一、 准备工作 二、安装Codesys 软件 PLC 三、 使用Codesys IDE 编程测试 CODESYS* 是领先的独立于制造商的 IEC 61131-3 自动化软件,适用于工程控制系统。它用于 Intel Edge Controls for Industrial(Intel ECI 或 ECI),…

SQL语句练习

阅读《SQL必知必会》(第五版)然后结合往常表做的练习记录 这里使用的数据库时sqlite3,使用的工具时navicat 表资源链接https://wenku.baidu.com/view/349fb3639b6648d7c1c74652.html 表录入后如上图所示。后面如果有多张表之间的操作,在引入…

Java游戏开发基础:从零开始制作一个简单的2D游戏

目录 游戏开发概述 开发工具 项目结构 1. 创建游戏窗口 2. 游戏面板 解释: 3. 玩家类 解释: 4. 障碍物类 解释: 5. 游戏循环与碰撞检测 总结 在现代游戏开发中,Java被广泛应用于创建各种类型的游戏,特别是…