You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

717 lines
25 KiB

using ReactiveUI.Avalonia;
using ReactiveUI;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia;
using Avalonia.Media;
using Avalonia.Collections;
using Avalonia.VisualTree;
using AuroraDesk.Presentation.ViewModels.Pages;
using AuroraDesk.Core.Entities;
using System.Linq;
using System.Reactive.Disposables;
using System;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Interactivity;
namespace AuroraDesk.Presentation.Views.Pages;
public partial class NodeCanvasPageView : ReactiveUserControl<NodeCanvasPageViewModel>
{
private Canvas? _canvasContainer;
private Canvas? _gridBackgroundLayer;
private ScrollViewer? _canvasScrollViewer;
private Node? _draggedNode;
private Point? _dragStartPoint;
private ConnectionPoint? _hoveredConnectionPoint;
private Path? _tempConnectionLine;
private bool _isDraggingCanvas;
private Point? _canvasDragStartPoint;
private double _canvasDragStartOffsetX;
private double _canvasDragStartOffsetY;
private bool _isSpaceKeyPressed;
private const double GridSize = 20;
public NodeCanvasPageView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
// 延迟初始化,等待加载完成
this.Loaded += (s, e) =>
{
_canvasContainer = this.FindControl<Canvas>("CanvasContainer");
_gridBackgroundLayer = this.FindControl<Canvas>("GridBackgroundLayer");
_canvasScrollViewer = this.FindControl<ScrollViewer>("CanvasScrollViewer");
if (_canvasContainer != null)
{
_canvasContainer.PointerPressed += OnCanvasPointerPressed;
_canvasContainer.PointerMoved += OnCanvasPointerMoved;
_canvasContainer.PointerReleased += OnCanvasPointerReleased;
// 监听 Canvas 尺寸变化,更新网格
_canvasContainer.SizeChanged += (sender, args) => DrawGridBackground();
}
// 在 ScrollViewer 上添加事件支持
if (_canvasScrollViewer != null)
{
// 添加鼠标滚轮缩放支持(需要按住Ctrl键)
_canvasScrollViewer.PointerWheelChanged += OnScrollViewerPointerWheelChanged;
// 添加画布拖动支持(鼠标中键或空格键+左键)
_canvasScrollViewer.PointerPressed += OnScrollViewerPointerPressed;
_canvasScrollViewer.PointerMoved += OnScrollViewerPointerMoved;
_canvasScrollViewer.PointerReleased += OnScrollViewerPointerReleased;
}
// 添加键盘事件监听(空格键)
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
topLevel.KeyDown += OnKeyDown;
topLevel.KeyUp += OnKeyUp;
}
// 绘制网格背景
DrawGridBackground();
// 监听连接变化,更新连接线
if (ViewModel != null)
{
// 监听 Connections 集合变化
ViewModel.WhenAnyValue(x => x.Connections)
.Subscribe(_ => UpdateConnectionLines());
// 监听 Nodes 集合变化
ViewModel.WhenAnyValue(x => x.Nodes)
.Subscribe(nodes =>
{
if (nodes != null)
{
UpdateConnectionLines();
// 为现有节点订阅位置变化
foreach (var node in nodes)
{
SubscribeToNodePositionChanges(node);
}
// 订阅集合内容变化事件
nodes.CollectionChanged += (sender, e) =>
{
if (e.NewItems != null)
{
foreach (Node node in e.NewItems)
{
SubscribeToNodePositionChanges(node);
}
}
UpdateConnectionLines();
// 强制刷新 ItemsControl 并更新节点位置
var itemsControl = this.FindControl<ItemsControl>("NodesItemsControl");
if (itemsControl != null)
{
Avalonia.Threading.Dispatcher.UIThread.Post(async () =>
{
itemsControl.InvalidateMeasure();
itemsControl.InvalidateArrange();
// 等待容器创建
await Task.Delay(100);
// 手动更新所有节点的 Canvas 位置(重试机制)
for (int i = 0; i < 3; i++)
{
UpdateNodePositions(itemsControl, nodes);
if (i < 2) await Task.Delay(50);
}
// 如果有新节点,滚动到第一个新节点位置
if (e.NewItems != null && e.NewItems.Count > 0)
{
var firstNode = e.NewItems[0] as Node;
if (firstNode != null)
{
await Task.Delay(50);
ScrollToNode(firstNode);
}
}
}, Avalonia.Threading.DispatcherPriority.Normal);
}
};
}
});
ViewModel.WhenAnyValue(x => x.IsConnecting, x => x.ConnectingSourcePoint)
.Subscribe(_ => UpdateConnectionLines());
}
};
}
/// <summary>
/// 处理模板双击事件,添加节点到画布
/// </summary>
private void OnTemplateButtonDoubleTapped(object? sender, RoutedEventArgs e)
{
if (sender is Button button && button.DataContext is NodeTemplate template && ViewModel != null)
{
ViewModel.AddNodeToCanvasCommand.Execute(template).Subscribe();
}
e.Handled = true;
}
private void UpdateConnectionLines()
{
if (_canvasContainer == null || ViewModel == null) return;
// 移除临时连接线
if (_tempConnectionLine != null && _canvasContainer.Children.Contains(_tempConnectionLine))
{
_canvasContainer.Children.Remove(_tempConnectionLine);
_tempConnectionLine = null;
}
// 更新所有连接线
var connections = ViewModel.Connections.ToList();
foreach (var connection in connections)
{
UpdateConnectionLine(connection);
}
// 如果正在连接,显示临时连接线
if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
{
ShowTempConnectionLine(ViewModel.ConnectingSourcePoint);
}
}
private void UpdateConnectionLine(Connection connection)
{
if (_canvasContainer == null || connection.SourcePoint == null || connection.TargetPoint == null)
return;
var sourcePoint = GetConnectionPointPosition(connection.SourcePoint);
var targetPoint = GetConnectionPointPosition(connection.TargetPoint);
// 查找或创建连接线Path
var path = _canvasContainer.Children.OfType<Path>()
.FirstOrDefault(p => p.Tag == connection);
if (path == null)
{
path = new Path
{
Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
StrokeThickness = 2,
Tag = connection
};
_canvasContainer.Children.Add(path);
}
// 创建贝塞尔曲线
var geometry = new PathGeometry();
var figure = new PathFigure
{
StartPoint = sourcePoint
};
var controlPoint1 = new Point(sourcePoint.X + 50, sourcePoint.Y);
var controlPoint2 = new Point(targetPoint.X - 50, targetPoint.Y);
var bezierSegment = new BezierSegment
{
Point1 = controlPoint1,
Point2 = controlPoint2,
Point3 = targetPoint
};
figure.Segments.Add(bezierSegment);
geometry.Figures.Add(figure);
path.Data = geometry;
}
private void ShowTempConnectionLine(ConnectionPoint sourcePoint)
{
if (_canvasContainer == null) return;
var startPoint = GetConnectionPointPosition(sourcePoint);
var endPoint = new Point(startPoint.X + 100, startPoint.Y);
if (_tempConnectionLine == null)
{
_tempConnectionLine = new Path
{
Stroke = new SolidColorBrush(Color.FromRgb(52, 152, 219)),
StrokeThickness = 2,
StrokeDashArray = new AvaloniaList<double> { 5, 5 }
};
_canvasContainer.Children.Add(_tempConnectionLine);
}
var geometry = new PathGeometry();
var figure = new PathFigure { StartPoint = startPoint };
var lineSegment = new LineSegment { Point = endPoint };
figure.Segments.Add(lineSegment);
geometry.Figures.Add(figure);
_tempConnectionLine.Data = geometry;
}
private Point GetConnectionPointPosition(ConnectionPoint point)
{
if (point.Node == null || _canvasContainer == null)
return new Point(0, 0);
var nodeX = point.Node.X;
var nodeY = point.Node.Y;
var nodeHeight = point.Node.Height;
// 计算连接点在Canvas上的绝对位置
if (point.Type == ConnectionPointType.Input)
{
// 输入点在左侧,垂直居中
var inputIndex = point.Node.ConnectionPoints
.Where(p => p.Type == ConnectionPointType.Input)
.OrderBy(p => p.Index)
.ToList()
.IndexOf(point);
var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Input) + 1);
return new Point(nodeX, nodeY + spacing * (inputIndex + 1));
}
else
{
// 输出点在右侧,垂直排列
var outputIndex = point.Node.ConnectionPoints
.Where(p => p.Type == ConnectionPointType.Output)
.OrderBy(p => p.Index)
.ToList()
.IndexOf(point);
var spacing = nodeHeight / (point.Node.ConnectionPoints.Count(p => p.Type == ConnectionPointType.Output) + 1);
return new Point(nodeX + point.Node.Width, nodeY + spacing * (outputIndex + 1));
}
}
private void OnCanvasPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_canvasContainer == null || ViewModel == null) return;
var point = e.GetPosition(_canvasContainer);
var source = e.Source as Control;
var properties = e.GetCurrentPoint(_canvasContainer).Properties;
// 检查是否是画布拖动(鼠标中键或空格键+左键)
bool isMiddleButton = properties.IsMiddleButtonPressed;
if (isMiddleButton || (_isSpaceKeyPressed && properties.IsLeftButtonPressed))
{
// 开始拖动画布
_isDraggingCanvas = true;
_canvasDragStartPoint = e.GetPosition(_canvasScrollViewer);
_canvasDragStartOffsetX = ViewModel.CanvasOffsetX;
_canvasDragStartOffsetY = ViewModel.CanvasOffsetY;
if (_canvasScrollViewer != null)
{
_canvasScrollViewer.Cursor = new Cursor(StandardCursorType.Hand);
}
e.Handled = true;
return;
}
// 检查是否点击了连接点
var connectionPoint = FindConnectionPoint(source);
if (connectionPoint != null)
{
HandleConnectionPointClick(connectionPoint);
e.Handled = true;
return;
}
// 检查是否点击了节点(但不是连接点)
var node = FindNode(source);
if (node != null && connectionPoint == null)
{
ViewModel.SelectedNode = node;
_draggedNode = node;
_dragStartPoint = point;
e.Handled = true;
return;
}
// 点击空白区域,取消选中和连接
ViewModel.SelectedNode = null;
ViewModel.CancelConnection();
// 注意:点击画布空白处不再自动添加节点,用户需要从组件库拖拽或点击空白处两次来添加节点
// 这样可以确保属性面板始终显示正确的状态(未选中对象)
}
private void OnCanvasPointerMoved(object? sender, PointerEventArgs e)
{
if (_canvasContainer == null || ViewModel == null) return;
var point = e.GetPosition(_canvasContainer);
// 拖动画布
if (_isDraggingCanvas && _canvasDragStartPoint.HasValue && _canvasScrollViewer != null)
{
var currentPoint = e.GetPosition(_canvasScrollViewer);
var deltaX = currentPoint.X - _canvasDragStartPoint.Value.X;
var deltaY = currentPoint.Y - _canvasDragStartPoint.Value.Y;
// 考虑缩放因子,调整偏移量
var zoomFactor = ViewModel.CanvasZoom;
ViewModel.CanvasOffsetX = _canvasDragStartOffsetX + deltaX / zoomFactor;
ViewModel.CanvasOffsetY = _canvasDragStartOffsetY + deltaY / zoomFactor;
return;
}
// 拖拽节点
if (_draggedNode != null && _dragStartPoint.HasValue)
{
var deltaX = point.X - _dragStartPoint.Value.X;
var deltaY = point.Y - _dragStartPoint.Value.Y;
ViewModel.UpdateNodePosition(_draggedNode,
_draggedNode.X + deltaX,
_draggedNode.Y + deltaY);
_dragStartPoint = point;
}
// 如果正在连接,更新临时连接线
if (ViewModel.IsConnecting && ViewModel.ConnectingSourcePoint != null)
{
var sourcePos = GetConnectionPointPosition(ViewModel.ConnectingSourcePoint);
if (_tempConnectionLine != null)
{
var geometry = new PathGeometry();
var figure = new PathFigure { StartPoint = sourcePos };
var lineSegment = new LineSegment { Point = point };
figure.Segments.Add(lineSegment);
geometry.Figures.Add(figure);
_tempConnectionLine.Data = geometry;
}
}
}
private void OnCanvasPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_isDraggingCanvas)
{
_isDraggingCanvas = false;
_canvasDragStartPoint = null;
if (_canvasScrollViewer != null)
{
_canvasScrollViewer.Cursor = Cursor.Default;
}
}
_draggedNode = null;
_dragStartPoint = null;
}
private void HandleConnectionPointClick(ConnectionPoint connectionPoint)
{
if (ViewModel == null) return;
if (ViewModel.IsConnecting)
{
// 完成连接
if (connectionPoint.Type == ConnectionPointType.Input)
{
ViewModel.CompleteConnectionCommand.Execute(connectionPoint).Subscribe();
}
else
{
// 取消当前连接,开始新连接
ViewModel.CancelConnection();
ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
}
}
else
{
// 开始连接
if (connectionPoint.Type == ConnectionPointType.Output)
{
ViewModel.StartConnectionCommand.Execute(connectionPoint).Subscribe();
}
}
}
private Node? FindNode(Control? control)
{
if (control == null || ViewModel == null) return null;
// 向上查找直到找到包含Node的容器
var current = control;
while (current != null)
{
if (current.DataContext is Node node && ViewModel.Nodes.Contains(node))
{
return node;
}
current = current.Parent as Control;
}
return null;
}
private ConnectionPoint? FindConnectionPoint(Control? control)
{
if (control == null) return null;
var current = control;
while (current != null)
{
if (current.DataContext is ConnectionPoint point)
{
return point;
}
current = current.Parent as Control;
}
return null;
}
private void DrawGridBackground()
{
if (_gridBackgroundLayer == null || _canvasContainer == null) return;
// 清空之前的网格线
_gridBackgroundLayer.Children.Clear();
var width = _canvasContainer.Bounds.Width > 0 ? _canvasContainer.Bounds.Width : 2000;
var height = _canvasContainer.Bounds.Height > 0 ? _canvasContainer.Bounds.Height : 2000;
// 创建网格线画笔
var gridBrush = new SolidColorBrush(Color.FromRgb(224, 224, 224)); // #E0E0E0
// 绘制垂直线
for (double x = 0; x <= width; x += GridSize)
{
var line = new Line
{
StartPoint = new Point(x, 0),
EndPoint = new Point(x, height),
Stroke = gridBrush,
StrokeThickness = 0.5
};
_gridBackgroundLayer.Children.Add(line);
}
// 绘制水平线
for (double y = 0; y <= height; y += GridSize)
{
var line = new Line
{
StartPoint = new Point(0, y),
EndPoint = new Point(width, y),
Stroke = gridBrush,
StrokeThickness = 0.5
};
_gridBackgroundLayer.Children.Add(line);
}
}
/// <summary>
/// 更新所有节点的 Canvas 位置
/// </summary>
private void UpdateNodePositions(ItemsControl itemsControl, System.Collections.ObjectModel.ObservableCollection<Node> nodes)
{
if (itemsControl == null || nodes == null) return;
foreach (var node in nodes)
{
UpdateSingleNodePosition(itemsControl, node);
}
}
/// <summary>
/// 订阅节点位置属性变化
/// </summary>
private void SubscribeToNodePositionChanges(Node node)
{
if (node == null) return;
node.WhenAnyValue(x => x.X, x => x.Y)
.Subscribe(pos =>
{
var itemsControl = this.FindControl<ItemsControl>("NodesItemsControl");
if (itemsControl != null)
{
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
UpdateSingleNodePosition(itemsControl, node);
}, Avalonia.Threading.DispatcherPriority.Normal);
}
});
}
/// <summary>
/// 更新单个节点的 Canvas 位置(备用方案,主要依赖数据绑定)
/// </summary>
private void UpdateSingleNodePosition(ItemsControl itemsControl, Node node)
{
if (itemsControl == null || node == null) return;
try
{
var allPresenters = itemsControl.GetVisualDescendants().OfType<ContentPresenter>().ToList();
ContentPresenter? container = allPresenters.FirstOrDefault(cp => cp.Content == node)
?? allPresenters.FirstOrDefault(cp => cp.DataContext == node);
if (container == null)
{
foreach (var presenter in allPresenters)
{
var border = presenter.GetVisualChildren().OfType<Border>().FirstOrDefault();
if (border?.DataContext == node)
{
container = presenter;
break;
}
}
}
if (container != null)
{
Canvas.SetLeft(container, node.X);
Canvas.SetTop(container, node.Y);
}
}
catch
{
// 忽略异常,位置更新主要依赖数据绑定
}
}
/// <summary>
/// 滚动到指定节点的位置
/// </summary>
private void ScrollToNode(Node node)
{
if (node == null || _canvasScrollViewer == null || ViewModel == null) return;
try
{
// 考虑缩放和偏移
var scaledX = (node.X + ViewModel.CanvasOffsetX) * ViewModel.CanvasZoom;
var scaledY = (node.Y + ViewModel.CanvasOffsetY) * ViewModel.CanvasZoom;
// 获取 ScrollViewer 的可视区域大小
var viewportWidth = _canvasScrollViewer.Viewport.Width;
var viewportHeight = _canvasScrollViewer.Viewport.Height;
// 计算目标滚动位置(使节点居中)
var targetOffsetX = scaledX - viewportWidth / 2;
var targetOffsetY = scaledY - viewportHeight / 2;
// 限制在有效范围内
targetOffsetX = Math.Max(0, Math.Min(targetOffsetX, _canvasScrollViewer.Extent.Width - viewportWidth));
targetOffsetY = Math.Max(0, Math.Min(targetOffsetY, _canvasScrollViewer.Extent.Height - viewportHeight));
_canvasScrollViewer.Offset = new Vector(targetOffsetX, targetOffsetY);
}
catch
{
// 忽略滚动异常
}
}
private void OnScrollViewerPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (ViewModel == null || e.KeyModifiers != KeyModifiers.Control) return;
// 按住Ctrl键时,使用滚轮进行缩放
e.Handled = true;
var delta = e.Delta.Y;
if (delta > 0)
{
ViewModel.ZoomInCommand.Execute().Subscribe();
}
else if (delta < 0)
{
ViewModel.ZoomOutCommand.Execute().Subscribe();
}
}
private void OnScrollViewerPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_canvasScrollViewer == null || ViewModel == null) return;
var properties = e.GetCurrentPoint(_canvasScrollViewer).Properties;
bool isMiddleButton = properties.IsMiddleButtonPressed;
if (isMiddleButton || (_isSpaceKeyPressed && properties.IsLeftButtonPressed))
{
_isDraggingCanvas = true;
_canvasDragStartPoint = e.GetPosition(_canvasScrollViewer);
_canvasDragStartOffsetX = ViewModel.CanvasOffsetX;
_canvasDragStartOffsetY = ViewModel.CanvasOffsetY;
_canvasScrollViewer.Cursor = new Cursor(StandardCursorType.Hand);
e.Handled = true;
}
}
private void OnScrollViewerPointerMoved(object? sender, PointerEventArgs e)
{
if (!_isDraggingCanvas || _canvasScrollViewer == null || ViewModel == null || !_canvasDragStartPoint.HasValue)
return;
var currentPoint = e.GetPosition(_canvasScrollViewer);
var deltaX = currentPoint.X - _canvasDragStartPoint.Value.X;
var deltaY = currentPoint.Y - _canvasDragStartPoint.Value.Y;
// 考虑缩放因子,调整偏移量
var zoomFactor = ViewModel.CanvasZoom;
ViewModel.CanvasOffsetX = _canvasDragStartOffsetX + deltaX / zoomFactor;
ViewModel.CanvasOffsetY = _canvasDragStartOffsetY + deltaY / zoomFactor;
}
private void OnScrollViewerPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_isDraggingCanvas && _canvasScrollViewer != null)
{
_isDraggingCanvas = false;
_canvasDragStartPoint = null;
_canvasScrollViewer.Cursor = Cursor.Default;
}
}
private void OnKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
_isSpaceKeyPressed = true;
if (_canvasScrollViewer != null)
{
_canvasScrollViewer.Cursor = new Cursor(StandardCursorType.Hand);
}
}
}
private void OnKeyUp(object? sender, KeyEventArgs e)
{
if (e.Key == Key.Space)
{
_isSpaceKeyPressed = false;
if (_canvasScrollViewer != null && !_isDraggingCanvas)
{
_canvasScrollViewer.Cursor = Cursor.Default;
}
}
}
}