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
    评论
首先,我对蓝图的理解相对通俗,就是觉得蓝图对于视图方法模块化、大项目协同开发过程中的一个很好的工具. 1.下图是我们通常情况下使用的项目组织结构 看一下视图方法: #views.py 1 from app import app 2 3 4 @app.route('/user/index') 5 def index(): 6 return 'user_index' 7 8 @app.route('/user/show') 9 def show(): 10 return 'user_show' 11 12 @app.route('/user/add') 13 def add(): 14 return 'user_add' 15 16 @app.route('/admin/index') 17 def adminindex(): 18 return 'admin_index' 19 20 @app.route('/admin/show') 21 def adminshow(): 22 return 'admin_show' 23 24 @app.route('/admin/add') 25 def adminadd(): 26 return 'admin_add' #从视图方法中,我们看到有6个视图,分别对应admin,user两个不同用户的3个功能index,add,show. 这样写显然没问题,但是明显与python提倡的模块化,优雅的代码特点相违背,即不是很pythonpython代码. 让我们在这里假想两个问题: 1.如果admin和user不止只有3个功能呢,比如好几百个,导致views的代码量已经达到了上万行? 2.如果我们有多个同事分工开发admin,user和其它可能的模块呢,都同时往一个views里写代码吗,在做版本控制时,提交过程中频繁出现提交冲突了怎么办? 3.加入我们要抛弃admin或者user功能块时怎么办,要一一手动删除所有admin或是user相关的代码吗,这样当然可以,但是会不会太low呢? 当然根据Pythonic特点,我们肯定希望尽可能的把代码尽量的模块化,让我们的代码看起来更加的优雅和顺畅,这个时候flask.Blueprint(蓝图)就派上用场了 什么是蓝图? 一个蓝图定义了可用于单个应用的视图,模板,静态文件等等的集合。 我什么时候会用到蓝图蓝图的杀手锏是将你的应用组织成不同的组件,比如把这里的admin,user相关的视图方法分为两个组件,一个是admin组件,一个是user组件.这时我们可以 创建两个蓝图实现这两个独立的组件. 2.我们如何使用蓝本将上述的视图方法看上去更加pythonic呢? 由于上面的例子中只有两个组件(模块)admin,user,我们可以创建名为admin.py和user.py的两个文件,分别在里面创建两个蓝图的实例对象admin,user. 直接上代码: #admin.py #admin.py from flask import Blueprint,render_template, request admin = Blueprint('admin',__name__) @admin.route('/index') def index(): return render_template('admin/index.html') @admin.route('/add') def add(): return 'admin_add' @admin.route('/show') def show(): return 'admin_show' #要想创建一个蓝图对象,你需要import flask. Blueprint() 类并用参数 name 和 import_name 初始化。import_name通常用 __name__ ,一个表示当前模块的特殊的Python变量,作为 import_name 的取值。 #user.py from flask import Blueprint, render_template, redirect user = Blueprint('user',__name__) @user.route('/index') def index(): return render_template('user/index.html') @user.route('/add') def add(): return 'user_add' @user.route('/show') def show(): return 'user_show' 好了,视图函数已经分开了,我们如何使用它们的呢?再来看一下我们的views.py变成了什么样吧? #views.py from app import app from .admin import admin from .user import user #这里分别给app注册了两个蓝图admin,user #参数url_prefix='/xxx'的意思是设置request.url中的url前缀, #即当request.url是以/admin或者/user的情况下才会通过注册的蓝图的视图方法处理请求并返回 app.register_blueprint(admin,url_prefix='/admin') app.register_blueprint(user, url_prefix='/user') #现在我们的views是否已经变得很简单了呢?顺便回答第三个问题,如果想弃用某一个组件(模块)我们只需要相对应的注释掉给app注册蓝图的行即可. #细心的伙伴还可以发现,在views.py中在使用默认endpoint的前提下,我们是没有办法使用同一个视图方法名的(当然我们也不建议在同一个文件中有两个视图方法名相同, #尽管指向他们的request.url不同),但是使用了蓝图之后我们就可以在不同模块中使用相同的方法名了,例如add,show.. 3.到此我们就可以通过浏览器测试我们的程序 4.附上使用蓝图后的项目组织结构 当然如果项目不大的话就没有什么必要使用蓝图了,甚至我们可以把除了所有css,js,html的代码都写到一个文件中去,这里我们不在举例,说明.

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值