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
相关文章:
Flowable7.x学习笔记(二十)查看流程办理进度图
前言 本文是基于继承Flowable的【DefaultProcessDiagramCanvas】和【DefaultProcessDiagramGenerator】实现的自定义流程图生成器,通过自定义流程图生成器可以灵活的指定已经执行过的节点和当前正在活跃的节点样式,比如说已经执行完成的节点我们标绿&…...
【计算机网络 第8版】谢希仁编著 第四章网络层 地址类题型总结
小结 个人觉得地址类在网络层算好做的题,这部分知识本身并不多,理解到位了就是2进制和10进制的换算题了。而且这种题给你一小时例题和标答,肯定自己都能悟出来。但是计网网络层的整体我感觉很散,老师讲的也乱七八糟的,…...
一种基于条件生成对抗网络(cGAN)的CT重建算法
简介 简介:该文提出了一种基于条件生成对抗网络(cGAN)的CT重建算法,通过引入CBAM注意力机制增强网络对关键特征的提取能力,有效解决了CT成像中因噪声干扰导致的重建精度下降问题。实验采用固体火箭发动机模拟件数据集,将正弦图分为五组并添加不同程度的噪声进行训练。结…...
欧拉系统(openEuler)上部署OpenStack的完整指南 ——基于Yoga版本的全流程实践
(资源区里有上传的配置好的openstack镜像) 一、环境规划与前置准备 1. 硬件与节点规划升级 存储节点(可选):若需Cinder后端,建议配置SSDHDD混合存储网络拓扑强化: 管理网络:启用V…...
oceanbase不兼容SqlSugarCore的问题
问题发现 C#程序使用SqlSugarCore5.1.4.166进行数据库操作,而且项目需要在多台服务器上面部署,结果发现A服务器部署运行没有问题, B服务器部署却报错:SqlSugar.SqlSugarException:Connect timeout expired. 但是我们的C#代码是一…...
深入理解分布式锁——以Redis为例
一、分布式锁简介 1、什么是分布式锁 分布式锁是一种在分布式系统环境下,通过多个节点对共享资源进行访问控制的一种同步机制。它的主要目的是防止多个节点同时操作同一份数据,从而避免数据的不一致性。 线程锁: 也被称为互斥锁(…...
OrangePi Zero 3学习笔记(Android篇)1 - 搭建环境
目录 1. 下载安装Ubuntu22.04 1.1 安装增强功能 1.2 设置共享文件夹 1.3 创建AOSP.vdi 1.4 更新相关软件包 2. 解压AOSP源代码 3. 编译代码 3.1 编译uboot/Linux 3.2 编译AOSP源代码 3.3 内存问题调试记录 3.3.1 查看具体什么问题 3.3.2 关闭dex2oat(无…...
RabbitMq(尚硅谷)
RabbitMq 1.RabbitMq异步调用 2.work模型 3.Fanout交换机(广播模式) 4.Diret交换机(直连) 5.Topic交换机(主题交换机,通过路由匹配) 6.Headers交换机(头交换机) 6…...
OpenAI的“四面楚歌”:从营利到非营利,一场关于AGI控制权的革命
引言 当“奥特曼妥协”与“四面楚歌”并置时,OpenAI的这次重大调整,仿佛在科技史上投下一颗震撼弹。这家曾因“拒绝盈利”而备受争议的人工智能公司,如今却在资本与理想之间艰难抉择——放弃营利性转型,回归非营利初心。这不仅是对…...
[250505] Arch Linux 正式登陆 Linux 的 Windows 子系统
目录 Arch Linux 正式登陆 Windows Subsystem for Linux (WSL) Arch Linux 正式登陆 Windows Subsystem for Linux (WSL) Arch Linux 社区与 Microsoft 合作,正式宣布 Arch Linux 现已提供官方的 Windows Subsystem for Linux (WSL) 镜像。这意味着 Windows 用户现…...
MySQL 日期格式化:DATE_FORMAT 函数用法
MySQL 日期格式化:DATE_FORMAT 函数用法 在 MySQL 中,DATE_FORMAT() 函数用于将日期或时间值格式化为指定的字符串格式。 正确语法 DATE_FORMAT(date, format)常用格式说明符 说明符描述%Y四位数的年份(例如:2023)…...
java springboot解析出一个图片的多个二维码
引入 <dependencies><!-- ZXing --><dependency><groupId>com.google.zxing</groupId><artifactId>core</artifactId><version>3.4.1</version></dependency><dependency><groupId>com.google.zxing…...
【四川省专升本计算机基础】第一章 计算机基础知识(上)
前言 对计算机专业的同学来说这门课程可能很简单,容易拿高分(125分以上),但也可能很容易大意丢分。因为本门课程人称:背多分。大意丢分者的心态觉得计算机基础都学过,内容很简单,最后才开始背计…...
406错误,WARN 33820 --- [generator] [nio-8080-exec-4] .w.s.m.s.DefaultHa
在接口调用过程中,后端出现.w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]错误。检查了一个小时才发现我返回的对象没有写getter方法, 当Spring B…...
基于STM32、HAL库的SST26VF064B NOR FLASH存储器驱动应用程序设计
一、简介: SST26VF064B是Microchip公司生产的一款64Mbit(8MB)串行闪存器件,采用SPI接口通信,具有以下特点: 工作电压:2.7-3.6V 最高104MHz时钟频率 统一4KB扇区结构 快速擦除和编程时间 低功耗特性 支持标准SPI、Dual SPI和Quad SPI模式 二、硬件接口: STM32L4引脚SST26V…...
美信监控易:全栈式自主可控的底层架构优势
在当今数字化时代,企业的运维管理面临着越来越多的挑战。为了确保业务的稳定运行,企业需要一款高效、可靠的运维管理软件。北京美信时代的美信监控易运维管理软件,以其全栈式自主可控的底层架构优势,成为了运维团队的理想选择。 …...
Class AB OPA corner 仿真,有些corenr相位从0开始
Class AB OPA做 STB 仿真时,会遇到有些corner(c0_0、c0_4、c0_5)相位从0开始的情况。 首先应该去检查电路,电路里是否有正反馈;排除没有正反馈后,考虑是图中的红框中的线性跨导环中的Vds 太大导致了碰撞电离导致的。 查找了网上…...
【JEECG】BasicTable单元格编辑,插槽添加下拉组件样式错位
1.功能说明 BasicTable表格利用插槽,添加组件实现单元格编辑功能,选择组件下拉框错位 2.效果展示 3.解决方案 插槽内组件增加::getPopupContainer"getPopupContainer" <template #salesOrderProductStatus"{ column, re…...
第十五届蓝桥杯单片机国赛-串口解析
串口通信像是蓝桥杯单片机组国赛中一个若隐若现的秘境,总在不经意间为勇者们敞开大门。然而,初次探索这片领域的冒险者,常常会被其神秘莫测的特性所震慑,黯然退场(编不下去了,直接进入正题)。 附…...
Flutter开发HarmonyOS实战-鸿蒙App商业项目
Flutter开发HarmonyOS实战内容介绍: Flutter开发HarmonyOS 鸿蒙App商业项目(小米商城APP)实战视频教程 Flutter开发鸿蒙APP是在《FlutterGetx仿小米商城》项目基础之上讲解的,调试Flutter HarmonyOS应用需要有HarmonyOS Next的手机…...
【回眸】香橙派Zero2 超声波模块测距控制SG90舵机转动
前言 知识准备 超声波模块时序图 gettimeofday()函数作用 gettimeofday()函数原型 tv结构体 获取当前系统时间与格林威治时间的时间差 获取香橙派数10万秒花费的时间 使用超声波模块获取到障碍物距离 SG90舵机模块 舵机模块的作用 舵机模块方波时序图 舵机模块工作原…...
RabbitMQ 添加新用户和配置权限
以下是关于使用 sudo rabbitmqctl add_user 命令创建新用户的详细示例,同时包含创建用户后进行权限设置、角色设置等相关操作的示例。 1. 前提条件 确保你的 RabbitMQ 服务已经正常运行,并且你具有执行 sudo 命令的权限。 2. 创建新用户 假设我们要创…...
【前缀和】矩阵区域和
文章目录 1314. 矩阵区域和解题思路1314. 矩阵区域和 1314. 矩阵区域和 给你一个 m x n 的矩阵 mat 和一个整数 k ,请你返回一个矩阵 answer ,其中每个 answer[i][j] 是所有满足下述条件的元素 mat[r][c] 的和: i - k <= r <= i + k, j - k <= c <= j + k …...
编程日志4.25
栈的stl模板 可直接用<stack>库进行调用 #include<iostream> #include<stack>//栈的模板库 using namespace std; int main() { stack<int> intStk;//整数 栈 stack<double> doubleStk;//浮点数 栈 intStk.push(1); intStk.pu…...
【中间件】brpc之工作窃取队列
文章目录 BRPC Work Stealing Queue1 核心功能2 关键数据结构2.1 队列结构2.2 内存布局优化 3 核心操作3.1 本地线程操作(非线程安全)3.2 窃取操作(线程安全) 4 设计亮点4.1 无锁原子操作4.2 环形缓冲区优化4.3 线程角色分离 5 性…...
用OMS从MySQL迁移到OceanBase,字符集utf8与utf8mb4的差异
一、问题背景 在一次从MySQL数据库迁移到OceanBase的MySQL租户过程中,出现了一个转换提示: [WARN][CONVER] he table charset:utf8->utf8mb4, 你可能会担心这种转换可能导致字符集不兼容的问题。但通过查阅相关资料可知,utf8m…...
知乎前端面试题及参考答案
Webpack 和 Vite 的区别是什么? 构建原理: Webpack 是基于传统的打包方式,它会将所有的模块依赖进行分析,然后打包成一个或多个 bundle。在开发过程中,当代码发生变化时,需要重新构建整个项目,构建速度会随着项目规模的增大而变慢。Vite 利用了浏览器对 ES 模块的支持,…...
项目中为什么选择RabbitMQ
当被问及为什么选择某种技术时,应该结合开发中的实际情况以及类似的技术进行分析,适合的技术才是最好的。 在项目中为什么选择RabbitMQ 作为消息中间件,主要可以基于以下几方面进行分析: 1. 可靠性 消息持久化:Rabbi…...
深入解析二维矩阵搜索:LeetCode 74与240题的两种高效解法对比
文章目录 [toc]**引言** **一、问题背景与排序规则对比****1. LeetCode 74. 搜索二维矩阵****2. LeetCode 240. 搜索二维矩阵 II** **二、核心解法对比****方法1:二分查找法(适用于LeetCode 74)****方法2:线性缩小搜索范围法&…...
Qt案例 以单线程或者单生产者多消费者设计模式实现QFTP模块上传文件夹功能
前文:Qt案例 使用QFtpServerLib开源库实现Qt软件搭建FTP服务器,使用QFTP模块访问FTP服务器 已经介绍了Qt环境下搭建FTP服务器或者使用QFTP上传的方式示例, 这里主要介绍下使用QFTP模块上传整个文件夹的案例示例。 目录导读 前因后果单线程处理1.定义FTPFolderUpload 继承 QT…...
含锡废水回收率提升技术方案
一、预处理环节优化 物理分离强化 采用双层格栅系统(孔径1mm0.5mm)拦截悬浮物,配套旋流分离器去除密度>2.6g/cm的金属颗粒,使悬浮物去除率提升至85%。增设pH值智能调节模块,通过在线pH计联动碳酸钠/氢氧化钠投加系…...
第八章,STP(生成树协议)
广播风暴----广播帧在二层环路中形成逆时针或顺时针的转动的环路,并且无限循环,最终导致设备宕机,网络瘫痪。 MAC地址表的翻摆(漂移)----同一个数据帧,顺时针接收后将记录MAC地址及接口的对应信息ÿ…...
《面向对象程序设计-C++》实验五 虚函数的使用及抽象类
程序片段编程题 1.【问题描述】 基类shape类是一个表示形状的抽象类,area( )为求图形面积的函数。请从shape类派生三角形类(triangle)、圆类(circles)、并给出具体的求面积函数。注:圆周率取3.14 #include<iostream> #in…...
PCIe - ZCU106(RC) + KU5P(EP) + 固化
目录 1. 简介 1.1 Data Mover 1.2 描述符 2. ZCU102 2.1 Ubuntu OS 2.2 USB Host 2.2.1 连接拓扑 2.2.2 设备类型 2.2.3 USB 跳帽设置 2.3 无线网卡 2.4 PCIe Info 2.4.1 Diagram 2.4.2 lspci -tv 2.4.3 lspci -v 2.4.2.1 设备基本信息 2.4.2.2 控制与状态寄存…...
网络编程核心技术解析:从Socket基础到实战开发
网络编程核心技术解析:从Socket基础到实战开发 一、Socket编程核心基础 1. 主机字节序与网络字节序:数据传输的统一语言 在计算机系统中,不同架构对多字节数据的存储顺序存在差异,而网络通信需要统一的字节序标准,这…...
SQL注入总结
一.sql注入 原理:当一个网站存在与用户交互的功能(如登录表单、搜索框、评论区等),并且用户输入的数据未经充分过滤或转义,直接拼接到后台数据库查询语句中执行时,就可能引发SQL注入漏洞。攻击者可以通过构…...
conda 安装cudnn
通过 Conda 安装 cuDNN 确保你有 NVIDIA GPU 和 CUDA Toolkit:首先,确保你的系统上安装了 NVIDIA GPU 和 CUDA Toolkit。你可以通过运行以下命令来检查 CUDA 是否已安装:nvcc --version 如果没有安装 CUDA,你需要先从 NVIDIA CU…...
强啊!Oracle Database 23aiOracle Database 23ai:使用列别名进行分组排序!
大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构! 从 Oracle Database 23ai 开始,您可以在 GROUP BY 和 HAVING 子句中直接使用列别名。此功能在早期版本的 Oracle Database 中不…...
RAG 2.0 深入解读
一、Introduction 过去一年可谓是RAG元年,检索增强生成技术迅速发展与深刻变革,其创新与应用已深刻重塑了大模型落地的技术范式。站在2025年,RAG不仅突破了早期文本处理的局限,更通过多模态融合、混合检索优化和语义鸿沟跨越等突…...
Excel Vlookup
VLOOKUP(A2, Sheet2!A:B, 2, 0) 代表的是检查A2,匹配源是sheet2表AB两列 Sheet2!A:B:指定要在其中查找数据的范围,这里是 Sheet12中的 A 列和 B 列,A 列是查找的依据列,B 列是要返回值的列。2:表示要返回查找区域中的…...
css媒体查询及css变量
媒体查询是 CSS 样式表最重要的功能之一,所谓媒体查询指的就是根据不同的媒体类型(设备类型)和条件来区分各种设备(例如:电脑、手机、平板电脑、盲文设备等),并为它们分别定义不同的 CSS 样式。…...
CSS网格布局
网格布局将元素占用的空间划分为二维格子,下级元素放置在格子所在的位置上。划分格子的元素叫做网格容器,其 display 属性是 grid (块元素)或 inline-grid (内联块元素)。网格容器的下级元素叫做网格项。容…...
Windows远程连接MySQL报错,本地navicat能连接MySQL
一、报错 telnet 119.87.111.79 3306“无法打开到主机的连接。在端口 3306: 连接失败” 表明无法通过 TCP 协议连接到目标服务器的 3306 端口。 二、目的 (1)Telnet 测试的目的 Telnet 仅用于测试 TCP 端口是否开放ÿ…...
Github打不开怎么办?
国内无法打开github,使有watt toolkit一键加速即可打开。 加速器 加速器直接加速Github原站,在开发者使用或者需要登录账号时非常有效 Watt Toolkit(原Steam) 官网地址:Watt Toolkit 一、进入官网后,点…...
亿级流量系统架构设计与实战(四)
本章关键词 : 读 / 写分离 、 数据缓存 、 缓存更新 、 CQRS 、 数据分片 、 异步写。 高并发架构设计的要点 形成高并发系统的必要条件 高性能、高可用、可扩展。 高性能: 性能代表一个系统的并行处理能力,在同样的硬件设备条件下 , 性能越高 , 越能节约硬件资源。高可…...
Java基础问题——八股盛宴 | 3w字分享
目录 面向对象与面向过程的区别? Java面向对象有哪些特征,如何应用? 介绍下Java中的四种引用? Java中创建对象有几种方式? Java中的序列化和反序列化是什么? 什么是Java中不可变类? Java…...
保障企业的数据安全需要做什么?
守护企业数据安全,犹如构筑一座固若金汤的城堡,需要从技术壁垒、管理护城河、流程吊桥和人员守卫等多维度精心布局,打造环环相扣的立体防御体系。我们从以下关键项分析: 一、技术层面 数据加密 对敏感数据(如客户信息、…...
Flutter开发IOS蓝牙APP的大坑
Core Bluetooth 框架限制:iOS 的 Core Bluetooth 框架存在限制,如果指定的特征配置同时允许通知(Notifications)和指示(Indications),调用相关方法设置通知值时,默认仅会开启通知功能…...
LeetCode 解题思路 45(分割等和子集、最长有效括号)
解题思路: dp 数组的含义: 在数组中是否存在一个子集,其和为 i。递推公式: dp[i] | dp[i - num]。dp 数组初始化: dp[0] true。遍历顺序: 从大到小去遍历,从 i target 开始,直到 …...
AI Agent 入门指南:从 LLM 到智能体
AI. AI. AI. 最近耳朵里是不是总是被这些词轰炸?特别是“Agent”、“AI Agent”、“智能体”、“Agentic”…… 感觉一夜之间,AI 就从我们熟悉的聊天框里蹦出来,要拥有“独立思考”和“自主行动”的能力了? 说实话,一…...