原因
策划的需求真是奇葩,自己策划的人生模拟器玩法,自己设计的配表方式,结果因为配置太繁琐不知道配了,到最后还是要程序来背锅,给他们做个类似蓝图的东西给他们配表,不过最后做的好像更像思维导图0.0
环境
- Python 3.7.7
- PyQt5
- Qt Designer
- pandas
基本功能
- 节点创建、删除、链接、拖拽、搜索
- 界面缩放、移动
- 快捷键保存临时文件
- 读取excel或历史记录,自动生成节点
- 按照界面节点生成新的excel文件配置
- 界面自适应大小
结构
主要类
类 | 作用 |
---|---|
MainWindow | 用Qt Designer生成的主界面 |
EditorView | 继承自QGraphicsView的2D视图管理 |
EditorScene | 继承自QGraphicsScene的场景管理 |
LineItem | 线条和箭头 |
NodeItem | 节点 |
PortItem | 节点上的点 |
MessageItem | 节点上的文字,更具文字多少自适应大小 |
SelectItem | 右键选择创建节点的下拉框 |
OptionItem | 下拉框的选项 |
Dispatcher | 消息注册分发器,单例 |
DataBase | 数据model基类,当值改变时自动分发消息 |
包含关系
直接贴代码
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)