首页 / 资讯中心 / 文章详情

Winform原生FlowChart流程图控件开发实战

Winform原生FlowChart流程图控件开发实战 ★ FEATURED ARTICLE
简介这是一份基于WinForm开发的轻量级流程图绘制工具资源包面向C#初学者与小型项目开发者解决快速构建可视化业务流程图的需求。资源在FlowChart.Net开源库基础上进行精简改造功能聚焦于节点创建、连线编辑与基础布局适合教学演示、内部流程梳理或嵌入式小系统集成。压缩包共59个文件包含15个核心C#源码如FrmCreateNode.cs、FlowMain.cs等、12个依赖DLL含MindFusion.Diagramming.WinForms等图表渲染组件、6个XML文档提供API说明、以及CSProj/Sln工程配置和ResX资源文件整体体积仅2.48MB结构紧凑便于学习与二次开发。已有4784人下载学习读者可直接运行EXE体验交互逻辑通过源码理解WinForm事件驱动绘图机制、节点-连线数据模型设计及第三方图表控件集成方式是掌握图形化界面开发实践的优质入门范例。1. Winform简单的FlowChart流程图不是画图软件而是让业务逻辑“活”在界面上的轻量级可视化方案你有没有遇到过这种场景客户指着需求文档里一段文字说“这里要走审批流”开发却得靠脑补画草图或者团队交接时新同事对着几百行if-else嵌套直摇头问“这个状态到底怎么跳转的”——这时候一个能嵌在 Winform 窗体里、双击节点就能改文字、拖拽连线就自动重绘、不依赖外部服务、编译即跑的 FlowChart 流程图控件就不是“锦上添花”而是“救命稻草”。它不追求 BPMN 的严谨性也不对标 draw.io 的协作能力核心价值就三点零部署依赖、与 C# 业务代码同进程通信、支持运行时动态加载/修改节点和路径。适合做用户管理模块流程图、手机中框工艺流程图这类内部系统辅助视图也常被用作算法流程图的演示载体比如教学场景下展示排序或状态机。本文讲的不是“怎么用 Visio 导出图片贴进 Winform”而是真正在 Winform 界面里原生承载可交互流程图的最小可行方案——从空白窗体开始30 分钟内跑通一个带节点增删、连线拖拽、右键编辑的 FlowChart 控件所有代码基于 .NET Framework 4.7.2 VS2015 兼容写法不引入第三方 UI 框架不调用 COM 组件不依赖任何安装包。2. 用 GDI 手写 FlowChart 控件为什么不用现成库因为可控性就是生产力提示本节所有代码均在System.Drawing和System.Windows.Forms原生命名空间下完成无需 NuGet 安装任何第三方图表库如 Graphviz、GoDiagram 或开源 FlowChart 控件避免版本冲突、许可证风险和黑匣子调试困境。2.1 为什么放弃“拿来主义”三个真实翻车现场很多开发者第一反应是搜 “Winform flowchart control github”结果拉下来一个 2000 行的FlowChartPanel.cs发现它强依赖System.ComponentModel.Design设计器组件一放到生产环境就报TypeLoadException或者另一个项目用 WPF 渲染后硬塞进 Winform 的ElementHost结果 DPI 缩放错乱、鼠标坐标偏移 15 像素还有人直接调用Process.Start(draw.io.exe)看似省事实则彻底脱离 Winform 生命周期管理——用户关窗时流程图进程还在后台吃内存。这些都不是玄学是 Winform 在企业级桌面应用中长期存在的“集成失重”问题。而手写 GDI 方案的好处在于每一像素的绘制、每一次鼠标的 HitTest、每一个节点的序列化结构都在你眼皮底下。你可以精确控制缩放比例比如工艺流程图需要 150% 放大看细节、可以拦截右键菜单只允许编辑当前节点属性、可以在Paint事件里插入自定义水印如“测试环境 - 2024Q3”——这些在封装库里要么藏得太深要么干脆不开放。2.2 核心类设计Node、Connection、FlowChartPanel 三件套我们不建“万能流程图引擎”只聚焦最常用子集矩形节点Start/End/Process、菱形判断节点Decision、正交连线直角折线、单向箭头。所有数据结构全部用struct或轻量class实现避免 GC 频繁抖动public struct Node { public Point Location; // 左上角坐标非中心点便于 Layout 计算 public Size Size; // 固定尺寸Process120x60Decision120x80 public string Text; // 显示文本支持换行符 \n public NodeType Type; // enum: Start, End, Process, Decision public string Id; // 全局唯一生成规则node_ Guid.NewGuid().ToString(N).Substring(0,8) } public class Connection { public string FromId { get; set; } public string ToId { get; set; } public Point[] Points { get; set; } // 正交路径点数组至少 2 个点起点终点 public string Id ${FromId}_to_{ToId}; }关键点说明Node.Location是左上角而非中心点这是 GDI 坐标系惯例也是后续Graphics.DrawRectangle直接使用的参数避免每次绘制都做(x-w/2, y-h/2)转换Size不设为可变工艺流程图/用户管理模块流程图对节点尺寸一致性要求极高强行支持自由缩放会导致连线锚点错位、文字溢出等连锁问题Points数组长度 ≥2正交连线必须至少有“起点→拐点→终点”三段但实际存储只存拐点序列首尾两点固定为 From/To 中心渲染时再插值生成完整路径。2.3 FlowChartPanel继承 Panel接管 Paint 与 Mouse 事件链新建一个FlowChartPanel.cs继承Panel并重写以下方法public partial class FlowChartPanel : Panel { private ListNode _nodes new ListNode(); private ListConnection _connections new ListConnection(); private Node? _draggingNode; private Point _dragOffset; private bool _isDrawingConnection; private string? _pendingConnectionFromId; public FlowChartPanel() { this.DoubleBuffered true; // 关键禁用闪烁 this.ResizeRedraw true; this.AutoScroll true; this.BackColor Color.White; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g e.Graphics; g.SmoothingMode SmoothingMode.AntiAlias; g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; // 1. 绘制所有连线底层 foreach (var conn in _connections) { DrawConnection(g, conn); } // 2. 绘制所有节点上层 foreach (var node in _nodes) { DrawNode(g, node); } // 3. 绘制临时连线鼠标拖拽中 if (_isDrawingConnection _pendingConnectionFromId ! null) { var fromNode _nodes.FirstOrDefault(n n.Id _pendingConnectionFromId); if (fromNode ! null) { var fromCenter new Point(fromNode.Location.X fromNode.Size.Width / 2, fromNode.Location.Y fromNode.Size.Height / 2); using (var pen new Pen(Color.FromArgb(100, 0, 120, 255), 2)) { g.DrawLine(pen, fromCenter, e.ClipRectangle.Location); // 简化连到鼠标位置 } } } } private void DrawNode(Graphics g, Node node) { var rect new Rectangle(node.Location, node.Size); var brush node.Type switch { NodeType.Start Brushes.LightGreen, NodeType.End Brushes.LightCoral, NodeType.Decision Brushes.LightYellow, _ Brushes.LightBlue }; // 绘制背景矩形/菱形 if (node.Type NodeType.Decision) { var points GetDiamondPoints(rect); g.FillPolygon(brush, points); g.DrawPolygon(Pens.Black, points); } else { g.FillRectangle(brush, rect); g.DrawRectangle(Pens.Black, rect); } // 绘制文字居中自动换行 using (var sf new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center }) using (var font new Font(Microsoft Sans Serif, 9f)) { g.DrawString(node.Text, font, Brushes.Black, rect, sf); } } private Point[] GetDiamondPoints(Rectangle r) { return new[] { new Point(r.X r.Width / 2, r.Y), new Point(r.X r.Width, r.Y r.Height / 2), new Point(r.X r.Width / 2, r.Y r.Height), new Point(r.X, r.Y r.Height / 2) }; } private void DrawConnection(Graphics g, Connection conn) { if (conn.Points.Length 2) return; using (var pen new Pen(Color.FromArgb(180, 60, 60, 60), 2)) { for (int i 0; i conn.Points.Length - 1; i) { g.DrawLine(pen, conn.Points[i], conn.Points[i 1]); } // 绘制箭头仅最后一段 DrawArrowHead(g, conn.Points[conn.Points.Length - 2], conn.Points[conn.Points.Length - 1], pen); } } private void DrawArrowHead(Graphics g, Point from, Point to, Pen pen) { const int arrowSize 8; var dx to.X - from.X; var dy to.Y - from.Y; var len Math.Sqrt(dx * dx dy * dy); if (len 1) return; dx / len; dy / len; // 单位向量 var perpX -dy; var perpY dx; // 法向量 var p1 new Point( (int)(to.X - arrowSize * dx arrowSize * 0.5 * perpX), (int)(to.Y - arrowSize * dy arrowSize * 0.5 * perpY) ); var p2 to; var p3 new Point( (int)(to.X - arrowSize * dx - arrowSize * 0.5 * perpX), (int)(to.Y - arrowSize * dy - arrowSize * 0.5 * perpY) ); using (var brush new SolidBrush(pen.Color)) { g.FillPolygon(brush, new[] { p1, p2, p3 }); } } }逻辑说明DoubleBuffered true是 Winform 流程图不闪烁的后悔药必须开启ResizeRedraw true确保窗体缩放时重绘整个画布否则会出现白边残留AutoScroll true是工艺流程图长宽比失衡时的刚需比如手机中框工艺图横向超长配合this.AutoScrollMinSize new Size(2000, 1200)可手动设定画布边界DrawArrowHead用几何计算而非CustomLineCap后者在高 DPI 下易变形且无法控制箭头大小文字居中使用StringFormat而非TextRenderer.DrawText前者支持\n换行后者不支持。3. 节点增删与连线交互把“画图”变成“编程式流程建模”3.1 右键菜单驱动的节点创建精准控制类型与初始位置在FlowChartPanel中添加右键菜单支持不依赖ContextMenuStrip设计器拖拽而是纯代码注册private ContextMenuStrip _contextMenu; private ToolStripMenuItem _addStartItem; private ToolStripMenuItem _addProcessItem; private ToolStripMenuItem _addDecisionItem; private void InitializeContextMenu() { _contextMenu new ContextMenuStrip(); _addStartItem new ToolStripMenuItem(添加开始节点); _addProcessItem new ToolStripMenuItem(添加处理节点); _addDecisionItem new ToolStripMenuItem(添加判断节点); _addStartItem.Click (s, e) AddNodeAtMouse(NodeType.Start); _addProcessItem.Click (s, e) AddNodeAtMouse(NodeType.Process); _addDecisionItem.Click (s, e) AddNodeAtMouse(NodeType.Decision); _contextMenu.Items.AddRange(new ToolStripItem[] { _addStartItem, _addProcessItem, _addDecisionItem, new ToolStripSeparator(), new ToolStripMenuItem(清空所有节点, null, ClearAllNodes) }); this.ContextMenuStrip _contextMenu; } private void AddNodeAtMouse(NodeType type) { var mousePos this.PointToClient(Cursor.Position); var newNode new Node { Location new Point(mousePos.X - 60, mousePos.Y - 30), // 默认居中鼠标点 Size type NodeType.Decision ? new Size(120, 80) : new Size(120, 60), Text type switch { NodeType.Start 开始, NodeType.End 结束, NodeType.Decision 是否通过, _ 处理步骤 }, Type type, Id $node_{Guid.NewGuid().ToString(N)[..8]} }; _nodes.Add(newNode); this.Invalidate(); // 触发重绘 } private void ClearAllNodes(object sender, EventArgs e) { _nodes.Clear(); _connections.Clear(); this.Invalidate(); }参数说明PointToClient(Cursor.Position)是获取鼠标在 Panel 内坐标的唯一可靠方式MouseEventArgs.Location在右键菜单触发时已失效mousePos.X - 60是为了让 120px 宽节点以鼠标为中心避免“点哪出哪”的错觉ClearAllNodes同时清空_nodes和_connections这是常见误操作——只删节点不删连线导致DrawConnection中FromId查不到节点而崩溃。3.2 拖拽节点移动捕获 MouseDown/Move/Up 事件链GDI 绘图控件的拖拽不是“设置AllowDroptrue”那么简单必须手动管理状态机protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); if (e.Button ! MouseButtons.Left) return; // 检查是否点击到节点 var hitNode _nodes.LastOrDefault(n { var rect new Rectangle(n.Location, n.Size); return rect.Contains(e.Location); }); if (hitNode ! null) { _draggingNode hitNode; _dragOffset new Point(e.Location.X - hitNode.Location.X, e.Location.Y - hitNode.Location.Y); this.Capture true; // 关键防止鼠标移出 Panel 时丢失事件 this.Cursor Cursors.SizeAll; } else if (_nodes.Count 0) { // 点击空白处尝试启动连线从最近节点 var closest _nodes.OrderBy(n { var center new Point(n.Location.X n.Size.Width / 2, n.Location.Y n.Size.Height / 2); return Math.Pow(center.X - e.Location.X, 2) Math.Pow(center.Y - e.Location.Y, 2); }).First(); _isDrawingConnection true; _pendingConnectionFromId closest.Id; this.Invalidate(); } } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (_draggingNode.HasValue this.Capture) { var newX e.Location.X - _dragOffset.X; var newY e.Location.Y - _dragOffset.Y; // 边界限制不许拖出可视区太远防丢失 newX Math.Max(0, Math.Min(newX, this.AutoScrollMinSize.Width - _draggingNode.Value.Size.Width)); newY Math.Max(0, Math.Min(newY, this.AutoScrollMinSize.Height - _draggingNode.Value.Size.Height)); var updatedNode _draggingNode.Value; updatedNode.Location new Point(newX, newY); _draggingNode updatedNode; // 更新所有以此节点为起点或终点的连线 UpdateConnectionsForNode(updatedNode.Id); this.Invalidate(); } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); if (_draggingNode.HasValue this.Capture) { // 提交移动替换原节点 var index _nodes.FindIndex(n n.Id _draggingNode.Value.Id); if (index 0) { _nodes[index] _draggingNode.Value; } _draggingNode null; this.Capture false; this.Cursor Cursors.Default; } else if (_isDrawingConnection _pendingConnectionFromId ! null) { // 尝试连接到目标节点 var targetNode _nodes.FirstOrDefault(n { var center new Point(n.Location.X n.Size.Width / 2, n.Location.Y n.Size.Height / 2); return Math.Abs(center.X - e.Location.X) 40 Math.Abs(center.Y - e.Location.Y) 40; }); if (targetNode ! null targetNode.Id ! _pendingConnectionFromId) { CreateConnection(_pendingConnectionFromId, targetNode.Id); } _isDrawingConnection false; _pendingConnectionFromId null; this.Invalidate(); } } private void UpdateConnectionsForNode(string nodeId) { foreach (var conn in _connections) { if (conn.FromId nodeId) { var fromNode _nodes.First(n n.Id nodeId); var toNode _nodes.First(n n.Id conn.ToId); conn.Points GenerateOrthogonalPath( new Point(fromNode.Location.X fromNode.Size.Width / 2, fromNode.Location.Y fromNode.Size.Height / 2), new Point(toNode.Location.X toNode.Size.Width / 2, toNode.Location.Y toNode.Size.Height / 2) ); } else if (conn.ToId nodeId) { var fromNode _nodes.First(n n.Id conn.FromId); var toNode _nodes.First(n n.Id nodeId); conn.Points GenerateOrthogonalPath( new Point(fromNode.Location.X fromNode.Size.Width / 2, fromNode.Location.Y fromNode.Size.Height / 2), new Point(toNode.Location.X toNode.Size.Width / 2, toNode.Location.Y toNode.Size.Height / 2) ); } } } private Point[] GenerateOrthogonalPath(Point from, Point to) { // 简化正交路径L型水平垂直或反L型垂直水平优先选拐点少的 var midX (from.X to.X) / 2; var midY (from.Y to.Y) / 2; if (Math.Abs(from.X - to.X) Math.Abs(from.Y - to.Y)) { return new[] { from, new Point(from.X, midY), new Point(to.X, midY), to }; } else { return new[] { from, new Point(midX, from.Y), new Point(midX, to.Y), to }; } }关键细节this.Capture true是拖拽不丢事件的铁律Winform 中没有它鼠标快速拖拽时MouseMove会突然停止UpdateConnectionsForNode必须在OnMouseMove中实时调用否则连线会“悬空”在旧位置直到松开鼠标才刷新体验极差GenerateOrthogonalPath返回 4 个点from→拐点1→拐点2→to这是正交连线的最小表达比 Bezier 曲线更符合“流程图”语义也便于后续导出为 XML 或 JSON。3.3 双击编辑节点文本用 Form.ShowDialog 实现轻量弹窗不引入InputBox已废弃或复杂对话框手写一个极简编辑窗private void ShowNodeTextEdit(Node node) { var editForm new Form { Text 编辑节点文本, Size new Size(400, 180), FormBorderStyle FormBorderStyle.FixedDialog, MaximizeBox false, MinimizeBox false, StartPosition FormStartPosition.CenterParent }; var label new Label { Text 请输入节点文字支持换行, Location new Point(15, 15), AutoSize true }; var textBox new TextBox { Multiline true, ScrollBars ScrollBars.Vertical, Location new Point(15, 40), Size new Size(350, 60), Text node.Text }; var okBtn new Button { Text 确定, Location new Point(210, 110), DialogResult DialogResult.OK }; var cancelBtn new Button { Text 取消, Location new Point(300, 110), DialogResult DialogResult.Cancel }; editForm.Controls.AddRange(new Control[] { label, textBox, okBtn, cancelBtn }); editForm.AcceptButton okBtn; editForm.CancelButton cancelBtn; if (editForm.ShowDialog() DialogResult.OK) { var updatedNode node; updatedNode.Text textBox.Text.Trim(); var index _nodes.FindIndex(n n.Id node.Id); if (index 0) _nodes[index] updatedNode; this.Invalidate(); } } // 在 OnDoubleClick 中调用 protected override void OnDoubleClick(EventArgs e) { base.OnDoubleClick(e); var mousePos this.PointToClient(Cursor.Position); var hitNode _nodes.LastOrDefault(n { var rect new Rectangle(n.Location, n.Size); return rect.Contains(mousePos); }); if (hitNode ! null) { ShowNodeTextEdit(hitNode); } }为什么不用MessageBox.Show因为TextBox.Multiline true是编辑多行文本的刚需而MessageBox不支持。这个弹窗虽小却是用户管理模块流程图中“审批条件”“驳回原因”等字段可维护的关键入口。4. 避坑Winform FlowChart 开发中 4 个血泪经验总结4.1 现象流程图缩放后连线断裂、箭头消失原因Graphics.DrawLine和FillPolygon在ScaleTransform下行为不一致部分 GDI 方法如DrawString不响应缩放而DrawLine会缩放线宽导致箭头被压扁。解决绝不使用g.ScaleTransform()。正确做法是在OnPaint开始前根据缩放比例如zoomFactor 1.5f重新计算所有Point和Size然后传入原始Graphics对象绘制。例如var scaledFrom new Point((int)(conn.Points[0].X * zoomFactor), (int)(conn.Points[0].Y * zoomFactor)); // ... 所有点都预缩放再调用 g.DrawLine并在OnMouseWheel中维护zoomFactor同时Invalidate()整个区域。4.2 现象添加大量节点50后界面严重卡顿原因每次Invalidate()都触发全量重绘而DrawNode中new Font(...)和new StringFormat()在循环内反复创建GC 压力暴增。解决将字体和格式对象提升为类字段复用private readonly Font _nodeFont new Font(Microsoft Sans Serif, 9f); private readonly StringFormat _centerFormat new StringFormat { Alignment StringAlignment.Center, LineAlignment StringAlignment.Center };并在Dispose()中释放它们。4.3 现象DPI 缩放125%/150%下节点位置偏移、连线错位原因Winform 默认不启用 DPI 感知Control.Location返回的是设备无关单位DIP但Graphics绘制时按物理像素执行。解决在App.config中强制启用 Per-Monitor DPI 感知configuration system.windows.forms dpiAwaretrue / /configuration并在FlowChartPanel构造函数中添加this.SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.AllPaintingInWmPaint, true); this.UpdateStyles();4.4 现象导出 PNG 图片时背景为黑色或文字模糊原因Bitmap构造时未指定PixelFormat.Format32bppPArgb且未启用SmoothingMode.AntiAlias。解决导出方法必须这样写public Bitmap ExportToBitmap() { var bmp new Bitmap(this.AutoScrollMinSize.Width, this.AutoScrollMinSize.Height, PixelFormat.Format32bppPArgb); using (var g Graphics.FromImage(bmp)) { g.Clear(Color.White); g.SmoothingMode SmoothingMode.AntiAlias; g.TextRenderingHint TextRenderingHint.ClearTypeGridFit; // 复制 OnPaint 逻辑但传入 g foreach (var conn in _connections) DrawConnection(g, conn); foreach (var node in _nodes) DrawNode(g, node); } return bmp; }注意AutoScrollMinSize是画布真实尺寸不是ClientSize。5. 进阶技巧把 FlowChart 当作业务逻辑的“活文档”来用5.1 与业务代码双向绑定节点 ID 直接映射到方法名流程图的价值不在“好看”而在“可执行”。我们让每个节点Id成为 C# 方法名的约定前缀例如节点Id node_approve_order→ 对应方法void ApproveOrder()节点Id node_send_sms→ 对应方法void SendSms()实现运行时反射调用public class FlowChartExecutor { private readonly object _targetInstance; private readonly Dictionarystring, MethodInfo _methodCache new(); public FlowChartExecutor(object targetInstance) { _targetInstance targetInstance; } public void ExecuteNode(string nodeId) { var methodName Regex.Replace(nodeId, ^node_, ).Replace(_, ); methodName char.ToUpper(methodName[0]) methodName.Substring(1); if (!_methodCache.TryGetValue(methodName, out var method)) { method _targetInstance.GetType() .GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance); if (method null) throw new InvalidOperationException($未找到方法: {methodName}); _methodCache[methodName] method; } try { method.Invoke(_targetInstance, null); } catch (TargetInvocationException ex) { MessageBox.Show($执行失败: {ex.InnerException?.Message ?? ex.Message}); } } }使用方式在主窗体中初始化private readonly FlowChartExecutor _executor new FlowChartExecutor(this); // 然后在 FlowChartPanel 的双击事件中 private void OnNodeDoubleClicked(Node node) _executor.ExecuteNode(node.Id);这样用户管理模块流程图中的“创建用户”节点双击就真实调用CreateUser()方法手机中框工艺流程图中的“CNC 加工”节点双击就触发CncMachining()——流程图不再是静态示意图而是可点击、可调试、可断点的业务入口。5.2 导出为标准格式生成 PlantUML 文本供团队协作PlantUML 是研发团队最易接受的文本化流程图标准支持 Git 版本对比。我们把_nodes和_connections转为.puml文件public string GeneratePlantUml() { var sb new StringBuilder(startuml\n); sb.AppendLine(title 用户管理模块流程图); // 定义节点 foreach (var node in _nodes) { var id node.Id.Replace(node_, ); var type node.Type switch { NodeType.Start start, NodeType.End stop, NodeType.Decision $if ({node.Text}) then (yes), _ $[\{node.Text}\] }; sb.AppendLine(${id}: {type};); } // 定义连线 foreach (var conn in _connections) { var from conn.FromId.Replace(node_, ); var to conn.ToId.Replace(node_, ); sb.AppendLine(${from} -- {to};); } sb.AppendLine(enduml); return sb.ToString(); } // 保存为文件 File.WriteAllText(C:\flowchart.puml, GeneratePlantUml());生成的文本可直接粘贴到 PlantText 预览或集成到 CI 流程中自动生成文档站。5.3 保存/加载用 JSON 序列化实现跨会话持久化不依赖数据库用System.Text.Json.NET Core 3.0或Newtonsoft.Json.NET Frameworkpublic class FlowChartData { public ListNode Nodes { get; set; } new(); public ListConnection Connections { get; set; } new(); } public void SaveToFile(string path) { var data new FlowChartData { Nodes _nodes, Connections _connections }; File.WriteAllText(path, JsonSerializer.Serialize(data, new JsonSerializerOptions { WriteIndented true })); } public void LoadFromFile(string path) { var json File.ReadAllText(path); var data JsonSerializer.DeserializeFlowChartData(json); _nodes data.Nodes; _connections data.Connections; this.Invalidate(); }注意NodeType枚举需加[JsonConverter(typeof(JsonStringEnumConverter))]否则序列化为数字而非字符串。我一般会在主窗体FormClosing事件中自动保存到AppDomain.CurrentDomain.BaseDirectory \\last_flow.json下次启动时检测文件存在则自动加载——这招在算法流程图教学场景中特别实用学生关机前流程没画完重启后一键恢复。最后说一句血泪教训别在FlowChartPanel里写业务逻辑它只负责“画”和“传参”。真正的审批规则、工艺参数校验、短信发送逻辑全部放在独立的BusinessService类里通过Actionstring或事件委托注入。这样当某天客户说“这个流程图要改成 Web 版”你只需重写FlowChartPanel的 Web 实现业务内核一行不动。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站