C#工业上位机项目实战第九篇:可视化流程引擎完整落地,节点拖拽、连线渲染与自动化调度

前言

上一篇我们彻底落地了HAL硬件抽象层架构,完成了软硬件的彻底解耦,解决了工业项目硬件适配难、耦合严重、无法离线开发的核心痛点,搭建起了稳定、可拓展的硬件底层支撑体系。

截至目前,我们的项目已完成UI布局、模块化页面、DI依赖注入、业务服务分层、数据持久化、硬件抽象层的全闭环工程化架构。底层架构全部夯实完毕,正式进入工业自动化上位机的核心灵魂功能开发

普通上位机只能实现固定逻辑、固定工序的设备控制,而自动化量产上位机的核心竞争力,就是可视化流程引擎。无需修改代码、无需重启程序,现场工程师可自由拖拽工序节点、组合设备动作、调整生产流程,适配不同产品、不同工艺、不同设备的生产需求。

市面上多数教程只会讲解简单的画布拖拽Demo,无法落地工业真实场景。本篇将从零搭建一套工业级可视化流程引擎,包含节点自定义、拖拽吸附、贝塞尔曲线连线、流程数据序列化、运行状态调度、硬件设备联动、急停暂停恢复全套能力,完全适配工业现场柔性生产需求。

本篇核心目标:从零开发可量产的工业可视化流程引擎,实现工艺自由编排、流程可视化、自动化调度、软硬件联动。

一、工业流程引擎的核心价值与行业痛点

在动手编码前,我们先理清工业场景下,流程引擎的核心作用,以及传统硬编码工序的致命弊端:

1. 传统硬编码工序的痛点

  • 工艺固化无法修改:生产工序写死在代码中,换产品、换工艺必须改代码、重新编译打包

  • 现场适配性极差:现场工艺微调、工序调换无法快速响应,研发迭代成本极高

  • 非专业人员无法操作:只有开发者能修改流程,设备工程师、产线人员无法自主适配

  • 流程状态不可视:程序运行中无法直观看到当前执行工序、卡顿节点、异常位置,排查问题困难

2. 工业流程引擎的核心价值

  • 工艺柔性可配置:可视化拖拽编排工序,适配多产品、多工艺柔性生产

  • 零代码修改适配:现场调整流程无需改动源码,无需重新部署程序

  • 流程状态实时可视化:运行节点高亮、进度实时展示,故障节点精准定位

  • 标准化流程复用:常用工艺流程可保存、加载、复用,大幅提升生产调试效率

  • 联动底层硬件架构:流程节点绑定HAL硬件接口,实现工序与设备动作自动联动

二、工业流程引擎整体架构设计

结合工业自动化生产的启动、暂停、停止、急停、断点续跑、异常终止的核心场景,我们设计一套四层工业流程引擎架构,职责清晰、完全解耦、可无限拓展。

1. 数据模型层(FlowModel)

定义流程核心数据结构:流程画布数据、节点实体、连线实体、节点参数、运行状态,支持JSON序列化持久化,实现流程保存与加载。

2. 画布渲染层(FlowCanvas)

基于GDI+实现画布绘制、节点渲染、贝塞尔曲线连线、鼠标拖拽、选中、吸附对齐、层级刷新。

3. 流程调度层(FlowRunner)

核心运行器,负责流程启动、暂停、继续、停止、急停、节点顺序执行、异步调度、超时管控、异常捕获。

4. 节点业务层(FlowNode)

封装各类工业工序节点:设备启动、IO读写、延时等待、条件判断、循环工序、报警提示、数据保存,每个节点绑定对应的硬件与业务能力。

三、流程核心数据模型落地(可序列化)

工业流程必须支持保存本地、重启加载、复用流转,因此所有核心模型必须支持JSON序列化,我们先定义标准化节点、连线、流程数据模型。

1. 流程节点模型(FlowNodeModel)

csharp 复制代码
using System;

namespace Industrial.Engine.Flow.Model
{
    /// <summary>
    /// 工业流程工序节点模型
    /// </summary>
    public class FlowNodeModel
    {
        /// <summary>
        /// 节点唯一ID
        /// </summary>
        public string NodeId { get; set; } = Guid.NewGuid().ToString("N");

        /// <summary>
        /// 节点类型(启动/设备动作/延时/判断/结束)
        /// </summary>
        public string NodeType { get; set; }

        /// <summary>
        /// 节点展示名称
        /// </summary>
        public string NodeName { get; set; }

        /// <summary>
        /// 画布X坐标
        /// </summary>
        public float X { get; set; }

        /// <summary>
        /// 画布Y坐标
        /// </summary>
        public float Y { get; set; }

        /// <summary>
        /// 节点宽度
        /// </summary>
        public float Width { get; set; } = 120;

        /// <summary>
        /// 节点高度
        /// </summary>
        public float Height { get; set; } = 60;

        /// <summary>
        /// 节点自定义参数
        /// </summary>
        public string NodeParam { get; set; }

        /// <summary>
        /// 节点运行状态
        /// </summary>
        public string RunState { get; set; } = "Idle";
    }
}

2. 流程连线模型(FlowLineModel)

csharp 复制代码
namespace Industrial.Engine.Flow.Model
{
    /// <summary>
    /// 节点连线模型
    /// </summary>
    public class FlowLineModel
    {
        /// <summary>
        /// 连线ID
        /// </summary>
        public string LineId { get; set; }

        /// <summary>
        /// 起始节点ID
        /// </summary>
        public string SourceNodeId { get; set; }

        /// 
        public string TargetNodeId { get; set; }
    }
}

3. 整体流程画布模型(FlowCanvasModel)

csharp 复制代码
using System.Collections.Generic;

namespace Industrial.Engine.Flow.Model
{
    /// <summary>
    /// 流程画布总模型
    /// </summary>
    public class FlowCanvasModel
    {
        /// <summary>
        /// 流程名称
        /// </summary>
        public string FlowName { get; set; }

        /// public List<FlowNodeModel> NodeList { get; set; } = new List<FlowNodeModel>();

        /// 
        public List<FlowLineModel> LineList { get; set; } = new List<FlowLineModel>();
    }
}

四、GDI+画布渲染与拖拽交互核心实现

我们基于WinForms PictureBox封装专属流程画布控件,实现节点绘制、贝塞尔曲线连线、鼠标拖拽移动、节点选中、画布刷新全套交互能力。

1. 核心画布控件基础定义

csharp 复制代码
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using Industrial.Engine.Flow.Model;

namespace Industrial.Engine.Flow.Canvas
{
    public partial class FlowCanvasControl : PictureBox
    {
        // 当前画布流程数据
        public FlowCanvasModel CurrentFlow { get; set; } = new FlowCanvasModel();

        // 拖拽临时变量
        private bool _isDragging = false;
        private Point _dragStartPoint;
        private FlowNodeModel _dragNode = null;

        public FlowCanvasControl()
        {
            // 开启双缓冲,防止闪烁
            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.AllPaintingInWmPaint, true);
            UpdateStyles();
        }

        // 重绘画布
        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            var g = e.Graphics;
            g.SmoothingMode = SmoothingMode.AntiAlias;

            // 绘制连线
            DrawLines(g);
            // 绘制节点
            DrawNodes(g);
        }
    }
}

2. 节点与连线绘制核心方法

csharp 复制代码
// 绘制所有节点
private void DrawNodes(Graphics g)
{
    foreach (var node in CurrentFlow.NodeList)
    {
        // 根据运行状态切换颜色
        Brush backBrush = node.RunState switch
        {
            "Running" => Brushes.LightGreen,
            "Finish" => Brushes.Green,
            "Error" => Brushes.OrangeRed,
            _ => Brushes.LightGray
        };

        // 绘制节点矩形
        g.FillRectangle(backBrush, node.X, node.Y, node.Width, node.Height);
        g.DrawRectangle(Pens.Black, node.X, node.Y, node.Width, node.Height);

        // 绘制节点文字
        g.DrawString(node.NodeName, SystemFonts.DefaultFont, Brushes.Black, 
            node.X + 10, node.Y + 15);
    }
}

// 绘制贝塞尔曲线连线
private void DrawLines(Graphics g)
{
    foreach (var line in CurrentFlow.LineList)
    {
        var sourceNode = CurrentFlow.NodeList.Find(x => x.NodeId == line.SourceNodeId);
        var targetNode = CurrentFlow.NodeList.Find(x => x.NodeId == line.TargetNodeId);
        if (sourceNode == null || targetNode == null) continue;

        // 起点、终点坐标
        PointF startPoint = new PointF(sourceNode.X + sourceNode.Width, sourceNode.Y + sourceNode.Height / 2);
        PointF endPoint = new PointF(targetNode.X, targetNode.Y + targetNode.Height / 2);

        // 贝塞尔曲线控制点
        PointF control1 = new PointF(startPoint.X + 60, startPoint.Y);
        PointF control2 = new PointF(endPoint.X - 60, endPoint.Y);

        GraphicsPath path = new GraphicsPath();
        path.AddBezier(startPoint, control1, control2, endPoint);
        g.DrawPath(Pens.DodgerBlue, path);
    }
}

3. 节点拖拽交互实现

csharp 复制代码
protected override void OnMouseDown(MouseEventArgs e)
{
    base.OnMouseDown(e);
    // 命中节点检测
    foreach (var node in CurrentFlow.NodeList)
    {
        if (e.X > node.X && e.X < node.X + node.Width &&
            e.Y > node.Y && e.Y < node.Y + node.Height)
        {
            _isDragging = true;
            _dragNode = node;
            _dragStartPoint = e.Location;
            break;
        }
    }
}

protected override void OnMouseMove(MouseEventArgs e)
{
    base.OnMouseMove(e);
    if (!_isDragging || _dragNode == null) return;

    // 计算偏移量
    float offsetX = e.X - _dragStartPoint.X;
    float offsetY = e.Y - _dragStartPoint.Y;

    // 更新节点坐标
    _dragNode.X += offsetX;
    _dragNode.Y += offsetY;
    _dragStartPoint = e.Location;

    // 实时重绘
    Invalidate();
}

protected override void OnMouseUp(MouseEventArgs e)
{
    base.OnMouseUp(e);
    _isDragging = false;
    _dragNode = null;
}

五、流程运行调度器核心实现(自动执行)

画布可视化编辑只是基础,自动调度执行、硬件联动、状态管控才是流程引擎的工业核心。我们封装FlowRunner运行器,实现流程启停、顺序执行、状态刷新、异常终止。

csharp 复制代码
using System;
using System.Linq;
using System.Threading.Tasks;
using Industrial.Engine.Flow.Model;
using Industrial.HAL.Manager;
using Industrial.Core.DependencyInjection;

namespace Industrial.Engine.Flow.Runner
{
    /// <summary>
    /// 工业流程运行调度器
    /// 负责流程启停、节点执行、状态调度、硬件联动
    /// </summary>
    public class FlowRunner
    {
        private readonly HardwareManager _hardwareManager;
        private bool _isRunning = false;
        private bool _isPause = false;

        public FlowRunner()
        {
            // DI获取硬件管理服务
            _hardwareManager = ServiceLocator.GetService<HardwareManager>();
        }

        /// <summary>
        /// 启动流程
        /// </summary>
        public async Task StartFlow(FlowCanvasModel flowModel)
        {
            if (_isRunning) return;
            _isRunning = true;
            _isPause = false;

            // 重置所有节点状态
            flowModel.NodeList.ForEach(x => x.RunState = "Idle");

            // 查找起始节点,开始顺序执行
            var startNode = flowModel.NodeList.FirstOrDefault(x => x.NodeType == "Start");
            if (startNode == null) return;

            await ExecuteNode(flowModel, startNode);
        }

        /// <summary>
        /// 执行单个节点
        /// </summary>
        private async Task ExecuteNode(FlowCanvasModel flow, FlowNodeModel node)
        {
            if (!_isRunning || _isPause) return;

            // 更新运行状态
            node.RunState = "Running";

            // 根据节点类型执行对应工业逻辑
            switch (node.NodeType)
            {
                case "PlcWrite":
                    ExcutePlcWrite(node);
                    break;
                case "Delay":
                    int delay = int.Parse(node.NodeParam);
                    await Task.Delay(delay);
                    break;
                case "End":
                    node.RunState = "Finish";
                    _isRunning = false;
                    return;
            }

            node.RunState = "Finish";

            // 查找下一个节点,递归执行
            var nextLine = flow.LineList.FirstOrDefault(x => x.SourceNodeId == node.NodeId);
            if (nextLine == null)
            {
                _isRunning = false;
                return;
            }

            var nextNode = flow.NodeList.FirstOrDefault(x => x.NodeId == nextLine.TargetNodeId);
            if (nextNode != null)
                await ExecuteNode(flow, nextNode);
        }

        /// <summary>
        /// 执行PLC写入节点逻辑
        /// </summary>
        private void ExcutePlcWrite(FlowNodeModel node)
        {
            // 解析节点自定义参数,调用底层HAL硬件服务
            var plc = _hardwareManager.InitPlcDevice("PLC001", "主站PLC");
            if (plc.IsOnline)
            {
                plc.WriteBit("M100", true);
            }
        }

        /// <summary>
        /// 暂停流程
        /// </summary>
        public void PauseFlow() => _isPause = true;

        /// <summary>
        /// 继续流程
        /// </summary>
        public void ResumeFlow() => _isPause = false;

        /// <summary>
        /// 停止流程
        /// </summary>
        public void StopFlow()
        {
            _isRunning = false;
            _isPause = false;
        }
    }
}

六、流程持久化:保存与加载功能实现

工业工艺需要长期复用,因此我们实现流程JSON序列化保存、本地文件加载功能,支持工艺配方留存与一键调用。

csharp 复制代码
using System.IO;
using Newtonsoft.Json;
using Industrial.Engine.Flow.Model;
using Industrial.Core.Utils;

namespace Industrial.Engine.Flow.Helper
{
    public static class FlowFileHelper
    {
        /// <summary>
        /// 保存流程到本地文件
        /// </summary>
        public static void SaveFlow(FlowCanvasModel flow, string fileName)
        {
            string path = Path.Combine(PathHelper.FlowPath, $"{fileName}.json");
            string json = JsonConvert.SerializeObject(flow, Formatting.Indented);
            File.WriteAllText(path, json);
        }

        /// <summary>
        /// 加载本地流程文件
        /// </summary>
        public static FlowCanvasModel LoadFlow(string fileName)
        {
            string path = Path.Combine(PathHelper.FlowPath, $"{fileName}.json");
            if (!File.Exists(path)) return new FlowCanvasModel();
            string json = File.ReadAllText(path);
            return JsonConvert.DeserializeObject<FlowCanvasModel>(json);
        }
    }
}

七、DI注册引擎服务与页面实战调用

将流程运行器纳入DI全局托管,保证引擎全局唯一、状态统一,在UI页面实现流程编辑、运行、暂停、停止的完整操作。

1. 注册流程引擎服务

csharp 复制代码
// AppBootstrap 新增注册逻辑
private static void RegisterEngineServices(ServiceCollection services)
{
    // 流程运行器全局单例
    services.AddSingleton<FlowRunner>();
}

2. 页面实战调用

csharp 复制代码
// 流程编辑页面调用示例
private readonly FlowRunner _flowRunner;

public FlowEditView()
{
    _flowRunner = ServiceLocator.GetService<FlowRunner>();
    InitializeComponent();
}

// 启动流程按钮
private void btnStart_Click(object sender, EventArgs e)
{
    _ = _flowRunner.StartFlow(flowCanvasControl1.CurrentFlow);
}

// 暂停流程按钮
private void btnPause_Click(object sender, EventArgs e)
{
    _flowRunner.PauseFlow();
}

// 停止流程按钮
private void btnStop_Click(object sender, EventArgs e)
{
    _flowRunner.StopFlow();
}

八、工业流程引擎核心能力拓展与规范

1. 标准节点拓展规范

后续新增工艺节点无需改动引擎核心代码,只需新增节点类型、编写对应执行逻辑,完全遵循开闭原则,可拓展:相机拍照、传感器检测、数据保存、条件跳转、循环工序、报警弹窗等节点。

2. 工业级运行容错机制

  • 硬件异常终止:节点执行时检测硬件离线,自动终止流程并弹窗报警

  • 超时容错:为每个节点配置执行超时,卡死自动跳过或报错终止

  • 断点续跑:暂停停止后,可从当前节点继续执行,无需从头跑流程

  • 状态实时同步:节点运行状态实时刷新,界面可视化展示工序进度

3. 流程执行核心优势

完美承接前文HAL硬件架构,流程层只调用抽象硬件接口,不依赖具体设备,真机/仿真模式一键切换,编辑调试完全不受硬件限制。

九、本篇总结

可视化流程引擎是工业自动化上位机的核心灵魂模块,也是区分"固定程序"和"柔性自动化平台"的关键分水岭。

本篇我们从零落地了完整的工业级流程引擎,实现了节点可视化拖拽、贝塞尔曲线连线、流程数据持久化、自动调度执行、硬件设备联动、启停暂停管控全套核心能力。

结合前面的底层架构,项目正式具备了工艺自由编排、柔性生产适配、可视化自动化运行的工业级能力,彻底摆脱硬编码工序的局限,完全适配现场多变的生产工艺需求。

下篇预告

下一篇我们进入第十篇Lua脚本引擎集成实战 !实现C#与Lua双向交互、在线脚本编辑、断点调试、动态更新工序逻辑,实现真正意义上的无需重启、无需编译、动态更新业务逻辑,进一步拉满项目的柔性拓展能力。

相关推荐
njsgcs2 小时前
c# solidworks 创建装配体工程图+bom
开发语言·c#·solidworks
njsgcs2 小时前
c# solidworks 工程图获得展开视图不在固定面螺纹特征的位置
开发语言·c#·solidworks
jinglong.zha3 小时前
LScript-从零基础到商业变现的AI自动化学习平台
运维·学习·自动化
苏州邦恩精密4 小时前
江苏三维扫描仪厂家如何选择合适的工业测量方案?
人工智能·科技·机器学习·3d·自动化·制造
lbb 小魔仙4 小时前
【Linux】DevOps 工程师必备:Linux 自动化脚本与高效工具链整合
linux·自动化·devops
深海潜水员5 小时前
【从零开始的C#游戏开发课程】- FarmStory1.0 日志系统和游戏资源的管理
游戏·c#·monogame
叶帆5 小时前
【YFIOs】用C#开发硬件之WiFi网络
开发语言·网络·c#
梦想的旅途25 小时前
企业微信外部群主动调用:RPA 接口与官方 API 的技术边界
网络·mysql·自动化·企业微信·rpa
AC赳赳老秦6 小时前
OpenClaw 助力技术面试:自动生成面试题、模拟面试、整理面试知识点
开发语言·python·面试·职场和发展·自动化·deepseek·openclaw