python实现蓝图

原因

策划的需求真是奇葩,自己策划的人生模拟器玩法,自己设计的配表方式,结果因为配置太繁琐不知道配了,到最后还是要程序来背锅,给他们做个类似蓝图的东西给他们配表,不过最后做的好像更像思维导图0.0

环境

  1. Python 3.7.7
  2. PyQt5
  3. Qt Designer
  4. pandas

基本功能

  1. 节点创建、删除、链接、拖拽、搜索
  2. 界面缩放、移动
  3. 快捷键保存临时文件
  4. 读取excel或历史记录,自动生成节点
  5. 按照界面节点生成新的excel文件配置
  6. 界面自适应大小
    界面示例

结构

主要类

作用
MainWindow用Qt Designer生成的主界面
EditorView继承自QGraphicsView的2D视图管理
EditorScene继承自QGraphicsScene的场景管理
LineItem线条和箭头
NodeItem节点
PortItem节点上的点
MessageItem节点上的文字,更具文字多少自适应大小
SelectItem右键选择创建节点的下拉框
OptionItem下拉框的选项
Dispatcher消息注册分发器,单例
DataBase数据model基类,当值改变时自动分发消息

包含关系

MainWindow
EditorView
EditorScene
LineItem
NodeItem
SelectItem
OptionItem
PortItem
MessageItem

直接贴代码

MainWindow

from ui import Ui_MainWindow
from PyQt5.QtWidgets import QMainWindow, QFileDialog, QAbstractItemView
from PyQt5.QtGui import QColor, QBrush, QPainter, QIcon
from .EditorView import EditorView
from common.Constant import *
from utils.event.Dispatcher import Dispatcher
from data.MudMgr import MudMgr
from utils.config.Config import Config

class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)
        ...
        self.initView()
        self.setWindowTitle("人生模拟器")
        self.setWindowIcon(QIcon('assets/res/image/icon/icon.jpg'))
	def initView(self):
		...
        self.editorView = EditorView(self.widgetEditor) #将view添加到window中
        ...
	
	# 窗口大小变化时触发的事件处理函数,自适应
	def resizeEvent(self, event):
        new_size = event.size()
        self.tabWidget.resize(new_size.width() - 2, new_size.height() - 2)
        self.mud.resize(new_size.width() - 2, new_size.height() - 2)
        self.widgetRight.move(new_size.width() - 330, 10)
        self.widgetEditor.setGeometry(0, 0, new_size.width() - 332, new_size.height() - 50)
        self.editorView.setGeometry(0, 0, new_size.width() - 332, new_size.height() - 50)

EditorView

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem, QSizePolicy
from PyQt5.QtCore import QPointF, QRectF, QLineF, QEvent
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QBrush, QPainter, QPen
from PyQt5 import QtCore, QtGui, QtWidgets
from .EditorScene import EditorScene
from common.Constant import *
from utils.event.Dispatcher import Dispatcher

class EditorView(QGraphicsView):
    def __init__(self, parent=None):
        super(EditorView, self).__init__(parent)
        self.parent = parent
        self.scene = EditorScene(self)
        self.setScene(self.scene) # 添加scene
        self.setSceneRect(-1 << 30, -1 << 30, 1 << 31, 1 << 31)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) # 禁用水平滚动条
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)  # 禁用垂直滚动条

        self.verticalScrollBar().setEnabled(False)
        self.horizontalScrollBar().setEnabled(False)
        self.registerEvents()

        self.setRenderHint(QPainter.Antialiasing)
        self.setDragMode(QGraphicsView.NoDrag)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QGraphicsView.AnchorUnderMouse)
        self.lastMousePos = None
        # 连接鼠标滚动事件到处理函数
        self.viewport().installEventFilter(self)
    
    # 将场景移动到某个节点为中心的位置
    def moveSceneToNodeCenter(self, node):
        # 获取场景中的某个节点(例如,第一个添加的矩形)
        node_center = node.boundingRect().center()
        # 将视口中心设置为节点中心
        self.centerOn(node.mapToScene(node_center))
	
	# 滚轮缩放
    def eventFilter(self, obj, event):
        if event.type() == QEvent.Wheel:
            # 获取滚动角度
            angle = event.angleDelta().y() / 120

            # 缩放因子
            scale_factor = 1.2

            # 计算新的缩放级别
            if angle > 0:
                self.scale(scale_factor, scale_factor)
            else:
                self.scale(1.0 / scale_factor, 1.0 / scale_factor)

            return True

        return super().eventFilter(obj, event)

    def mousePressEvent(self, event):
        if event.button() == Qt.MiddleButton:
            self.lastMousePos = event.pos()
        else:
            super().mousePressEvent(event)

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.MiddleButton:
            self.lastMousePos = None
        else:
            super().mouseReleaseEvent(event)
	
	# 鼠标中键移动场景
    def mouseMoveEvent(self, event):
        if self.lastMousePos is not None:
            delta = event.pos() - self.lastMousePos
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - delta.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - delta.y())
            self.lastMousePos = event.pos()
        else:
            super().mouseMoveEvent(event)

EditorScene

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem
from PyQt5.QtCore import QPointF, QRectF, QLineF
from PyQt5.QtCore import Qt

from PyQt5.QtGui import QColor, QBrush, QPainter, QPen, QTransform, QCursor
from PyQt5 import QtCore, QtGui, QtWidgets
from .NodeItem import NodeItem
from .ItemMessage import ItemMessage
from .LineItem import LineItem
from .ItemSelect import ItemSelect
from common.Constant import *
from utils.event.Dispatcher import Dispatcher
from data.DataMessage import DataMessage
from data.DataStep import DataStep
from data.DataChoice import DataChoice
from data.MudMgr import MudMgr
import traceback
import re

class EditorScene(QGraphicsScene):
    def __init__(self, parent=None):
        super(EditorScene, self).__init__(parent)
        ...
   def addNode(self, pos, item_type, data=None):
        if not item_type in self.item_pool:
            self.item_pool[item_type] = []
        temp = self.item_pool[item_type]
        if len(temp) > 0:
            node = temp.pop()
            node.updateData(item_type, data)
            node.setPos(pos)
        else:
            node = NodeItem(pos.x(), pos.y(), item_type, data)
            self.addItem(node)
        node.setVisible(True)
        self.item_pool[item_type] = temp
        Dispatcher().dispatch(EventId.NODE_ADD, node)
        self.update() # 添加节点后update刷新界面
        return node

LineItem

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem
from PyQt5.QtCore import QPointF, QRectF, QLineF
from PyQt5.QtCore import Qt

from PyQt5.QtGui import QColor, QBrush, QPainter, QPen, QTransform, QPainterPath
from PyQt5 import QtCore, QtGui, QtWidgets
from common.Constant import *
from utils.event.Dispatcher import Dispatcher
from data.MudMgr import MudMgr
import math

class LineItem(QGraphicsItem):
    def __init__(self, posStart, posEnd):
        super().__init__()
        self.item_type = ItemType.LINE
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
        self.setFlag(QGraphicsItem.ItemIsFocusable, True)
        self.posStart = posStart
        self.posEnd = posEnd
        self.is_select = False
        self.linking = False
        self.left = None
        self.right = None
        self.setZValue(2)
        self.registerEvents()

    def registerEvents(self):
        Dispatcher().add(self, EventId.NODE_MOVE, "onNodeMoved") #监听消息节点移动时线条跟随移动
        Dispatcher().add(self, EventId.NODE_DELED, "onNodeDeleted")
    
    # 这个是它的区域,一定要实现,否则不会展示
    def boundingRect(self):
        mapPos = self.mapToScene(0, 0)
        posStart = self.posStart
        posEnd = self.posEnd
        min_x = min(posStart.x(), posEnd.x())
        min_y = min(posStart.y(), posEnd.y())
        max_x = max(posStart.x(), posEnd.x())
        max_y = max(posStart.y(), posEnd.y())

        margin = 3
        return QRectF(min_x - margin, min_y - margin, max_x - min_x + 2 * margin, max_y - min_y + 2 * margin)
    
    # 画线和箭头
    def paint(self, painter, style, *args, **kwargs):
        midPos = (self.posStart + self.posEnd) / 2
        if self.is_select:
            lineColor = QColor(0x00, 0xff, 0x00, 0xff)
        else:
            lineColor = QColor(0xff, 0x00, 0x00, 0xff)

        pen = QPen()
        pen.setColor(lineColor)
        pen.setWidth(2)
        painter.setRenderHint(QPainter.Antialiasing)  # 设置抗锯齿
        painter.setPen(pen)

        linePath = QPainterPath()
        linePath.moveTo(self.posStart)
        linePath.cubicTo(QPointF(midPos.x(), self.posStart.y()), midPos, self.posEnd)
        painter.drawPath(linePath)

        if not self.right or self.right.item_type != ItemType.MESSAGE:
            painter.save()  # 保存当前的绘图状态
            delta = self.posEnd - self.posStart # 根据起始点和终点算箭头角度
            angle = math.atan2(delta.y(), delta.x())
            length = math.sqrt(delta.x() ** 2 + delta.y() ** 2)
            if length != 0:
                unit_vector = QPointF(delta.x() / length, delta.y() / length)
            else:
                unit_vector = QPointF(0, 0)
            arrow_end = self.posEnd + 10 * unit_vector

            painter.translate(arrow_end)  
            painter.rotate(math.degrees(angle)) 

            arrow_length = 15
            arrow_width = 6
            arrow_p1 = QPointF(0, 0)
            arrow_p2 = QPointF(-arrow_length, -arrow_width)
            arrow_p3 = QPointF(-arrow_length, arrow_width)

            control_point1 = arrow_p1 - QPointF(arrow_length * 0.5, 0)
            control_point2 = arrow_p1 + QPointF(arrow_length * 0.5, 0)
            arrow_path = QPainterPath()
            arrow_path.moveTo(arrow_p1) #根据三个点画出带弧度的箭头
            arrow_path.quadTo(control_point1, arrow_p2)
            arrow_path.quadTo(control_point1, arrow_p3)
            arrow_path.quadTo(control_point1, arrow_p1)
            arrow_path.closeSubpath()

            painter.fillPath(arrow_path, QBrush(lineColor))
            painter.restore()

NodeItem

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem
from PyQt5.QtCore import QPointF, QRectF, QLineF
from PyQt5.QtCore import Qt

from PyQt5.QtGui import QColor, QBrush, QPainter, QPen, QTransform
from PyQt5 import QtCore, QtGui, QtWidgets
from utils.event.Dispatcher import Dispatcher
from .PortItem import PortItem
from common.Constant import *
from .ItemMessage import ItemMessage

# 节点
class NodeItem(QGraphicsItem):
    def __init__(self, x, y, item_type, data):
        super().__init__()
        ...
        self.item_message = None
        self.portList = []
        self.width = 120
        self.height = 40
        self.setPos(x, y)
        self.addPort()
        self.addText()
        self.setZValue(1)
        self.eventList = []
        self.registerEvents()
        
	def addPort(self):
        leftPort = PortItem(5, self.height/2.7, self)
        leftPort.setPortType(PortType.START)
        rightPort = PortItem(self.width-20, self.height/2.7, self)
        rightPort.setPortType(PortType.END)
        self.portList.append(leftPort)
        self.portList.append(rightPort)

	def addText(self):
        content = ""
        self.item_message = ItemMessage(20, 5, content, self)
        self.onSetText(content)
    
    # 设置文本内容自动调整节点大小
    def onSetText(self, text):
        self.item_message.onSetText(text)
        width, height = self.item_message.onGetWidthAndHeight()
        self.width = width + 40
        self.height = height + 10
        self.portList[0].setPos(5, height/2.7)
        self.portList[1].setPos(self.width-20, height/2.7)
        self.update()
        Dispatcher().dispatch(EventId.NODE_MOVE, self)
    
    def boundingRect(self):
        return QRectF(0, 0, self.width, self.height)

    def paint(self, painter, style, *args, **kwargs):
        if self.is_select:
            brush = QBrush(QColor(0xff, 0xff, 0xaa, 0xff))
        else:
            if self.item_type == ItemType.STEP:
                brush = QBrush(QColor(0xaa, 0xaa, 0xff, 0xaa))
            elif self.item_type == ItemType.CHOICE:
                brush = QBrush(QColor(0xaa, 0xff, 0xaa, 0xaa))
            elif self.item_type == ItemType.MESSAGE:
                brush = QBrush(QColor(0xcc, 0xcc, 0xcc, 0xaa))
                
        painter.setBrush(brush)
        pen = QPen()
        if self.item_type == ItemType.STEP:
            pen.setWidth(3)
        elif self.item_type == ItemType.CHOICE:
            pen.setWidth(2)
        else:
            pen.setWidth(1)
        painter.setPen(pen)
        painter.drawRect(0, 0, self.width, self.height)

ItemMessage

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem, QGraphicsTextItem
from PyQt5.QtCore import QPointF, QRectF, QLineF
from PyQt5.QtCore import Qt
from PyQt5.QtCore import QCoreApplication

from PyQt5.QtGui import QColor, QBrush, QPainter, QPen, QTransform, QTextOption, QFontMetricsF
from PyQt5 import QtCore, QtGui, QtWidgets
from common.Constant import *
from .PortItem import PortItem
from utils.event.Dispatcher import Dispatcher

# 下拉选择
class ItemMessage(QGraphicsTextItem):
    def __init__(self, x, y, content=None, parent=None):
        super(ItemMessage, self).__init__(content, parent)
        self.setPos(x, y)
        self.setZValue(2)
        self.setTextInteractionFlags(Qt.TextEditorInteraction)
        self.textWidth = 300
        self.minWidth = 60
        self.setDefaultTextColor(Qt.black)  # 默认文本颜色为黑色
        self.setTextWidthAndHeight()

    def setTextWidthAndHeight(self):
        option = QTextOption()
        option.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere)
        self.document().setDefaultTextOption(option)
        self.setTextWidth(-1)
        fm = self.document().documentLayout().documentSize().toSize()
        width = fm.width()
        
        if width > self.textWidth:
            self.setTextWidth(self.textWidth)
        elif width < self.minWidth:
            self.setTextWidth(self.minWidth)
        self.prepareGeometryChange()

    def paint(self, painter, option, widget):
        self.setTextWidthAndHeight()
        super().paint(painter, option, widget)

    def onGetWidthAndHeight(self):
        fm = self.document().documentLayout().documentSize().toSize()
        width = fm.width()
        height = fm.height()
        if width > self.textWidth:
            width = self.textWidth
        return width, height

PortItem

from PyQt5.QtWidgets import QGraphicsView
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QGraphicsItem
from PyQt5.QtCore import QPointF, QRectF, QLineF
from PyQt5.QtCore import Qt

from PyQt5.QtGui import QColor, QBrush, QPainter, QPen, QTransform
from PyQt5 import QtCore, QtGui, QtWidgets
from common.Constant import *

# 节点端口
class PortItem(QGraphicsItem):
    def __init__(self, x, y, parent=None):
        super(PortItem, self).__init__(parent)
        self.portDiam = 15
        self.width = 20
        self.height = 20
        self.is_select = False
        self.setPos(x, y)

	def boundingRect(self):
        return QRectF(0, 0, self.width, self.height)
	
	def paint(self, painter, style, *args, **kwargs):
        if self.is_select:
            portColor = QColor(0xaa, 0x00, 0x00, 0x66)
        else:
            portColor = QColor(0x00, 0xaa, 0x00, 0x66)
        painter.setBrush(portColor)
        pen = QPen()
        pen.setColor(portColor)
        pen.setWidth(2)
        painter.setPen(pen)
        painter.drawEllipse(0, 0, self.portDiam, self.portDiam)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值