Flowable7.x学习笔记(二十)查看流程办理进度图
前言
本文是基于继承Flowable的【DefaultProcessDiagramCanvas】和【DefaultProcessDiagramGenerator】实现的自定义流程图生成器,通过自定义流程图生成器可以灵活的指定已经执行过的节点和当前正在活跃的节点样式,比如说已经执行完成的节点我们标绿,正在处理的节点标红,这样我们就可以直观的看到进度了。
一、自定义CustomProcessDiagramCanvas
CustomProcessDiagramCanvas 类继承自 Flowable 的 DefaultProcessDiagramCanvas,主要功能是定制流程图绘制样式(如任务框颜色、高亮颜色、连线颜色等)。类中定义了若干静态颜色属性(例如高亮连线颜色、字体颜色、任务框背景色等)。
这些属性用于在后续方法中设置不同元素的绘制样式。该类的构造方法接受画布尺寸、最小坐标、图像类型和字体名称等参数,并在调用父类构造后执行 initialize(imageType) 方法进行画布初始化。
总体来看,类的结构包括:静态属性(颜色常量)、构造函数、初始化方法 以及若干绘制方法(如绘制任务框、绘制连线、绘制标签、高亮绘制等)。
① 构造方法与初始化
构造方法:调用父类构造函数将参数传入 DefaultProcessDiagramCanvas 进行基本初始化,然后调用自定义的 initialize(imageType) 方法进行额外的配置。
initialize 方法:此方法根据图像格式决定是否启用透明通道:若为 PNG,则创建含透明通道的 BufferedImage 实例,否则使用 RGB 模式;并通过 Graphics2D 对象设置背景、字体、抗锯齿等绘制环境。例如:对于非 PNG 格式,调用 g.setBackground(new Color(255,255,255,0)) 将背景设为透明并清空画布。此方法还负责加载流程图所需的图标资源(此部分代码在父类中实现,当前类主要聚焦画布初始化)。总体而言,initialize 方法确保画布(BufferedImage)创建并配置好绘图参数。
② 绘制连线:drawConnection
drawConnection(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault, String connectionType, AssociationDirection associationDirection, boolean highLighted, double scaleFactor) 方法用于绘制流程中的连线(SequenceFlow 或 Association 等)。
主要逻辑包括:首先保存原始画笔(Paint)和线型(Stroke)样式,然后根据是否为关联线(connectionType.equals("association"))或是否需要高亮(highLighted)设置不同的画笔样式。
例如,对于普通连线使用黑色实线,对于“关联”类型使用虚线,对于高亮连线则将颜色设为绿色粗线。之后,方法通过遍历坐标点数组(xPoints, yPoints)逐段绘制折线。接着,根据 isDefault 标志绘制默认流向指示器、根据 conditional 绘制条件流向箭头,并根据 associationDirection 绘制单向或双向关联箭头。最后恢复画笔和线条样式。该方法综合了连线各种可能的装饰(箭头、条件标记、高亮等),是流程图连线绘制的核心之一。
③ 绘制文本标签:drawLabel
drawLabel(String text, GraphicInfo graphicInfo, boolean centered)方法用于在画布上绘制文字标签,支持自动换行和居中对齐。方法先检查文本是否非空,然后保存当前画笔和字体状态,接着设置为标签专用的字体与颜色(LABEL_FONT, LABEL_COLOR)。
文本换行宽度被设为固定的 wrapWidth = 100 像素。利用 AttributedString 和 LineBreakMeasurer(Java AWT 字体布局工具),方法将长文本自动拆分成多行。对于每一行文本:计算行的边界框(TextLayout.getBounds),并根据 centered 标志决定是否水平居中。
然后调用 TextLayout.draw 在指定坐标绘制文字。完成所有行绘制后恢复原始字体和画笔。整个流程确保在给定框内自动换行,通过 LineBreakMeasurer 实现的动态换行机制,可以根据 wrapWidth 自动断行,使文字在任务框或流程元素标签中不至于超出边界。
④ 绘制任务节点:drawTask
protected void drawTask(String name, GraphicInfo graphicInfo, boolean thickBorder, double scaleFactor)方法负责绘制流程图中的任务节点(Activity)矩形框。步骤如下:
(1)保存原始画笔状态
(2)提取任务框的位置和尺寸(x, y, width, height)
(3)设置任务框填充色为默认的 TASK_BOX_COLOR(父类定义,一般为白色)
(4)根据参数 thickBorder 决定圆角矩形的弧度(arcR),粗边框时更圆一些
(5)使用 RoundRectangle2D 对象创建带圆角的矩形并填充背景
(6)切换画笔颜色为 TASK_BORDER_COLOR(任务框边框色)
(7)如果 thickBorder 为真,则临时设置粗线样式绘制边框,否则用默认线条绘制
(8)恢复原始画笔颜色
(9)如果当前缩放比例是 1.0(实际大小)且任务名称非空,则计算文本绘制区域并使用 drawLabel 在框内居中绘制任务名称
该方法最终绘制一个填充背景的圆角矩形任务框,并在其中绘制任务名文字,使流程中的任务节点可视化。
⑤ 绘制高亮边框:drawHighLight
本类重载了几种高亮绘制方法,用于标记流程执行状态:
(1)drawHighLight(int x, int y, int width, int height)
绘制一个带圆角的粗线绿色边框。实现上先保存当前画笔和画笔粗细,然后将画笔颜色设为绿色(HIGHLIGHT_COLOR),线条样式设为粗边框(THICK_TASK_BORDER_STROKE)。再用 RoundRectangle2D 绘制一个圆角矩形边框,最后恢复原始画笔和线条。
(2)drawHighLightNow(int x, int y, int width, int height)
用于绘制当前正在执行的任务。该方法颜色为红色,表示正在进行的任务边框(可见 [8] 后面被调用的代码实现,需要引用父类或此类中定义的红。
(3)drawHighLightEnd(int x, int y, int width, int height)
用于绘制结束事件的高亮。通常对结束节点使用不同的高亮样式,以区别于一般任务。实现上可以与 drawHighLight 类似,只是颜色或线型不同。
⑥ 完整代码
package com.ceair.config;import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.AssociationDirection;
import org.flowable.bpmn.model.GraphicInfo;
import org.flowable.image.impl.DefaultProcessDiagramCanvas;
import org.flowable.image.util.ReflectUtil;import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.font.LineBreakMeasurer;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;/*** @author wangbaohai* @ClassName ProcessDiagramConfig* @description: 流程图配置类* @date 2025年05月05日* @version: 1.0.0*/
@Slf4j
public class CustomProcessDiagramCanvas extends DefaultProcessDiagramCanvas {// 定义走过流程连线颜色为绿色protected static Color HIGHLIGHT_SequenceFlow_COLOR = Color.GREEN;// 设置未走过流程的连接线颜色protected static Color CONNECTION_COLOR = Color.BLACK;// 设置flows连接线字体颜色redprotected static Color LABEL_COLOR = new Color(0, 0, 0);// 高亮显示task框颜色protected static Color HIGHLIGHT_COLOR = Color.GREEN;protected static Color HIGHLIGHT_COLOR1 = Color.RED;// 设置任务框颜色protected static Color EVENT_COLOR = new Color(255, 255, 255);/*** 构造函数用于初始化自定义流程图画布** @param width 画布的宽度* @param height 画布的高度* @param minX 画布的最小X坐标* @param minY 画布的最小Y坐标* @param imageType 图像类型* @param activityFontName 活动字体名称* @param labelFontName 标签字体名称* @param annotationFontName 注释字体名称* @param customClassLoader 自定义类加载器*/public CustomProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType,String activityFontName, String labelFontName, String annotationFontName,ClassLoader customClassLoader) {// 调用父类构造函数进行初始化super(width, height, minX, minY, imageType, activityFontName, labelFontName, annotationFontName,customClassLoader);// 初始化画布this.initialize(imageType);}/*** 初始化流程图画布并配置图形绘制环境。* <p>* 该方法负责:* - 根据图像类型创建 BufferedImage 实例* - 设置画布背景、字体样式、抗锯齿等绘图参数* - 加载所有流程图所需的图标资源(如任务、事件、连接线箭头等)** @param imageType 图像类型,用于判断是否启用透明通道("png" 启用)*/@Overridepublic void initialize(String imageType) {// 初始化画布:根据图像类型选择是否启用透明通道int bufferedImageType = "png".equalsIgnoreCase(imageType)? BufferedImage.TYPE_INT_ARGB // 含透明通道(PNG): BufferedImage.TYPE_INT_RGB; // 不含透明通道(非PNG)// 创建指定大小的图像缓冲区作为绘图画布this.processDiagram = new BufferedImage(this.canvasWidth, this.canvasHeight, bufferedImageType);// 获取 Graphics2D 对象用于后续绘图操作this.g = this.processDiagram.createGraphics();// 非PNG格式下手动设置背景为透明并清空内容if (!"png".equalsIgnoreCase(imageType)) {// 设置背景颜色为透明(白色+0透明度)this.g.setBackground(new Color(255, 255, 255, 0));// 清除当前画布区域,确保背景干净this.g.clearRect(0, 0, this.canvasWidth, this.canvasHeight);}// 启用抗锯齿渲染,提高图形绘制质量this.g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);// 设置默认绘图颜色为黑色this.g.setPaint(Color.BLACK);// 创建主活动字体(加粗 14 号字体)Font font = new Font(this.activityFontName, Font.BOLD, 14);this.g.setFont(font); // 应用字体到画布this.fontMetrics = this.g.getFontMetrics(); // 获取字体度量信息用于文本布局// 设置标签文字字体(加粗 15 号)LABEL_FONT = new Font(this.labelFontName, Font.BOLD, 15);// 设置注释文字字体(普通 11 号)ANNOTATION_FONT = new Font(this.annotationFontName, Font.PLAIN, 11);// 加载流程图所需的所有图标资源try {// 用户任务图标USERTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/userTask.png",this.customClassLoader));// 脚本任务图标SCRIPTTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/scriptTask.png",this.customClassLoader));// 服务任务图标SERVICETASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/serviceTask.png",this.customClassLoader));// 接收任务图标RECEIVETASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/receiveTask.png",this.customClassLoader));// 发送任务图标SENDTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/sendTask.png",this.customClassLoader));// 案例任务图标CASETASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/caseTask.png",this.customClassLoader));// 手动任务图标MANUALTASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/manualTask.png",this.customClassLoader));// 业务规则任务图标BUSINESS_RULE_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/businessRuleTask.png", this.customClassLoader));// Shell任务图标SHELL_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/shellTask.png",this.customClassLoader));// 决策任务图标DMN_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/dmnTask.png",this.customClassLoader));// Camel任务图标CAMEL_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/camelTask.png",this.customClassLoader));// HTTP任务图标HTTP_TASK_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/httpTask.png",this.customClassLoader));// 定时器图标TIMER_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/timer.png", this.customClassLoader));// 补偿抛出图标COMPENSATE_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/compensate-throw.png",this.customClassLoader));// 补偿捕获图标COMPENSATE_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/compensate.png",this.customClassLoader));// 条件捕获图标CONDITIONAL_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/conditional.png",this.customClassLoader));// 错误抛出图标ERROR_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/error-throw.png",this.customClassLoader));// 错误捕获图标ERROR_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/error.png",this.customClassLoader));// 升级抛出图标ESCALATION_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/escalation-throw.png",this.customClassLoader));// 升级捕获图标ESCALATION_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/escalation.png",this.customClassLoader));// 消息抛出图标MESSAGE_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/message-throw.png",this.customClassLoader));// 消息捕获图标MESSAGE_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/message.png",this.customClassLoader));// 信号抛出图标SIGNAL_THROW_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/signal-throw.png",this.customClassLoader));// 信号捕获图标SIGNAL_CATCH_IMAGE = ImageIO.read(ReflectUtil.getResource("org/flowable/icons/signal.png",this.customClassLoader));} catch (IOException e) {// 日志记录加载失败原因log.warn("Could not load image for process diagram creation: {}", e.getMessage());}}/*** 重写绘制连接线的方法,支持多种类型连线样式、高亮显示及方向箭头。** @param xPoints X轴坐标点数组* @param yPoints Y轴坐标点数组* @param conditional 是否为条件流向* @param isDefault 是否为默认流向* @param connectionType 连接线类型(如 "association")* @param associationDirection 关联方向(ONE, BOTH)* @param highLighted 是否高亮显示* @param scaleFactor 缩放比例因子,用于图像缩放处理*/@Overridepublic void drawConnection(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault,String connectionType, AssociationDirection associationDirection, boolean highLighted,double scaleFactor) {// 保存原始画笔和线条样式,便于后续恢复Paint originalPaint = g.getPaint();Stroke originalStroke = g.getStroke();// 设置默认连接线颜色g.setPaint(CONNECTION_COLOR);// 根据连接线类型设置不同的线条样式if ("association".equals(connectionType)) {// 如果是关联线,使用虚线样式g.setStroke(ASSOCIATION_STROKE);} else if (highLighted) {// 如果是高亮状态,使用绿色粗线样式g.setPaint(HIGHLIGHT_SequenceFlow_COLOR);g.setStroke(HIGHLIGHT_FLOW_STROKE);}// 绘制主连接线段:从起点到终点依次绘制折线for (int i = 1; i < xPoints.length; i++) {drawLine(xPoints[i - 1], yPoints[i - 1], xPoints[i], yPoints[i]);}// 如果是默认流向,绘制默认箭头指示器if (isDefault) {Line2D.Double line = createLine(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);drawDefaultSequenceFlowIndicator(line, scaleFactor);}// 如果是条件流向,绘制条件箭头指示器if (conditional) {Line2D.Double line = createLine(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);drawConditionalSequenceFlowIndicator(line, scaleFactor);}// 如果是单向或双向关联,绘制箭头头部if (associationDirection == AssociationDirection.ONE || associationDirection == AssociationDirection.BOTH) {Line2D.Double line = createLine(xPoints[xPoints.length - 2],yPoints[xPoints.length - 2],xPoints[xPoints.length - 1],yPoints[xPoints.length - 1]);drawArrowHead(line, scaleFactor);}// 如果是双向关联,再反向绘制一个箭头if (associationDirection == AssociationDirection.BOTH) {Line2D.Double line = createLine(xPoints[1], yPoints[1], xPoints[0], yPoints[0]);drawArrowHead(line, scaleFactor);}// 恢复画笔与线条样式至初始状态g.setPaint(originalPaint);g.setStroke(originalStroke);}/*** 绘制流程图中的文本标签。* <p>* 该方法负责:* - 根据给定文本内容和图形信息绘制多行文本* - 支持居中对齐与左对齐两种方式* - 使用当前画笔设置的字体和颜色进行绘制* - 自动换行处理,最大宽度为 wrapWidth(100像素)** @param text 要绘制的文本内容* @param graphicInfo 图形位置信息对象* @param centered 是否居中显示文本*/@Overridepublic void drawLabel(String text, GraphicInfo graphicInfo, boolean centered) {// 行间距系数,默认为1倍float interline = 1.0f;try {// 只有非空字符串才进行绘制if (text != null && !text.isEmpty()) {// 保存当前画笔和字体状态,便于绘制结束后恢复Paint originalPaint = g.getPaint();Font originalFont = g.getFont();// 设置标签专用颜色和字体g.setPaint(LABEL_COLOR);g.setFont(LABEL_FONT);int wrapWidth = 100; // 每行最大宽度,超过则换行int textY = (int) graphicInfo.getY(); // 初始 Y 坐标// 创建带样式属性的字符串,用于支持复杂排版AttributedString as = new AttributedString(text);as.addAttribute(TextAttribute.FOREGROUND, g.getPaint()); // 文本颜色as.addAttribute(TextAttribute.FONT, g.getFont()); // 字体样式// 获取字符迭代器和字体渲染上下文AttributedCharacterIterator aci = as.getIterator();FontRenderContext frc = new FontRenderContext(null, true, false);// 使用 LineBreakMeasurer 实现自动换行功能LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);// 循环绘制每一行文本while (lbm.getPosition() < text.length()) {TextLayout tl = lbm.nextLayout(wrapWidth); // 获取一行布局textY += (int) tl.getAscent(); // 移动到文字顶部基准线Rectangle2D bb = tl.getBounds(); // 获取当前行边界框double tX = graphicInfo.getX(); // 起始 X 坐标// 如果需要居中显示,则调整 X 坐标使其水平居中if (centered) {tX += (int) (graphicInfo.getWidth() / 2 - bb.getWidth() / 2);}// 在指定坐标上绘制当前行文本tl.draw(g, (float) tX, textY);// 更新 Y 坐标:移动下一行的基线位置textY += (int) (tl.getDescent() + tl.getLeading() + (interline - 1.0f) * tl.getAscent());}// 恢复原始字体和画笔颜色g.setFont(originalFont);g.setPaint(originalPaint);}} catch (Exception e) {// 出现任何异常时记录日志并继续执行log.warn("绘制标签失败,文本内容: [{}], 异常原因: {}", text, e.getMessage());}}/*** 绘制高亮显示的任务矩形框边框。* <p>* 该方法用于绘制一个带有圆角的矩形边框,表示任务已被高亮选中。* - 使用预定义的高亮颜色和粗线样式进行绘制* - 在绘制前后会保存并恢复画布状态(颜色和笔触)** @param x 矩形左上角 X 坐标* @param y 矩形左上角 Y 坐标* @param width 矩形宽度* @param height 矩形高度*/@Overridepublic void drawHighLight(int x, int y, int width, int height) {try {// 保存当前画笔颜色和线条样式,以便绘制完成后恢复Paint originalPaint = g.getPaint();Stroke originalStroke = g.getStroke();// 设置高亮颜色和粗线样式g.setPaint(HIGHLIGHT_COLOR);g.setStroke(THICK_TASK_BORDER_STROKE);// 创建一个带圆角的矩形区域(圆角大小为 20x20)RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);// 在画布上绘制矩形边框g.draw(rect);// 恢复原始画笔颜色和线条样式,保证不影响后续绘制g.setPaint(originalPaint);g.setStroke(originalStroke);} catch (Exception e) {// 出现异常时记录警告信息,避免流程图生成中断log.warn("绘制高亮框失败,位置: ({}, {}), 尺寸: {}x{}, 异常原因: {}", x, y, width, height, e.getMessage());}}/*** 绘制任务框图形。* <p>* 该方法用于绘制流程图中的任务节点矩形框,并支持:* - 圆角矩形填充与边框绘制* - 边框粗细控制(thickBorder)* - 文本居中显示(仅当 scaleFactor == 1.0 时生效)** @param name 要绘制的任务名称文本* @param graphicInfo 图形位置信息对象(包含坐标和尺寸)* @param thickBorder 是否使用粗边框样式* @param scaleFactor 缩放比例因子,影响绘制大小*/@Overrideprotected void drawTask(String name, GraphicInfo graphicInfo, boolean thickBorder, double scaleFactor) {try {// 保存当前画笔颜色,便于后续恢复原始状态Paint originalPaint = g.getPaint();// 提取图形绘制区域的位置和尺寸信息int x = (int) graphicInfo.getX();int y = (int) graphicInfo.getY();int width = (int) graphicInfo.getWidth();int height = (int) graphicInfo.getHeight();// 设置任务框填充颜色(统一为 TASK_BOX_COLOR)g.setPaint(TASK_BOX_COLOR);// 根据是否为粗边框决定圆角大小int arcR = thickBorder ? 3 : 6;// 创建带圆角的矩形区域(用于任务框形状)RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, arcR, arcR);// 填充任务框背景色g.fill(rect);// 设置任务框边框颜色g.setPaint(TASK_BORDER_COLOR);// 如果是粗边框,则临时更换画笔样式进行绘制if (thickBorder) {Stroke originalStroke = g.getStroke(); // 保存原线条样式g.setStroke(THICK_TASK_BORDER_STROKE); // 使用粗线样式g.draw(rect); // 绘制边框g.setStroke(originalStroke); // 恢复原线条样式} else {// 否则直接使用默认线条样式绘制边框g.draw(rect);}// 恢复原始画笔颜色,确保不影响其他图形绘制g.setPaint(originalPaint);// 只有在非缩放状态下且存在文本内容时才绘制文字if (scaleFactor == 1.0 && name != null && !name.isEmpty()) {// 计算文本绘制区域的宽度和高度int boxWidth = width - (2 * TEXT_PADDING);int boxHeight = height - 16 - ICON_PADDING - ICON_PADDING - MARKER_WIDTH - 2 - 2;// 计算文本水平居中 X 坐标int boxX = x + width / 2 - boxWidth / 2;// 计算文本垂直居中 Y 坐标(考虑图标和标记的偏移量)int boxY = y + height / 2 - boxHeight / 2 + ICON_PADDING + ICON_PADDING - 2 - 2;// 调用工具方法绘制多行居中文本drawMultilineCentredText(name, boxX, boxY, boxWidth, boxHeight);}} catch (Exception e) {// 出现异常时记录警告日志,避免中断流程图生成log.warn("绘制任务框失败,任务名: [{}], 异常原因: {}", name, e.getMessage());}}/*** 绘制流程图中的开始事件图形(圆形)。* <p>* 该方法负责:* - 使用预定义颜色绘制一个填充圆表示开始事件* - 若存在图标图像,则将其居中绘制于圆内* - 支持根据缩放因子调整图像大小** @param graphicInfo 图形位置和尺寸信息* @param image 要绘制的图标图像(可为 null)* @param scaleFactor 缩放比例因子,用于图像缩放计算*/@Overridepublic void drawStartEvent(GraphicInfo graphicInfo, BufferedImage image, double scaleFactor) {try {// 保存当前画笔颜色,便于后续恢复Paint originalPaint = g.getPaint();// 设置填充颜色并创建椭圆(圆形)区域g.setPaint(EVENT_COLOR);Ellipse2D circle = new Ellipse2D.Double(graphicInfo.getX(),graphicInfo.getY(),graphicInfo.getWidth(),graphicInfo.getHeight());// 填充圆形背景g.fill(circle);// 设置边框颜色并绘制圆形轮廓g.setPaint(EVENT_BORDER_COLOR);g.draw(circle);// 恢复原始画笔颜色g.setPaint(originalPaint);// 如果提供了图标图像,则进行绘制if (image != null) {// 计算图像绘制起始点 X 坐标,使其水平居中int imageX = (int) Math.round(graphicInfo.getX() + (graphicInfo.getWidth() / 2) -(image.getWidth() / (2 * scaleFactor)));// 计算图像绘制起始点 Y 坐标,使其垂直居中int imageY = (int) Math.round(graphicInfo.getY() + (graphicInfo.getHeight() / 2) -(image.getHeight() / (2 * scaleFactor)));// 绘制缩放后的图像g.drawImage(image,imageX,imageY,(int) (image.getWidth() / scaleFactor),(int) (image.getHeight() / scaleFactor),null);}} catch (Exception e) {// 异常时记录警告日志,避免中断流程图生成log.warn("绘制开始事件失败,位置: ({}, {}), 异常原因: {}",graphicInfo.getX(), graphicInfo.getY(), e.getMessage());}}/*** 绘制流程图中的结束事件图形(空心圆)。* <p>* 该方法负责:* - 使用预定义颜色绘制一个填充圆形表示结束事件* - 根据缩放因子设置不同的边框粗细* - 在绘制前后保存并恢复画布状态(颜色和线条样式)** @param graphicInfo 图形位置和尺寸信息* @param scaleFactor 缩放比例因子,影响图像绘制大小*/@Overridepublic void drawNoneEndEvent(GraphicInfo graphicInfo, double scaleFactor) {try {// 保存当前画笔颜色和线条样式,便于后续恢复Paint originalPaint = g.getPaint();Stroke originalStroke = g.getStroke();// 设置圆形填充颜色g.setPaint(EVENT_COLOR);// 创建椭圆(圆形)区域Ellipse2D circle = new Ellipse2D.Double(graphicInfo.getX(),graphicInfo.getY(),graphicInfo.getWidth(),graphicInfo.getHeight());// 填充圆形背景g.fill(circle);// 设置边框颜色g.setPaint(EVENT_BORDER_COLOR);// 根据缩放比例设置不同粗细的边框if (scaleFactor == 1.0) {// 正常比例下使用预定义粗线样式g.setStroke(END_EVENT_STROKE);} else {// 缩放状态下使用固定宽度的粗线样式(2像素)g.setStroke(new BasicStroke(2.0f));}// 绘制圆形边框g.draw(circle);// 恢复原始画笔颜色和线条样式g.setStroke(originalStroke);g.setPaint(originalPaint);} catch (Exception e) {// 异常时记录警告日志,避免中断流程图生成log.warn("绘制结束事件失败,位置: ({}, {}), 异常原因: {}",graphicInfo.getX(), graphicInfo.getY(), e.getMessage());}}/*** 绘制当前任务位置的高亮矩形框。* <p>* 该方法用于绘制一个带有圆角的矩形边框,表示当前任务正在执行或被选中。* - 使用预定义的高亮颜色(红色)和粗线样式进行绘制* - 在绘制前后会保存并恢复画布状态(颜色和笔触),避免影响其他图形** @param x 矩形左上角 X 坐标* @param y 矩形左上角 Y 坐标* @param width 矩形宽度* @param height 矩形高度*/public void drawHighLightNow(int x, int y, int width, int height) {try {// 保存当前画笔颜色和线条样式,便于绘制完成后恢复Paint originalPaint = g.getPaint();Stroke originalStroke = g.getStroke();// 设置高亮颜色为红色(HIGHLIGHT_COLOR1)g.setPaint(HIGHLIGHT_COLOR1);// 设置粗线样式用于高亮边框g.setStroke(THICK_TASK_BORDER_STROKE);// 创建一个带圆角的矩形区域(固定圆角大小为 20x20)RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);// 绘制矩形边框g.draw(rect);// 恢复原始画笔颜色和线条样式,保证不影响后续绘图g.setPaint(originalPaint);g.setStroke(originalStroke);} catch (Exception e) {// 异常时记录警告日志,避免中断流程图生成log.warn("绘制当前任务高亮框失败,位置: ({}, {}), 尺寸: {}x{}, 异常原因: {}",x, y, width, height, e.getMessage());}}/*** 绘制流程图中的结束任务高亮矩形框。* <p>* 该方法用于绘制一个带有圆角的矩形边框,表示流程结束节点。* - 使用预定义的高亮颜色(绿色)和粗线样式进行绘制* - 在绘制前后保存并恢复画布状态(颜色和线条样式),避免影响其他图形** @param x 矩形左上角 X 坐标* @param y 矩形左上角 Y 坐标* @param width 矩形宽度* @param height 矩形高度*/public void drawHighLightEnd(int x, int y, int width, int height) {try {// 保存当前画笔颜色和线条样式,便于后续恢复Paint originalPaint = g.getPaint();Stroke originalStroke = g.getStroke();// 设置高亮颜色为绿色(HIGHLIGHT_COLOR)g.setPaint(HIGHLIGHT_COLOR);// 设置粗线样式用于高亮边框g.setStroke(THICK_TASK_BORDER_STROKE);// 创建带圆角的矩形区域(固定圆角大小为 20x20)RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);// 在画布上绘制矩形边框g.draw(rect);// 恢复原始画笔颜色和线条样式,保证不影响后续绘图操作g.setPaint(originalPaint);g.setStroke(originalStroke);} catch (Exception e) {// 出现异常时记录警告日志,避免中断流程图生成log.warn("绘制结束节点高亮框失败,位置: ({}, {}), 尺寸: {}x{}, 异常原因: {}",x, y, width, height, e.getMessage());}}/*** 快捷绘制一条直线段。** @param x1 起始X坐标* @param y1 起始Y坐标* @param x2 结束X坐标* @param y2 结束Y坐标*/private void drawLine(int x1, int y1, int x2, int y2) {g.draw(new Line2D.Double(x1, y1, x2, y2));}/*** 构造一个新的 Line2D.Double 对象。** @param x1 起始X坐标* @param y1 起始Y坐标* @param x2 结束X坐标* @param y2 结束Y坐标* @return 构造完成的 Line2D.Double 线段对象*/private Line2D.Double createLine(int x1, int y1, int x2, int y2) {return new Line2D.Double(x1, y1, x2, y2);}}
这三种方法配合在流程图中高亮不同角色的节点,增强流程状态可视化。它们的调用时机在 CustomProcessDiagramGenerator.drawActivity 方法中决定(见下文)。
二、自定义CustomProcessDiagramGenerator
CustomProcessDiagramGenerator 继承自 Flowable 的 DefaultProcessDiagramGenerator,用于根据 BPMN 模型生成流程图画布并绘制流程元素。这个定制类主要通过覆盖父类的绘图流程,扩展了流程图的初始化和节点绘制过程,例如自定义画布大小、调用 CustomProcessDiagramCanvas、支持高亮指定节点/连线等。
① 初始化画布并计算边界:initProcessDiagramCanvas
protected static DefaultProcessDiagramCanvas initProcessDiagramCanvas(BpmnModel bpmnModel, String imageType, String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) 方法负责根据 BPMN 模型中所有元素位置计算画布尺寸,并创建 CustomProcessDiagramCanvas 实例。
(1)边界初始化
设定初始的 minX
为极大值、minY
为极大值,maxX
和 maxY
为 0。这样便于后续比较逐步缩小/扩大边界范围。
(2)遍历 Pool(泳道池)
对于每个 Pool,获取其图形信息 GraphicInfo(包含位置和尺寸)。调用 updateBoundaryWithSingleGraphic 工具方法更新边界。此方法检查图形的位置和大小,将 minX, maxX, minY, maxY 调整到包含该 Pool。
(3)遍历所有 FlowNode(任务、事件、网关等)
通过 gatherAllFlowNodes(bpmnModel) 获得所有节点。对每个节点,若存在图形信息,则调用 updateBoundaryWithSingleGraphic 更新边界。然后遍历其所有出站连线(SequenceFlow),获取连线路径坐标列表 pathCoordinates。对每条连线的路径,通过 updateBoundaryWithPathCoordinates 更新边界。这样逐条连线的中间点也计入边界计算。
(4)遍历 Artifact(注释、组等)
对每个 Artifact 类似地更新边界。同时获取每个 Artifact 关联的路径坐标(如连接线对应的图形信息),通过 updateBoundaryWithPathCoordinates 更新边界。
(5)遍历 Lane(泳道)
对每个 Lane 更新边界。处理无图元素情形:如果没有任何节点、泳道、泳道,则将 minX = 0, minY = 0,避免出现空白图。
(6)创建画布
最后,计算好的边界值用于创建 CustomProcessDiagramCanvas 对象:宽度取 maxX + 10 (留10px边距),高度取 maxY + 10;最小坐标取 minX, minY。
(7)updateBoundaryWithSingleGraphic
updateBoundaryWithSingleGraphic(GraphicInfo graphicInfo, double[] boundaries)是一个辅助方法:给定一个元素的 GraphicInfo(含 X, Y, width, height),它检查该元素的四条边是否超出当前 boundaries,并更新 boundaries 的最小/最大值。
(8)updateBoundaryWithPathCoordinates
updateBoundaryWithPathCoordinates(List<GraphicInfo> pathCoordinates, double[] boundaries)则遍历路径上的每个点(GraphicInfo),判断该点的坐标是否超出当前边界并相应更新。这样路径折线也参与边界计算。
② 绘制流程图:generateProcessDiagram
整个 generateProcessDiagram 确保按层次先后绘制池、泳道、节点和附属元素,最后返回 CustomProcessDiagramCanvas 作为结果画布。如果中间发生异常,会打印警告日志并返回一个空画布,保证流程不中断。
(1)准备模型
调用 prepareBpmnModel(父类逻辑)预处理模型,如添加默认值等。
(2)初始化画布
调用 initProcessDiagramCanvas(上面描述)得到配置好的 CustomProcessDiagramCanvas。
(3)绘制 Pool 和 Lane
遍历模型中的所有 Pool,如果找到相应图形信息,则调用 diagramCanvas.drawPoolOrLane(name, graphicInfo, scaleFactor) 绘制泳道池背景和名称。随后遍历所有 Process 中的 Lane,若有图形信息则也使用 drawPoolOrLane 绘制。
(4)绘制节点
再次遍历每个 Process 的所有 FlowNode(任务、事件、网关等)。对于每个节点,若不属于已折叠的子流程(isPartOfCollapsedSubProcess 判断),调用 drawActivity(diagramCanvas, bpmnModel, currentNode, highLightedActivities, highLightedFlows, scaleFactor, drawSequenceFlowNameWithNoLabelDI)。drawActivity 方法负责具体绘制当前节点及其出站连线。绘制 Artifact 和 SubProcess 里的元素:最后,遍历所有 Process 的 Artifact 元素(注释、组等非执行对象),调用 drawArtifact 绘制。同时,对每个子流程(SubProcess),若其处于展开状态,也递归绘制其中的 Artifact。
③ 绘制活动节点:drawActivity
综合运用了 ActivityDrawInstruction 分发机制:通过 activityDrawInstructions 映射自动选择合适的绘制指令(例如任务、事件、网关各有不同的绘制策略)。这使得 CustomProcessDiagramGenerator 不需要逐一识别每种节点类型,而是复用父类预定义好的绘制行为。然后再在基础绘制之上添加标记、多实例图标、以及依据高亮条件额外绘制高亮边框。
(1)基础绘制指令
通过 activityDrawInstructions 映射查找 FlowNode 类型对应的绘制指令(ActivityDrawInstruction 对象)。如果找到了,就调用 drawInstruction.draw(processDiagramCanvas, bpmnModel, flowNode),让它执行节点形状的基础绘制(例如画圆圈、矩形等,这些指令由父类 DefaultProcessDiagramGenerator 初始化时填充)。
(2)多实例和折叠标志
检查当前 FlowNode 是否是 Activity(例如任务或子流程),若包含多实例标记(MultiInstanceLoopCharacteristics),则设置 multiInstanceSequential 或 multiInstanceParallel 标志。同时检查是否为折叠的子流程或调用子流程(SubProcess 或 CallActivity)。
(3)绘制标记图标
若缩放比例为 1.0,调用 processDiagramCanvas.drawActivityMarkers(x, y, width, height, multiInstanceSequential, multiInstanceParallel, collapsed)在节点图形的右下角绘制多实例条并行标记或折叠图标。这是 Flowable 的标准做法,用于指示任务是否有并行或串行多实例,或是否为已折叠的子流程。
(4)高亮节点
判断当前节点 ID 是否在高亮列表 highLightedActivities 中。如果是:
若该节点是列表中的最后一个(即当前执行节点),并且节点 ID 不含 "endenv",则根据节点类型选择不同的高亮样式:如果 ID 包含 "Event_",调用 drawHighLightEnd(canvas, graphicInfo) 绘制结束事件样式,否则调用 drawHighLightNow(canvas, graphicInfo) 绘制“当前执行”样式否则(高亮节点中非最后一个,表示已完成节点),调用 drawHighLight(canvas, graphicInfo)绘制普通高亮边框。通过这个逻辑,高亮列表中最后一个被视为“进行中节点”,用不同颜色显示;其余高亮节点用绿色边框表示曾经过的步骤。
(5)绘制出站连线
遍历节点的所有出站 SequenceFlow。对每条连线,判断其是否需要高亮(ID 在 highLightedFlows 中)。计算是否为默认流向(根据活动或网关的 defaultFlow 属性),以及是否需要绘制条件标记(有条件表达式且非网关)。获取连线起点和终点元素,通过 bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId()) 得到路径点列表。对路径列表调用 connectionPerfectionizer 优化折线路径(父类工具方法)。然后将路径坐标转换为整数数组 xPoints, yPoints。调用 processDiagramCanvas.drawSequenceflow(xPoints, yPoints, drawConditionalIndicator, isDefault, highLighted, scaleFactor) 绘制连线。若连线有标签,则调用 drawLabel 在指定位置绘制连线名称;如果允许无标签位置绘制(drawSequenceFlowNameWithNoLabelDI),则计算连线路径中心点并调用 drawLabel。
(6)递归绘制嵌套元素
如果当前 flowNode 是 FlowElementsContainer(例如 SubProcess),则遍历其内部的 FlowElement,对其中未折叠的子节点递归调用 drawActivity。这样保证子流程内部的节点也能被绘制和高亮。
(7)异常处理
整个绘制过程包裹在 try-catch 中,如果出现异常打印警告日志,不影响整个图形生成。
④ 完整代码
package com.ceair.config;import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
import org.flowable.image.impl.DefaultProcessDiagramCanvas;
import org.flowable.image.impl.DefaultProcessDiagramGenerator;import java.util.Iterator;
import java.util.List;/*** @author wangbaohai* @ClassName CustomProcessDiagramGenerator* @description: 定义流程图生成器,继承自 Flowable 的 DefaultProcessDiagramGenerator,* 提供了对流程图绘制过程的扩展和定制化支持,例如高亮显示特定节点。* @date 2025年05月06日* @version: 1.0.0*/
@Slf4j
public class CustomProcessDiagramGenerator extends DefaultProcessDiagramGenerator {/*** 初始化流程图画布,并根据 BPMN 模型图形信息计算画布尺寸。** <p>该方法会遍历以下元素来确定画布的最大边界:* - Pool:泳道池* - FlowNode:活动节点(任务、事件、网关等)* - SequenceFlow:连接线路径* - Artifact:非执行元素(如注释、组等)* - Lane:泳道区域** <p>最终基于这些元素的位置和大小,创建一个包含适当边距的 CustomProcessDiagramCanvas 实例。** @param bpmnModel BPMN 模型对象,用于获取图形信息* @param imageType 图像格式(如 png、jpeg)* @param activityFontName 活动文本字体名称* @param labelFontName 标签文本字体名称* @param annotationFontName 注解文本字体名称* @param customClassLoader 自定义类加载器,用于加载图标资源等* @return DefaultProcessDiagramCanvas 返回初始化好的画布对象*//*** 初始化流程图画布,并根据 BPMN 模型图形信息计算画布尺寸。** <p>该方法会遍历以下元素来确定画布的最大边界:* - Pool:泳道池* - FlowNode:活动节点(任务、事件、网关等)* - SequenceFlow:连接线路径* - Artifact:非执行元素(如注释、组等)* - Lane:泳道区域** <p>最终基于这些元素的位置和大小,创建一个包含适当边距的 CustomProcessDiagramCanvas 实例。** @param bpmnModel BPMN 模型对象,用于获取图形信息* @param imageType 图像格式(如 png、jpeg)* @param activityFontName 活动文本字体名称* @param labelFontName 标签文本字体名称* @param annotationFontName 注解文本字体名称* @param customClassLoader 自定义类加载器,用于加载图标资源等* @return DefaultProcessDiagramCanvas 返回初始化好的画布对象*/protected static DefaultProcessDiagramCanvas initProcessDiagramCanvas(BpmnModel bpmnModel, String imageType,String activityFontName,String labelFontName,String annotationFontName,ClassLoader customClassLoader) {// 初始最小值设为极大值,便于后续比较取最小值double minX = 1.7976931348623157E308D; // 双精度最大正值(约等于无穷大)double maxX = 0.0D;double minY = 1.7976931348623157E308D;double maxY = 0.0D;// 获取并遍历所有 Pool 泳道池GraphicInfo poolGraphicInfo;Iterator<?> poolIterator = bpmnModel.getPools().iterator();// 遍历所有 Pool 泳道池,更新画布最大 X 和 Y 值while (poolIterator.hasNext()) {try {Pool currentPool = (Pool) poolIterator.next();poolGraphicInfo = bpmnModel.getGraphicInfo(currentPool.getId());// 将当前边界封装进数组,传入工具方法进行更新double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithSingleGraphic(poolGraphicInfo, boundaries);// 更新主变量minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];} catch (Exception e) {log.warn("处理 Pool 节点时发生异常: {}", e.getMessage(), e);}}// 收集所有 FlowNode 节点,并开始遍历处理其位置信息List<FlowNode> flowNodes = gatherAllFlowNodes(bpmnModel);Iterator<FlowNode> flowNodeIterator = flowNodes.iterator();// label155 用于 continue 控制多层循环退出当前 FlowNode 的处理label155:while (flowNodeIterator.hasNext()) {try {FlowNode currentFlowNode = flowNodeIterator.next();GraphicInfo flowNodeGraphicInfo = bpmnModel.getGraphicInfo(currentFlowNode.getId());if (flowNodeGraphicInfo != null) {// 如果图形信息存在,则使用工具方法更新边界double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithSingleGraphic(flowNodeGraphicInfo, boundaries);minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];}// 遍历出站连线(SequenceFlow),进一步扩展画布边界Iterator<SequenceFlow> sequenceFlowIterator = currentFlowNode.getOutgoingFlows().iterator();while (true) {List<GraphicInfo> pathCoordinates;do {// 若无更多 SequenceFlow,则跳转到 label155,进入下一个 FlowNodeif (!sequenceFlowIterator.hasNext()) {continue label155;}SequenceFlow sequenceFlow = sequenceFlowIterator.next();pathCoordinates = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());} while (pathCoordinates == null); // 忽略空路径数据// 使用路径坐标集合更新边界double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithPathCoordinates(pathCoordinates, boundaries);minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];}} catch (Exception e) {log.warn("处理 FlowNode 节点时发生异常", e);}}// 收集所有 Artifact 并处理其图形信息List<Artifact> artifactList = gatherAllArtifacts(bpmnModel);Iterator<Artifact> artifactIterator = artifactList.iterator();GraphicInfo artifactGraphicInfo;while (artifactIterator.hasNext()) {try {Artifact currentArtifact = artifactIterator.next();artifactGraphicInfo = bpmnModel.getGraphicInfo(currentArtifact.getId());if (artifactGraphicInfo != null) {// 处理单个 Artifact 元素的图形信息double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithSingleGraphic(artifactGraphicInfo, boundaries);minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];}// 获取 Artifact 关联的路径信息,继续扩展画布List<GraphicInfo> artifactPathCoordinates =bpmnModel.getFlowLocationGraphicInfo(currentArtifact.getId());// 使用路径点集合更新画布边界double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithPathCoordinates(artifactPathCoordinates, boundaries);minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];} catch (Exception e) {log.warn("处理 Artifact 元素时发生异常", e);}}// 遍历 Process 中的 Lane 泳道区域int laneCount = 0;Iterator<?> processIterator = bpmnModel.getProcesses().iterator();while (processIterator.hasNext()) {try {Process currentProcess = (Process) processIterator.next();for (Lane currentLane : currentProcess.getLanes()) {++laneCount;GraphicInfo laneGraphicInfo = bpmnModel.getGraphicInfo(currentLane.getId());// 更新泳道的边界信息double[] boundaries = {minX, maxX, minY, maxY};updateBoundaryWithSingleGraphic(laneGraphicInfo, boundaries);minX = boundaries[0];maxX = boundaries[1];minY = boundaries[2];maxY = boundaries[3];}} catch (Exception e) {log.warn("处理 Process 或 Lane 时发生异常", e);}}// 如果没有任何 FlowNode、Pool 和 Lane,则从原点开始绘制if (flowNodes.isEmpty() && bpmnModel.getPools().isEmpty() && laneCount == 0) {minX = 0.0D;minY = 0.0D;}// 创建并返回自定义流程图绘制画布对象,+10 是为了留出边距return new CustomProcessDiagramCanvas((int) maxX + 10, // 加 10 像素边距(int) maxY + 10,(int) minX,(int) minY,imageType,activityFontName,labelFontName,annotationFontName,customClassLoader);}/*** 在流程图中绘制高亮区域* 此方法用于在给定的流程图画布上,根据图形信息对象中的坐标和尺寸数据,绘制一个高亮区域* 主要用于强调或突出流程图中的特定部分,以便用户能够快速识别** @param processDiagramCanvas 流程图画布对象,提供绘制方法* @param graphicInfo 图形信息对象,包含绘制高亮所需的坐标和尺寸信息*/private static void drawHighLight(DefaultProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {// 调用画布的绘制高亮方法,传入根据图形信息对象提取的位置和尺寸参数processDiagramCanvas.drawHighLight((int) graphicInfo.getX(),(int) graphicInfo.getY(),(int) graphicInfo.getWidth(),(int) graphicInfo.getHeight());}/*** 在流程图中绘制高亮当前步骤的边框* 此方法用于在图形化界面中突出显示当前操作的步骤,通过传递图形信息对象来确定绘制的位置和大小** @param processDiagramCanvas 流程图画布对象,用于绘制高亮边框* @param graphicInfo 图形信息对象,包含绘制高亮边框所需的位置和尺寸信息*/private static void drawHighLightNow(CustomProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {// 调用CustomProcessDiagramCanvas的drawHighLightNow方法,传入转换为整型的图形信息的坐标和尺寸,以绘制高亮边框processDiagramCanvas.drawHighLightNow((int) graphicInfo.getX(),(int) graphicInfo.getY(),(int) graphicInfo.getWidth(),(int) graphicInfo.getHeight());}/*** 在流程图中绘制高亮结束点* 该方法用于在流程图中绘制一个高亮的结束点,通过提供的图形信息确定位置和大小** @param processDiagramCanvas 流程图画布对象,用于绘制高亮结束点* @param graphicInfo 图形信息对象,包含绘制所需的位置和大小信息*/private static void drawHighLightEnd(CustomProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {// 调用画布的绘制高亮结束点方法,传入转换为整数的图形信息坐标和尺寸processDiagramCanvas.drawHighLightEnd((int) graphicInfo.getX(),(int) graphicInfo.getY(),(int) graphicInfo.getWidth(),(int) graphicInfo.getHeight());}/*** 更新画布边界,基于一个 GraphicInfo 对象的位置和尺寸。** @param graphicInfo 包含图形位置信息的对象* @param boundaries 当前画布边界 [minX, maxX, minY, maxY]*/private static void updateBoundaryWithSingleGraphic(GraphicInfo graphicInfo, double[] boundaries) {if (graphicInfo != null) {// 如果当前图形的右边界大于最大 X,则更新 maxXif (graphicInfo.getX() + graphicInfo.getWidth() > boundaries[1]) {boundaries[1] = graphicInfo.getX() + graphicInfo.getWidth();}// 如果当前图形的左边界小于最小 X,则更新 minXif (graphicInfo.getX() < boundaries[0]) {boundaries[0] = graphicInfo.getX();}// 如果当前图形的下边界大于最大 Y,则更新 maxYif (graphicInfo.getY() + graphicInfo.getHeight() > boundaries[3]) {boundaries[3] = graphicInfo.getY() + graphicInfo.getHeight();}// 如果当前图形的上边界小于最小 Y,则更新 minYif (graphicInfo.getY() < boundaries[2]) {boundaries[2] = graphicInfo.getY();}}}/*** 基于路径点集合更新画布边界。** @param pathCoordinates 路径上的多个坐标点* @param boundaries 当前画布边界 [minX, maxX, minY, maxY]*/private static void updateBoundaryWithPathCoordinates(List<GraphicInfo> pathCoordinates, double[] boundaries) {if (pathCoordinates != null && !pathCoordinates.isEmpty()) {for (GraphicInfo point : pathCoordinates) {// 如果当前点的 X 大于最大 X,则更新 maxXif (point.getX() > boundaries[1]) {boundaries[1] = point.getX();}// 如果当前点的 X 小于最小 X,则更新 minXif (point.getX() < boundaries[0]) {boundaries[0] = point.getX();}// 如果当前点的 Y 大于最大 Y,则更新 maxYif (point.getY() > boundaries[3]) {boundaries[3] = point.getY();}// 如果当前点的 Y 小于最小 Y,则更新 minYif (point.getY() < boundaries[2]) {boundaries[2] = point.getY();}}}}/*** 生成流程图并绘制 BPMN 模型的可视化表示。** <p>该方法负责:* - 初始化画布尺寸与字体配置* - 绘制 Pool 和 Lane 等容器元素* - 遍历所有 FlowNode 并调用 drawActivity 方法绘制活动节点* - 绘制 Artifact 元素和 SubProcess 中的嵌套内容* - 支持高亮显示特定活动和连线* - 处理缩放比例和标签绘制策略** @param bpmnModel BPMN 模型对象* @param imageType 图像格式(如 png)* @param highLightedActivities 需要高亮的活动节点 ID 列表* @param highLightedFlows 需要高亮的连接线 ID 列表* @param activityFontName 活动文本字体名称* @param labelFontName 标签文本字体名称* @param annotationFontName 注解文本字体名称* @param customClassLoader 自定义类加载器(用于图标资源等)* @param scaleFactor 缩放因子,控制图像大小* @param drawSequenceFlowNameWithNoLabelDI 是否在无标签位置时也绘制序列流名称* @return DefaultProcessDiagramCanvas 返回生成好的画布对象*/@Overrideprotected DefaultProcessDiagramCanvas generateProcessDiagram(BpmnModel bpmnModel, String imageType,List<String> highLightedActivities, List<String> highLightedFlows,String activityFontName, String labelFontName, String annotationFontName,ClassLoader customClassLoader,double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {try {// 准备 BPMN 模型数据,可能包含对节点位置信息、连接线路径等的预处理this.prepareBpmnModel(bpmnModel);// 根据 BPMN 模型中的图形信息初始化画布大小,并设置字体样式等配置DefaultProcessDiagramCanvas diagramCanvas = initProcessDiagramCanvas(bpmnModel, imageType,activityFontName, labelFontName, annotationFontName, customClassLoader);// 遍历所有 Pool(泳道池),绘制每个 Pool 的边界框及名称for (Pool currentPool : bpmnModel.getPools()) {// 获取当前 Pool 的图形信息(如位置、尺寸)GraphicInfo poolGraphicInfo = bpmnModel.getGraphicInfo(currentPool.getId());// 如果图形信息存在,则调用画布绘制该 Poolif (poolGraphicInfo != null) {diagramCanvas.drawPoolOrLane(currentPool.getName(), poolGraphicInfo, scaleFactor);}}// 遍历所有 Process,进而遍历其 Lane(泳道)并绘制Iterator<?> processIterator = bpmnModel.getProcesses().iterator();while (processIterator.hasNext()) {Process currentProcess = (Process) processIterator.next();// 遍历当前 Process 中定义的所有 Lanefor (Lane currentLane : currentProcess.getLanes()) {// 获取当前 Lane 的图形信息GraphicInfo laneGraphicInfo = bpmnModel.getGraphicInfo(currentLane.getId());// 如果图形信息存在,则调用画布绘制该 Laneif (laneGraphicInfo != null) {diagramCanvas.drawPoolOrLane(currentLane.getName(), laneGraphicInfo, scaleFactor);}}}// 再次遍历所有 Process,绘制其中的 FlowNode 节点(任务、事件、网关等)processIterator = bpmnModel.getProcesses().iterator();while (processIterator.hasNext()) {Process currentProcess = (Process) processIterator.next();// 查找当前 Process 下所有 FlowNode 类型的元素(流程节点)for (FlowNode currentNode : currentProcess.findFlowElementsOfType(FlowNode.class)) {// 如果当前节点不在折叠的子流程中,则进行绘制if (!this.isPartOfCollapsedSubProcess(currentNode, bpmnModel)) {// 绘制活动节点,并根据高亮列表进行高亮显示this.drawActivity(diagramCanvas, bpmnModel, currentNode, highLightedActivities,highLightedFlows, scaleFactor, drawSequenceFlowNameWithNoLabelDI);}}}// 最后一次遍历 Process,绘制 Artifact 和 SubProcess 中的嵌套内容processIterator = bpmnModel.getProcesses().iterator();label75:while (true) {List<SubProcess> subProcessList;do {// 如果没有更多 Process,结束循环并返回最终画布if (!processIterator.hasNext()) {return diagramCanvas;}// 获取当前 Process 实例Process currentProcess = (Process) processIterator.next();// 遍历当前 Process 下所有的 Artifact(注释、组等非执行元素)for (Artifact currentArtifact : currentProcess.getArtifacts()) {// 绘制 Artifact 元素到画布上this.drawArtifact(diagramCanvas, bpmnModel, currentArtifact);}// 查找当前 Process 及其子流程下的所有 SubProcess 元素subProcessList = currentProcess.findFlowElementsOfType(SubProcess.class, true);} while (subProcessList == null); // 如果未找到 SubProcess,继续下一个 Process// 获取 SubProcess 列表的迭代器Iterator<SubProcess> subProcessIterator = subProcessList.iterator();while (true) {GraphicInfo graphicInfo;SubProcess currentSubProcess;do {do {// 如果没有更多 SubProcess,跳出内层循环,继续外层 Process 循环if (!subProcessIterator.hasNext()) {continue label75;}// 获取当前 SubProcess 实例currentSubProcess = subProcessIterator.next();// 获取该 SubProcess 的图形信息graphicInfo = bpmnModel.getGraphicInfo(currentSubProcess.getId());} while (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded());} while (this.isPartOfCollapsedSubProcess(currentSubProcess, bpmnModel));// 遍历当前 SubProcess 下的所有 Artifact 并绘制for (Artifact subProcessArtifact : currentSubProcess.getArtifacts()) {this.drawArtifact(diagramCanvas, bpmnModel, subProcessArtifact);}}}} catch (Exception e) {// 异常捕获:记录日志但不中断流程图生成过程log.warn("生成流程图失败,原因: {}", e.getMessage(), e);// 返回一个默认尺寸为 0 的空画布作为兜底方案,防止程序崩溃return new DefaultProcessDiagramCanvas(0, 0, 0, 0, imageType, activityFontName, labelFontName,annotationFontName, customClassLoader);}}/*** 绘制流程图中的活动节点(如任务、事件等)并支持高亮显示。* <p>* 该方法负责:* - 调用对应 ActivityDrawInstruction 实现绘制基本图形* - 处理多实例标记和折叠状态标识* - 支持高亮当前活动节点(根据 highLightedActivities 列表判断)* - 绘制出站连线(SequenceFlow)及其标签* - 对嵌套元素递归绘制** @param processDiagramCanvas 当前绘图画布对象* @param bpmnModel BPMN 模型数据* @param flowNode 当前要绘制的流程节点(FlowNode)* @param highLightedActivities 高亮活动 ID 列表* @param highLightedFlows 高亮连接线 ID 列表* @param scaleFactor 缩放比例因子,用于图像缩放计算* @param drawSequenceFlowNameWithNoLabelDI 是否在无标签信息时也绘制序列流名称*/@Overrideprotected void drawActivity(DefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel,FlowNode flowNode, List<String> highLightedActivities, List<String> highLightedFlows,double scaleFactor, Boolean drawSequenceFlowNameWithNoLabelDI) {try {// 获取对应的绘制指令,若存在则进行绘制ActivityDrawInstruction drawInstruction = activityDrawInstructions.get(flowNode.getClass());if (drawInstruction != null) {// 执行基础图形绘制drawInstruction.draw(processDiagramCanvas, bpmnModel, flowNode);// 初始化多实例标记相关标志boolean multiInstanceSequential = false;boolean multiInstanceParallel = false;boolean collapsed = false;// 如果是 Activity 类型,检查是否为多实例任务if (flowNode instanceof Activity activity) {MultiInstanceLoopCharacteristics loopCharacteristics = activity.getLoopCharacteristics();if (loopCharacteristics != null) {multiInstanceSequential = loopCharacteristics.isSequential();multiInstanceParallel = !multiInstanceSequential;}}// 判断当前节点是否为折叠状态(SubProcess 或 CallActivity)GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());if (flowNode instanceof SubProcess) {collapsed = graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded();} else if (flowNode instanceof CallActivity) {collapsed = true;}// 若为标准缩放比例(1.0),则绘制多实例或折叠图标标记if (scaleFactor == 1.0 && graphicInfo != null) {processDiagramCanvas.drawActivityMarkers((int) graphicInfo.getX(),(int) graphicInfo.getY(),(int) graphicInfo.getWidth(),(int) graphicInfo.getHeight(),multiInstanceSequential,multiInstanceParallel,collapsed);}// 判断是否需要高亮当前节点if (highLightedActivities != null && highLightedActivities.contains(flowNode.getId())) {// 如果是最后一个高亮节点且不是 "endenv" 类型,则绘制“当前执行”样式if (highLightedActivities.get(highLightedActivities.size() - 1).equals(flowNode.getId()) && !"endenv".equals(flowNode.getId())) {// 如果是 Event 类型,使用结束高亮样式;否则使用当前高亮样式if (flowNode.getId().contains("Event_")) {drawHighLightEnd((CustomProcessDiagramCanvas) processDiagramCanvas,bpmnModel.getGraphicInfo(flowNode.getId()));} else {drawHighLightNow((CustomProcessDiagramCanvas) processDiagramCanvas,bpmnModel.getGraphicInfo(flowNode.getId()));}} else {// 否则使用普通高亮框样式if (graphicInfo != null) {drawHighLight(processDiagramCanvas, graphicInfo);}}}}// 绘制所有出站连接线(SequenceFlow)for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {boolean highLighted = highLightedFlows != null && highLightedFlows.contains(sequenceFlow.getId());// 判断是否为默认流向String defaultFlow = null;if (flowNode instanceof Activity) {defaultFlow = ((Activity) flowNode).getDefaultFlow();} else if (flowNode instanceof Gateway) {defaultFlow = ((Gateway) flowNode).getDefaultFlow();}boolean isDefault = defaultFlow != null && defaultFlow.equalsIgnoreCase(sequenceFlow.getId());// 判断是否需要绘制条件指示器boolean drawConditionalIndicator =sequenceFlow.getConditionExpression() != null && !(flowNode instanceof Gateway);// 获取源与目标元素String sourceRef = sequenceFlow.getSourceRef();String targetRef = sequenceFlow.getTargetRef();FlowElement sourceElement = bpmnModel.getFlowElement(sourceRef);FlowElement targetElement = bpmnModel.getFlowElement(targetRef);// 获取路径坐标点列表List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());if (graphicInfoList != null && !graphicInfoList.isEmpty()) {// 使用 connectionPerfectionizer 优化路径走向graphicInfoList = connectionPerfectionizer(processDiagramCanvas, bpmnModel, sourceElement,targetElement, graphicInfoList);// 构建 x 和 y 坐标数组int[] xPoints = new int[graphicInfoList.size()];int[] yPoints = new int[graphicInfoList.size()];for (int i = 0; i < graphicInfoList.size(); i++) {GraphicInfo info = graphicInfoList.get(i);xPoints[i] = (int) info.getX();yPoints[i] = (int) info.getY();}// 绘制连接线processDiagramCanvas.drawSequenceflow(xPoints, yPoints, drawConditionalIndicator,isDefault, highLighted, scaleFactor);// 绘制连接线标签(如果有 labelGraphicInfo)GraphicInfo labelGraphicInfo = bpmnModel.getLabelGraphicInfo(sequenceFlow.getId());if (labelGraphicInfo != null) {processDiagramCanvas.drawLabel(sequenceFlow.getName(), labelGraphicInfo, false);} else if (drawSequenceFlowNameWithNoLabelDI) {// 若允许无标签位置绘制,则计算中心位置绘制名称GraphicInfo lineCenter = getLineCenter(graphicInfoList);processDiagramCanvas.drawLabel(sequenceFlow.getName(), lineCenter, false);}}}// 递归绘制嵌套子元素(如果节点是容器类型)if (flowNode instanceof FlowElementsContainer) {for (FlowElement nestedElement : ((FlowElementsContainer) flowNode).getFlowElements()) {if (nestedElement instanceof FlowNode &&!isPartOfCollapsedSubProcess(nestedElement, bpmnModel)) {drawActivity(processDiagramCanvas, bpmnModel, (FlowNode) nestedElement,highLightedActivities, highLightedFlows, scaleFactor,drawSequenceFlowNameWithNoLabelDI);}}}} catch (Exception e) {// 异常时记录警告日志,避免中断流程图生成log.warn("绘制活动节点失败,节点ID: [{}], 异常原因: {}", flowNode.getId(), e.getMessage());}}}
三、使用自定义流程图生成器
① 定义请求参数
我们需要指定流程实例ID,准确的展示对应流程进度图
package com.ceair.entity.request;import lombok.Data;import java.io.Serial;
import java.io.Serializable;/*** @author wangbaohai* @ClassName QueryTaskImageReq* @description: 查看任务图片请求对象* @date 2025年05月06日* @version: 1.0.0*/
@Data
public class QueryTaskImageReq implements Serializable {@Serialprivate static final long serialVersionUID = 1L;// 流程实例IDprivate String processInstanceId;}
② 定义服务接口
/*** 根据流程实例ID查询任务的图像Base64编码字符串** @param processInstanceId 流程实例ID,用于唯一标识一个流程实例* @return 返回与流程实例ID关联的任务图像的Base64编码字符串表示*/
String queryTaskImage(String processInstanceId);
③ 实现服务接口
这里是使用自定义流程图生成器的地方,我们需要根据流程实例ID查询出来哪些节点和哪些连线需要高亮,并且指定图片的格式,将这些信息传递到自定义流程图生成器中,由生成器生成流程图,使用base64编码传递给前端展示。
/*** 查询指定流程实例的任务图片,并返回其 Base64 编码字符串。* <p>* 此方法用于根据传入的流程实例 ID 获取对应的流程图,* 并高亮显示该流程中已经执行过的节点和连线,* 最终将生成的图片转换为 Base64 编码字符串以便在前端展示。* </p>** @param processInstanceId 流程实例的唯一标识符,不能为空或空白字符串* @return 返回流程图的 Base64 编码字符串表示* @throws IllegalArgumentException 如果传入的流程实例ID为空或无效参数* @throws BusinessException 如果流程实例不存在或业务处理过程中发生异常* @throws Exception 其他未预期的异常*/
@Override
public String queryTaskImage(String processInstanceId) {// 初始化流程定义ID、高亮节点列表和连线列表、Base64 图片结果String processDefinitionId;List<String> highLightedFlows = new ArrayList<>();List<String> highLightedNodes = new ArrayList<>();String base64Image = "";try {// 参数校验:判断流程实例ID是否为空if (StringUtils.isBlank(processInstanceId)) {log.error("查询任务图片失败:非法的流程实例ID");throw new IllegalArgumentException("查询任务图片失败:非法的流程实例ID");}// 尝试获取当前运行中的流程实例ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();// 判断流程是否结束if (Objects.isNull(processInstance)) {// 流程已结束,尝试从历史记录中获取流程定义IDHistoricProcessInstance historicProcessInstance =historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();if (Objects.isNull(historicProcessInstance)) {// 历史记录也不存在,说明流程数据不合法log.error("查询任务图片失败:流程实例结束节点不存在");throw new BusinessException("查询任务图片失败:流程实例结束节点不存在");}// 使用历史流程实例获取流程定义IDprocessDefinitionId = historicProcessInstance.getProcessDefinitionId();} else {// 流程仍在运行,使用运行时实例获取流程定义IDprocessDefinitionId = processInstance.getProcessDefinitionId();}// 查询所有已执行的历史活动节点(按开始时间升序排列)List<HistoricActivityInstance> activityInstances = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();// 遍历历史活动实例,分类为高亮节点和高亮连线for (HistoricActivityInstance instance : activityInstances) {if ("sequenceFlow".equals(instance.getActivityType())) {// 当前为序列流(连接线),加入高亮连线列表highLightedFlows.add(instance.getActivityId());} else {// 当前为节点类型(如用户任务、服务任务等),加入高亮节点列表highLightedNodes.add(instance.getActivityId());}}// 获取 BpmnModel 对象,用于后续绘制流程图BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);// 获取流程引擎配置信息,包括字体、类加载器等ProcessEngineConfiguration processEngineConfiguration = processEngine.getProcessEngineConfiguration();// 创建自定义的流程图生成器,支持高亮显示ProcessDiagramGenerator diagramGenerator = new CustomProcessDiagramGenerator();// 调用 generateDiagram 方法生成 PNG 格式的流程图图像try (InputStream inputStream = diagramGenerator.generateDiagram(bpmnModel, "png", highLightedNodes, highLightedFlows,processEngineConfiguration.getActivityFontName(),processEngineConfiguration.getLabelFontName(),processEngineConfiguration.getAnnotationFontName(),processEngineConfiguration.getClassLoader(), 1.0, true)) {// 将输入流中的字节全部读取并编码为 Base64 字符串// 注意:readAllBytes() 在大文件下可能占用较多内存,如有需要可改用缓冲方式读取base64Image = Base64.getEncoder().encodeToString(inputStream.readAllBytes());} catch (IOException e) {// IO 异常处理,如读取流程图失败log.error("查询任务图片失败:IO异常", e);throw new BusinessException("查询任务图片失败:IO异常", e);}// 返回 Base64 编码的图片字符串return base64Image;} catch (IllegalArgumentException e) {// 捕获参数非法异常并封装为业务异常重新抛出log.error("查询任务图片失败:非法参数异常", e);throw new BusinessException("查询任务图片失败:非法参数异常", e);} catch (BusinessException e) {// 捕获业务逻辑异常并重新抛出,避免重复日志输出log.error("查询任务图片失败:业务异常", e);throw new BusinessException("查询任务图片失败:业务异常", e);} catch (Exception e) {// 捕获未知异常并封装为业务异常抛出log.error("查询任务图片失败:未知异常", e);throw new BusinessException("查询任务图片失败:未知异常", e);}
}
④ 定义功能接口
/*** 查询任务图片。* <p>* 权限: /api/v1/myTask/queryTaskImage* 参数: queryTaskImageReq - 包含查询任务图片所需信息的请求对象* 返回: Result<String> 返回封装后的任务图片信息(如图片路径或Base64数据)* <p>* 异常处理:* - 业务层异常 返回查询任务图片失败信息* - 其他未知异常 系统异常提示*/
@PreAuthorize("hasAnyAuthority('/api/v1/myTask/queryTaskImage')")
@Parameter(name = "queryTaskImageReq", description = "查询任务图片请求对象", required = true)
@Operation(summary = "查询任务图片")
@PostMapping("/queryTaskImage")
public Result<String> queryTaskImage(@RequestBody QueryTaskImageReq queryTaskImageReq) {try {// 调用业务层方法,将任务分配给当前登录用户return Result.success(mayTaskService.queryTaskImage(queryTaskImageReq.getProcessInstanceId()));} catch (Exception e) {log.error("查询任务图片失败,原因:{}", e.getMessage());return Result.error("查询任务图片失败,原因:" + e.getMessage());}
}
四、完善前端展示
① 定义前端请求参数类型
// 查看流程进度 请求参数
export interface QueryTaskImageReq {processInstanceId: string
}
② 封装请求接口
/*** 查询任务进度流程图*/
export function queryTaskImage(data: QueryTaskImageReq) {return request.post<any>({url: '/pm-process/api/v1/myTask/queryTaskImage',data,})
}
③ 新增按钮
<el-button v-hasButton="`btn.myTask.queryTaskImage`" type="primary" @click="onShowImage(scope.row)">查看流程进度
</el-button>
④ 完善按钮功能
/*** 异步函数:用于显示流程定义的图片* @param row 任务视图对象,包含流程实例 ID 等信息*/
async function onShowImage(row: TaskVO) {try {// 组装查询参数,包括流程定义 IDconst param: QueryTaskImageReq = {processInstanceId: row.procInsId,}// 调用后端接口获取流程定义的图片数据const result: any = await queryTaskImage(param)// 判断查询结果是否成功if (result.success && result.code === 200) {// 如果成功,则更新流程定义的图片数据imageData.value = result.data// 打开图片对话框showImage.value = true}else {// 提示操作失败的错误提示信息ElMessage({message: `查看流程进度失败原因:${result.message}`,})}}catch (error) {// 捕获异常并提取错误信息let errorMessage = '未知错误'if (error instanceof Error) {errorMessage = error.message}// 显示操作失败的错误提示信息ElMessage({message: `查看流程进度失败: ${errorMessage || '未知错误'}`,type: 'error',})}
}
五、维护权限
① 增加按钮权限
② 分配按钮权限
六、验证功能
① 定义流程
② 发布流程
③ 启动流程
④ 查看流程图
可以看到当前节点到第一步是我自己(adiin)审批
⑤ 审批
可以看到admin审批之后,任务扭转到第二个节点BOB用户名下
⑥ 第二节点人查看流程图
可以看到已办节点绿色,当前界面红色
后记
至此我们完成了流程图的查看功能。
本文的后端分支是 process-11
本文的前端分支是 process-13