首页 > 编程语言 >项目demo —— PyQt5简单画板程序

项目demo —— PyQt5简单画板程序

时间:2022-11-22 10:31:08浏览次数:37  
标签:node demo self 画板 PyQt5 mode action QtCore def


文章目录

  • ​​前情提要​​
  • ​​demo演示​​
  • ​​代码​​
  • ​​1. Node 节点类​​
  • ​​2. Canvas 画布类​​
  • ​​3. Editor 编辑器类​​

前情提要

  • 最近在看强化学习,想着快速做一个 MDP 的可视化,主体是一个画板,类似visio那样的,然后可以实时运行RL算法看价值变化情况
  • 但问题是,我可视化工具就会用一个PyQt5,还是半瓶水的水平…所以就想着不要造轮子。一开始感觉这东西和自动机(DFA)或者图灵机差不多,找个可视化的开源库改改就行了,实在不行思维导图的库可能也差不多。没想到啊,真就找不到这种库,又转头去找开源代码什么的,也是寥寥…
  • 最后狠下心来自己用 ​​QPainter​​ 实现,到目前为止已经搞了有六七个小时,越搞感觉越麻烦,代码也有混乱化的趋势,麻烦得不想弄了…
  • 没想到刚刚搜画箭头的方法,发现PyQt5居然有个图形视图框架(Graphics-View),就是专门用来做我这种东西的…裂开,怪不得找不到库…于是,果断放弃自己搞,回头有空学一下那个东西再说。
  • 耽误两天时间,教训是磨刀不误砍柴工…开学就要真正读研了,RL/DL/传统ML三个方向基础还没打好,烦
  • 现在这个半成品有点弃之可惜,干脆发上来水一篇博客好了

demo演示

  • 本来计划绘制的 MDP 示意图,大圈是状态节点,小圈是动作节点,中间用有向边连接
  • 项目demo —— PyQt5简单画板程序_QPainter

  • 目前只做了绘制圆形(状态/动作节点),拖动,缩放,框选等功能,演示如下
  • 项目demo —— PyQt5简单画板程序_pyqt5_02

  • 我做的这部分在图形视图框架中已经有了完整的轮子…

代码

  • 大概的思路是,
  1. 绘制部分作为一个widget对象嵌入主窗口,称其为 “画布”
  2. MDP 中的核心元素是节点和连线,于是把它们抽象为 “节点” 和 “连线” 对象,封装相关控制方法,由 “画布” 对象进行实例化、管理和控制
  3. 在 “画布” widget上重载各种鼠标事件,并利用 ​​QPainter​​ 提供的图形绘制方法绘图
  4. 主窗口其他部分实现外围功能,比如文件管理、RL算法测试等
  • 目前一共就做了节点、画布、主窗口(编辑器)这三个类,下面三段复制到三个.py文件,放在同一个文件夹下就可以运行了

1. Node 节点类

  • 主体是一个圆形,四周有四个控制点用于缩放大小
from PyQt5 import QtGui, QtCore, QtWidgets
import math

COLORS = {'state':'#DDDDDD','action':'#888888','select':'#FAC8C8'}
X = [-0.707,0.707,0.707,-0.707]
Y = [-0.707,-0.707,0.707,0.707]


def pointDistance(point1,point2):
deltaX = point1.x()-point2.x()
deltaY = point1.y()-point2.y()
return math.sqrt(deltaY*deltaY + deltaX*deltaX)

class Node:
def __init__(self,canvas,type,fixPoint,text='S'):
self.center = QtCore.QPoint() # 中心坐标
self.lastCenterPonit = None # 上次中心坐标
self.ctrlPoints = [QtCore.QPoint(),QtCore.QPoint(),QtCore.QPoint(),QtCore.QPoint()] # 控制点:左上/右上/右下/左下
self.ctrlPointsRadius = 4 # 控制点半径

self.fixPoint = fixPoint # init和resize时直径线段固定点
self.movePoint = None # init和resize时直径线段移动点
self.canvas = canvas # 画布对象引用
self.type = type # 'action' or 'state'
self.text = text # 提示文字
self.color = COLORS['select']

self.radius = 0
self.zoom = 1.0
self.selected = False # 处于选中状态
self.drawn = False # 绘制结束
self.resizing = False # 正在缩放

def setResizing(self,resizing,moveIndex=-1):
self.resizing = resizing
if not resizing:
self.fixPoint = None
elif moveIndex != -1:
fixIndex = moveIndex-2 if moveIndex >=2 else moveIndex+2
self.fixPoint = QtCore.QPoint(self.ctrlPoints[fixIndex])
print('set fix')
else:
assert False

def setSelected(self,selected):
self.selected = selected
if selected:
self.color = COLORS['select']
else:
self.color = COLORS[self.type]
self.lastCenterPonit = QtCore.QPoint(self.center)

def setDrawn(self,drawn):
self.drawn = drawn
if drawn:
self.resetCtrlPoints()

def setRadius(self,radius):
if radius < 0:
radius = 0
self.radius = radius

def getRadius(self):
return self.radius

def setColor(self,color):
self.color = color

def getType(self):
return self.type

def resize(self,endPoint):
self.center.setX(0.5*(self.fixPoint.x() + endPoint.x()))
self.center.setY(0.5*(self.fixPoint.y() + endPoint.y()))
self.radius = 0.5*pointDistance(endPoint,self.fixPoint)*self.zoom

def move(self,startPoint,endPoint):
self.center.setX(self.lastCenterPonit.x() + endPoint.x() - startPoint.x())
self.center.setY(self.lastCenterPonit.y() + endPoint.y() - startPoint.y())
self.resetCtrlPoints()

# 重置控制点
def resetCtrlPoints(self):
for i in range(4):
point = self.ctrlPoints[i]
point.setX(self.center.x()+X[i]*self.radius)
point.setY(self.center.y()+Y[i]*self.radius)

# 光标是否指向圆内
def cursorInside(self,cursorPos):
if not self.selected:
return pointDistance(cursorPos,self.center) < self.radius
else:
return pointDistance(cursorPos,self.center) < self.radius or self.cursorInCtrlPoint(cursorPos) != -1

# 光标指向哪个控制点
def cursorInCtrlPoint(self,cursorPos):
for i in range(4):
point = self.ctrlPoints[i]
if pointDistance(cursorPos,point) < self.ctrlPointsRadius:
return i
return -1

# 绘制控制点
def printCtrlPoint(self):
self.canvas.setPainterColor('#FFFFFF')
if not self.drawn:
for i in range(4):
point = QtCore.QPoint()
point.setX(self.center.x()+X[i]*self.radius)
point.setY(self.center.y()+Y[i]*self.radius)
self.canvas.painter.drawEllipse(point,self.ctrlPointsRadius,self.ctrlPointsRadius)
else:
for point in self.ctrlPoints:
self.canvas.painter.drawEllipse(point,self.ctrlPointsRadius,self.ctrlPointsRadius)

# 绘制节点
def print(self):
self.canvas.setPainterColor(self.color)
self.canvas.painter.drawEllipse(self.center,self.radius,self.radius)
self.canvas.painter.drawText(QtCore.QRect(self.center.x()-self.radius,self.center.y()-self.radius,2*self.radius,2*self.radius),QtCore.Qt.AlignCenter ,self.text)
if self.selected:
self.printCtrlPoint()

2. Canvas 画布类

  • 继承自 ​​QWidget​​​,核心是要重载鼠标实践,以及 ​​QPainter​​ 绘图
from PyQt5 import QtGui, QtCore, QtWidgets
from sklearn.metrics import pairwise
import numpy as np
import math
from Node import Node

class Canvas(QtWidgets.QWidget):
newNodeSignal = QtCore.pyqtSignal()

def __init__(self):
super().__init__()
self.brush = QtGui.QBrush(QtGui.QColor('#222222'),QtCore.Qt.SolidPattern)
self.painter = QtGui.QPainter(self)
self.nodes = [] # state node & action node

self.startPoint = QtCore.QPoint() # 光标拖动起点
self.endPoint = QtCore.QPoint() # 光标拖动终点

self.mode = 'select' # select/state/action/resize/boxing (选择/画状态点/画动作点/节点缩放/框选)
self.selectedNodes = [] # 目前选中的点
self.drawingNode = None # 正在初始化绘制的点

self.initUI()

def initUI(self):
pass

def setMode(self,mode):
if self.mode == 'select' and mode != 'resize':
self.clearSelected()
self.mode = mode

def setPainterColor(self,color):
self.brush.setColor(QtGui.QColor(color))
self.painter.setBrush(self.brush)

def mouseMoveEvent(self, ev):
pos = ev.pos() # 鼠标位置
if ev.buttons() & QtCore.Qt.LeftButton:
# 创建新节点时调整大小
if self.drawingNode != None and self.mode in ['state','action']:
self.drawingNode.resize(ev.pos())
# 框选
elif self.mode == 'boxing':
self.endPoint = ev.pos()
# 移动选中的节点
elif self.selectedNodes != [] and self.mode == 'select':
self.endPoint = ev.pos()
for node in self.selectedNodes:
node.move(self.startPoint,self.endPoint)
# 调整节点大小
elif len(self.selectedNodes) == 1 and self.mode == 'resize':
node = self.selectedNodes[0]
node.resize(ev.pos())
node.resetCtrlPoints()
else:
pass
self.update()

def mousePressEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
self.startPoint = self.endPoint = ev.pos()


# 创建新节点
if self.mode in ['state','action']:
self.drawingNode = Node(self,self.mode,ev.pos(),self.mode)
self.nodes.append(self.drawingNode)
# 选中节点调整大小
elif len(self.selectedNodes) == 1 and self.selectedNodes[0].cursorInCtrlPoint(ev.pos()) != -1:
self.setMode('resize')
self.selectedNodes[0].setResizing(True,self.selectedNodes[0].cursorInCtrlPoint(ev.pos()))
# 选择节点
elif self.nodes != [] and self.mode == 'select':
clickedSpace = True
# 点中节点,单点选择
for node in reversed(self.nodes): # 根据遮挡关系选中节点
if node.cursorInside(ev.pos()):
if len(self.selectedNodes) <= 1:
self.clearSelected() # 清除上次选中标记
node.setSelected(True)
self.selectedNodes = [node]
self.nodes.remove(node) # 选中的节点置于顶层
self.nodes.append(node)
clickedSpace = False
break
# 点中空白,框选
if clickedSpace:
self.setMode('boxing')
# 空白画布,框选模式
elif self.nodes == []:
self.setMode('boxing')
else:
pass
self.update()

def mouseReleaseEvent(self, ev):
if ev.button() == QtCore.Qt.LeftButton:
# 新节点已创建,转到选中模式
if self.mode in ['state','action']:
self.newNodeSignal.emit()
self.selectedNodes = [self.drawingNode]
self.drawingNode.setDrawn(True)
self.drawingNode.setSelected(True)
self.drawingNode = None
# 节点大小调整完成
elif self.mode == 'resize':
self.setMode('select')
self.selectedNodes[0].setResizing(False)
# 框选完毕
elif self.mode == 'boxing':
self.boxNodes()
self.startPoint.setX(0),self.startPoint.setY(0)
self.endPoint.setX(0),self.endPoint.setY(0)
self.setMode('select')
else:
pass
self.update()

def wheelEvent(self, ev):
angle = ev.angleDelta() / 8 # 返回QPoint对象,为滚轮转过的数值,单位为1/8度
angleY = angle.y() # 竖直滚过的距离
print(angleY)

#self.update()

def paintEvent(self,event):
self.painter.begin(self)
self.paintEvent = event
self.updateCanvas(self.painter)
self.painter.end()

def updateCanvas(self,painter):
# 节点
if self.nodes != []:
for node in self.nodes:
node.print()

# 选择框
self.painter.setBrush(QtCore.Qt.NoBrush)
self.painter.setPen(QtCore.Qt.darkGreen)
if self.mode == 'boxing':
self.painter.drawRect(self.startPoint.x(), self.startPoint.y(), self.endPoint.x() - self.startPoint.x(), self.endPoint.y() - self.startPoint.y())

def clearSelected(self):
if self.selectedNodes != []:
for node in self.selectedNodes:
node.setSelected(False)
self.selectedNodes = []
self.update()

def deleteNode(self):
if self.selectedNodes != []:
for node in self.selectedNodes:
self.nodes.remove(node)
self.selectedNodes = []
self.setMode('select')
self.update()

def boxNodes(self):
self.clearSelected()
for node in self.nodes:
if min(self.startPoint.x(),self.endPoint.x()) <= node.center.x() <= max(self.startPoint.x(),self.endPoint.x()) and \
min(self.startPoint.y(),self.endPoint.y()) <= node.center.y() <= max(self.startPoint.y(),self.endPoint.y()):
node.setSelected(True)
self.selectedNodes.append(node)
self.update()

3. Editor 编辑器类

  • 这个就是主窗口
# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtWidgets import QMainWindow, QApplication, QAction, QGraphicsView ,QScrollArea
from PyQt5.QtGui import QPainter, QPainterPath, QIcon
from PyQt5.QtCore import Qt
import sys
from Canvas import Canvas

class Editor(QMainWindow):

def __init__(self):
super().__init__()

self.canves = Canvas()
self.setupUi()
self.setupToolBar()

def setupUi(self):
self.setObjectName("EditorWindow")
self.resize(760, 544)

self.centralwidget = QtWidgets.QWidget(self)
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")

self.scroller = QScrollArea(self.centralwidget)
self.scrollerGridLayout = QtWidgets.QGridLayout(self.scroller)
self.scrollerGridLayout.setObjectName("scrollerGridLayout")
self.scrollerGridLayout.addWidget(self.canves,1,1,1,1)
self.scroller.setLayout(self.scrollerGridLayout)
self.gridLayout.addWidget(self.scroller,1,1,1,1)
self.setCentralWidget(self.centralwidget)

# 一旦绘制新节点,转入选中状态
self.canves.newNodeSignal.connect(lambda:self.setMode('select'))

def setupToolBar(self):
self.toolbar = self.addToolBar('toolbar')
self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)

action_new_file = QAction(QIcon('./images/filenew.png'), 'New File', self)
#action_new_file.triggered.connect(self.file_new)
self.toolbar.addAction(action_new_file)

action_save_file = QAction(QIcon('./images/filesave.png'), 'Save File', self)
#action_save_file.triggered.connect(self.file_save)
self.toolbar.addAction(action_save_file)

self.action_state_ponit = QAction(QIcon('./images/state.png'), 'State node', self)
self.action_state_ponit.triggered.connect(lambda:self.setMode('state'))
self.toolbar.addAction(self.action_state_ponit)

self.action_act_ponit = QAction(QIcon('./images/action.png'), 'Action node', self)
self.action_act_ponit.triggered.connect(lambda:self.setMode('action'))
self.toolbar.addAction(self.action_act_ponit)

self.action_selcet_ponit = QAction(QIcon('./images/select2.png'), 'select', self)
self.action_selcet_ponit.triggered.connect(lambda:self.setMode('select'))
self.toolbar.addAction(self.action_selcet_ponit)

action_delete_point = QAction(QIcon('./images/delete.png'), 'Delete node', self)
action_delete_point.triggered.connect(self.canves.deleteNode)
self.toolbar.addAction(action_delete_point)

def setMode(self,mode):
# 图标变化以指示当前模式(resize和boxing都属于select模式)
self.action_state_ponit.setIcon(QIcon('./images/state.png'))
self.action_act_ponit.setIcon(QIcon('./images/action.png'))
self.action_selcet_ponit.setIcon(QIcon('./images/select.png'))

if self.canves.mode == mode or mode == 'select':
self.action_selcet_ponit.setIcon(QIcon('./images/select2.png'))
mode == 'select'
elif mode == 'state':
self.action_state_ponit.setIcon(QIcon('./images/state2.png'))
elif mode == 'action':
self.action_act_ponit.setIcon(QIcon('./images/action2.png'))
else:
pass

# 设置模式
self.canves.setMode(mode)

if __name__ == '__main__':
app = QApplication(sys.argv)
editor = Editor()
editor.show()
sys.exit(app.exec_())


标签:node,demo,self,画板,PyQt5,mode,action,QtCore,def
From: https://blog.51cto.com/u_15887260/5876725

相关文章