using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Xml.Linq;
using TeamAAS.Dialogs;
using TeamAAS.FlowEditor.Models;
using TeamAAS.FlowEditor.Plugins;
using TeamAAS.FlowEngine;
namespace TeamAAS.FlowEditor.Controls
{
///
/// 流程编辑器画布 - 支持缩放、平移、拖拽节点、绘制连线、框选
///
public class FlowCanvas : Canvas
{
#region 拖放坐标(高DPI安全)
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
private struct POINT { public int X; public int Y; }
///
/// 用系统光标位置反推画布本地坐标。
/// 不能用拖放事件的 e.GetPosition:PerMonitorV2 + 高 DPI 下 WPF 的拖放事件坐标
/// 换算因子错误(已知问题),节点会落在光标旁边。
/// 注意:PointFromScreen 的结果就是正确的 WPF 坐标,不要再乘 TransformToDevice——
/// 进程按系统 DPI 虚拟化运行时,乘上去会造成预览超速偏移。
///
public Point GetCursorCanvasPosition()
{
GetCursorPos(out POINT pt);
var parent = (Visual)VisualTreeHelper.GetParent(this);
var parentPt = parent.PointFromScreen(new Point(pt.X, pt.Y));
// 逆画布 RenderTransform(缩放 + 平移)→ 画布本地坐标
var t = RenderTransform;
return t != null ? t.Inverse.Transform(parentPt) : parentPt;
}
#endregion
#region 常量
private const double MinZoom = 0.2;
private const double MaxZoom = 3.0;
private const double ZoomFactor = 1.15;
#endregion
#region 字段
private readonly ScaleTransform _scale;
private readonly TranslateTransform _translate;
private FlowGraph _graph;
private readonly Dictionary _nodeControls = new Dictionary();
private readonly Dictionary _connectionPaths = new Dictionary();
// 自动拉线预览(虚线)
private Path _previewLine;
// 平移状态
private bool _isPanning;
private Point _panStart;
private Point _panOrigin;
// 连线绘制状态
private bool _isConnecting;
private FlowNode _connectSourceNode;
private PortSide _connectSourceSide;
private Path _tempPath;
// 框选状态
private bool _isBoxSelecting;
private Point _boxSelectStart;
private Rectangle _selectionBox;
// 选中项
private FlowNode _selectedNode;
private FlowConnection _selectedConnection;
private readonly List _selectedNodes = new List();
private const string ClipboardPrefix = "TeamAASFlow:";
#endregion
#region 事件
public event Action NodeDoubleClicked;
public event Action NodeSelected;
public event Action ConnectionSelected;
public event Action SelectionCleared;
public event Action NodeDropRequested;
public event Action ZoomChanged;
public event Action NodeRunRequested;
public event Action NodeRunFromHereRequested;
public event Action NodePropertiesRequested;
#endregion
#region 属性
public double Zoom => _scale.ScaleX;
public double TranslateX => _translate.X;
public double TranslateY => _translate.Y;
public void SetTranslateX(double x) { _translate.X = x; }
public void SetTranslateY(double y) { _translate.Y = y; }
public Size ViewportSize => GetViewportSize();
public FlowGraph Graph
{
get => _graph;
set
{
if (_graph != null) UnsubscribeGraph();
ClearCanvas();
_graph = value;
if (_graph != null) SubscribeGraph();
}
}
/// 流程运行中:锁定节点移动和双击编辑
public static readonly DependencyProperty IsRunningProperty =
DependencyProperty.Register(nameof(IsRunning), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
public bool IsRunning
{
get => (bool)GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
/// 只读模式(运行产品监控中):允许选中节点查看结果,禁止一切修改操作
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(FlowCanvas), new PropertyMetadata(false));
public bool IsReadOnly
{
get => (bool)GetValue(IsReadOnlyProperty);
set => SetValue(IsReadOnlyProperty, value);
}
#endregion
#region 构造函数
public FlowCanvas()
{
_scale = new ScaleTransform(1, 1);
_translate = new TranslateTransform(0, 0);
var group = new TransformGroup();
group.Children.Add(_scale);
group.Children.Add(_translate);
RenderTransform = group;
ClipToBounds = true;
Focusable = true;
Background = new SolidColorBrush(Colors.White);
AllowDrop = true;
// 初始画布尺寸
Width = 5000;
Height = 3500;
// 初始偏移,显示画布的一部分
_translate.X = 0;
_translate.Y = 0;
}
#endregion
#region Graph 订阅
private void SubscribeGraph()
{
_graph.Nodes.CollectionChanged += OnNodesChanged;
_graph.Connections.CollectionChanged += OnConnectionsChanged;
foreach (var node in _graph.Nodes)
AddNodeVisual(node);
foreach (var conn in _graph.Connections)
AddConnectionVisual(conn);
}
private void UnsubscribeGraph()
{
_graph.Nodes.CollectionChanged -= OnNodesChanged;
_graph.Connections.CollectionChanged -= OnConnectionsChanged;
}
private void ClearCanvas()
{
foreach (var node in _nodeControls.Values)
{
if (node.DataContext is FlowNode fn)
fn.PropertyChanged -= OnNodePropertyChanged;
}
_nodeControls.Clear();
_connectionPaths.Clear();
Children.Clear();
}
private void OnNodesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Clear()/整体替换会触发 Reset(NewItems/OldItems 均为 null),
// 不处理会导致导入流程后旧节点可视化残留成“影子”。
if (e.Action == NotifyCollectionChangedAction.Reset)
{
RebuildVisuals();
return;
}
if (e.NewItems != null)
foreach (FlowNode node in e.NewItems)
AddNodeVisual(node);
if (e.OldItems != null)
foreach (FlowNode node in e.OldItems)
RemoveNodeVisual(node);
}
private void OnConnectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Reset)
{
RebuildVisuals();
return;
}
if (e.NewItems != null)
foreach (FlowConnection conn in e.NewItems)
AddConnectionVisual(conn);
if (e.OldItems != null)
foreach (FlowConnection conn in e.OldItems)
RemoveConnectionVisual(conn);
}
///
/// 集合被整体 Clear(Reset)时重建全部节点/连线可视化,避免旧图形残留(如导入流程后)。
///
private void RebuildVisuals()
{
foreach (var kv in _nodeControls.ToList())
{
if (kv.Value.DataContext is FlowNode fn)
fn.PropertyChanged -= OnNodePropertyChanged;
Children.Remove(kv.Value);
}
_nodeControls.Clear();
foreach (var kv in _connectionPaths.ToList())
Children.Remove(kv.Value);
_connectionPaths.Clear();
if (_graph == null) return;
foreach (var node in _graph.Nodes)
AddNodeVisual(node);
foreach (var conn in _graph.Connections)
AddConnectionVisual(conn);
}
#endregion
#region 节点可视化
private void AddNodeVisual(FlowNode node)
{
var ctrl = new NodeControl { DataContext = node };
SetLeft(ctrl, node.X);
SetTop(ctrl, node.Y);
Children.Add(ctrl);
_nodeControls[node.NodeId] = ctrl;
// 节点渲染后更新实际尺寸,用于端口位置和碰撞检测
ctrl.SizeChanged += (s, e) =>
{
node.NodeWidth = ctrl.ActualWidth;
node.NodeHeight = ctrl.ActualHeight;
UpdateConnectionsForNode(node.NodeId);
UpdateCanvasSize();
};
node.PropertyChanged += OnNodePropertyChanged;
}
private void RemoveNodeVisual(FlowNode node)
{
if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
{
Children.Remove(ctrl);
_nodeControls.Remove(node.NodeId);
node.PropertyChanged -= OnNodePropertyChanged;
}
// 移除相关连线可视化
var toRemove = _connectionPaths
.Where(kvp => kvp.Value.Tag is FlowConnection fc &&
(fc.SourceNodeId == node.NodeId || fc.TargetNodeId == node.NodeId))
.ToList();
foreach (var kvp in toRemove)
{
Children.Remove(kvp.Value);
_connectionPaths.Remove(kvp.Key);
}
if (_selectedNode == node)
_selectedNode = null;
_selectedNodes.Remove(node);
}
private void OnNodePropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var node = (FlowNode)sender;
if (e.PropertyName == nameof(FlowNode.X) || e.PropertyName == nameof(FlowNode.Y))
{
if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
{
SetLeft(ctrl, node.X);
SetTop(ctrl, node.Y);
}
UpdateConnectionsForNode(node.NodeId);
UpdateCanvasSize();
}
else if (e.PropertyName == nameof(FlowNode.IsEnabled))
{
if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
ctrl.UpdateDisabledVisual();
}
else if (e.PropertyName == nameof(FlowNode.Status))
{
node.NotifyStatusChanged();
}
}
#endregion
#region 连线可视化
private void AddConnectionVisual(FlowConnection conn)
{
var path = new Path
{
Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
StrokeThickness = 2,
Fill = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
Tag = conn,
Cursor = Cursors.Hand
};
UpdateConnectionPath(path, conn);
Children.Insert(0, path);
_connectionPaths[conn.ConnectionId] = path;
path.MouseLeftButtonDown += (s, e) =>
{
SelectConnection(conn);
e.Handled = true;
};
}
private void RemoveConnectionVisual(FlowConnection conn)
{
if (_connectionPaths.TryGetValue(conn.ConnectionId, out var path))
{
Children.Remove(path);
_connectionPaths.Remove(conn.ConnectionId);
}
if (_selectedConnection == conn)
_selectedConnection = null;
}
private void UpdateConnectionPath(Path path, FlowConnection conn)
{
var sourceNode = _graph?.GetNode(conn.SourceNodeId);
var targetNode = _graph?.GetNode(conn.TargetNodeId);
if (sourceNode == null || targetNode == null) return;
Point start = GetPortPosition(sourceNode, conn.SourceSide);
Point end = GetPortPosition(targetNode, conn.TargetSide);
path.Data = CreateConnectionGeometry(start, end, conn.SourceSide, conn.TargetSide);
if (conn.IsSelected)
{
path.Stroke = Brushes.White;
path.Fill = Brushes.White;
path.StrokeThickness = 3;
}
else
{
// 连接到异常分支节点的线用红色,其余橙色
bool isException = targetNode.Category == NodeCategory.ExceptionBranch;
var color = isException
? Color.FromRgb(0xB7, 0x1C, 0x1C)
: Color.FromRgb(232, 145, 73);
path.Stroke = new SolidColorBrush(color);
path.Fill = new SolidColorBrush(color);
path.StrokeThickness = 2;
}
}
private void UpdateConnectionsForNode(string nodeId)
{
foreach (var kvp in _connectionPaths)
{
if (kvp.Value.Tag is FlowConnection conn)
{
if (conn.SourceNodeId == nodeId || conn.TargetNodeId == nodeId)
UpdateConnectionPath(kvp.Value, conn);
}
}
}
#endregion
#region 端口位置 & 连线几何
public static Point GetPortPosition(FlowNode node, PortDirection direction, int portIndex)
{
double w = node.NodeWidth;
double h = node.NodeHeight;
if (direction == PortDirection.Input)
{
// portIndex 0 = Left, 1 = Top
if (portIndex == 0) return new Point(node.X, node.Y + h / 2);
return new Point(node.X + w / 2, node.Y);
}
else // Output
{
// portIndex 0 = Right, 1 = Bottom
if (portIndex == 0) return new Point(node.X + w, node.Y + h / 2);
return new Point(node.X + w / 2, node.Y + h);
}
}
///
/// 根据边获取端口位置(通用,不区分输入输出)
///
public static Point GetPortPosition(FlowNode node, PortSide side)
{
double w = node.NodeWidth;
double h = node.NodeHeight;
switch (side)
{
case PortSide.Left: return new Point(node.X, node.Y + h / 2);
case PortSide.Top: return new Point(node.X + w / 2, node.Y);
case PortSide.Right: return new Point(node.X + w, node.Y + h / 2);
case PortSide.Bottom: return new Point(node.X + w / 2, node.Y + h);
default: return new Point(node.X, node.Y + h / 2);
}
}
///
/// 根据端口方向和索引推断所在边
///
public static PortSide GetPortSide(PortDirection direction, int portIndex)
{
if (direction == PortDirection.Input)
return portIndex == 0 ? PortSide.Left : PortSide.Top;
return portIndex == 0 ? PortSide.Right : PortSide.Bottom;
}
public static Geometry CreateConnectionGeometry(Point start, Point end, PortSide startSide = PortSide.Right, PortSide endSide = PortSide.Left)
{
// 偏移起止点,留出间距让箭头可见
Point adjStart = OffsetPoint(start, startSide, 8);
Point adjEnd = OffsetPoint(end, endSide, 12);
var geo = new StreamGeometry();
using (var ctx = geo.Open())
{
// 控制点方向(端口朝外方向)
Vector startDir = GetSideDirection(startSide);
Vector endDir = GetSideDirection(endSide);
// 控制点偏移量:基于距离动态调整,保证曲度自然
double dist = (adjEnd - adjStart).Length;
double offset = Math.Max(50, dist * 0.4);
Point cp1 = adjStart + startDir * offset;
Point cp2 = adjEnd + endDir * offset;
// 绘制平滑贝塞尔曲线
ctx.BeginFigure(adjStart, false, false);
ctx.BezierTo(cp1, cp2, adjEnd, true, false);
// 箭头方向 = 贝塞尔终点切线 (end - cp2)
double angle = Math.Atan2(adjEnd.Y - cp2.Y, adjEnd.X - cp2.X);
double arrowSize = 7;
var p1 = new Point(
adjEnd.X - arrowSize * Math.Cos(angle - Math.PI / 6),
adjEnd.Y - arrowSize * Math.Sin(angle - Math.PI / 6));
var p2 = new Point(
adjEnd.X - arrowSize * Math.Cos(angle + Math.PI / 6),
adjEnd.Y - arrowSize * Math.Sin(angle + Math.PI / 6));
ctx.BeginFigure(adjEnd, true, true);
ctx.LineTo(p1, true, true);
ctx.LineTo(p2, true, true);
}
geo.Freeze();
return geo;
}
///
/// 端口朝外方向单位向量
///
private static Vector GetSideDirection(PortSide side)
{
switch (side)
{
case PortSide.Left: return new Vector(-1, 0);
case PortSide.Right: return new Vector(1, 0);
case PortSide.Top: return new Vector(0, -1);
case PortSide.Bottom: return new Vector(0, 1);
default: return new Vector(1, 0);
}
}
///
/// 根据鼠标位置推断目标端口边
///
private static PortSide DetermineEndSide(Point start, Point end, PortSide startSide)
{
double dx = end.X - start.X;
double dy = end.Y - start.Y;
switch (startSide)
{
case PortSide.Right:
return dx >= 0 ? PortSide.Left : PortSide.Right;
case PortSide.Bottom:
return dy >= 0 ? PortSide.Top : PortSide.Bottom;
case PortSide.Left:
return dx <= 0 ? PortSide.Right : PortSide.Left;
case PortSide.Top:
return dy <= 0 ? PortSide.Bottom : PortSide.Top;
default:
return PortSide.Left;
}
}
///
/// 沿端口方向偏移点,留出间距
///
private static Point OffsetPoint(Point p, PortSide side, double offset)
{
switch (side)
{
case PortSide.Left: return new Point(p.X - offset, p.Y);
case PortSide.Right: return new Point(p.X + offset, p.Y);
case PortSide.Top: return new Point(p.X, p.Y - offset);
case PortSide.Bottom: return new Point(p.X, p.Y + offset);
default: return p;
}
}
#endregion
#region 坐标转换
public Point ToCanvasPoint(Point screenPoint)
{
return new Point(
(screenPoint.X - _translate.X) / _scale.ScaleX,
(screenPoint.Y - _translate.Y) / _scale.ScaleY);
}
#endregion
#region 缩放 & 平移
public void ZoomAt(Point center, double newScale)
{
newScale = Math.Max(MinZoom, Math.Min(MaxZoom, newScale));
if (Math.Abs(newScale - _scale.ScaleX) < 0.001) return;
// center 是 GetPosition(this) 返回的画布本地坐标
// 保持 center 对应的屏幕点不变:screen = center * oldScale + oldTranslate = center * newScale + newTranslate
_translate.X = _translate.X + center.X * (_scale.ScaleX - newScale);
_translate.Y = _translate.Y + center.Y * (_scale.ScaleY - newScale);
_scale.ScaleX = newScale;
_scale.ScaleY = newScale;
ClampTranslate();
ZoomChanged?.Invoke(newScale);
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
if ((Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
// Ctrl + 滚轮 = 缩放
var mousePos = e.GetPosition(this);
double factor = e.Delta > 0 ? ZoomFactor : 1 / ZoomFactor;
ZoomAt(mousePos, _scale.ScaleX * factor);
e.Handled = true;
}
else if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
{
// Shift + 滚轮 = 水平平移
_translate.X += e.Delta * 0.5;
ClampTranslate();
e.Handled = true;
}
else
{
// 滚轮 = 上下平移
_translate.Y += e.Delta * 0.5;
ClampTranslate();
e.Handled = true;
}
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
base.OnMouseDown(e);
if (e.ChangedButton == MouseButton.Middle)
{
_isPanning = true;
_panStart = e.GetPosition(Parent as IInputElement);
_panOrigin = new Point(_translate.X, _translate.Y);
CaptureMouse();
e.Handled = true;
}
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
var pos = e.GetPosition(this);
if (_isPanning)
{
var panPos = e.GetPosition(Parent as IInputElement);
_translate.X = _panOrigin.X + (panPos.X - _panStart.X);
_translate.Y = _panOrigin.Y + (panPos.Y - _panStart.Y);
ClampTranslate();
}
else if (_isConnecting && _tempPath != null)
{
Point start = GetPortPosition(_connectSourceNode, _connectSourceSide);
PortSide endSide = DetermineEndSide(start, pos, _connectSourceSide);
_tempPath.Data = CreateConnectionGeometry(start, pos, _connectSourceSide, endSide);
}
else if (_isBoxSelecting)
{
UpdateSelectionBox(pos);
}
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
base.OnMouseUp(e);
if (e.ChangedButton == MouseButton.Middle)
{
_isPanning = false;
ReleaseMouseCapture();
e.Handled = true;
}
}
#endregion
#region 左键交互
// 双击检测 + 单击选中都用 Preview(隧道事件),在 MoveThumb 捕获鼠标之前触发
protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
{
if (e.ClickCount == 2)
{
var dblNode = FindAncestor(e.OriginalSource as DependencyObject);
if (dblNode != null)
{
// 流程运行中 / 只读监控模式禁止双击打开编辑器
if (IsRunning || IsReadOnly) { e.Handled = true; return; }
NodeDoubleClicked?.Invoke(dblNode.GetNode());
e.Handled = true;
return;
}
}
else if (e.ClickCount == 1)
{
// 单击节点选中(在 MoveThumb 捕获鼠标之前处理)
var nodeCtrl = FindAncestor(e.OriginalSource as DependencyObject);
if (nodeCtrl != null)
{
Focus();
bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
SelectNode(nodeCtrl.GetNode(), isCtrl);
// 不设 Handled,让 MoveThumb 继续处理拖拽
}
}
base.OnPreviewMouseLeftButtonDown(e);
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonDown(e);
Focus();
var pos = e.GetPosition(this);
var src = e.OriginalSource as DependencyObject;
// 1. 检查连接点
var connector = FindAncestor(src);
if (connector != null)
{
StartConnection(connector);
e.Handled = true;
return;
}
// 2. 检查连线(Path with FlowConnection Tag)
if (src is Path path && path.Tag is FlowConnection conn)
{
SelectConnection(conn);
e.Handled = true;
return;
}
// 3. 检查节点
var nodeCtrl = FindAncestor(src);
if (nodeCtrl != null)
{
bool isCtrl = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;
SelectNode(nodeCtrl.GetNode(), isCtrl);
return; // 不设 Handled,让 MoveThumb 处理拖拽
}
// 4. 空白区域 - 框选
ClearSelection();
StartBoxSelect(pos);
e.Handled = true;
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
if (_isConnecting)
EndConnection(e.GetPosition(this));
if (_isBoxSelecting)
EndBoxSelect();
}
#endregion
#region 右键菜单
protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
{
base.OnMouseRightButtonDown(e);
Focus();
var pos = e.GetPosition(this);
var src = e.OriginalSource as DependencyObject;
var nodeCtrl = FindAncestor(src);
if (nodeCtrl != null)
{
var node = nodeCtrl.GetNode();
// 如果右键的节点不在已选列表中,单选它
if (!_selectedNodes.Contains(node) && _selectedNode != node)
SelectNode(node, false);
else if (_selectedNode == null && _selectedNodes.Count == 0)
SelectNode(node, false);
ShowNodeContextMenu(node);
e.Handled = true;
}
else
{
ShowCanvasContextMenu(pos);
e.Handled = true;
}
}
private void ShowNodeContextMenu(FlowNode node)
{
var menu = new ContextMenu();
bool readOnly = IsReadOnly || IsRunning;
// 复制
var miCopy = new MenuItem { Header = "复制", IsEnabled = !readOnly };
miCopy.Click += (s, e) => CopySelectedNodes();
menu.Items.Add(miCopy);
// 禁用/启用节点
var miToggleEnabled = new MenuItem { Header = node.IsEnabled ? "禁用节点" : "启用节点", IsEnabled = !readOnly };
miToggleEnabled.Click += (s, e) => ToggleNodeEnabled(node);
menu.Items.Add(miToggleEnabled);
menu.Items.Add(new Separator());
// 删除节点
var miDelNode = new MenuItem { Header = "删除节点", IsEnabled = !readOnly };
miDelNode.Click += (s, e) => DeleteSelectedNodes();
menu.Items.Add(miDelNode);
// 删除连接线
var miDelConn = new MenuItem { Header = "删除连接线", IsEnabled = !readOnly };
miDelConn.Click += (s, e) => DeleteSelectedConnections();
menu.Items.Add(miDelConn);
// 运行和从此处运行:仅单选时显示(只读模式下不允许从编辑器发起运行)
bool isSingle = _selectedNodes.Count <= 1 && _selectedNode != null;
if (isSingle)
{
menu.Items.Add(new Separator());
var miRun = new MenuItem { Header = "运行", IsEnabled = !readOnly };
miRun.Click += (s, e) => NodeRunRequested?.Invoke(node);
menu.Items.Add(miRun);
var miRunFrom = new MenuItem { Header = "此处开始运行", IsEnabled = !readOnly };
miRunFrom.Click += (s, e) => NodeRunFromHereRequested?.Invoke(node);
menu.Items.Add(miRunFrom);
menu.Items.Add(new Separator());
var miProp = new MenuItem { Header = "属性", IsEnabled = !readOnly };
miProp.Click += (s, e) => NodePropertiesRequested?.Invoke(node);
menu.Items.Add(miProp);
}
menu.IsOpen = true;
}
///
/// 切换节点启用状态(禁用后不执行、背景变灰)
///
private void ToggleNodeEnabled(FlowNode node)
{
if (node == null) return;
node.IsEnabled = !node.IsEnabled;
}
private void ShowCanvasContextMenu(Point pos)
{
var menu = new ContextMenu();
var miLayout = new MenuItem { Header = "自动排版" };
miLayout.Click += (s, e) => AutoLayout();
menu.Items.Add(miLayout);
menu.Items.Add(new Separator());
var miPaste = new MenuItem { Header = "粘贴" };
miPaste.Click += (s, e) => PasteNodesAt(pos);
miPaste.IsEnabled = !IsReadOnly && !IsRunning && HasClipboardData();
menu.Items.Add(miPaste);
menu.IsOpen = true;
}
#endregion
#region 连线绘制
private void StartConnection(ConnectorControl connector)
{
if (IsRunning || IsReadOnly) return; // 流程运行中 / 只读模式禁止新建连线
var node = connector.GetNode();
if (node == null) return;
_isConnecting = true;
_connectSourceNode = node;
_connectSourceSide = connector.Side;
_tempPath = new Path
{
Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
StrokeThickness = 2,
StrokeDashArray = new DoubleCollection { 4, 2 },
IsHitTestVisible = false
};
Children.Add(_tempPath);
CaptureMouse();
}
private void EndConnection(Point mousePos)
{
_isConnecting = false;
ReleaseMouseCapture();
if (_tempPath != null)
{
Children.Remove(_tempPath);
_tempPath = null;
}
if (_connectSourceNode == null || _graph == null) return;
// 手动命中检测:遍历所有节点的4个端口,找到最近的
const double hitRadius = 22.0;
double minDist = double.MaxValue;
FlowNode targetNode = null;
PortSide targetSide = PortSide.Left;
var sides = new[] { PortSide.Left, PortSide.Top, PortSide.Right, PortSide.Bottom };
foreach (var node in _graph.Nodes)
{
if (node.NodeId == _connectSourceNode.NodeId) continue;
foreach (var side in sides)
{
var pos = GetPortPosition(node, side);
double d = PointDist(pos, mousePos);
if (d < hitRadius && d < minDist)
{
minDist = d;
targetNode = node;
targetSide = side;
}
}
}
if (targetNode == null) { _connectSourceNode = null; return; }
// 源 = 拖出端,目标 = 放入端。不检查方向类型。
var conn = new FlowConnection
{
SourceNodeId = _connectSourceNode.NodeId,
SourceSide = _connectSourceSide,
TargetNodeId = targetNode.NodeId,
TargetSide = targetSide
};
_graph?.TryAddConnection(conn);
_connectSourceNode = null;
}
private static double PointDist(Point a, Point b)
{
double dx = a.X - b.X;
double dy = a.Y - b.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
#endregion
#region 框选
private void StartBoxSelect(Point pos)
{
_isBoxSelecting = true;
_boxSelectStart = pos;
_selectionBox = new Rectangle
{
Stroke = new SolidColorBrush(Color.FromArgb(100, 0, 122, 204)),
Fill = new SolidColorBrush(Color.FromArgb(30, 0, 122, 204)),
StrokeThickness = 1,
IsHitTestVisible = false
};
Children.Add(_selectionBox);
CaptureMouse();
}
private void UpdateSelectionBox(Point pos)
{
if (_selectionBox == null) return;
double x = Math.Min(_boxSelectStart.X, pos.X);
double y = Math.Min(_boxSelectStart.Y, pos.Y);
double w = Math.Abs(pos.X - _boxSelectStart.X);
double h = Math.Abs(pos.Y - _boxSelectStart.Y);
SetLeft(_selectionBox, x);
SetTop(_selectionBox, y);
_selectionBox.Width = w;
_selectionBox.Height = h;
}
private void EndBoxSelect()
{
_isBoxSelecting = false;
ReleaseMouseCapture();
if (_selectionBox != null)
{
double x = GetLeft(_selectionBox);
double y = GetTop(_selectionBox);
double w = _selectionBox.Width;
double h = _selectionBox.Height;
var rect = new Rect(x, y, w, h);
// 清除旧选择
foreach (var n in _selectedNodes)
n.IsSelected = false;
_selectedNodes.Clear();
// 多选模式:选中框选范围内的所有节点
foreach (var node in _graph?.Nodes ?? Enumerable.Empty())
{
var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
if (rect.IntersectsWith(nodeRect))
{
node.IsSelected = true;
_selectedNodes.Add(node);
}
}
if (_selectedNodes.Count > 0)
{
_selectedNode = _selectedNodes[_selectedNodes.Count - 1];
NodeSelected?.Invoke(_selectedNode);
}
Children.Remove(_selectionBox);
_selectionBox = null;
}
}
#endregion
#region 选择
public void SelectNode(FlowNode node, bool isMultiSelect = false)
{
if (isMultiSelect && node != null)
{
// Ctrl+Click: 切换该节点的选中状态
if (_selectedNodes.Contains(node))
{
_selectedNodes.Remove(node);
node.IsSelected = false;
}
else
{
_selectedNodes.Add(node);
node.IsSelected = true;
}
_selectedNode = node;
if (_selectedConnection != null)
{
_selectedConnection.IsSelected = false;
if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
UpdateConnectionPath(p, _selectedConnection);
_selectedConnection = null;
}
NodeSelected?.Invoke(node);
return;
}
// 普通点击:如果点击的节点已在多选列表中,保持多选不变(用于拖拽)
if (node != null && _selectedNodes.Contains(node))
{
_selectedNode = node;
if (_selectedConnection != null)
{
_selectedConnection.IsSelected = false;
if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
UpdateConnectionPath(p, _selectedConnection);
_selectedConnection = null;
}
NodeSelected?.Invoke(node);
return;
}
// 否则清除多选,单选
foreach (var n in _selectedNodes)
n.IsSelected = false;
_selectedNodes.Clear();
if (_selectedNode == node && _selectedConnection == null) return;
if (_selectedNode != null)
_selectedNode.IsSelected = false;
if (_selectedConnection != null)
{
_selectedConnection.IsSelected = false;
if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
UpdateConnectionPath(p, _selectedConnection);
}
_selectedNode = node;
_selectedConnection = null;
if (node != null)
node.IsSelected = true;
NodeSelected?.Invoke(node);
}
private void SelectConnection(FlowConnection conn)
{
foreach (var n in _selectedNodes)
n.IsSelected = false;
_selectedNodes.Clear();
if (_selectedNode != null)
_selectedNode.IsSelected = false;
if (_selectedConnection != null)
{
_selectedConnection.IsSelected = false;
if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
UpdateConnectionPath(p, _selectedConnection);
}
_selectedNode = null;
_selectedConnection = conn;
if (conn != null)
{
conn.IsSelected = true;
if (_connectionPaths.TryGetValue(conn.ConnectionId, out var p))
UpdateConnectionPath(p, conn);
}
ConnectionSelected?.Invoke(conn);
}
public void ClearSelection()
{
foreach (var n in _selectedNodes)
n.IsSelected = false;
_selectedNodes.Clear();
if (_selectedNode != null)
_selectedNode.IsSelected = false;
if (_selectedConnection != null)
{
_selectedConnection.IsSelected = false;
if (_connectionPaths.TryGetValue(_selectedConnection.ConnectionId, out var p))
UpdateConnectionPath(p, _selectedConnection);
}
_selectedNode = null;
_selectedConnection = null;
SelectionCleared?.Invoke();
}
#endregion
#region 拖放
protected override void OnDrop(DragEventArgs e)
{
base.OnDrop(e);
NodePluginInfo pluginInfo = null;
// 尝试从 DataObject 获取插件信息
if (e.Data.GetDataPresent(typeof(NodePluginInfo)))
{
pluginInfo = e.Data.GetData(typeof(NodePluginInfo)) as NodePluginInfo;
}
else if (e.Data.GetDataPresent(DataFormats.StringFormat))
{
var str = e.Data.GetData(DataFormats.StringFormat) as string;
if (!string.IsNullOrEmpty(str))
pluginInfo = PluginLoader.Instance.GetPluginInfo(str);
}
if (pluginInfo != null)
{
if (IsRunning || IsReadOnly)
{
e.Handled = true; // 流程运行中 / 只读模式禁止添加插件
return;
}
// 预览与放置共用 e.GetPosition(实测正确);不用 GetCursorPos/PointFromScreen
//(其内部窗口原点与 PointToScreen 不一致,PerMonitorV2 下会算出错误位置)
var pos = e.GetPosition(this);
double dropX = pos.X - NodeControl.DefaultWidth / 2;
double dropY = pos.Y - NodeControl.DefaultHeight / 2;
// 避免与现有节点重叠
var freePos = FindFreePosition(dropX, dropY);
NodeDropRequested?.Invoke(freePos.X, freePos.Y, pluginInfo);
UpdateCanvasSize();
e.Handled = true;
}
}
#endregion
#region 键盘
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.Delete || e.Key == Key.Back)
{
if (IsRunning || IsReadOnly)
{
e.Handled = true;
return;
}
// 批量删除多选节点
if (_selectedNodes.Count > 0)
{
foreach (var node in _selectedNodes.ToList())
_graph?.RemoveNode(node.NodeId);
_selectedNodes.Clear();
_selectedNode = null;
e.Handled = true;
}
else if (_selectedNode != null)
{
_graph?.RemoveNode(_selectedNode.NodeId);
_selectedNode = null;
e.Handled = true;
}
else if (_selectedConnection != null)
{
_graph?.RemoveConnection(_selectedConnection.ConnectionId);
_selectedConnection = null;
e.Handled = true;
}
}
else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
if (IsRunning || IsReadOnly)
{
e.Handled = true;
return;
}
CopySelectedNodes();
e.Handled = true;
}
else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
if (IsRunning || IsReadOnly)
{
e.Handled = true;
return;
}
PasteNodes();
e.Handled = true;
}
else if (e.Key == Key.A && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
{
SelectAllNodes();
e.Handled = true;
}
}
#endregion
#region 复制粘贴
///
/// 获取当前选中的节点列表(供 MoveThumb 多选拖拽使用)
///
public List GetSelectedNodes() => _selectedNodes;
///
/// 全选所有节点
///
private void SelectAllNodes()
{
foreach (var n in _selectedNodes)
n.IsSelected = false;
_selectedNodes.Clear();
foreach (var node in _graph?.Nodes ?? Enumerable.Empty())
{
node.IsSelected = true;
_selectedNodes.Add(node);
}
if (_selectedNodes.Count > 0)
{
_selectedNode = _selectedNodes[_selectedNodes.Count - 1];
NodeSelected?.Invoke(_selectedNode);
}
}
private bool HasClipboardData()
{
try
{
var text = System.Windows.Clipboard.GetText();
return !string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix);
}
catch { return false; }
}
private void CopySelectedNodes()
{
// 如果多选列表为空但单选不为空,将单选加入多选列表
if (_selectedNodes.Count == 0 && _selectedNode != null)
{
_selectedNode.IsSelected = true;
_selectedNodes.Add(_selectedNode);
}
if (_selectedNodes.Count == 0) return;
var nodes = new List();
var idMap = new Dictionary();
foreach (var node in _selectedNodes)
{
var clone = CloneNode(node);
clone.NodeId = System.Guid.NewGuid().ToString("N");
idMap[node.NodeId] = clone.NodeId;
nodes.Add(clone);
}
// 复制选中节点之间的连接线(映射到剪贴板节点的ID)
var nodeIds = new HashSet(_selectedNodes.Select(n => n.NodeId));
var connections = _graph?.Connections
.Where(c => nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
.Select(c => new FlowConnection
{
SourceNodeId = idMap[c.SourceNodeId],
SourceSide = c.SourceSide,
TargetNodeId = idMap[c.TargetNodeId],
TargetSide = c.TargetSide,
BranchCondition = c.BranchCondition
})
.ToList();
// 序列化到系统剪贴板
try
{
var data = new ClipboardData { Nodes = nodes, Connections = connections };
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
using (var ms = new System.IO.MemoryStream())
{
formatter.Serialize(ms, data);
var base64 = Convert.ToBase64String(ms.ToArray());
System.Windows.Clipboard.SetText(ClipboardPrefix + base64);
}
}
catch { }
}
private void PasteNodes()
{
PasteNodesAt(Mouse.GetPosition(this));
}
///
/// 在指定位置粘贴节点
///
public void PasteNodesAt(Point pos)
{
if (_graph == null || IsRunning || IsReadOnly) return;
// 从系统剪贴板读取
List clipNodes = null;
List clipConns = null;
try
{
var text = System.Windows.Clipboard.GetText();
if (!string.IsNullOrEmpty(text) && text.StartsWith(ClipboardPrefix))
{
var bytes = Convert.FromBase64String(text.Substring(ClipboardPrefix.Length));
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
using (var ms = new System.IO.MemoryStream(bytes))
{
var data = (ClipboardData)formatter.Deserialize(ms);
clipNodes = data.Nodes;
clipConns = data.Connections;
}
}
}
catch { return; }
if (clipNodes == null || clipNodes.Count == 0) return;
ClearSelection();
// 计算偏移:让剪贴板内容的左上角对齐到鼠标位置
double minX = double.MaxValue, minY = double.MaxValue;
foreach (var data in clipNodes)
{
if (data.X < minX) minX = data.X;
if (data.Y < minY) minY = data.Y;
}
double offsetX = pos.X - minX;
double offsetY = pos.Y - minY;
var newNodes = new List();
foreach (var data in clipNodes)
{
var newNode = CloneNode(data);
newNode.X = data.X + offsetX;
newNode.Y = data.Y + offsetY;
newNode.IsSelected = true;
int suffix = 1;
while (Graph.Nodes.Any(n => n.NodeName == newNode.NodeName))
newNode.NodeName = $"{newNode.PluginModel.GetModel.ToolName}{++suffix}";
// 重新初始化所有ID(FlowId/FlowName/NodeId/PluginModel/Group子节点)
newNode.InitializeNode(_graph.GraphId, _graph.GraphName, _graph.Registry);
_graph.AddNode(newNode);
newNodes.Add(newNode);
}
_selectedNodes.AddRange(newNodes);
if (newNodes.Count > 0)
_selectedNode = newNodes[0];
// 复制连接线
if (clipConns != null && clipConns.Count > 0)
{
var idMap = new Dictionary();
for (int i = 0; i < clipNodes.Count && i < newNodes.Count; i++)
idMap[clipNodes[i].NodeId] = newNodes[i].NodeId;
foreach (var conn in clipConns)
{
if (idMap.TryGetValue(conn.SourceNodeId, out var newSrcId) &&
idMap.TryGetValue(conn.TargetNodeId, out var newTgtId))
{
_graph.TryAddConnection(new FlowConnection
{
SourceNodeId = newSrcId,
SourceSide = conn.SourceSide,
TargetNodeId = newTgtId,
TargetSide = conn.TargetSide,
BranchCondition = conn.BranchCondition
});
}
}
}
UpdateCanvasSize();
}
///
/// 删除所有选中节点
///
public void DeleteSelectedNodes()
{
if (_graph == null || IsRunning || IsReadOnly) return;
if (_selectedNodes.Count > 0)
{
foreach (var node in _selectedNodes.ToList())
_graph.RemoveNode(node.NodeId);
_selectedNodes.Clear();
_selectedNode = null;
}
else if (_selectedNode != null)
{
_graph.RemoveNode(_selectedNode.NodeId);
_selectedNode = null;
}
}
///
/// 删除选中节点的所有连接线
///
public void DeleteSelectedConnections()
{
if (_graph == null || IsRunning || IsReadOnly) return;
var nodes = _selectedNodes.Count > 0 ? _selectedNodes.ToList() :
(_selectedNode != null ? new List { _selectedNode } : new List());
if (nodes.Count == 0) return;
var nodeIds = new HashSet(nodes.Select(n => n.NodeId));
var connectionsToRemove = _graph.Connections
.Where(c => nodeIds.Contains(c.SourceNodeId) || nodeIds.Contains(c.TargetNodeId))
.ToList();
foreach (var conn in connectionsToRemove)
_graph.RemoveConnection(conn.ConnectionId);
}
///
/// 深拷贝节点(使用 BinaryFormatter,完整复制 PluginModel)
///
private static FlowNode CloneNode(FlowNode source)
{
try
{
#pragma warning disable SYSLIB0011
var formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
using (var stream = new System.IO.MemoryStream())
{
formatter.Serialize(stream, source);
stream.Position = 0;
return (FlowNode)formatter.Deserialize(stream);
}
#pragma warning restore SYSLIB0011
}
catch(Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[CloneNode] 序列化失败: {ex}");
DialogHelper.Error($"节点复制失败: {ex.Message}");
return default;
}
}
#endregion
[System.Serializable]
private class ClipboardData
{
public List Nodes;
public List Connections;
}
#region 辅助
private static T FindAncestor(DependencyObject current) where T : DependencyObject
{
while (current != null && !(current is T))
current = VisualTreeHelper.GetParent(current);
return current as T;
}
#endregion
#region 节点碰撞检测
///
/// 检查指定位置是否会与其他节点碰撞
///
public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY)
{
return CheckNodeCollision(draggingNode, newX, newY, null);
}
///
/// 检查指定位置是否会与其他节点碰撞。
/// ignoreNodes 中的节点视为"与拖拽节点同步移动"(多选拖拽的同组成员):
/// 它们相对位置不变、不会与拖拽成员相撞,因此不作为障碍参与检测。
///
public bool CheckNodeCollision(FlowNode draggingNode, double newX, double newY, System.Collections.Generic.ICollection ignoreNodes)
{
if (_graph?.Nodes == null) return false;
var newRect = new Rect(newX, newY, draggingNode.NodeWidth, draggingNode.NodeHeight);
foreach (var node in _graph.Nodes)
{
if (node.NodeId == draggingNode.NodeId) continue;
if (ignoreNodes != null && ignoreNodes.Contains(node)) continue;
var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
if (newRect.IntersectsWith(nodeRect))
return true;
}
return false;
}
///
/// 从指定位置开始查找不与任何节点重叠的空闲位置
///
public Point FindFreePosition(double startX, double startY)
{
double x = startX, y = startY;
if (_graph?.Nodes == null) return new Point(x, y);
bool collision;
do
{
collision = false;
var dropRect = new Rect(x, y, NodeControl.DefaultWidth, NodeControl.DefaultHeight);
foreach (var node in _graph.Nodes)
{
var nodeRect = new Rect(node.X, node.Y, node.NodeWidth, node.NodeHeight);
if (nodeRect.IntersectsWith(dropRect))
{
x += 20;
y += 20;
collision = true;
break;
}
}
} while (collision);
return new Point(x, y);
}
#endregion
#region 自动拉线
///
/// 自动拉线 - 单节点拖拽结束时,若节点贴近其他节点则自动建立连接。
/// 规则:靠下和靠右时其他节点当头(本节点作目标);靠上和靠左时本节点当头(本节点作源)。
/// 阈值:贴边距离 40px 内,且对应方向重叠量达到较小节点尺寸的 80%。
/// 拖拽过程中通过 UpdateAutoConnectPreview 显示虚线预览,拉远自动消失。
///
public void TryAutoConnect(FlowNode node)
{
if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
{
ClearPreviewLine();
return;
}
const double threshold = 40; // 贴边距离阈值(px)
var pos = GetNodeVisualBounds(node);
foreach (var other in _graph.Nodes)
{
if (other == node) continue;
var oPos = GetNodeVisualBounds(other);
// 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
{
if (TryAutoAdd(other, PortSide.Bottom, node, PortSide.Top))
{
ClearPreviewLine();
return; // 每次拖拽只自动连一条
}
}
// 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
{
if (TryAutoAdd(node, PortSide.Bottom, other, PortSide.Top))
{
ClearPreviewLine();
return;
}
}
// 3. 本节点在 other 正右方 →(other.Right → node.Left)
if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
{
if (TryAutoAdd(other, PortSide.Right, node, PortSide.Left))
{
ClearPreviewLine();
return;
}
}
// 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
{
if (TryAutoAdd(node, PortSide.Right, other, PortSide.Left))
{
ClearPreviewLine();
return;
}
}
}
// 没有成功连接 → 清除预览
ClearPreviewLine();
}
///
/// 拖拽过程中更新自动拉线虚线预览;靠近且可连接时显示,否则清除
///
public void UpdateAutoConnectPreview(FlowNode node)
{
ClearPreviewLine();
if (node == null || _graph == null || _graph.Nodes == null || _graph.Connections == null)
return;
const double threshold = 40; // 贴边距离阈值(px)
var pos = GetNodeVisualBounds(node);
foreach (var other in _graph.Nodes)
{
if (other == node) continue;
var oPos = GetNodeVisualBounds(other);
FlowNode source = null;
PortSide sourceSide = PortSide.Bottom;
FlowNode target = null;
PortSide targetSide = PortSide.Top;
// 1. 本节点在 other 正下方 → 上连下(other.Bottom → node.Top)
if (HorizontalOverlap(pos, oPos) && VerticalGap(pos.Top, oPos.Bottom) <= threshold)
{
source = other; sourceSide = PortSide.Bottom;
target = node; targetSide = PortSide.Top;
}
// 2. 本节点在 other 正上方 → 本节点当头(node.Bottom → other.Top)
else if (HorizontalOverlap(pos, oPos) && VerticalGap(oPos.Top, pos.Bottom) <= threshold)
{
source = node; sourceSide = PortSide.Bottom;
target = other; targetSide = PortSide.Top;
}
// 3. 本节点在 other 正右方 →(other.Right → node.Left)
else if (VerticalOverlap(pos, oPos) && HorizontalGap(pos.Left, oPos.Right) <= threshold)
{
source = other; sourceSide = PortSide.Right;
target = node; targetSide = PortSide.Left;
}
// 4. 本节点在 other 正左方 → 本节点当头(node.Right → other.Left)
else if (VerticalOverlap(pos, oPos) && HorizontalGap(oPos.Left, pos.Right) <= threshold)
{
source = node; sourceSide = PortSide.Right;
target = other; targetSide = PortSide.Left;
}
if (source == null) continue;
// 预检:能连才显示预览(复用防重/防环逻辑)
var connection = new FlowConnection
{
SourceNodeId = source.NodeId,
SourceSide = sourceSide,
TargetNodeId = target.NodeId,
TargetSide = targetSide
};
if (!_graph.CanConnect(connection))
continue;
Point start = GetPortPosition(source, sourceSide);
Point end = GetPortPosition(target, targetSide);
if (_previewLine == null)
{
_previewLine = new Path
{
Stroke = new SolidColorBrush(Color.FromRgb(232, 145, 73)),
StrokeThickness = 2,
StrokeDashArray = new DoubleCollection { 4, 3 },
IsHitTestVisible = false
};
Panel.SetZIndex(_previewLine, 10000);
Children.Add(_previewLine);
}
_previewLine.Data = CreateConnectionGeometry(start, end, sourceSide, targetSide);
return;
}
}
///
/// 清除自动拉线虚线预览
///
public void ClearPreviewLine()
{
if (_previewLine != null)
{
Children.Remove(_previewLine);
_previewLine = null;
}
}
///
/// 构造连线并交给 Graph 添加(复用已有防重/防环逻辑)
///
private bool TryAutoAdd(FlowNode source, PortSide sourceSide, FlowNode target, PortSide targetSide)
{
var connection = new FlowConnection
{
SourceNodeId = source.NodeId,
SourceSide = sourceSide,
TargetNodeId = target.NodeId,
TargetSide = targetSide
};
// 预检:能连才添加(复用防重/防环逻辑)
if (!_graph.CanConnect(connection))
return false;
return _graph.TryAddConnection(connection);
}
private Rect GetNodeVisualBounds(FlowNode node)
{
double left = node.X;
double top = node.Y;
double w = NodeControl.DefaultWidth;
double h = NodeControl.DefaultHeight;
return new Rect(left, top, w, h);
}
private static bool HorizontalOverlap(Rect a, Rect b)
{
double overlap = Math.Min(a.Right, b.Right) - Math.Max(a.Left, b.Left);
double minW = Math.Min(a.Width, b.Width);
return minW > 0 && overlap >= minW * 0.8;
}
private static bool VerticalOverlap(Rect a, Rect b)
{
double overlap = Math.Min(a.Bottom, b.Bottom) - Math.Max(a.Top, b.Top);
double minH = Math.Min(a.Height, b.Height);
return minH > 0 && overlap >= minH * 0.8;
}
private static double VerticalGap(double aBottom, double bTop)
{
return Math.Abs(bTop - aBottom);
}
private static double HorizontalGap(double aRight, double bLeft)
{
return Math.Abs(bLeft - aRight);
}
#endregion
#region 鸟瞰图支持
///
/// 动态更新画布大小,使内容超出时自动扩展
///
public void UpdateCanvasSize()
{
if (_graph?.Nodes == null || _graph.Nodes.Count == 0) return;
var bounds = GetContentBounds();
double minW = ActualWidth > 0 ? ActualWidth : 800;
double minH = ActualHeight > 0 ? ActualHeight : 600;
double newW = Math.Max(bounds.Right + 300, minW);
double newH = Math.Max(bounds.Bottom + 300, minH);
if (Math.Abs(Width - newW) > 1 || Math.Abs(Height - newH) > 1)
{
Width = newW;
Height = newH;
}
}
///
/// 适应内容 - 计算所有节点边界,缩放平移使内容完整显示并居中
///
public void FitToContent()
{
var bounds = GetContentBounds();
var vp = GetViewportSize();
if (bounds.Width <= 0 || bounds.Height <= 0 || vp.Width <= 0 || vp.Height <= 0)
{
// 无内容或视口异常:复位 100% 原点
_scale.ScaleX = 1.0;
_scale.ScaleY = 1.0;
_translate.X = 0;
_translate.Y = 0;
ZoomChanged?.Invoke(1.0);
return;
}
const double padding = 50; // 四周留白
double scaleX = (vp.Width - padding * 2) / bounds.Width;
double scaleY = (vp.Height - padding * 2) / bounds.Height;
double newScale = Math.Min(scaleX, scaleY);
newScale = Math.Max(MinZoom, Math.Min(1.0, newScale)); // 内容小时不放大超过100%
_scale.ScaleX = newScale;
_scale.ScaleY = newScale;
// 居中:内容中心对齐视口中心,但 0,0 原点固定在左上角(不跑到画布外)
double centerX = bounds.X + bounds.Width / 2;
double centerY = bounds.Y + bounds.Height / 2;
_translate.X = Math.Min(0, vp.Width / 2 - centerX * newScale);
_translate.Y = Math.Min(0, vp.Height / 2 - centerY * newScale);
ZoomChanged?.Invoke(newScale);
}
///
/// 获取所有节点的内容边界
///
public Rect GetContentBounds()
{
if (_graph?.Nodes == null || _graph.Nodes.Count == 0)
return new Rect(0, 0, 0, 0);
double minX = double.MaxValue, minY = double.MaxValue;
double maxX = double.MinValue, maxY = double.MinValue;
foreach (var node in _graph.Nodes)
{
minX = Math.Min(minX, node.X);
minY = Math.Min(minY, node.Y);
maxX = Math.Max(maxX, node.X + node.NodeWidth);
maxY = Math.Max(maxY, node.Y + node.NodeHeight);
}
return new Rect(minX, minY, maxX - minX, maxY - minY);
}
///
/// 获取当前视口(画布坐标空间中的可见区域)
///
public Rect GetViewport()
{
var vp = GetViewportSize();
double x = -_translate.X / _scale.ScaleX;
double y = -_translate.Y / _scale.ScaleY;
double w = vp.Width / _scale.ScaleX;
double h = vp.Height / _scale.ScaleY;
return new Rect(x, y, w, h);
}
///
/// 获取可视区域大小(屏幕像素)- 取父容器的实际尺寸
///
private Size GetViewportSize()
{
var parent = System.Windows.Media.VisualTreeHelper.GetParent(this) as FrameworkElement;
if (parent != null && parent.ActualWidth > 0 && parent.ActualHeight > 0)
return new Size(parent.ActualWidth, parent.ActualHeight);
return new Size(800, 600);
}
///
/// 将画布视口居中到指定画布坐标点
///
public void CenterOn(Point canvasPoint)
{
var vp = GetViewportSize();
_translate.X = vp.Width / 2 - canvasPoint.X * _scale.ScaleX;
_translate.Y = vp.Height / 2 - canvasPoint.Y * _scale.ScaleY;
ClampTranslate();
}
#endregion
#region 自动排版
///
/// 层次化自动排版:从左到右分层,同层从上到下排列,最小化连线交叉。
/// 算法:拓扑排序分层 → 重心启发式排序 → 居中对齐 → 连线方向归一化
///
public void AutoLayout()
{
if (_graph == null || _graph.Nodes.Count == 0) return;
if (IsRunning || IsReadOnly) return;
var nodes = _graph.Nodes.ToList();
var conns = _graph.Connections.ToList();
var nodeIds = nodes.Select(n => n.NodeId).ToHashSet();
// 1. 构建邻接表
var succ = new Dictionary>();
var pred = new Dictionary>();
foreach (var n in nodes)
{
succ[n.NodeId] = new List();
pred[n.NodeId] = new List();
}
foreach (var c in conns)
{
if (nodeIds.Contains(c.SourceNodeId) && nodeIds.Contains(c.TargetNodeId))
{
succ[c.SourceNodeId].Add(c.TargetNodeId);
pred[c.TargetNodeId].Add(c.SourceNodeId);
}
}
// 2. 层次分配(最长路径:node.layer = max(前驱 layer) + 1)
var layer = new Dictionary();
var inDeg = nodes.ToDictionary(n => n.NodeId, n => pred[n.NodeId].Count);
var queue = new Queue();
foreach (var n in nodes)
if (inDeg[n.NodeId] == 0) queue.Enqueue(n.NodeId);
while (queue.Count > 0)
{
var id = queue.Dequeue();
int maxPred = -1;
foreach (var p in pred[id])
if (layer.TryGetValue(p, out var pl)) maxPred = Math.Max(maxPred, pl);
layer[id] = maxPred + 1;
foreach (var s in succ[id])
if (--inDeg[s] == 0) queue.Enqueue(s);
}
// 未分配(孤立节点)放第 0 层
foreach (var n in nodes)
if (!layer.ContainsKey(n.NodeId)) layer[n.NodeId] = 0;
// 3. 按层分组
int maxLayer = layer.Values.Max();
var layers = new List>();
for (int i = 0; i <= maxLayer; i++) layers.Add(new List());
foreach (var kv in layer) layers[kv.Value].Add(kv.Key);
// 4. 层内排序:重心启发式(按前驱 Y 中位数排序,减少交叉)
const double layerGap = 300; // 层间距(水平)
const double nodeGap = 50; // 同层节点间距(垂直)
const double startX = 40;
const double startY = 40;
var yPos = new Dictionary();
for (int li = 0; li < layers.Count; li++)
{
var layerNodes = layers[li];
if (li > 0 && layerNodes.Count > 1)
{
layerNodes.Sort((a, b) =>
{
double ma = MedianY(pred[a], yPos);
double mb = MedianY(pred[b], yPos);
return ma.CompareTo(mb);
});
}
double y = startY;
foreach (var id in layerNodes)
{
var node = _graph.GetNode(id);
if (node == null) continue;
node.X = startX + li * layerGap;
node.Y = y;
yPos[id] = y;
y += node.NodeHeight + nodeGap;
}
}
// 5. 各层垂直居中
double maxLayerH = 0;
var layerHeights = new double[layers.Count];
for (int li = 0; li < layers.Count; li++)
{
var ln = layers[li];
if (ln.Count == 0) continue;
var first = _graph.GetNode(ln[0]);
var last = _graph.GetNode(ln[ln.Count - 1]);
if (first == null || last == null) continue;
double h = (last.Y + last.NodeHeight) - first.Y;
layerHeights[li] = h;
maxLayerH = Math.Max(maxLayerH, h);
}
for (int li = 0; li < layers.Count; li++)
{
double offset = (maxLayerH - layerHeights[li]) / 2;
if (offset <= 0) continue;
foreach (var id in layers[li])
{
var node = _graph.GetNode(id);
if (node != null) node.Y += offset;
}
}
// 6. 连线方向归一化:跨层 Right→Left,同层 Bottom→Top
foreach (var c in conns)
{
var src = _graph.GetNode(c.SourceNodeId);
var tgt = _graph.GetNode(c.TargetNodeId);
if (src == null || tgt == null) continue;
if (layer.TryGetValue(c.SourceNodeId, out var sl) &&
layer.TryGetValue(c.TargetNodeId, out var tl) && sl == tl)
{
c.SourceSide = PortSide.Bottom;
c.TargetSide = PortSide.Top;
}
else
{
c.SourceSide = PortSide.Right;
c.TargetSide = PortSide.Left;
}
}
// 7. 刷新视觉
foreach (var node in nodes)
{
if (_nodeControls.TryGetValue(node.NodeId, out var ctrl))
{
SetLeft(ctrl, node.X);
SetTop(ctrl, node.Y);
}
}
foreach (var kvp in _connectionPaths)
if (kvp.Value.Tag is FlowConnection fc) UpdateConnectionPath(kvp.Value, fc);
// 8. 适应内容
UpdateCanvasSize();
FitToContent();
}
private static double MedianY(List predIds, Dictionary yPos)
{
var ys = predIds.Where(yPos.ContainsKey).Select(id => yPos[id]).OrderBy(v => v).ToList();
if (ys.Count == 0) return 0;
int mid = ys.Count / 2;
return ys.Count % 2 == 0 ? (ys[mid - 1] + ys[mid]) / 2 : ys[mid];
}
#endregion
#region 平移限制
///
/// 限制平移范围,确保不会看到内容区域之外
///
private void ClampTranslate()
{
// 原点固定,不超出画布边界
var vp = GetViewportSize();
// 用画布尺寸判断平移范围,而非节点范围
double canvasW = (Width > 0 ? Width : 5000) * _scale.ScaleX;
double canvasH = (Height > 0 ? Height : 3500) * _scale.ScaleY;
// X 轴:原点不动,不超出右下边界
if (canvasW <= vp.Width)
_translate.X = 0;
else
{
double minX = vp.Width - canvasW;
if (_translate.X < minX) _translate.X = minX;
if (_translate.X > 0) _translate.X = 0;
}
// Y 轴
if (canvasH <= vp.Height)
_translate.Y = 0;
else
{
double minY = vp.Height - canvasH;
if (_translate.Y < minY) _translate.Y = minY;
if (_translate.Y > 0) _translate.Y = 0;
}
}
#endregion
#region 对齐引导线
private readonly List _alignLines = new List();
public void AddAlignLine(Point start, Point end)
{
var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
if (layer == null) return;
// Adorner 坐标空间与画布布局坐标空间一致,直接传画布坐标
var line = new SelectionAlignLine(this, start, end);
layer.Add(line);
_alignLines.Add(line);
}
public void ClearAlignLines()
{
var layer = System.Windows.Documents.AdornerLayer.GetAdornerLayer(this);
if (layer != null)
{
foreach (var line in _alignLines)
layer.Remove(line);
}
_alignLines.Clear();
}
///
/// 检查节点对齐并返回对齐偏移量
///
public void CheckAlignment(FlowNode draggingNode, ref double x, ref double y)
{
ClearAlignLines();
const double threshold = 5.0;
double nodeLeft = x;
double nodeTop = y;
double nodeRight = x + draggingNode.NodeWidth;
double nodeBottom = y + draggingNode.NodeHeight;
double nodeCenterX = x + draggingNode.NodeWidth / 2;
double nodeCenterY = y + draggingNode.NodeHeight / 2;
double snapX = double.NaN;
double snapY = double.NaN;
if (_graph?.Nodes == null) return;
foreach (var other in _graph.Nodes)
{
if (other.NodeId == draggingNode.NodeId) continue;
double otherLeft = other.X;
double otherTop = other.Y;
double otherRight = other.X + other.NodeWidth;
double otherBottom = other.Y + other.NodeHeight;
double otherCenterX = other.X + other.NodeWidth / 2;
double otherCenterY = other.Y + other.NodeHeight / 2;
// 垂直对齐检测(显示垂直引导线,调整 X)
if (!double.IsNaN(snapX))
{
// 已有 X 对齐,只检查 Y
}
else if (Math.Abs(nodeLeft - otherLeft) < threshold)
{
snapX = otherLeft;
AddAlignLine(new Point(otherLeft, Math.Min(nodeTop, otherTop)),
new Point(otherLeft, Math.Max(nodeBottom, otherBottom)));
}
else if (Math.Abs(nodeRight - otherRight) < threshold)
{
snapX = otherRight - draggingNode.NodeWidth;
AddAlignLine(new Point(otherRight, Math.Min(nodeTop, otherTop)),
new Point(otherRight, Math.Max(nodeBottom, otherBottom)));
}
else if (Math.Abs(nodeCenterX - otherCenterX) < threshold)
{
snapX = otherCenterX - draggingNode.NodeWidth / 2;
AddAlignLine(new Point(otherCenterX, Math.Min(nodeTop, otherTop)),
new Point(otherCenterX, Math.Max(nodeBottom, otherBottom)));
}
// 水平对齐检测(显示水平引导线,调整 Y)
if (!double.IsNaN(snapY))
{
// 已有 Y 对齐
}
else if (Math.Abs(nodeTop - otherTop) < threshold)
{
snapY = otherTop;
AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherTop),
new Point(Math.Max(nodeRight, otherRight), otherTop));
}
else if (Math.Abs(nodeBottom - otherBottom) < threshold)
{
snapY = otherBottom - draggingNode.NodeHeight;
AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherBottom),
new Point(Math.Max(nodeRight, otherRight), otherBottom));
}
else if (Math.Abs(nodeCenterY - otherCenterY) < threshold)
{
snapY = otherCenterY - draggingNode.NodeHeight / 2;
AddAlignLine(new Point(Math.Min(nodeLeft, otherLeft), otherCenterY),
new Point(Math.Max(nodeRight, otherRight), otherCenterY));
}
}
if (!double.IsNaN(snapX)) x = snapX;
if (!double.IsNaN(snapY)) y = snapY;
}
#endregion
}
}