配方编辑部分的交互逻辑

This commit is contained in:
zhengxuan.zhang
2026-04-23 16:12:55 +08:00
parent e0326c2d80
commit 3f3820073f
6 changed files with 482 additions and 131 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace XplorePlane.Models
{
public class PipelineModel
public class PipelineModel //流程图
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
@@ -13,7 +13,7 @@ namespace XplorePlane.Models
public List<PipelineNodeModel> Nodes { get; set; } = new();
}
public class PipelineNodeModel
public class PipelineNodeModel //节点
{
public Guid Id { get; set; } = Guid.NewGuid();
public string OperatorKey { get; set; } = string.Empty;
@@ -1,4 +1,4 @@
using Microsoft.Win32;
using Microsoft.Win32;
using Prism.Commands;
using Prism.Mvvm;
using System;
@@ -44,6 +44,8 @@ namespace XplorePlane.ViewModels.Cnc
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -79,6 +81,10 @@ namespace XplorePlane.ViewModels.Cnc
public ICommand RemoveOperatorCommand { get; }
public ICommand ReorderOperatorCommand { get; }
public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; }
@@ -152,13 +158,10 @@ namespace XplorePlane.ViewModels.Cnc
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node);
RenumberNodes();
if (SelectedNode == node)
{
SelectedNode = PipelineNodes.LastOrDefault();
}
SelectNeighborAfterRemoval(removedIndex);
PersistActiveModule($"已移除算子:{node.DisplayName}");
}
@@ -177,6 +180,26 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已上移算子:{node.DisplayName}");
}
private void ReorderOperator(PipelineReorderArgs args)
{
if (!HasActiveModule || args == null)
return;
var oldIndex = args.OldIndex;
var newIndex = args.NewIndex;
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count)
return;
if (newIndex < 0 || newIndex >= PipelineNodes.Count || oldIndex == newIndex)
return;
var node = PipelineNodes[oldIndex];
PipelineNodes.Move(oldIndex, newIndex);
RenumberNodes();
SelectedNode = node;
PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
}
private void MoveNodeDown(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null)
@@ -191,6 +214,18 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已下移算子:{node.DisplayName}");
}
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
PersistActiveModule(node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}");
}
private void NewPipeline()
{
if (!HasActiveModule)
@@ -359,6 +394,20 @@ namespace XplorePlane.ViewModels.Cnc
}
}
private void SelectNeighborAfterRemoval(int removedIndex)
{
if (PipelineNodes.Count == 0)
{
SelectedNode = null;
return;
}
var nextIndex = removedIndex < PipelineNodes.Count
? removedIndex
: PipelineNodes.Count - 1;
SelectedNode = PipelineNodes[nextIndex];
}
private void RaiseModuleVisibilityChanged()
{
RaisePropertyChanged(nameof(HasActiveModule));
@@ -15,6 +15,10 @@ namespace XplorePlane.ViewModels
ICommand RemoveOperatorCommand { get; }
ICommand ReorderOperatorCommand { get; }
ICommand ToggleOperatorEnabledCommand { get; }
ICommand MoveNodeUpCommand { get; }
ICommand MoveNodeDownCommand { get; }
@@ -1,4 +1,4 @@
using Microsoft.Win32;
using Microsoft.Win32;
using Prism.Events;
using Prism.Commands;
using Prism.Mvvm;
@@ -59,6 +59,7 @@ namespace XplorePlane.ViewModels
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -152,6 +153,7 @@ namespace XplorePlane.ViewModels
public DelegateCommand<string> AddOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
public DelegateCommand ExecutePipelineCommand { get; }
public DelegateCommand CancelExecutionCommand { get; }
public DelegateCommand NewPipelineCommand { get; }
@@ -168,6 +170,8 @@ namespace XplorePlane.ViewModels
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
@@ -217,6 +221,7 @@ namespace XplorePlane.ViewModels
};
LoadNodeParameters(node);
PipelineNodes.Add(node);
SelectedNode = node;
_logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count);
StatusMessage = $"已添加算子:{displayName}";
@@ -227,11 +232,10 @@ namespace XplorePlane.ViewModels
{
if (node == null || !PipelineNodes.Contains(node)) return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node);
RenumberNodes();
if (SelectedNode == node)
SelectedNode = null;
SelectNeighborAfterRemoval(removedIndex);
StatusMessage = $"已移除算子:{node.DisplayName}";
TriggerDebouncedExecution();
@@ -271,6 +275,20 @@ namespace XplorePlane.ViewModels
PipelineNodes.RemoveAt(oldIndex);
PipelineNodes.Insert(newIndex, node);
RenumberNodes();
SelectedNode = node;
StatusMessage = $"已调整算子顺序:{node.DisplayName}";
TriggerDebouncedExecution();
}
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (node == null || !PipelineNodes.Contains(node)) return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
StatusMessage = node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}";
TriggerDebouncedExecution();
}
@@ -280,6 +298,20 @@ namespace XplorePlane.ViewModels
PipelineNodes[i].Order = i;
}
private void SelectNeighborAfterRemoval(int removedIndex)
{
if (PipelineNodes.Count == 0)
{
SelectedNode = null;
return;
}
var nextIndex = removedIndex < PipelineNodes.Count
? removedIndex
: PipelineNodes.Count - 1;
SelectedNode = PipelineNodes[nextIndex];
}
private void LoadNodeParameters(PipelineNodeViewModel node)
{
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
@@ -4,7 +4,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="700"
d:DesignWidth="350"
mc:Ignorable="d">
@@ -13,13 +12,27 @@
<SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<SolidColorBrush x:Key="AccentBlue" Color="#D9ECFF" />
<SolidColorBrush x:Key="DisabledNodeBg" Color="#F3F3F3" />
<SolidColorBrush x:Key="DisabledNodeLine" Color="#B9B9B9" />
<SolidColorBrush x:Key="DisabledNodeText" Color="#8A8A8A" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource AccentBlue}" />
<Setter Property="BorderBrush" Value="#5B9BD5" />
<Setter Property="BorderThickness" Value="1" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="ToolbarBtn" TargetType="Button">
@@ -43,7 +56,7 @@
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="180" />
<RowDefinition Height="4*" MinHeight="180" />
<RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="80" />
<RowDefinition Height="Auto" />
@@ -66,19 +79,19 @@
Command="{Binding SavePipelineCommand}"
Content="保存"
Style="{StaticResource ToolbarBtn}"
ToolTip="保存当前检测模块流水线" />
ToolTip="保存当前流水线" />
<Button
Width="60"
Command="{Binding SaveAsPipelineCommand}"
Content="另存为"
Style="{StaticResource ToolbarBtn}"
ToolTip="导出当前检测模块流水线" />
ToolTip="另存当前流水线" />
<Button
Width="52"
Command="{Binding LoadPipelineCommand}"
Content="加载"
Style="{StaticResource ToolbarBtn}"
ToolTip="流水线模板加载到当前检测模块" />
ToolTip="加载流水线" />
</StackPanel>
</Grid>
</Border>
@@ -90,114 +103,157 @@
BorderThickness="0"
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}"
ItemsSource="{Binding PipelineNodes}"
KeyboardNavigation.TabNavigation="Continue"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="NodeRoot" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
x:Name="NodeContainer"
Margin="2"
Padding="2"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="3">
<Grid x:Name="NodeRoot" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Line
x:Name="TopLine"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Stroke="#5B9BD5"
StrokeThickness="2"
X1="0"
X2="0"
Y1="0"
Y2="10" />
<Line
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Stroke="#5B9BD5"
StrokeThickness="2"
X1="0"
X2="0"
Y1="0"
Y2="14" />
<Line
x:Name="TopLine"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Stroke="#5B9BD5"
StrokeThickness="2"
X1="0"
X2="0"
Y1="0"
Y2="10" />
<Line
x:Name="BottomLine"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Stroke="#5B9BD5"
StrokeThickness="2"
X1="0"
X2="0"
Y1="0"
Y2="14" />
<Border
Grid.Column="0"
Width="28"
Height="28"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#E8F0FE"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
CornerRadius="4">
<TextBlock
<Border
x:Name="IconBorder"
Grid.Column="0"
Width="28"
Height="28"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="13"
Text="{Binding IconPath}" />
</Border>
Background="#E8F0FE"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
CornerRadius="4">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="13"
Text="{Binding IconPath}" />
</Border>
<TextBlock
Grid.Column="1"
Margin="6,0,0,0"
VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="12"
Text="{Binding DisplayName}" />
<StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
<TextBlock
x:Name="NodeTitle"
FontFamily="Microsoft YaHei UI"
FontSize="12"
Text="{Binding DisplayName}" />
<TextBlock
x:Name="NodeState"
Margin="0,2,0,0"
FontFamily="Microsoft YaHei UI"
FontSize="10"
Foreground="#6E6E6E"
Text="已启用" />
</StackPanel>
<StackPanel
x:Name="NodeActions"
Grid.Column="2"
Margin="0,0,4,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="Collapsed">
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="上"
Cursor="Hand"
FontSize="10"
ToolTip="上移" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="下"
Cursor="Hand"
FontSize="10"
ToolTip="下移" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#E05050"
BorderThickness="1"
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="删"
Cursor="Hand"
FontSize="10"
ToolTip="删除" />
</StackPanel>
</Grid>
<StackPanel
x:Name="NodeActions"
Grid.Column="2"
Margin="0,0,4,0"
VerticalAlignment="Center"
Orientation="Horizontal"
Visibility="Collapsed">
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="上"
Cursor="Hand"
FontSize="10"
ToolTip="上移" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="下"
Cursor="Hand"
FontSize="10"
ToolTip="下移" />
<Button
Width="22"
Height="22"
Margin="1,0"
Background="Transparent"
BorderBrush="#E05050"
BorderThickness="1"
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}"
Content="删"
Cursor="Hand"
FontSize="10"
ToolTip="删除" />
</StackPanel>
</Grid>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Order}" Value="0">
<Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter TargetName="NodeContainer" Property="Background" Value="{StaticResource DisabledNodeBg}" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="0.78" />
<Setter TargetName="TopLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="BottomLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" />
<Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" />
<Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" />
<Setter TargetName="NodeState" Property="Text" Value="已停用" />
<Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" />
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True">
<Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" />
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsEnabled}" Value="False" />
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter TargetName="NodeContainer" Property="Background" Value="#E6EEF7" />
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" />
<Setter TargetName="NodeContainer" Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
</Trigger>
@@ -2,15 +2,26 @@ using Prism.Ioc;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.ViewModels;
namespace XplorePlane.Views
{
public partial class PipelineEditorView : UserControl
{
private const string PipelineNodeDragFormat = "PipelineNodeDrag";
private readonly ILoggerService _logger;
private Point _dragStartPoint;
private bool _isInternalDragging;
private bool _suppressClickToggle;
private PipelineNodeViewModel _draggedNode;
public PipelineEditorView()
{
InitializeComponent();
@@ -35,50 +46,249 @@ namespace XplorePlane.Views
}
}
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
DataContext?.GetType().Name);
PipelineListBox.AllowDrop = true;
PipelineListBox.Drop += OnOperatorDropped;
PipelineListBox.Focusable = true;
PipelineListBox.Drop -= OnListBoxDrop;
PipelineListBox.Drop += OnListBoxDrop;
PipelineListBox.DragOver -= OnDragOver;
PipelineListBox.DragOver += OnDragOver;
_logger?.Debug("PipelineEditorView 原生 Drop 目标已注册");
PipelineListBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
PipelineListBox.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
PipelineListBox.PreviewMouseMove -= OnPreviewMouseMove;
PipelineListBox.PreviewMouseMove += OnPreviewMouseMove;
PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown;
PipelineListBox.PreviewKeyDown += OnPreviewKeyDown;
}
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_dragStartPoint = e.GetPosition(PipelineListBox);
_isInternalDragging = false;
_draggedNode = FindNodeFromOriginalSource(e.OriginalSource);
if (_draggedNode != null)
{
PipelineListBox.SelectedItem = _draggedNode;
PipelineListBox.Focus();
}
}
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _draggedNode == null || IsInteractiveChild(e.OriginalSource))
return;
var position = e.GetPosition(PipelineListBox);
var delta = position - _dragStartPoint;
if (_isInternalDragging
|| (Math.Abs(delta.X) < SystemParameters.MinimumHorizontalDragDistance
&& Math.Abs(delta.Y) < SystemParameters.MinimumVerticalDragDistance))
{
return;
}
_isInternalDragging = true;
var data = new DataObject(PipelineNodeDragFormat, _draggedNode);
DragDrop.DoDragDrop(PipelineListBox, data, DragDropEffects.Move);
_suppressClickToggle = true;
}
private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
var vm = DataContext as IPipelineEditorHostViewModel;
var clickedNode = FindNodeFromOriginalSource(e.OriginalSource);
if (_isInternalDragging)
{
ResetDragState();
return;
}
if (_suppressClickToggle)
{
_suppressClickToggle = false;
ResetDragState();
return;
}
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
{
ResetDragState();
return;
}
PipelineListBox.SelectedItem = clickedNode;
PipelineListBox.Focus();
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
e.Handled = true;
ResetDragState();
}
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
return;
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
e.Handled = true;
}
private void OnDragOver(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
? DragDropEffects.Copy
: DragDropEffects.None;
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{
e.Effects = DragDropEffects.Copy;
}
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
{
e.Effects = DragDropEffects.Move;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true;
}
private void OnOperatorDropped(object sender, DragEventArgs e)
private void OnListBoxDrop(object sender, DragEventArgs e)
{
if (DataContext is not IPipelineEditorHostViewModel vm)
{
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel");
ResetDragState();
return;
}
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat);
return;
OnOperatorDropped(vm, e);
}
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
{
OnInternalNodeDropped(vm, e);
}
ResetDragState();
e.Handled = true;
}
private void OnOperatorDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
{
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
if (string.IsNullOrWhiteSpace(operatorKey))
{
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空");
_logger?.Warn("Drop 触发,但 OperatorKey 为空");
return;
}
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
operatorKey, vm.PipelineNodes.Count);
vm.AddOperatorCommand.Execute(operatorKey);
_logger?.Info("AddOperator 执行后节点数={Count}PipelineListBox.Items.Count={ItemsCount}",
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
e.Handled = true;
}
private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
{
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|| !vm.PipelineNodes.Contains(draggedNode))
{
return;
}
var oldIndex = vm.PipelineNodes.IndexOf(draggedNode);
var targetIndex = GetDropTargetIndex(e.GetPosition(PipelineListBox));
if (targetIndex < 0)
{
targetIndex = vm.PipelineNodes.Count - 1;
}
if (targetIndex >= oldIndex && targetIndex < vm.PipelineNodes.Count - 1)
{
var targetItem = GetItemAtPosition(e.GetPosition(PipelineListBox));
if (targetItem != null)
{
var itemBounds = VisualTreeHelper.GetDescendantBounds(targetItem);
var itemTopLeft = targetItem.TranslatePoint(new Point(0, 0), PipelineListBox);
var itemMidY = itemTopLeft.Y + (itemBounds.Height / 2);
if (e.GetPosition(PipelineListBox).Y > itemMidY)
{
targetIndex = Math.Min(targetIndex + 1, vm.PipelineNodes.Count - 1);
}
}
}
targetIndex = Math.Max(0, Math.Min(targetIndex, vm.PipelineNodes.Count - 1));
if (oldIndex == targetIndex)
{
return;
}
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs
{
OldIndex = oldIndex,
NewIndex = targetIndex
});
}
private int GetDropTargetIndex(Point position)
{
var item = GetItemAtPosition(position);
return item == null
? -1
: PipelineListBox.ItemContainerGenerator.IndexFromContainer(item);
}
private ListBoxItem GetItemAtPosition(Point position)
{
var element = PipelineListBox.InputHitTest(position) as DependencyObject;
while (element != null)
{
if (element is ListBoxItem item)
{
return item;
}
element = VisualTreeHelper.GetParent(element);
}
return null;
}
private PipelineNodeViewModel FindNodeFromOriginalSource(object originalSource)
{
var current = originalSource as DependencyObject;
while (current != null)
{
if (current is FrameworkElement element && element.DataContext is PipelineNodeViewModel node)
{
return node;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private static bool IsInteractiveChild(object originalSource)
{
var current = originalSource as DependencyObject;
while (current != null)
{
if (current is ButtonBase || current is TextBoxBase || current is Selector || current is ScrollBar)
{
return true;
}
current = VisualTreeHelper.GetParent(current);
}
return false;
}
private void ResetDragState()
{
_isInternalDragging = false;
_draggedNode = null;
}
}
}