
做了一个开箱即用的流程审批拖拽工具,包含核心功能:
直接复制代码就能运行,无需额外修改!
import sysimport jsonfrom PyQt5.QtWidgets import *from PyQt5.QtGui import *from PyQt5.QtCore import *# 审批节点类型NODE_TYPES = {"start": {"name": "发起人", "color": "#4CAF50"},"approve": {"name": "审批人", "color": "#2196F3"},"copy": {"name": "抄送人", "color": "#FF9800"},"end": {"name": "结束", "color": "#F44336"}}# 可拖拽的节点列表项classNodeListWidget(QListWidget):def__init__(self, parent=None): super().__init__(parent) self.setDragEnabled(True) # 允许拖拽 self.setFixedWidth(120) self.add_nodes()defadd_nodes(self):# 添加所有审批节点for node_type, info in NODE_TYPES.items(): item = QListWidgetItem(info["name"]) item.setData(Qt.UserRole, node_type)# 设置节点样式 item.setBackground(QColor(info["color"])) item.setForeground(QColor("white")) item.setTextAlignment(Qt.AlignCenter) self.addItem(item)defstartDrag(self, supportedActions): drag = QDrag(self) mimedata = QMimeData()# 获取选中的节点类型 current_item = self.currentItem() node_type = current_item.data(Qt.UserRole) mimedata.setText(node_type) drag.setMimeData(mimedata)# 拖拽预览样式 pixmap = QPixmap(100, 40) pixmap.fill(QColor(NODE_TYPES[node_type]["color"])) painter = QPainter(pixmap) painter.setPen(QColor("white")) painter.drawText(pixmap.rect(), Qt.AlignCenter, current_item.text()) painter.end() drag.setPixmap(pixmap) drag.setHotSpot(QPoint(50, 20)) drag.exec_(Qt.CopyAction)# 画布上的节点控件classCanvasNode(QWidget):def__init__(self, node_type, parent=None): super().__init__(parent) self.node_type = node_type self.info = NODE_TYPES[node_type] self.setFixedSize(100, 40) self.setCursor(Qt.OpenHandCursor) self.setToolTip(f"{self.info['name']}\n右键可删除")defpaintEvent(self, event): painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing)# 绘制节点背景 painter.setBrush(QColor(self.info["color"])) painter.setPen(Qt.NoPen) painter.drawRoundedRect(self.rect(), 8, 8)# 绘制文字 painter.setPen(QColor("white")) painter.setFont(QFont("Arial", 10, QFont.Bold)) painter.drawText(self.rect(), Qt.AlignCenter, self.info["name"])# 流程画布(支持拖拽、连线、右键菜单)classFlowCanvas(QWidget): node_moved = pyqtSignal() # 节点移动信号def__init__(self, parent=None): super().__init__(parent) self.setAcceptDrops(True) self.nodes = [] # 所有节点 self.lines = [] # 所有连线 self.dragging_node = None self.drag_offset = QPoint() self.setStyleSheet("background-color: #f5f5f5;")defdragEnterEvent(self, event):if event.mimeData().hasText(): event.acceptProposedAction()defdropEvent(self, event):# 创建新节点 node_type = event.mimeData().text() node = CanvasNode(node_type, self) pos = event.pos() - QPoint(50, 20) node.move(pos) node.show() self.nodes.append(node) self.node_moved.emit() event.acceptProposedAction()defmousePressEvent(self, event):# 右键菜单if event.button() == Qt.RightButton: self.show_context_menu(event.pos())return# 左键拖动节点for node in self.nodes:if node.geometry().contains(event.pos()): self.dragging_node = node self.drag_offset = event.pos() - node.pos() self.setCursor(Qt.ClosedHandCursor)breakdefmouseMoveEvent(self, event):if self.dragging_node: self.dragging_node.move(event.pos() - self.drag_offset) self.node_moved.emit()defmouseReleaseEvent(self, event):if self.dragging_node: self.setCursor(Qt.OpenHandCursor) self.dragging_node = NonedefpaintEvent(self, event): super().paintEvent(event) painter = QPainter(self) painter.setRenderHint(QPainter.Antialiasing) painter.setPen(QPen(QColor("#666"), 2))# 绘制节点连线(按顺序连接)for i in range(len(self.nodes)-1): n1 = self.nodes[i] n2 = self.nodes[i+1] p1 = n1.pos() + QPoint(n1.width()//2, n1.height()//2) p2 = n2.pos() + QPoint(n2.width()//2, n2.height()//2) painter.drawLine(p1, p2)defshow_context_menu(self, pos): menu = QMenu(self)# 删除选中节点 delete_action = menu.addAction("删除节点")# 清空画布 clear_action = menu.addAction("清空画布") action = menu.exec_(self.mapToGlobal(pos))if action == delete_action:for node in self.nodes:if node.geometry().contains(pos): node.deleteLater() self.nodes.remove(node) self.node_moved.emit() self.update()returnelif action == clear_action:for node in self.nodes: node.deleteLater() self.nodes.clear() self.update()# 主窗口classMainWindow(QMainWindow):def__init__(self): super().__init__() self.setWindowTitle("流程审批拖拽工具") self.setGeometry(100, 100, 1000, 700) self.init_ui()definit_ui(self):# 主布局 main_widget = QWidget() self.setCentralWidget(main_widget) layout = QHBoxLayout(main_widget) layout.setContentsMargins(5,5,5,5) layout.setSpacing(5)# 左侧节点库 self.node_list = NodeListWidget() layout.addWidget(self.node_list)# 中间画布 self.canvas = FlowCanvas() layout.addWidget(self.canvas) self.canvas.node_moved.connect(self.canvas.update)# 顶部工具栏 self.init_toolbar()definit_toolbar(self): toolbar = QToolBar() self.addToolBar(toolbar)# 保存按钮 save_btn = QAction("保存流程", self) save_btn.triggered.connect(self.save_flow) toolbar.addAction(save_btn)# 加载按钮 load_btn = QAction("加载流程", self) load_btn.triggered.connect(self.load_flow) toolbar.addAction(load_btn) toolbar.addSeparator()# 说明 toolbar.addWidget(QLabel("操作:左键拖拽 | 右键删除/清空"))defsave_flow(self):# 保存为JSON文件ifnot self.canvas.nodes: QMessageBox.warning(self, "提示", "画布上没有流程节点!")return path, _ = QFileDialog.getSaveFileName(self, "保存流程", "", "JSON Files (*.json)")ifnot path:return data = {"nodes": [ {"type": node.node_type,"x": node.x(),"y": node.y() } for node in self.canvas.nodes ] }with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) QMessageBox.information(self, "成功", "流程已保存!")defload_flow(self):# 加载JSON流程 path, _ = QFileDialog.getOpenFileName(self, "加载流程", "", "JSON Files (*.json)")ifnot path:returntry:with open(path, "r", encoding="utf-8") as f: data = json.load(f)# 清空画布for node in self.canvas.nodes: node.deleteLater() self.canvas.nodes.clear() self.canvas.update()# 重建节点for item in data["nodes"]: node = CanvasNode(item["type"], self.canvas) node.move(item["x"], item["y"]) node.show() self.canvas.nodes.append(node) self.canvas.update() QMessageBox.information(self, "成功", "流程加载完成!")except Exception as e: QMessageBox.critical(self, "错误", f"加载失败:{str(e)}")if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec_())pip install pyqt5这个工具已经实现了你要的拖拽、画布、保存、右键操作全部核心功能,代码结构清晰,直接可用,也方便你后续二次开发。