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 } }