using LB_SmartVisionCommon;
|
using System.Collections.Concurrent;
|
using System.Diagnostics;
|
using System.Xml.Linq;
|
|
namespace LB_VisionFlowNode
|
{
|
public class FlowPanel : Panel, IMessageFilter
|
{
|
#region 私有变量
|
private PanelMode PanelMode = PanelMode.Normal;
|
|
private Point mousePosition;
|
private Point mouseRightStartLocation;
|
private Point mouseLeftStartLocation;
|
private Point dragStartPoint;
|
|
private int connectStartNodeBranchIndex = -1;
|
private FlowNode connectStartNode = null;
|
|
private FlowNode selectedNode = null;
|
private FlowConnection selectedConnection = null;
|
private int selectedBranchIndex = -1;
|
|
/// <summary>
|
/// nodes的索引为节点名称
|
/// </summary>
|
private ConcurrentDictionary<string, FlowNode> nodes
|
= new ConcurrentDictionary<string, FlowNode>();
|
|
public ConcurrentDictionary<string, FlowNode> GetAllNodes() { return nodes; }
|
|
/// <summary>
|
/// connections的索引为起点节点名称(分支节点会增加{-Branch connectStartNodeBranchIndex})
|
/// </summary>
|
private ConcurrentDictionary<string, FlowConnection> connections
|
= new ConcurrentDictionary<string, FlowConnection>();
|
|
private ContextMenuStrip rContextMenu = null;
|
|
private bool messageFilterInstalled = false;
|
|
#endregion
|
|
#region 外部调用事件
|
/// <summary>
|
/// 新增节点(name,class)
|
/// </summary>
|
public Action<string, string> AddNodeAction;
|
|
/// <summary>
|
/// 新增分支
|
/// </summary>
|
public Action<string> AddBranchAction;
|
|
/// <summary>
|
/// 重命名节点
|
/// </summary>
|
public Action<string, string> RenameNodeAction;
|
|
/// <summary>
|
/// 删除节点
|
/// </summary>
|
public Action<string> DeleteNodeAction;
|
|
/// <summary>
|
/// 删除节点
|
/// </summary>
|
public Action<string> DeleteBranchAction;
|
|
/// <summary>
|
/// 编辑节点
|
/// </summary>
|
public Action<string> EditNodeAction;
|
|
/// <summary>
|
/// 输入输出节点
|
/// </summary>
|
public Action<string> InAndOutNodeAction;
|
#endregion
|
|
public FlowPanel()
|
{
|
// 启用双缓冲和自定义绘制
|
this.SetStyle(ControlStyles.AllPaintingInWmPaint |
|
ControlStyles.UserPaint |
|
ControlStyles.DoubleBuffer |
|
ControlStyles.ResizeRedraw, true);
|
|
this.UpdateStyles();
|
|
this.BackColor = Color.White;
|
this.Font = new Font("宋体", 9F);
|
this.Paint += OnPaint;
|
this.MouseDoubleClick += OnMouseDoubleClick;
|
this.MouseDown += OnMouseDown;
|
this.MouseMove += OnMouseMove;
|
this.MouseUp += OnMouseUp;
|
|
this.GotFocus += (s, e) => InstallMessageFilter();
|
this.LostFocus += (s, e) => RemoveMessageFilter();
|
this.MouseDown += (s, e) => this.Focus();
|
|
rContextMenu = new ContextMenuStrip();
|
this.ContextMenuStrip = rContextMenu;
|
|
this.AutoScroll = false; // 禁用自动滚动
|
}
|
|
public readonly IFlowContext Context = null;
|
|
public FlowPanel(IFlowContext context) : this()
|
{
|
this.Context = context;
|
}
|
|
// 定义委托类型
|
private delegate void NodeHandler(FlowNode node);
|
|
// 使用委托字典
|
private Dictionary<string, NodeHandler> _nodeHandlers = new Dictionary<string, NodeHandler>();
|
|
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
|
string NodesMsg = string.Empty;
|
|
private async Task<bool> ExecuteFlowAsync(FlowNode startNode, ExecutionContext ExecutionContext)
|
{
|
string strRunTime = $"运行时间记录\r\n";
|
// 假设 nameWidth 是动态计算的列宽(比如 20)
|
int nameWidth = 30; //步骤名称小于20个列宽
|
var formatString = "{0,-" + nameWidth + "}{1,-10}";
|
try
|
{
|
var currentNode = startNode;
|
string nextNodeName = string.Empty;
|
DateTime StartTime = DateTime.Now;
|
while (currentNode != null && !_cancellationTokenSource.IsCancellationRequested)
|
{
|
|
await ExecuteNodeAsync(currentNode, ExecutionContext);
|
|
// 防止死循环,运行时间超过60秒则强制终止
|
if ((DateTime.Now - StartTime).TotalSeconds > 10)
|
{
|
_cancellationTokenSource.Cancel();
|
NodesMsg = $"执行所有节点超过10s";
|
return false;
|
}
|
|
// 执行完当前节点后交换到下一个节点运行
|
switch (currentNode.NodeType)
|
{
|
case NodeType.End:
|
return GetResult(ExecutionContext);
|
case NodeType.Switch:
|
case NodeType.MultiBranch:
|
if (currentNode.BranchNodes.ContainsKey($"{currentNode.Text}-Branch{currentNode.BranchIndex}"))
|
{
|
nextNodeName = currentNode.BranchNodes[$"{currentNode.Text}-Branch{currentNode.BranchIndex}"];
|
|
if (nextNodeName == null || !nodes.ContainsKey(nextNodeName))
|
{
|
return GetResult(ExecutionContext);
|
}
|
else
|
currentNode = nodes[nextNodeName];
|
}
|
else
|
{
|
return GetResult(ExecutionContext);
|
}
|
break;
|
case NodeType.Parallel:
|
// 并行执行多分支(直至当前branch都走到了Join汇聚分支节点(等待所有分支完成))
|
currentNode = await ExecuteParallelBranches(currentNode, ExecutionContext);
|
break;
|
case NodeType.Join:
|
default:
|
if (currentNode.BranchNodes.ContainsKey($"{currentNode.Text}-Branch0"))
|
nextNodeName = currentNode.BranchNodes[$"{currentNode.Text}-Branch0"];
|
else
|
return GetResult(ExecutionContext);
|
|
if (nextNodeName == null || !nodes.ContainsKey(nextNodeName))
|
return GetResult(ExecutionContext);
|
else
|
currentNode = nodes[nextNodeName];
|
break;
|
}
|
}
|
|
return GetResult(ExecutionContext);
|
}
|
catch (Exception ex) { NodesMsg = $"执行流程发生意外,{ex.Message}【{ex.StackTrace}】"; return false; }
|
}
|
|
private async Task<FlowNode> ExecuteParallelBranches(FlowNode ParallelNode, ExecutionContext ExecutionContext)
|
{
|
try
|
{
|
var currentNode = ParallelNode;
|
string nextNodeName = string.Empty;
|
DateTime StartTime = DateTime.Now;
|
while (currentNode != null && !_cancellationTokenSource.IsCancellationRequested)
|
{
|
// 防止死循环,运行时间超过10秒则强制终止
|
if ((DateTime.Now - StartTime).TotalSeconds > 10)
|
{
|
_cancellationTokenSource.Cancel();
|
NodesMsg = $"{currentNode.Text},并行开始运行超过10s";
|
return null;
|
}
|
|
// 执行完当前节点后交换到下一个节点运行
|
switch (currentNode.NodeType)
|
{
|
case NodeType.End:
|
return null;
|
case NodeType.Switch:
|
case NodeType.MultiBranch:
|
await ExecuteNodeAsync(currentNode, ExecutionContext);
|
if (currentNode.BranchNodes.ContainsKey($"{currentNode.Text}-Branch{currentNode.BranchIndex}"))
|
{
|
nextNodeName = currentNode.BranchNodes[$"{currentNode.Text}-Branch{currentNode.BranchIndex}"];
|
|
if (nextNodeName == null || !nodes.ContainsKey(nextNodeName))
|
return null;
|
else
|
currentNode = nodes[nextNodeName];
|
}
|
else
|
return null;
|
break;
|
case NodeType.Parallel:
|
// 并行执行多分支(直至当前branch都走到了Join汇聚分支节点(等待所有分支完成))
|
var parallelTasks = new List<Task<FlowNode>>();
|
|
foreach (var branch in currentNode.BranchNodes)
|
{
|
if (!nodes.ContainsKey(branch.Value))
|
return null;
|
else
|
{
|
parallelTasks.Add(Task.Run(async () =>
|
{
|
return await ExecuteParallelBranches(nodes[branch.Value], ExecutionContext); ;
|
}));
|
}
|
}
|
|
// 等待所有并行任务完成或超时
|
var timeoutTask = Task.Delay(currentNode.TimeoutMilliseconds, _cancellationTokenSource.Token);
|
var completedTask = await Task.WhenAny(Task.WhenAll(parallelTasks), timeoutTask);
|
|
if (completedTask == timeoutTask)
|
{
|
NodesMsg = $"并行执行超时:{currentNode.Text}";
|
return null;
|
}
|
|
var JoinNodes = await Task.WhenAll(parallelTasks);
|
|
// 找到JoinNodes第一个不为null的节点作为下一个节点
|
return JoinNodes.FirstOrDefault(node => node != null);
|
case NodeType.Join:
|
await ExecuteNodeAsync(currentNode, ExecutionContext);
|
nextNodeName = currentNode.BranchNodes[$"{currentNode.Text}-Branch0"];
|
|
if (nextNodeName == null || !nodes.ContainsKey(nextNodeName))
|
return null;
|
else
|
return nodes[nextNodeName];
|
default:
|
await ExecuteNodeAsync(currentNode, ExecutionContext);
|
nextNodeName = currentNode.BranchNodes[$"{currentNode.Text}-Branch0"];
|
|
if (nextNodeName == null || !nodes.ContainsKey(nextNodeName))
|
return null;
|
else
|
currentNode = nodes[nextNodeName];
|
break;
|
}
|
}
|
|
return null;
|
}
|
catch { return null; }
|
}
|
|
private bool GetResult(ExecutionContext ExecutionContext)
|
{
|
if (ExecutionContext.BranchResults.Values.Contains(false))
|
return false;
|
else
|
return true;
|
}
|
|
private async Task ExecuteNodeAsync(FlowNode currentNode, ExecutionContext context)
|
{
|
if (currentNode == null) return;
|
|
// 执行当前节点
|
context.CurrentconnectStartNodeBranchIndex = currentNode.BranchIndex;
|
context.CurrentBranchName = $"{currentNode.Text}-Branch{currentNode.BranchIndex}";
|
bool result = Context.ExecuteNode(currentNode);
|
#if DEBUG
|
Debug.WriteLine(DateTime.Now.ToString("[yyyy:MM:dd:HH:mm:ss:fff] ") + $"执行节点[{currentNode.Text}],结果为{result}");
|
AsyncLogHelper.Debug(DateTime.Now.ToString("[yyyy:MM:dd:HH:mm:ss:fff] ") + $"执行节点[{currentNode.Text}],结果为{result}");
|
#endif
|
context.BranchResults.TryAdd(context.CurrentBranchName, currentNode.Result);
|
currentNode.Result = result;
|
}
|
|
public bool Run(out string msg)
|
{
|
NodesMsg = string.Empty;
|
if (nodes == null || nodes.Count <= 0 || Context == null)
|
{
|
msg = "未配置节点或上下文,无法运行";
|
return false;
|
}
|
|
var beginNodes = nodes.AsParallel()
|
.Where(n => n.Value.Text == "开始")
|
.ToList();
|
|
if (beginNodes.Count > 0)
|
{
|
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource = new CancellationTokenSource();
|
bool result = Task.Run(() =>
|
ExecuteFlowAsync(beginNodes[0].Value, new ExecutionContext())
|
).Result;
|
|
if (result)
|
msg = string.Empty;
|
else
|
msg = NodesMsg;
|
return result;
|
}
|
|
msg = "未找到开始节点";
|
return false;
|
}
|
|
private void AddNode(FlowNode node)
|
{
|
// 名称重复则添加(副本)
|
string nodeName = node.Text;
|
|
while (nodes.Any(n => n.Value.Text == nodeName))
|
{
|
nodeName += "(Copy)";
|
}
|
|
node.Text = nodeName;
|
if (this.nodes.TryAdd(nodeName, node))
|
AddNodeAction?.Invoke(nodeName, node.Description);
|
|
this.Invalidate();
|
}
|
|
public void ClearNodes()
|
{
|
this.nodes.Clear();
|
this.connections.Clear();
|
this.Invalidate();
|
}
|
|
string filePath = string.Empty;
|
|
public bool Load(string filePath)
|
{
|
try
|
{
|
PanelMode = PanelMode.Run;
|
|
nodes.Clear();
|
connections.Clear();
|
|
string json = File.ReadAllText(filePath);
|
// 反序列化
|
var (deserializedNodes, deserializedConnections) = FlowSerializer.Deserialize(json);
|
nodes = deserializedNodes;
|
connections = deserializedConnections;
|
|
PanelMode = PanelMode.Normal;
|
this.filePath = filePath;
|
return true;
|
}
|
catch (Exception ex)
|
{
|
MessageBox.Show($"加载失败: {ex.Message}", "异常");
|
return false;
|
}
|
}
|
|
public bool Save(string filePath)
|
{
|
try
|
{
|
if (string.IsNullOrEmpty(filePath))
|
return true;
|
|
PanelMode = PanelMode.Run;
|
|
string json = FlowSerializer.Serialize(nodes, connections);
|
File.WriteAllText(filePath, json);
|
|
PanelMode = PanelMode.Normal;
|
return true;
|
}
|
catch { return false; }
|
}
|
|
#region 私有函数
|
|
#region 检测点击是否在节点上
|
private FlowNode GetNodeAt(Point location)
|
{
|
foreach (var node in nodes)
|
{
|
if (node.Value.GetBounds().Contains(location))
|
return node.Value;
|
}
|
return null;
|
}
|
#endregion
|
|
#region 检测点击是否在连接线上
|
public FlowConnection GetConnectionAt(Point mousePoint)
|
{
|
foreach (var connection in connections)
|
{
|
if (IsPointNearConnection(mousePoint, connection.Value))
|
return connection.Value;
|
}
|
return null;
|
}
|
|
private bool IsPointNearConnection(Point point, FlowConnection connection, int tolerance = 3)
|
{
|
if (connection == null || connection.StartNode == null || connection.EndNode == null)
|
return false;
|
|
Point[] points = GetConnectionAllPoints(connection);
|
|
// 检查每个线段
|
for (int i = 0; i < points.Length - 1; i++)
|
{
|
if (IsPointNearLine(point, points[i], points[i + 1]))
|
return true;
|
}
|
return false;
|
}
|
|
// 判断点是否靠近线段
|
private bool IsPointNearLine(Point point, Point lineStart, Point lineEnd, int tolerance = 3)
|
{
|
// 计算点到线段的距离
|
double distance = PointToLineDistance(point, lineStart, lineEnd);
|
return distance <= tolerance;
|
}
|
|
// 计算点到线段的距离
|
private double PointToLineDistance(Point point, Point lineStart, Point lineEnd)
|
{
|
double lineLengthSquared = Math.Pow(lineEnd.X - lineStart.X, 2) + Math.Pow(lineEnd.Y - lineStart.Y, 2);
|
|
if (lineLengthSquared == 0) // 线段长度为0
|
return Distance(point, lineStart);
|
|
// 计算投影比例
|
double t = Math.Max(0, Math.Min(1,
|
((point.X - lineStart.X) * (lineEnd.X - lineStart.X) +
|
(point.Y - lineStart.Y) * (lineEnd.Y - lineStart.Y)) / lineLengthSquared));
|
|
// 计算投影点
|
Point projection = new Point(
|
(int)(lineStart.X + t * (lineEnd.X - lineStart.X)),
|
(int)(lineStart.Y + t * (lineEnd.Y - lineStart.Y))
|
);
|
|
// 返回点到投影点的距离
|
return Distance(point, projection);
|
}
|
#endregion
|
|
#region 检测点击是否在分支上
|
|
private int GetBranchAt(Point location)
|
{
|
foreach (var connection in connections)
|
{
|
if (IsPointNearConnection(location, connection.Value))
|
return connection.Value.BranchIndex;
|
}
|
return -1;
|
}
|
|
private FlowNode OnNodeBranch(Point location, out int index)
|
{
|
index = -1;
|
foreach (var node in nodes.Values)
|
{
|
index = GetPointIndexNearLocation(location, node.GetBranchPoints());
|
if (index != -1)
|
return node;
|
}
|
return null;
|
}
|
|
/// <summary>
|
/// 检测鼠标位置是否在点列表中的任意点附近,并返回该点的索引
|
/// </summary>
|
/// <param name="mousePoint">鼠标位置</param>
|
/// <param name="points">点列表</param>
|
/// <param name="tolerance">容差范围(像素)</param>
|
/// <returns>找到的点的索引,如果没找到返回-1</returns>
|
public int GetPointIndexNearLocation(Point mousePoint, List<Point> points, float tolerance = 8f)
|
{
|
if (points == null || points.Count == 0)
|
return -1;
|
|
for (int i = 0; i < points.Count; i++)
|
{
|
if (IsPointNearPoint(mousePoint, points[i], tolerance))
|
return i;
|
}
|
|
return -1;
|
}
|
|
/// <summary>
|
/// 判断两个点是否在容差范围内接近
|
/// </summary>
|
private bool IsPointNearPoint(Point point1, Point point2, float tolerance)
|
{
|
float distance = Distance(point1, point2);
|
return distance <= tolerance;
|
}
|
|
/// <summary>
|
/// 计算两点之间的距离
|
/// </summary>
|
private float Distance(Point p1, Point p2)
|
{
|
return (float)Math.Sqrt(Math.Pow(p2.X - p1.X, 2) + Math.Pow(p2.Y - p1.Y, 2));
|
}
|
#endregion
|
|
#region 绘制相关
|
/// <summary>
|
/// 绘制节点
|
/// </summary>
|
/// <param name="g"></param>
|
/// <param name="node"></param>
|
private void DrawNode(Graphics g, FlowNode node)
|
{
|
// 绘制节点背景
|
Color background = node.GetColor();
|
using (Brush brush = new SolidBrush(background))
|
{
|
switch (node.NodeType)
|
{
|
case NodeType.Switch:
|
case NodeType.MultiBranch:
|
g.FillPolygon(brush, FlowNode.GetDiamondPoints(node.GetBounds()));
|
break;
|
case NodeType.Begin:
|
case NodeType.End:
|
case NodeType.Parallel:
|
case NodeType.Join:
|
g.FillEllipse(brush, node.GetBounds());
|
break;
|
default:
|
g.FillRectangle(brush, node.GetBounds());
|
break;
|
}
|
}
|
|
// 绘制边框
|
if (node == selectedNode)
|
{
|
Color borderColor = Color.Coral;
|
int borderWidth = 3;
|
|
using (Pen pen = new Pen(borderColor, borderWidth))
|
{
|
switch (node.NodeType)
|
{
|
case NodeType.Switch:
|
case NodeType.MultiBranch:
|
g.DrawPolygon(pen, FlowNode.GetDiamondPoints(node.GetBounds()));
|
break;
|
case NodeType.Begin:
|
case NodeType.End:
|
case NodeType.Parallel:
|
case NodeType.Join:
|
g.DrawEllipse(pen, node.GetBounds());
|
break;
|
default:
|
g.DrawRectangle(pen, node.GetBounds());
|
break;
|
}
|
}
|
}
|
|
// 绘制文本
|
using (StringFormat sf = new StringFormat())
|
{
|
sf.Alignment = StringAlignment.Center;
|
sf.LineAlignment = StringAlignment.Center;
|
using (Brush textBrush = new SolidBrush(Color.Black))
|
{
|
g.DrawString(node.Text, this.Font, textBrush, node.GetBounds(), sf);
|
}
|
}
|
|
// 绘制连接点
|
using (Brush brush = new SolidBrush(Color.Coral))
|
{
|
foreach (var point in node.GetConnectionPoints())
|
g.FillEllipse(brush, point.X - 2, point.Y - 2, 4, 4);
|
}
|
}
|
|
/// <summary>
|
/// 绘制连接线
|
/// </summary>
|
/// <param name="g"></param>
|
/// <param name="connection"></param>
|
private void DrawConnection(Graphics g, FlowConnection connection, Pen pen)
|
{
|
if (connection == null || connection.StartNode == null || connection.EndNode == null)
|
return;
|
|
Point[] allPoints = GetConnectionAllPoints(connection);
|
|
if (allPoints.Length > 2)
|
{
|
// 绘制折线
|
g.DrawLines(pen, allPoints);
|
// 在最后一段线段上绘制箭头
|
DrawArrow(g, pen, allPoints[allPoints.Length - 2], allPoints[allPoints.Length - 1]);
|
}
|
else if (allPoints.Length == 2)
|
{
|
// 从上往下直接绘制直线
|
g.DrawLine(pen, allPoints[0], allPoints[1]);
|
DrawArrow(g, pen, allPoints[0], allPoints[1]);
|
}
|
return;
|
}
|
|
/// <summary>
|
/// 获取连接线的所有点
|
/// </summary>
|
/// <param name="connection"></param>
|
/// <returns></returns>
|
Point[] GetConnectionAllPoints(FlowConnection connection)
|
{
|
if (connection.StartNode == null || connection.EndNode == null)
|
return new Point[] { };
|
|
Point startPoint = connection.StartNode.BtmPoint;
|
Point endPoint = connection.EndNode.TopPoint;
|
|
switch (connection.StartNode.NodeType)
|
{
|
case NodeType.Switch:
|
if (connection.BranchIndex == 0)
|
{
|
// connectStartNodeBranchIndex为StartNode的左边
|
startPoint = connection.StartNode.LeftPoint;
|
if (endPoint.X > startPoint.X || startPoint.Y > endPoint.Y)
|
{
|
return new Point[]
|
{
|
startPoint,
|
new Point(startPoint.X - 10, startPoint.Y),
|
new Point(startPoint.X - 10, endPoint.Y - 10),
|
new Point(endPoint.X, endPoint.Y - 10) ,
|
endPoint
|
};
|
}
|
else
|
{
|
return new Point[]
|
{
|
startPoint,
|
new Point(endPoint.X, startPoint.Y),
|
endPoint
|
};
|
}
|
}
|
else if (connection.BranchIndex == 1)
|
{
|
// connectStartNodeBranchIndex为StartNode的右边
|
startPoint = connection.StartNode.RightPoint;
|
if (endPoint.X < startPoint.X || startPoint.Y > endPoint.Y)
|
{
|
return new Point[]
|
{
|
startPoint,
|
new Point(startPoint.X + 10, startPoint.Y),
|
new Point(startPoint.X + 10, endPoint.Y - 10),
|
new Point(endPoint.X, endPoint.Y - 10) ,
|
endPoint
|
};
|
}
|
else
|
{
|
return new Point[]
|
{
|
startPoint,
|
new Point(endPoint.X, startPoint.Y),
|
endPoint
|
};
|
}
|
}
|
break;
|
case NodeType.MultiBranch:
|
case NodeType.Parallel:
|
// 多分支节点:使用分支索引获取连接点
|
if (connection.BranchIndex >= 0 && connection.BranchIndex < connection.StartNode.BranchNodes.Count)
|
return CalculateMultiBranchConnectionPath(
|
connection.StartNode.GetBranchPoints(connection.BranchIndex)
|
, endPoint, connection.StartNode, connection.EndNode);
|
break;
|
default:
|
startPoint = connection.StartNode.BtmPoint;
|
return CalculateMultiBranchConnectionPath(startPoint, endPoint, connection.StartNode, connection.EndNode);
|
}
|
return new Point[] { };
|
}
|
|
/// <summary>
|
/// 上下连接的节点路径计算
|
/// </summary>
|
/// <param name="startPoint"></param>
|
/// <param name="endPoint"></param>
|
/// <param name="connectStartNodeBranchIndex"></param>
|
/// <returns></returns>
|
private Point[] CalculateMultiBranchConnectionPath(Point startPoint, Point endPoint
|
, FlowNode StartNode, FlowNode EndNode)
|
{
|
if (startPoint.Y > endPoint.Y)
|
{
|
if (startPoint.X > endPoint.X)
|
return new Point[]
|
{
|
startPoint,
|
new Point(startPoint.X, startPoint.Y + 10),
|
new Point(startPoint.X - StartNode.Width, startPoint.Y + 10),
|
new Point(startPoint.X - StartNode.Width, endPoint.Y - 10),
|
new Point(endPoint.X, endPoint.Y - 10),
|
endPoint
|
};
|
else
|
return new Point[]
|
{
|
startPoint,
|
new Point(startPoint.X, startPoint.Y + 10),
|
new Point(startPoint.X + StartNode.Width, startPoint.Y + 10),
|
new Point(startPoint.X + StartNode.Width, endPoint.Y - 10),
|
new Point(endPoint.X, endPoint.Y - 10),
|
endPoint
|
};
|
}
|
else if (startPoint.X != endPoint.X)
|
{
|
return new Point[]
|
{
|
startPoint,
|
new Point(startPoint.X, (startPoint.Y + endPoint.Y) / 2),
|
new Point(endPoint.X, (startPoint.Y + endPoint.Y) / 2),
|
endPoint
|
};
|
}
|
else
|
{
|
return new Point[]
|
{
|
startPoint,
|
endPoint
|
};
|
}
|
}
|
|
/// <summary>
|
/// 绘制箭头
|
/// </summary>
|
/// <param name="g"></param>
|
/// <param name="pen"></param>
|
/// <param name="start"></param>
|
/// <param name="end"></param>
|
private void DrawArrow(Graphics g, Pen pen, Point start, Point end)
|
{
|
// 计算箭头方向
|
double dx = end.X - start.X;
|
double dy = end.Y - start.Y;
|
double angle = Math.Atan2(dy, dx);
|
|
int arrowLength = 8; // 增加箭头长度
|
double arrowAngle = Math.PI / 6; // 30度角
|
|
// 计算箭头的两个端点
|
Point arrowPoint1 = new Point(
|
(int)(end.X - arrowLength * Math.Cos(angle - arrowAngle)),
|
(int)(end.Y - arrowLength * Math.Sin(angle - arrowAngle)));
|
|
Point arrowPoint2 = new Point(
|
(int)(end.X - arrowLength * Math.Cos(angle + arrowAngle)),
|
(int)(end.Y - arrowLength * Math.Sin(angle + arrowAngle)));
|
|
// 绘制箭头
|
using (Pen arrowPen = new Pen(pen.Color, pen.Width))
|
{
|
g.DrawLine(arrowPen, end, arrowPoint1);
|
g.DrawLine(arrowPen, end, arrowPoint2);
|
|
// 可选:填充箭头(实心箭头)
|
Point[] arrowPoints = { end, arrowPoint1, arrowPoint2 };
|
using (Brush arrowBrush = new SolidBrush(pen.Color))
|
{
|
g.FillPolygon(arrowBrush, arrowPoints);
|
}
|
}
|
}
|
#endregion
|
|
#region 键盘相关
|
protected override void OnKeyDown(KeyEventArgs e)
|
{
|
base.OnKeyDown(e);
|
|
switch (e.KeyCode)
|
{
|
case Keys.Delete:
|
if (selectedNode != null)
|
{
|
RemoveNode(selectedNode);
|
selectedNode = null;
|
e.Handled = true;
|
return;
|
}
|
|
if (selectedConnection != null)
|
{
|
RemoveConnection(selectedConnection, selectedBranchIndex);
|
selectedConnection = null;
|
e.Handled = true;
|
return;
|
}
|
break;
|
case Keys.Escape:
|
selectedConnection = null;
|
selectedNode = null;
|
Invalidate();
|
e.Handled = true;
|
return;
|
}
|
|
Save(filePath);
|
return;
|
}
|
|
private void InstallMessageFilter()
|
{
|
if (!messageFilterInstalled)
|
{
|
Application.AddMessageFilter(this);
|
messageFilterInstalled = true;
|
}
|
}
|
|
private void RemoveMessageFilter()
|
{
|
if (messageFilterInstalled)
|
{
|
Application.RemoveMessageFilter(this);
|
messageFilterInstalled = false;
|
}
|
}
|
|
public bool PreFilterMessage(ref Message m)
|
{
|
const int WM_KEYDOWN = 0x100;
|
const int WM_KEYUP = 0x101;
|
|
// 只在Panel有焦点时处理消息
|
if (this.Focused && (m.Msg == WM_KEYDOWN || m.Msg == WM_KEYUP))
|
{
|
Keys keyData = (Keys)(int)m.WParam;
|
|
if (m.Msg == WM_KEYDOWN)
|
{
|
var e = new KeyEventArgs(keyData);
|
OnKeyDown(e);
|
return e.Handled;
|
}
|
}
|
|
return false;
|
}
|
#endregion
|
|
#region 内部事件(重绘/鼠标)
|
private void OnPaint(object sender, PaintEventArgs e)
|
{
|
Graphics g = e.Graphics;
|
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
// 绘制节点
|
foreach (var node in nodes.Values)
|
DrawNode(g, node);
|
|
// 绘制连接线
|
foreach (var connection in connections.Values)
|
{
|
if (connection == selectedConnection)
|
DrawConnection(g, connection, new Pen(Color.Coral, connection.LineWidth));
|
else
|
DrawConnection(g, connection, new Pen(connection.LineColor, connection.LineWidth));
|
}
|
|
// 绘制临时连接线
|
if (this.PanelMode == PanelMode.CreatingConnection && connectStartNode != null)
|
{
|
using (Pen tempPen = new Pen(Color.Coral, 2))
|
{
|
tempPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
|
|
// 虚拟节点用于绘制连接线终点
|
FlowNode virtualEndNode = new FlowNode(NodeType.Normal
|
, new Point(mousePosition.X, mousePosition.Y + connectStartNode.Height / 2)
|
, "virtualNode", "virtualNode", connectStartNode.Width, connectStartNode.Height);
|
DrawConnection(g, new FlowConnection(connectStartNode, virtualEndNode, connectStartNodeBranchIndex), tempPen);
|
}
|
}
|
}
|
|
private void OnMouseDoubleClick(object sender, MouseEventArgs e)
|
{
|
switch (e.Button)
|
{
|
case MouseButtons.Left:
|
{
|
FlowNode clickedNode = GetNodeAt(e.Location);
|
|
if (clickedNode != null)
|
{
|
try
|
{
|
EditNodeAction?.Invoke(clickedNode.Text);
|
}
|
catch { }
|
}
|
}
|
break;
|
case MouseButtons.Right:
|
default:
|
break;
|
}
|
}
|
|
private void OnMouseDown(object sender, MouseEventArgs e)
|
{
|
PanelMode = PanelMode.Normal;
|
selectedNode = GetNodeAt(e.Location);
|
selectedBranchIndex = GetBranchAt(e.Location);
|
switch (e.Button)
|
{
|
case MouseButtons.Left:
|
FlowNode isConnectingNode = OnNodeBranch(e.Location, out connectStartNodeBranchIndex);
|
selectedConnection = GetConnectionAt(e.Location);
|
mouseLeftStartLocation = e.Location;
|
// 点击在边界上并未点击在节点上,进入判断是否进入连接线模式
|
if (isConnectingNode != null && selectedNode == null)
|
{
|
// 只能从连接点开始连线 没有选中节点 选中了连接点
|
if (connectStartNodeBranchIndex < 0)
|
return;
|
|
PanelMode = PanelMode.CreatingConnection;
|
connectStartNode = isConnectingNode;
|
this.Invalidate();
|
return;
|
}
|
|
// 点击在节点上,预备进入拖动节点模式
|
if (selectedNode != null)
|
{
|
this.PanelMode = PanelMode.DraggingNode;
|
dragStartPoint = e.Location;
|
return;
|
}
|
break;
|
case MouseButtons.Right:
|
rContextMenu.Items.Clear();
|
mouseRightStartLocation = e.Location;
|
if (selectedNode == null && Context != null)
|
{
|
// 没有选中节点时,显示动态节点菜单
|
var categorizedItems = Context.GetCategorizedMenuItems(ContextMenuItem_Click);
|
|
foreach (var category in categorizedItems)
|
{
|
// 创建类别下拉菜单
|
var categoryMenu = new ToolStripMenuItem(category.Key);
|
|
// 添加该类别的所有菜单项
|
foreach (var menuItem in category.Value)
|
{
|
categoryMenu.DropDownItems.Add(menuItem);
|
}
|
|
rContextMenu.Items.Add(categoryMenu);
|
}
|
}
|
else if (selectedNode != null)
|
{
|
if (selectedNode.NodeType == NodeType.Begin
|
|| selectedNode.NodeType == NodeType.End)
|
{
|
rContextMenu.Items.Clear();
|
return;
|
}
|
var renameItem = new ToolStripMenuItem("编辑");
|
renameItem.Click += (s, ev) => EditInAndOutNode();
|
rContextMenu.Items.Add(renameItem);
|
|
var deleteItem = new ToolStripMenuItem("重命名");
|
deleteItem.Click += (s, ev) => RenameNode();
|
rContextMenu.Items.Add(deleteItem);
|
|
if (selectedNode.NodeType == NodeType.Normal
|
|| selectedNode.NodeType == NodeType.Switch
|
|| selectedNode.NodeType == NodeType.MultiBranch)
|
{
|
var breakItem = new ToolStripMenuItem("禁用");
|
breakItem.Checked = selectedNode.Break;
|
breakItem.Click += (s, ev) => BreakNode();
|
rContextMenu.Items.Add(breakItem);
|
}
|
|
if (selectedNode.NodeType == NodeType.Parallel
|
|| selectedNode.NodeType == NodeType.MultiBranch)
|
{
|
var addBranchItem = new ToolStripMenuItem("添加分支");
|
addBranchItem.Click += (s, ev) => AddBranch();
|
rContextMenu.Items.Add(addBranchItem);
|
|
var removeBranchItem = new ToolStripMenuItem("移除分支");
|
removeBranchItem.Click += (s, ev) => RemoveBranch();
|
rContextMenu.Items.Add(removeBranchItem);
|
}
|
}
|
break;
|
default:
|
selectedNode = null;
|
break;
|
}
|
this.Invalidate();
|
}
|
|
private void OnMouseUp(object sender, MouseEventArgs e)
|
{
|
try
|
{
|
switch (e.Button)
|
{
|
case MouseButtons.Left:
|
switch (this.PanelMode)
|
{
|
case PanelMode.CreatingConnection:
|
FlowNode connectEndNode = GetNodeAt(e.Location);
|
|
if (connectEndNode == null)
|
{
|
connectStartNode = null;
|
connectStartNodeBranchIndex = -1;
|
return;
|
}
|
|
// 完成连线操作后需要判断该连接线所连接的起始节点和终点节点是否已经被连接
|
foreach (var conn in connections.Values)
|
{
|
// 已经存在该连接线
|
if (conn.StartNode == connectStartNode
|
&& conn.EndNode == connectEndNode)
|
{
|
connectStartNode = null;
|
connectStartNodeBranchIndex = -1;
|
return;
|
}
|
|
// 连接线的起点已经连接了其他节点,则断开之前的连接
|
switch (conn.StartNode.NodeType)
|
{
|
// 分支节点逻辑单独处理
|
case NodeType.Switch:
|
if (conn.StartNode == connectStartNode && connectStartNodeBranchIndex >= 0
|
&& conn.BranchIndex == connectStartNodeBranchIndex)
|
{
|
RemoveConnection(conn, connectStartNodeBranchIndex);
|
break;
|
}
|
break;
|
// 多分支节点逻辑单独处理
|
case NodeType.MultiBranch:
|
case NodeType.Parallel:
|
if (conn.StartNode == connectStartNode && connectStartNodeBranchIndex >= 0
|
&& conn.BranchIndex == connectStartNodeBranchIndex)
|
{
|
// 断开之前的连接
|
RemoveConnection(conn, connectStartNodeBranchIndex);
|
break;
|
}
|
break;
|
default:
|
if (conn.StartNode == connectStartNode)
|
{
|
// 断开之前的连接
|
RemoveConnection(conn, connectStartNodeBranchIndex);
|
break;
|
}
|
break;
|
}
|
}
|
|
// 确保连接的起点和终点都不为空且不相同,创建连接线
|
if (connectStartNode != null
|
//&& connectEndNode != null
|
&& connectEndNode != connectStartNode && connectStartNodeBranchIndex >= 0)
|
ConnectAsBranch(connectStartNode, connectEndNode, connectStartNodeBranchIndex);
|
|
connectStartNode = null;
|
connectStartNodeBranchIndex = -1;
|
break;
|
case PanelMode.DraggingNode:
|
// 完成拖动操作后的逻辑
|
break;
|
}
|
break;
|
case MouseButtons.Right:
|
default:
|
break;
|
}
|
}
|
catch { }
|
finally
|
{
|
PanelMode = PanelMode.Normal;
|
this.Invalidate();
|
Save(filePath);
|
}
|
|
}
|
|
private void OnMouseMove(object sender, MouseEventArgs e)
|
{
|
mousePosition = e.Location;
|
switch (this.PanelMode)
|
{
|
case PanelMode.CreatingConnection:
|
// 绘制临时连接线逻辑在paint
|
//this.Cursor = Cursors.Cross;
|
break;
|
case PanelMode.DraggingNode:
|
// 绘制拖动节点逻辑在paint,当前只是修改被
|
int deltaX = e.X - dragStartPoint.X;
|
int deltaY = e.Y - dragStartPoint.Y;
|
if (selectedNode != null)
|
{
|
selectedNode.X += deltaX;
|
selectedNode.Y += deltaY;
|
}
|
dragStartPoint = e.Location;
|
this.Cursor = Cursors.Hand;
|
break;
|
case PanelMode.Normal:
|
FlowNode isConnectingNode = OnNodeBranch(e.Location, out int index);
|
|
// 点击在边界上并未点击在节点上,进入判断是否进入连接线模式
|
if (isConnectingNode != null && selectedNode == null && index >= 0)
|
this.Cursor = Cursors.Cross;
|
else
|
this.Cursor = Cursors.Default;
|
break;
|
}
|
this.Invalidate();
|
}
|
|
private void ContextMenuItem_Click(object sender, EventArgs e)
|
{
|
if (sender is ToolStripMenuItem menuItem && menuItem.Tag is string description)
|
{
|
// 只能有一个开始节点
|
if (description == "开始"
|
&& nodes.Any(n => n.Value.Text == "开始"))
|
return;
|
|
// 只能有一个结束节点
|
if (description == "结束"
|
&& nodes.Any(n => n.Value.Text == "结束"))
|
return;
|
|
// 获取鼠标位置
|
Point mousePoint = mouseRightStartLocation;
|
|
// 名称重复则添加(副本)
|
string nodeName = description;
|
while (nodes.Any(n => n.Value.Text == nodeName))
|
nodeName += "(Copy)";
|
|
FlowNode newNode = new FlowNode();
|
switch (description)
|
{
|
case "开始":
|
newNode = new FlowNode(NodeType.Begin, mousePoint, nodeName, description);
|
break;
|
case "结束":
|
newNode = new FlowNode(NodeType.End, mousePoint, nodeName, description);
|
break;
|
case "分支":
|
newNode = new FlowNode(NodeType.Switch, mousePoint, nodeName, description);
|
newNode.BranchNodes.TryAdd($"{newNode.Text}-Branch0", string.Empty);
|
break;
|
case "多分支":
|
newNode = new FlowNode(NodeType.MultiBranch, mousePoint, nodeName, description);
|
break;
|
case "并行分支开始":
|
newNode = new FlowNode(NodeType.Parallel, mousePoint, nodeName, description);
|
break;
|
case "并行分支结束":
|
newNode = new FlowNode(NodeType.Join, mousePoint, nodeName, description);
|
break;
|
default:
|
newNode = new FlowNode(NodeType.Normal, mousePoint, nodeName, description);
|
break;
|
}
|
//分支节点默认添加两个分支
|
newNode.BranchNodes.TryAdd($"{newNode.Text}-Branch{newNode.BranchNodes.Count}", string.Empty);
|
|
if (nodes.TryAdd(newNode.Text, newNode))
|
{
|
AddNodeAction?.Invoke(newNode.Text, description);
|
Debug.WriteLine($"【{DateTime.Now:HH:mm:ss.fff}】创建了新节点: {description}");
|
}
|
|
}
|
Save(filePath);
|
}
|
#endregion
|
|
#region 其他函数
|
private void RemoveNode(FlowNode selectedNode)
|
{
|
try
|
{
|
foreach (var conn in connections)
|
{
|
if (conn.Value == null)
|
continue;
|
|
if (conn.Value.StartNode == selectedNode || conn.Value.EndNode == selectedNode)
|
{
|
RemoveConnection(conn.Value, -1);
|
//connections.TryRemove(conn);
|
//break;
|
}
|
}
|
|
if (this.nodes.Remove(selectedNode.Text, out _))
|
DeleteNodeAction?.Invoke(selectedNode.Text);
|
|
Save(filePath);
|
this.Invalidate();
|
}
|
catch { }
|
}
|
|
private void EditInAndOutNode()
|
{
|
try
|
{
|
PanelMode = PanelMode.Run;
|
if (selectedNode == null)
|
return;
|
InAndOutNodeAction?.Invoke(selectedNode.Text);
|
}
|
catch { }
|
finally { PanelMode = PanelMode.Normal; }
|
}
|
|
private void RenameNode()
|
{
|
try
|
{
|
PanelMode = PanelMode.Run;
|
if (selectedNode == null)
|
return;
|
|
RenameForm renameForm = new RenameForm(selectedNode.Text, true);
|
renameForm.ShowDialog();
|
if (renameForm.bRename)
|
{
|
if (nodes.Any(n => n.Value.Text == renameForm.strNewName))
|
{
|
MessageBox.Show("已存在同名节点,请修改后重试!", "异常");
|
return;
|
}
|
|
nodes.TryRemove(renameForm.strOriName, out _);
|
selectedNode.Text = renameForm.strNewName;
|
nodes.TryAdd(selectedNode.Text, selectedNode);
|
|
if (connections.TryRemove(renameForm.strOriName, out FlowConnection conn)
|
&& conn != null)
|
connections.TryAdd(selectedNode.Text, conn);
|
|
foreach (var node in nodes.Values)
|
{
|
foreach (var branch in node.BranchNodes)
|
{
|
if (branch.Key.StartsWith($"{renameForm.strOriName}-Branch"))
|
{
|
var newKey = branch.Key.Replace(renameForm.strOriName, renameForm.strNewName);
|
node.BranchNodes.TryRemove(branch.Key, out var value);
|
node.BranchNodes.TryAdd(newKey, value);
|
}
|
else if (branch.Value == renameForm.strOriName)
|
{
|
var newValue = branch.Value.Replace(renameForm.strOriName, renameForm.strNewName);
|
node.BranchNodes.TryRemove(branch.Key, out _);
|
node.BranchNodes.TryAdd(branch.Key, newValue);
|
}
|
}
|
}
|
this.Invalidate();
|
RenameNodeAction?.Invoke(renameForm.strOriName, renameForm.strNewName);
|
}
|
}
|
catch { }
|
finally { PanelMode = PanelMode.Normal; }
|
}
|
|
private void RemoveConnection(FlowConnection selectedConnection, int BranchIndex)
|
{
|
foreach (var conn in connections)
|
{
|
if (conn.Value == selectedConnection)
|
{
|
connections.TryRemove(conn);
|
//selectedConnection.StartNode.BranchNodes
|
// .TryRemove($"{selectedConnection.StartNode.Text}-Branch{BranchIndex}", out _);
|
|
if (selectedConnection.StartNode.BranchNodes.ContainsKey($"{selectedConnection.StartNode.Text}-Branch{selectedConnection.BranchIndex}"))
|
selectedConnection.StartNode.BranchNodes[$"{selectedConnection.StartNode.Text}-Branch{selectedConnection.BranchIndex}"] = null;
|
break;
|
}
|
}
|
this.Invalidate();
|
}
|
|
public void ConnectAsBranch(FlowNode parentNode, FlowNode branchNode, int connectStartNodeBranchIndex)
|
{
|
//// 确保分支节点注册到主集合
|
//RegisterNode(branchNode);
|
|
// 更新分支引用
|
string branchName = $"{parentNode.Text}-Branch{connectStartNodeBranchIndex}";
|
if (parentNode != null && branchNode != null)
|
{
|
// 更新 BranchNodes 字典
|
parentNode.BranchNodes.AddOrUpdate(branchName,
|
(branchNode.Text),
|
(key, existing) => (branchNode.Text));
|
}
|
|
// 创建连接线
|
var connection = new FlowConnection(parentNode, branchNode, connectStartNodeBranchIndex);
|
|
// 原来的连接线被替换为新的连接线
|
if (connections.ContainsKey(branchName))
|
connections[branchName] = connection;
|
else
|
connections.TryAdd(branchName, connection);
|
|
// 原来的连接节点被替换为新的节点
|
if (parentNode.BranchNodes.ContainsKey(branchName))
|
parentNode.BranchNodes[branchName] = branchNode.Text;
|
else
|
parentNode.BranchNodes.TryAdd(branchName, branchNode.Text);
|
|
Save(filePath);
|
}
|
|
private void BreakNode()
|
{
|
PanelMode = PanelMode.Run;
|
if (selectedNode == null)
|
return;
|
try
|
{
|
selectedNode.Break = !selectedNode.Break;
|
}
|
catch { }
|
finally { PanelMode = PanelMode.Normal; Save(filePath); }
|
}
|
|
private void AddBranch()
|
{
|
PanelMode = PanelMode.Run;
|
if (selectedNode == null)
|
return;
|
try
|
{
|
selectedNode.BranchNodes.TryAdd($"{selectedNode.Text}-Branch{selectedNode.BranchNodes.Count}", string.Empty);
|
AddBranchAction?.Invoke(selectedNode.Text);
|
}
|
catch { }
|
finally { PanelMode = PanelMode.Normal; Save(filePath); }
|
}
|
|
private void RemoveBranch()
|
{
|
PanelMode = PanelMode.Run;
|
if (selectedNode == null)
|
return;
|
try
|
{
|
int index = selectedNode.BranchNodes.Count - 1;
|
selectedNode.BranchNodes.TryRemove($"{selectedNode.Text}-Branch{index}", out _);
|
connections.TryRemove($"{selectedNode.Text}-Branch{index}", out _);
|
DeleteBranchAction?.Invoke(selectedNode.Text);
|
}
|
catch { }
|
finally { PanelMode = PanelMode.Normal; Save(filePath); }
|
}
|
#endregion
|
|
#endregion
|
}
|
|
public class ExecutionContext
|
{
|
public string CurrentconnectStartNodeBranchIndex { get; set; } = "";
|
public string CurrentBranchName { get; set; }
|
public ConcurrentDictionary<string, bool> BranchResults { get; set; } = new ConcurrentDictionary<string, bool>();
|
|
public ExecutionContext Clone()
|
{
|
return new ExecutionContext
|
{
|
CurrentconnectStartNodeBranchIndex = CurrentconnectStartNodeBranchIndex,
|
CurrentBranchName = CurrentBranchName,
|
BranchResults = new ConcurrentDictionary<string, bool>(BranchResults)
|
};
|
}
|
}
|
}
|