11 Commits

Author SHA1 Message Date
zhengxuan.zhang ed4b1d8031 紧凑树形结构布局 2026-04-23 18:14:23 +08:00
zhengxuan.zhang ca86e8f7e8 调整CNC树形结构风格,简化界面 2026-04-23 18:09:53 +08:00
zhengxuan.zhang 2042c6c949 显示CNC文件名,取消节点的上、下移动 2026-04-23 17:50:11 +08:00
zhengxuan.zhang c9c0d93d8a 只有当流程图执行异常时,才显示强调色 2026-04-23 17:33:12 +08:00
zhengxuan.zhang 24e0489cde 显示xmp文件名;取消实时图像底部栏 2026-04-23 17:29:25 +08:00
zhengxuan.zhang 92ece60c01 删除主页面 NavigationPanelView 区域 2026-04-23 17:11:15 +08:00
zhengxuan.zhang 5ae5963353 当因为算子参数输入不合理,或者执行异常,要在状态栏显示 2026-04-23 17:04:41 +08:00
zhengxuan.zhang 338358a71c 算子调节参数控件的显示与选择的优化 2026-04-23 16:47:13 +08:00
zhengxuan.zhang f816cf4b8b XP 模块流水线 (*.xpm)|*.xpm 2026-04-23 16:28:53 +08:00
zhengxuan.zhang 9bdd67ffb7 删除原有的上 下,删 按钮方式的操作逻辑,改用拖动,和左键激活 2026-04-23 16:24:49 +08:00
zhengxuan.zhang 3f3820073f 配方编辑部分的交互逻辑 2026-04-23 16:12:55 +08:00
17 changed files with 933 additions and 300 deletions
+2 -2
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
namespace XplorePlane.Models namespace XplorePlane.Models
{ {
public class PipelineModel public class PipelineModel //流程图
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@@ -13,7 +13,7 @@ namespace XplorePlane.Models
public List<PipelineNodeModel> Nodes { get; set; } = new(); public List<PipelineNodeModel> Nodes { get; set; } = new();
} }
public class PipelineNodeModel public class PipelineNodeModel //节点
{ {
public Guid Id { get; set; } = Guid.NewGuid(); public Guid Id { get; set; } = Guid.NewGuid();
public string OperatorKey { get; set; } = string.Empty; public string OperatorKey { get; set; } = string.Empty;
@@ -37,11 +37,10 @@ namespace XplorePlane.Services
if (enabledNodes.Count == 0) if (enabledNodes.Count == 0)
return source; return source;
// 大图像预览缩放
var current = ScaleForPreview(source); var current = ScaleForPreview(source);
int total = enabledNodes.Count; var total = enabledNodes.Count;
for (int step = 0; step < total; step++) for (var step = 0; step < total; step++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
@@ -53,15 +52,14 @@ namespace XplorePlane.Services
if (invalidParameters.Count > 0) if (invalidParameters.Count > 0)
{ {
var invalidParameterText = string.Join("、", invalidParameters);
throw new PipelineExecutionException( throw new PipelineExecutionException(
$"算子 '{node.DisplayName}' 存在无效参数:{string.Join("", invalidParameters)}", $"算子 '{node.DisplayName}' 存在无效参数:{invalidParameterText}",
node.Order, node.Order,
node.OperatorKey); node.OperatorKey);
} }
var parameters = node.Parameters var parameters = node.Parameters.ToDictionary(p => p.Name, p => p.Value);
.Where(p => p.IsValueValid)
.ToDictionary(p => p.Name, p => p.Value);
try try
{ {
@@ -69,9 +67,12 @@ namespace XplorePlane.Services
current, node.OperatorKey, parameters, null, cancellationToken); current, node.OperatorKey, parameters, null, cancellationToken);
if (current == null) if (current == null)
{
throw new PipelineExecutionException( throw new PipelineExecutionException(
$"算子 '{node.OperatorKey}' 返回了空图像", $"算子 '{node.DisplayName}' 返回了空图像",
node.Order, node.OperatorKey); node.Order,
node.OperatorKey);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -85,7 +86,9 @@ namespace XplorePlane.Services
{ {
throw new PipelineExecutionException( throw new PipelineExecutionException(
$"算子 '{node.DisplayName}' 执行失败:{ex.Message}", $"算子 '{node.DisplayName}' 执行失败:{ex.Message}",
node.Order, node.OperatorKey, ex); node.Order,
node.OperatorKey,
ex);
} }
progress?.Report(new PipelineProgress(step + 1, total, node.DisplayName)); progress?.Report(new PipelineProgress(step + 1, total, node.DisplayName));
@@ -102,7 +105,7 @@ namespace XplorePlane.Services
if (source.PixelWidth <= UhdThreshold && source.PixelHeight <= UhdThreshold) if (source.PixelWidth <= UhdThreshold && source.PixelHeight <= UhdThreshold)
return source; return source;
double scale = (double)PreviewMaxHeight / source.PixelHeight; var scale = (double)PreviewMaxHeight / source.PixelHeight;
if (source.PixelWidth * scale > UhdThreshold) if (source.PixelWidth * scale > UhdThreshold)
scale = (double)UhdThreshold / source.PixelWidth; scale = (double)UhdThreshold / source.PixelWidth;
@@ -74,7 +74,7 @@ namespace XplorePlane.Services
if (!Directory.Exists(directory)) if (!Directory.Exists(directory))
return Array.Empty<PipelineModel>(); return Array.Empty<PipelineModel>();
var files = Directory.GetFiles(directory, "*.imw"); var files = Directory.GetFiles(directory, "*.xpm");
var results = new List<PipelineModel>(); var results = new List<PipelineModel>();
foreach (var file in files) foreach (var file in files)
@@ -108,4 +108,4 @@ namespace XplorePlane.Services
throw new UnauthorizedAccessException($"不允许路径遍历:{directory}"); throw new UnauthorizedAccessException($"不允许路径遍历:{directory}");
} }
} }
} }
@@ -30,9 +30,11 @@ namespace XplorePlane.ViewModels.Cnc
private CncProgram _currentProgram; private CncProgram _currentProgram;
private ObservableCollection<CncNodeViewModel> _nodes; private ObservableCollection<CncNodeViewModel> _nodes;
private ObservableCollection<CncNodeViewModel> _treeNodes; private ObservableCollection<CncNodeViewModel> _treeNodes;
private ObservableCollection<CncProgramTreeRootViewModel> _programTreeRoots;
private CncNodeViewModel _selectedNode; private CncNodeViewModel _selectedNode;
private bool _isModified; private bool _isModified;
private string _programName; private string _programName;
private string _programDisplayName = "新建检测程序.xp";
private Guid? _preferredSelectedNodeId; private Guid? _preferredSelectedNodeId;
public CncEditorViewModel( public CncEditorViewModel(
@@ -48,6 +50,10 @@ namespace XplorePlane.ViewModels.Cnc
_nodes = new ObservableCollection<CncNodeViewModel>(); _nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>(); _treeNodes = new ObservableCollection<CncNodeViewModel>();
_programTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
{
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
};
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint)); InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage)); InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
@@ -84,6 +90,12 @@ namespace XplorePlane.ViewModels.Cnc
private set => SetProperty(ref _treeNodes, value); private set => SetProperty(ref _treeNodes, value);
} }
public ObservableCollection<CncProgramTreeRootViewModel> ProgramTreeRoots
{
get => _programTreeRoots;
private set => SetProperty(ref _programTreeRoots, value);
}
public CncNodeViewModel SelectedNode public CncNodeViewModel SelectedNode
{ {
get => _selectedNode; get => _selectedNode;
@@ -110,6 +122,16 @@ namespace XplorePlane.ViewModels.Cnc
set => SetProperty(ref _programName, value); set => SetProperty(ref _programName, value);
} }
public string ProgramDisplayName
{
get => _programDisplayName;
private set
{
if (SetProperty(ref _programDisplayName, value) && ProgramTreeRoots?.Count > 0)
ProgramTreeRoots[0].DisplayName = value;
}
}
public DelegateCommand InsertReferencePointCommand { get; } public DelegateCommand InsertReferencePointCommand { get; }
public DelegateCommand InsertSaveNodeWithImageCommand { get; } public DelegateCommand InsertSaveNodeWithImageCommand { get; }
public DelegateCommand InsertSaveNodeCommand { get; } public DelegateCommand InsertSaveNodeCommand { get; }
@@ -256,6 +278,7 @@ namespace XplorePlane.ViewModels.Cnc
return; return;
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName); await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false; IsModified = false;
} }
catch (Exception ex) catch (Exception ex)
@@ -280,6 +303,7 @@ namespace XplorePlane.ViewModels.Cnc
_currentProgram = await _cncProgramService.LoadAsync(dlg.FileName); _currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false; IsModified = false;
RefreshNodes(); RefreshNodes();
} }
@@ -291,9 +315,10 @@ namespace XplorePlane.ViewModels.Cnc
private void ExecuteNewProgram() private void ExecuteNewProgram()
{ {
var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName; const string name = "新建检测程序";
_currentProgram = _cncProgramService.CreateProgram(name); _currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name);
IsModified = false; IsModified = false;
RefreshNodes(); RefreshNodes();
} }
@@ -417,6 +442,10 @@ namespace XplorePlane.ViewModels.Cnc
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes); Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes); TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
ProgramTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
{
new CncProgramTreeRootViewModel(ProgramDisplayName, TreeNodes)
};
SelectedNode = selectedId.HasValue SelectedNode = selectedId.HasValue
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault() ? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
@@ -622,5 +651,11 @@ namespace XplorePlane.ViewModels.Cnc
.GetEvent<CncProgramChangedEvent>() .GetEvent<CncProgramChangedEvent>()
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified)); .Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
} }
private static string FormatProgramDisplayName(string programName)
{
var name = string.IsNullOrWhiteSpace(programName) ? "新建检测程序" : programName;
return name.EndsWith(".xp", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.xp";
}
} }
} }
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
using System; using System;
@@ -26,6 +26,7 @@ namespace XplorePlane.ViewModels.Cnc
private CncNodeViewModel _activeModuleNode; private CncNodeViewModel _activeModuleNode;
private PipelineNodeViewModel _selectedNode; private PipelineNodeViewModel _selectedNode;
private string _statusMessage = "请选择检测模块以编辑其流水线。"; private string _statusMessage = "请选择检测模块以编辑其流水线。";
private string _pipelineFileDisplayName = "未命名模块.xpm";
private string _currentFilePath; private string _currentFilePath;
private bool _isSynchronizing; private bool _isSynchronizing;
@@ -44,6 +45,8 @@ namespace XplorePlane.ViewModels.Cnc
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule); AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator); RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp); MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown); MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -69,6 +72,14 @@ namespace XplorePlane.ViewModels.Cnc
private set => SetProperty(ref _statusMessage, value); private set => SetProperty(ref _statusMessage, value);
} }
public bool IsStatusError => false;
public string PipelineFileDisplayName
{
get => _pipelineFileDisplayName;
private set => SetProperty(ref _pipelineFileDisplayName, value);
}
public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true; public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true;
public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed; public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed;
@@ -79,6 +90,10 @@ namespace XplorePlane.ViewModels.Cnc
public ICommand RemoveOperatorCommand { get; } public ICommand RemoveOperatorCommand { get; }
public ICommand ReorderOperatorCommand { get; }
public ICommand ToggleOperatorEnabledCommand { get; }
public ICommand MoveNodeUpCommand { get; } public ICommand MoveNodeUpCommand { get; }
public ICommand MoveNodeDownCommand { get; } public ICommand MoveNodeDownCommand { get; }
@@ -107,6 +122,7 @@ namespace XplorePlane.ViewModels.Cnc
_activeModuleNode = null; _activeModuleNode = null;
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
PipelineFileDisplayName = "未命名模块.xpm";
StatusMessage = "请选择检测模块以编辑其流水线。"; StatusMessage = "请选择检测模块以编辑其流水线。";
RaiseModuleVisibilityChanged(); RaiseModuleVisibilityChanged();
RaiseCommandCanExecuteChanged(); RaiseCommandCanExecuteChanged();
@@ -114,6 +130,7 @@ namespace XplorePlane.ViewModels.Cnc
} }
_activeModuleNode = selected; _activeModuleNode = selected;
_currentFilePath = null;
LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel
{ {
Name = _activeModuleNode.Name Name = _activeModuleNode.Name
@@ -152,13 +169,10 @@ namespace XplorePlane.ViewModels.Cnc
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node)) if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
return; return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
SelectNeighborAfterRemoval(removedIndex);
if (SelectedNode == node)
{
SelectedNode = PipelineNodes.LastOrDefault();
}
PersistActiveModule($"已移除算子:{node.DisplayName}"); PersistActiveModule($"已移除算子:{node.DisplayName}");
} }
@@ -177,6 +191,26 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已上移算子:{node.DisplayName}"); 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) private void MoveNodeDown(PipelineNodeViewModel node)
{ {
if (!HasActiveModule || node == null) if (!HasActiveModule || node == null)
@@ -191,6 +225,18 @@ namespace XplorePlane.ViewModels.Cnc
PersistActiveModule($"已下移算子:{node.DisplayName}"); 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() private void NewPipeline()
{ {
if (!HasActiveModule) if (!HasActiveModule)
@@ -199,6 +245,7 @@ namespace XplorePlane.ViewModels.Cnc
PipelineNodes.Clear(); PipelineNodes.Clear();
SelectedNode = null; SelectedNode = null;
_currentFilePath = null; _currentFilePath = null;
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
PersistActiveModule("已为当前检测模块新建空流水线。"); PersistActiveModule("已为当前检测模块新建空流水线。");
} }
@@ -217,7 +264,9 @@ namespace XplorePlane.ViewModels.Cnc
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
{ {
Filter = "图像处理流水线 (*.imw)|*.imw", Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
AddExtension = true,
FileName = GetActivePipelineName() FileName = GetActivePipelineName()
}; };
@@ -227,6 +276,7 @@ namespace XplorePlane.ViewModels.Cnc
var model = BuildPipelineModel(); var model = BuildPipelineModel();
await _persistenceService.SaveAsync(model, dialog.FileName); await _persistenceService.SaveAsync(model, dialog.FileName);
_currentFilePath = dialog.FileName; _currentFilePath = dialog.FileName;
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
StatusMessage = $"已导出模块流水线:{Path.GetFileName(dialog.FileName)}"; StatusMessage = $"已导出模块流水线:{Path.GetFileName(dialog.FileName)}";
} }
@@ -237,7 +287,8 @@ namespace XplorePlane.ViewModels.Cnc
var dialog = new OpenFileDialog var dialog = new OpenFileDialog
{ {
Filter = "图像处理流水线 (*.imw)|*.imw" Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm"
}; };
if (dialog.ShowDialog() != true) if (dialog.ShowDialog() != true)
@@ -245,6 +296,7 @@ namespace XplorePlane.ViewModels.Cnc
var model = await _persistenceService.LoadAsync(dialog.FileName); var model = await _persistenceService.LoadAsync(dialog.FileName);
_currentFilePath = dialog.FileName; _currentFilePath = dialog.FileName;
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
LoadPipelineModel(model); LoadPipelineModel(model);
PersistActiveModule($"已加载模块流水线:{model.Name}"); PersistActiveModule($"已加载模块流水线:{model.Name}");
} }
@@ -276,6 +328,8 @@ namespace XplorePlane.ViewModels.Cnc
} }
SelectedNode = PipelineNodes.FirstOrDefault(); SelectedNode = PipelineNodes.FirstOrDefault();
if (string.IsNullOrEmpty(_currentFilePath))
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
StatusMessage = HasActiveModule StatusMessage = HasActiveModule
? $"正在编辑检测模块:{_activeModuleNode.Name}" ? $"正在编辑检测模块:{_activeModuleNode.Name}"
: "请选择检测模块以编辑其流水线。"; : "请选择检测模块以编辑其流水线。";
@@ -351,6 +405,21 @@ namespace XplorePlane.ViewModels.Cnc
: _activeModuleNode.Pipeline.Name; : _activeModuleNode.Pipeline.Name;
} }
private string GetActivePipelineFileDisplayName()
{
var pipelineName = GetActivePipelineName();
return pipelineName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase)
? pipelineName
: $"{pipelineName}.xpm";
}
private static string FormatPipelinePath(string filePath)
{
return string.IsNullOrWhiteSpace(filePath)
? "未命名模块.xpm"
: Path.GetFullPath(filePath).Replace('\\', '/');
}
private void RenumberNodes() private void RenumberNodes()
{ {
for (var i = 0; i < PipelineNodes.Count; i++) for (var i = 0; i < PipelineNodes.Count; i++)
@@ -359,6 +428,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() private void RaiseModuleVisibilityChanged()
{ {
RaisePropertyChanged(nameof(HasActiveModule)); RaisePropertyChanged(nameof(HasActiveModule));
@@ -0,0 +1,31 @@
using Prism.Mvvm;
using System.Collections.ObjectModel;
namespace XplorePlane.ViewModels.Cnc
{
public class CncProgramTreeRootViewModel : BindableBase
{
private string _displayName;
private bool _isExpanded = true;
public CncProgramTreeRootViewModel(string displayName, ObservableCollection<CncNodeViewModel> children)
{
_displayName = displayName;
Children = children;
}
public string DisplayName
{
get => _displayName;
set => SetProperty(ref _displayName, value);
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public ObservableCollection<CncNodeViewModel> Children { get; }
}
}
@@ -11,10 +11,18 @@ namespace XplorePlane.ViewModels
string StatusMessage { get; } string StatusMessage { get; }
bool IsStatusError { get; }
string PipelineFileDisplayName { get; }
ICommand AddOperatorCommand { get; } ICommand AddOperatorCommand { get; }
ICommand RemoveOperatorCommand { get; } ICommand RemoveOperatorCommand { get; }
ICommand ReorderOperatorCommand { get; }
ICommand ToggleOperatorEnabledCommand { get; }
ICommand MoveNodeUpCommand { get; } ICommand MoveNodeUpCommand { get; }
ICommand MoveNodeDownCommand { get; } ICommand MoveNodeDownCommand { get; }
@@ -1,4 +1,4 @@
using Microsoft.Win32; using Microsoft.Win32;
using Prism.Events; using Prism.Events;
using Prism.Commands; using Prism.Commands;
using Prism.Mvvm; using Prism.Mvvm;
@@ -21,6 +21,7 @@ namespace XplorePlane.ViewModels
{ {
private const int MaxPipelineLength = 20; private const int MaxPipelineLength = 20;
private const int DebounceDelayMs = 300; private const int DebounceDelayMs = 300;
private const string DefaultPipelineFileDisplayName = "未命名模块.xpm";
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
private readonly IPipelineExecutionService _executionService; private readonly IPipelineExecutionService _executionService;
@@ -34,7 +35,9 @@ namespace XplorePlane.ViewModels
private string _pipelineName = "新建流水线"; private string _pipelineName = "新建流水线";
private string _selectedDevice = string.Empty; private string _selectedDevice = string.Empty;
private bool _isExecuting; private bool _isExecuting;
private bool _isStatusError;
private string _statusMessage = string.Empty; private string _statusMessage = string.Empty;
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
private string _currentFilePath; private string _currentFilePath;
private CancellationTokenSource _executionCts; private CancellationTokenSource _executionCts;
@@ -59,6 +62,7 @@ namespace XplorePlane.ViewModels
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator); AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator); RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator); ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null); ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting); CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
NewPipelineCommand = new DelegateCommand(NewPipeline); NewPipelineCommand = new DelegateCommand(NewPipeline);
@@ -147,11 +151,24 @@ namespace XplorePlane.ViewModels
set => SetProperty(ref _statusMessage, value); set => SetProperty(ref _statusMessage, value);
} }
public bool IsStatusError
{
get => _isStatusError;
private set => SetProperty(ref _isStatusError, value);
}
public string PipelineFileDisplayName
{
get => _pipelineFileDisplayName;
private set => SetProperty(ref _pipelineFileDisplayName, value);
}
// ── Commands ────────────────────────────────────────────────── // ── Commands ──────────────────────────────────────────────────
public DelegateCommand<string> AddOperatorCommand { get; } public DelegateCommand<string> AddOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; } public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; } public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
public DelegateCommand ExecutePipelineCommand { get; } public DelegateCommand ExecutePipelineCommand { get; }
public DelegateCommand CancelExecutionCommand { get; } public DelegateCommand CancelExecutionCommand { get; }
public DelegateCommand NewPipelineCommand { get; } public DelegateCommand NewPipelineCommand { get; }
@@ -168,6 +185,8 @@ namespace XplorePlane.ViewModels
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand; ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand; ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand; ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand; ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand; ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
@@ -186,7 +205,7 @@ namespace XplorePlane.ViewModels
if (string.IsNullOrWhiteSpace(operatorKey)) if (string.IsNullOrWhiteSpace(operatorKey))
{ {
StatusMessage = "算子键不能为空"; SetInfoStatus("算子键不能为空");
_logger.Warn("AddOperator 失败:operatorKey 为空"); _logger.Warn("AddOperator 失败:operatorKey 为空");
return; return;
} }
@@ -197,14 +216,14 @@ namespace XplorePlane.ViewModels
if (!available.Contains(operatorKey)) if (!available.Contains(operatorKey))
{ {
StatusMessage = $"算子 '{operatorKey}' 未注册"; SetInfoStatus($"算子 '{operatorKey}' 未注册");
_logger.Warn("AddOperator 失败:算子 {Key} 未注册", operatorKey); _logger.Warn("AddOperator 失败:算子 {Key} 未注册", operatorKey);
return; return;
} }
if (PipelineNodes.Count >= MaxPipelineLength) if (PipelineNodes.Count >= MaxPipelineLength)
{ {
StatusMessage = $"流水线节点数已达上限({MaxPipelineLength}"; SetInfoStatus($"流水线节点数已达上限({MaxPipelineLength}");
_logger.Warn("AddOperator 失败:节点数已达上限 {Max}", MaxPipelineLength); _logger.Warn("AddOperator 失败:节点数已达上限 {Max}", MaxPipelineLength);
return; return;
} }
@@ -217,9 +236,10 @@ namespace XplorePlane.ViewModels
}; };
LoadNodeParameters(node); LoadNodeParameters(node);
PipelineNodes.Add(node); PipelineNodes.Add(node);
SelectedNode = node;
_logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}", _logger.Info("节点已添加到 PipelineNodes{Key} ({DisplayName}),当前节点数={Count}",
operatorKey, displayName, PipelineNodes.Count); operatorKey, displayName, PipelineNodes.Count);
StatusMessage = $"已添加算子:{displayName}"; SetInfoStatus($"已添加算子:{displayName}");
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -227,13 +247,12 @@ namespace XplorePlane.ViewModels
{ {
if (node == null || !PipelineNodes.Contains(node)) return; if (node == null || !PipelineNodes.Contains(node)) return;
var removedIndex = PipelineNodes.IndexOf(node);
PipelineNodes.Remove(node); PipelineNodes.Remove(node);
RenumberNodes(); RenumberNodes();
SelectNeighborAfterRemoval(removedIndex);
if (SelectedNode == node) SetInfoStatus($"已移除算子:{node.DisplayName}");
SelectedNode = null;
StatusMessage = $"已移除算子:{node.DisplayName}";
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -271,6 +290,20 @@ namespace XplorePlane.ViewModels
PipelineNodes.RemoveAt(oldIndex); PipelineNodes.RemoveAt(oldIndex);
PipelineNodes.Insert(newIndex, node); PipelineNodes.Insert(newIndex, node);
RenumberNodes(); RenumberNodes();
SelectedNode = node;
SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
TriggerDebouncedExecution();
}
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
{
if (node == null || !PipelineNodes.Contains(node)) return;
node.IsEnabled = !node.IsEnabled;
SelectedNode = node;
SetInfoStatus(node.IsEnabled
? $"已启用算子:{node.DisplayName}"
: $"已停用算子:{node.DisplayName}");
TriggerDebouncedExecution(); TriggerDebouncedExecution();
} }
@@ -280,6 +313,20 @@ namespace XplorePlane.ViewModels
PipelineNodes[i].Order = i; 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) private void LoadNodeParameters(PipelineNodeViewModel node)
{ {
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey); var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
@@ -297,7 +344,12 @@ namespace XplorePlane.ViewModels
vm.PropertyChanged += (_, e) => vm.PropertyChanged += (_, e) =>
{ {
if (e.PropertyName == nameof(ProcessorParameterVM.Value)) if (e.PropertyName == nameof(ProcessorParameterVM.Value))
{
if (TryReportInvalidParameters())
return;
TriggerDebouncedExecution(); TriggerDebouncedExecution();
}
}; };
node.Parameters.Add(vm); node.Parameters.Add(vm);
} }
@@ -307,36 +359,39 @@ namespace XplorePlane.ViewModels
{ {
if (SourceImage == null || IsExecuting) return; if (SourceImage == null || IsExecuting) return;
if (TryReportInvalidParameters())
return;
_executionCts?.Cancel(); _executionCts?.Cancel();
_executionCts = new CancellationTokenSource(); _executionCts = new CancellationTokenSource();
var token = _executionCts.Token; var token = _executionCts.Token;
IsExecuting = true; IsExecuting = true;
StatusMessage = "正在执行流水线..."; SetInfoStatus("正在执行流水线...");
try try
{ {
var progress = new Progress<PipelineProgress>(p => var progress = new Progress<PipelineProgress>(p =>
StatusMessage = $"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"); SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"));
var result = await _executionService.ExecutePipelineAsync( var result = await _executionService.ExecutePipelineAsync(
PipelineNodes, SourceImage, progress, token); PipelineNodes, SourceImage, progress, token);
PreviewImage = result; PreviewImage = result;
StatusMessage = "流水线执行完成"; SetInfoStatus("流水线执行完成");
PublishPipelinePreviewUpdated(result, StatusMessage); PublishPipelinePreviewUpdated(result, StatusMessage);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
StatusMessage = "流水线执行已取消"; SetInfoStatus("流水线执行已取消");
} }
catch (PipelineExecutionException ex) catch (PipelineExecutionException ex)
{ {
StatusMessage = $"节点 '{ex.FailedOperatorKey}' 执行失败:{ex.Message}"; SetErrorStatus($"执行失败:{ex.Message}");
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusMessage = $"执行错误:{ex.Message}"; SetErrorStatus($"执行错误:{ex.Message}");
} }
finally finally
{ {
@@ -344,6 +399,36 @@ namespace XplorePlane.ViewModels
} }
} }
private bool TryReportInvalidParameters()
{
var firstInvalidNode = PipelineNodes
.Where(n => n.IsEnabled)
.OrderBy(n => n.Order)
.FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid));
if (firstInvalidNode == null)
return false;
var invalidNames = firstInvalidNode.Parameters
.Where(p => !p.IsValueValid)
.Select(p => p.DisplayName);
SetErrorStatus($"参数错误:算子 '{firstInvalidNode.DisplayName}' 的 {string.Join("", invalidNames)} 输入不合理,请修正后重试。");
return true;
}
private void SetInfoStatus(string message)
{
IsStatusError = false;
StatusMessage = message;
}
private void SetErrorStatus(string message)
{
IsStatusError = true;
StatusMessage = message;
PublishPipelinePreviewUpdated(PreviewImage ?? SourceImage, message);
}
private void LoadImage() private void LoadImage()
{ {
var dialog = new OpenFileDialog var dialog = new OpenFileDialog
@@ -361,7 +446,7 @@ namespace XplorePlane.ViewModels
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusMessage = $"加载图像失败:{ex.Message}"; SetErrorStatus($"加载图像失败:{ex.Message}");
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName); _logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
} }
} }
@@ -380,7 +465,7 @@ namespace XplorePlane.ViewModels
SourceImage = bitmap; SourceImage = bitmap;
PreviewImage = bitmap; PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
PublishManualImageLoaded(bitmap, filePath); PublishManualImageLoaded(bitmap, filePath);
} }
@@ -391,7 +476,7 @@ namespace XplorePlane.ViewModels
SourceImage = bitmap; SourceImage = bitmap;
PreviewImage = bitmap; PreviewImage = bitmap;
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}"; SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
PublishManualImageLoaded(bitmap, filePath); PublishManualImageLoaded(bitmap, filePath);
if (runPipeline) if (runPipeline)
@@ -419,7 +504,7 @@ namespace XplorePlane.ViewModels
SourceImage = payload.Image; SourceImage = payload.Image;
PreviewImage = payload.Image; PreviewImage = payload.Image;
StatusMessage = $"已加载图像:{payload.FileName}"; SetInfoStatus($"已加载图像:{payload.FileName}");
} }
private void CancelExecution() private void CancelExecution()
@@ -449,14 +534,15 @@ namespace XplorePlane.ViewModels
PipelineName = "新建流水线"; PipelineName = "新建流水线";
PreviewImage = null; PreviewImage = null;
_currentFilePath = null; _currentFilePath = null;
StatusMessage = "已新建流水线"; PipelineFileDisplayName = DefaultPipelineFileDisplayName;
SetInfoStatus("已新建流水线");
} }
private async Task SavePipelineAsync() private async Task SavePipelineAsync()
{ {
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100) if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
{ {
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符"; SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
return; return;
} }
@@ -473,13 +559,15 @@ namespace XplorePlane.ViewModels
{ {
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100) if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
{ {
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符"; SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
return; return;
} }
var dialog = new SaveFileDialog var dialog = new SaveFileDialog
{ {
Filter = "图像处理流水线 (*.imw)|*.imw", Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
AddExtension = true,
FileName = PipelineName, FileName = PipelineName,
InitialDirectory = GetPipelineDirectory() InitialDirectory = GetPipelineDirectory()
}; };
@@ -497,11 +585,12 @@ namespace XplorePlane.ViewModels
{ {
var model = BuildPipelineModel(); var model = BuildPipelineModel();
await _persistenceService.SaveAsync(model, filePath); await _persistenceService.SaveAsync(model, filePath);
StatusMessage = $"流水线已保存:{Path.GetFileName(filePath)}"; PipelineFileDisplayName = FormatPipelinePath(filePath);
SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}");
} }
catch (IOException ex) catch (IOException ex)
{ {
StatusMessage = $"保存失败:{ex.Message}"; SetErrorStatus($"保存失败:{ex.Message}");
} }
} }
@@ -515,11 +604,11 @@ namespace XplorePlane.ViewModels
File.Delete(_currentFilePath); File.Delete(_currentFilePath);
NewPipeline(); NewPipeline();
StatusMessage = "流水线已删除"; SetInfoStatus("流水线已删除");
} }
catch (IOException ex) catch (IOException ex)
{ {
StatusMessage = $"删除失败:{ex.Message}"; SetErrorStatus($"删除失败:{ex.Message}");
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -528,7 +617,8 @@ namespace XplorePlane.ViewModels
{ {
var dialog = new OpenFileDialog var dialog = new OpenFileDialog
{ {
Filter = "图像处理流水线 (*.imw)|*.imw", Filter = "XP 模块流水线 (*.xpm)|*.xpm",
DefaultExt = ".xpm",
InitialDirectory = GetPipelineDirectory() InitialDirectory = GetPipelineDirectory()
}; };
@@ -544,6 +634,7 @@ namespace XplorePlane.ViewModels
PipelineName = model.Name; PipelineName = model.Name;
SelectedDevice = model.DeviceId; SelectedDevice = model.DeviceId;
_currentFilePath = dialog.FileName; _currentFilePath = dialog.FileName;
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
foreach (var nodeModel in model.Nodes) foreach (var nodeModel in model.Nodes)
{ {
@@ -568,12 +659,12 @@ namespace XplorePlane.ViewModels
} }
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count); _logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
StatusMessage = $"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)"; SetInfoStatus($"已加载流水线:{model.Name}{PipelineNodes.Count} 个节点)");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.Warn("加载流水线失败:{Error}", ex.Message); _logger.Warn("加载流水线失败:{Error}", ex.Message);
StatusMessage = $"加载失败:{ex.Message}"; SetErrorStatus($"加载失败:{ex.Message}");
} }
} }
@@ -622,5 +713,12 @@ namespace XplorePlane.ViewModels
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
return dir; return dir;
} }
private static string FormatPipelinePath(string filePath)
{
return string.IsNullOrWhiteSpace(filePath)
? DefaultPipelineFileDisplayName
: Path.GetFullPath(filePath).Replace('\\', '/');
}
} }
} }
@@ -19,6 +19,7 @@ namespace XplorePlane.ViewModels
MinValue = parameter.MinValue; MinValue = parameter.MinValue;
MaxValue = parameter.MaxValue; MaxValue = parameter.MaxValue;
Options = parameter.Options; Options = parameter.Options;
IsVisible = parameter.IsVisible;
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
{ {
"int32" or "int" => "int", "int32" or "int" => "int",
@@ -34,7 +35,11 @@ namespace XplorePlane.ViewModels
public object MinValue { get; } public object MinValue { get; }
public object MaxValue { get; } public object MaxValue { get; }
public string[]? Options { get; } public string[]? Options { get; }
public bool IsVisible { get; }
public string ParameterType { get; } public string ParameterType { get; }
public bool HasOptions => Options is { Length: > 0 };
public bool IsBool => ParameterType == "bool";
public bool IsTextInput => !IsBool && !HasOptions;
public bool IsValueValid public bool IsValueValid
{ {
@@ -48,8 +53,40 @@ namespace XplorePlane.ViewModels
set set
{ {
var normalizedValue = NormalizeValue(value); var normalizedValue = NormalizeValue(value);
if (SetProperty(ref _value, normalizedValue)) if (!Equals(_value, normalizedValue))
{
_value = normalizedValue;
ValidateValue(normalizedValue); ValidateValue(normalizedValue);
RaisePropertyChanged(nameof(Value));
RaisePropertyChanged(nameof(BoolValue));
RaisePropertyChanged(nameof(SelectedOption));
}
}
}
public bool BoolValue
{
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
set
{
if (ParameterType == "bool")
{
Value = value;
}
}
}
public string SelectedOption
{
get => HasOptions
? Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty
: string.Empty;
set
{
if (HasOptions)
{
Value = value;
}
} }
} }
+99 -69
View File
@@ -24,7 +24,7 @@
<SolidColorBrush x:Key="TreeParentBg" Color="#F6F8FB" /> <SolidColorBrush x:Key="TreeParentBg" Color="#F6F8FB" />
<SolidColorBrush x:Key="TreeChildBg" Color="#FBFCFE" /> <SolidColorBrush x:Key="TreeChildBg" Color="#FBFCFE" />
<SolidColorBrush x:Key="TreeAccentBrush" Color="#2E6FA3" /> <SolidColorBrush x:Key="TreeAccentBrush" Color="#2E6FA3" />
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#B9CDE0" /> <SolidColorBrush x:Key="TreeChildLineBrush" Color="#C7D4DF" />
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem"> <Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
@@ -70,6 +70,19 @@
<Setter Property="FontFamily" Value="{StaticResource UiFont}" /> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
</Style> </Style>
<Style x:Key="TreeToolbarButton" TargetType="Button">
<Setter Property="Height" Value="24" />
<Setter Property="MinWidth" Value="42" />
<Setter Property="Margin" Value="0,0,4,0" />
<Setter Property="Padding" Value="8,0" />
<Setter Property="Background" Value="#F8F8F8" />
<Setter Property="BorderBrush" Value="#CFCFCF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
</Style>
</UserControl.Resources> </UserControl.Resources>
<Border <Border
@@ -97,37 +110,81 @@
<Border <Border
Grid.Row="0" Grid.Row="0"
Padding="10,8" Padding="6,5"
Background="{StaticResource HeaderBg}" Background="{StaticResource HeaderBg}"
BorderBrush="{StaticResource SeparatorBrush}" BorderBrush="{StaticResource SeparatorBrush}"
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<StackPanel> <WrapPanel>
<TextBlock <Button
FontFamily="{StaticResource UiFont}" Command="{Binding NewProgramCommand}"
FontSize="13" Content="新建"
FontWeight="SemiBold" Style="{StaticResource TreeToolbarButton}" />
Text="{Binding ProgramName, TargetNullValue=CNC编辑}" <Button
TextWrapping="Wrap" /> Command="{Binding SaveProgramCommand}"
<TextBlock Content="保存"
Margin="0,3,0,0" Style="{StaticResource TreeToolbarButton}" />
FontFamily="{StaticResource UiFont}" <Button
FontSize="10" Command="{Binding LoadProgramCommand}"
Foreground="#666666" Content="加载"
Text="保存位置节点下会自动显示检测标记和检测模块,清晰表达当前检测位置与检测内容的父子关系。" Style="{StaticResource TreeToolbarButton}" />
TextWrapping="Wrap" /> <Button
</StackPanel> Command="{Binding ExportCsvCommand}"
Content="导出"
Style="{StaticResource TreeToolbarButton}" />
</WrapPanel>
</Border> </Border>
<TreeView <TreeView
x:Name="CncTreeView" x:Name="CncTreeView"
Grid.Row="1" Grid.Row="1"
Padding="4,6" Padding="3,5"
Background="Transparent" Background="Transparent"
BorderThickness="0" BorderThickness="0"
ItemsSource="{Binding TreeNodes}" ItemsSource="{Binding ProgramTreeRoots}"
PreviewKeyDown="CncTreeView_PreviewKeyDown"
SelectedItemChanged="CncTreeView_SelectedItemChanged"> SelectedItemChanged="CncTreeView_SelectedItemChanged">
<TreeView.Resources> <TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}"> <HierarchicalDataTemplate
DataType="{x:Type vm:CncProgramTreeRootViewModel}"
ItemContainerStyle="{StaticResource TreeItemStyle}"
ItemsSource="{Binding Children}">
<Border
x:Name="ProgramRootCard"
Margin="0,1,0,3"
Padding="0,2"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="4">
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="1,0,4,0"
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="12"
Foreground="#2B8A3E"
Text="◆" />
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="12"
FontWeight="SemiBold"
Text="{Binding DisplayName}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
<Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" />
<Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" />
</DataTrigger>
</DataTemplate.Triggers>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type vm:CncNodeViewModel}"
ItemContainerStyle="{StaticResource TreeItemStyle}"
ItemsSource="{Binding Children}">
<Border <Border
x:Name="NodeCard" x:Name="NodeCard"
Margin="0,1,0,1" Margin="0,1,0,1"
@@ -136,10 +193,10 @@
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="1" BorderThickness="1"
CornerRadius="4"> CornerRadius="4">
<Grid x:Name="NodeRoot" MinHeight="28"> <Grid x:Name="NodeRoot" MinHeight="23">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="18" /> <ColumnDefinition Width="15" />
<ColumnDefinition Width="24" /> <ColumnDefinition Width="20" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -147,33 +204,31 @@
<Grid Grid.Column="0"> <Grid Grid.Column="0">
<Border <Border
x:Name="ChildStem" x:Name="ChildStem"
Width="2" Width="1"
Margin="8,0,0,0" Margin="7,0,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Background="{StaticResource TreeChildLineBrush}" Background="{StaticResource TreeChildLineBrush}" />
Visibility="Collapsed" />
<Border <Border
x:Name="ChildBranch" x:Name="ChildBranch"
Width="10" Width="8"
Height="2" Height="1"
Margin="8,0,0,0" Margin="7,0,0,0"
HorizontalAlignment="Left" HorizontalAlignment="Left"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="{StaticResource TreeChildLineBrush}" Background="{StaticResource TreeChildLineBrush}" />
Visibility="Collapsed" />
</Grid> </Grid>
<Border <Border
Grid.Column="1" Grid.Column="1"
Width="18" Width="16"
Height="18" Height="16"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Background="Transparent" Background="Transparent"
CornerRadius="4"> CornerRadius="4">
<Image <Image
Width="14" Width="13"
Height="14" Height="13"
Source="{Binding Icon}" Source="{Binding Icon}"
Stretch="Uniform" /> Stretch="Uniform" />
</Border> </Border>
@@ -186,13 +241,13 @@
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="10.5" FontSize="10"
Foreground="#888888" Foreground="#888888"
Text="{Binding Index, StringFormat='[{0}] '}" /> Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock <TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="11.5" FontSize="11"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{Binding Name}" /> Text="{Binding Name}" />
</StackPanel> </StackPanel>
@@ -204,32 +259,6 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Orientation="Horizontal" Orientation="Horizontal"
Visibility="Collapsed"> Visibility="Collapsed">
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="↑"
Cursor="Hand"
FontSize="10"
ToolTip="上移" />
<Button
Width="20"
Height="20"
Margin="1,0"
Background="White"
BorderBrush="#CDCBCB"
BorderThickness="1"
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="↓"
Cursor="Hand"
FontSize="10"
ToolTip="下移" />
<Button <Button
Width="20" Width="20"
Height="20" Height="20"
@@ -248,12 +277,13 @@
<DataTemplate.Triggers> <DataTemplate.Triggers>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True"> <Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="NodeCard" Property="Background" Value="#F3F7FB" /> <Setter TargetName="NodeCard" Property="Background" Value="#F6FAFD" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#D7E4F1" /> <Setter TargetName="NodeCard" Property="BorderBrush" Value="#DFEAF3" />
</Trigger> </Trigger>
<DataTrigger Binding="{Binding IsPositionChild}" Value="True"> <DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
<Setter TargetName="ChildStem" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
<Setter TargetName="ChildBranch" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" />
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" />
</DataTrigger> </DataTrigger>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</HierarchicalDataTemplate> </HierarchicalDataTemplate>
+13
View File
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
@@ -70,6 +71,18 @@ namespace XplorePlane.Views.Cnc
viewModel.SelectedNode = e.NewValue as CncNodeViewModel; viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
} }
} }
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Delete || DataContext is not CncEditorViewModel viewModel)
return;
if (!viewModel.DeleteNodeCommand.CanExecute())
return;
viewModel.DeleteNodeCommand.Execute();
e.Handled = true;
}
} }
public class NullToVisibilityConverter : IValueConverter public class NullToVisibilityConverter : IValueConverter
@@ -4,7 +4,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
d:DesignHeight="700" d:DesignHeight="700"
d:DesignWidth="350" d:DesignWidth="350"
mc:Ignorable="d"> mc:Ignorable="d">
@@ -13,13 +12,27 @@
<SolidColorBrush x:Key="PanelBg" Color="White" /> <SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" /> <SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" /> <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> <FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem"> <Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="Focusable" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> <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>
<Style x:Key="ToolbarBtn" TargetType="Button"> <Style x:Key="ToolbarBtn" TargetType="Button">
@@ -43,7 +56,7 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="180" /> <RowDefinition Height="4*" MinHeight="180" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="2*" MinHeight="80" /> <RowDefinition Height="2*" MinHeight="80" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@@ -56,30 +69,48 @@
BorderBrush="{StaticResource PanelBorder}" BorderBrush="{StaticResource PanelBorder}"
BorderThickness="0,1,0,1"> BorderThickness="0,1,0,1">
<Grid> <Grid>
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal"> <Grid.RowDefinitions>
<Button <RowDefinition Height="Auto" />
Command="{Binding NewPipelineCommand}" <RowDefinition Height="Auto" />
Content="新建" </Grid.RowDefinitions>
Style="{StaticResource ToolbarBtn}" <StackPanel
ToolTip="新建流水线" /> Grid.Row="0"
<Button Orientation="Horizontal">
Command="{Binding SavePipelineCommand}" <Button
Content="保存" Command="{Binding NewPipelineCommand}"
Style="{StaticResource ToolbarBtn}" Content="新建"
ToolTip="保存当前检测模块流水线" /> Style="{StaticResource ToolbarBtn}"
<Button ToolTip="新建流水线" />
Width="60" <Button
Command="{Binding SaveAsPipelineCommand}" Command="{Binding SavePipelineCommand}"
Content="另存为" Content="保存"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="导出当前检测模块流水线" /> ToolTip="保存当前流水线" />
<Button <Button
Width="52" Width="60"
Command="{Binding LoadPipelineCommand}" Command="{Binding SaveAsPipelineCommand}"
Content="加载" Content="另存为"
Style="{StaticResource ToolbarBtn}" Style="{StaticResource ToolbarBtn}"
ToolTip="将流水线模板加载到当前检测模块" /> ToolTip="另存当前流水线" />
<Button
Width="52"
Command="{Binding LoadPipelineCommand}"
Content="加载"
Style="{StaticResource ToolbarBtn}"
ToolTip="加载流水线" />
</StackPanel> </StackPanel>
<TextBlock
Grid.Row="1"
Margin="2,4,2,0"
VerticalAlignment="Center"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
FontWeight="SemiBold"
Foreground="#333333"
Text="{Binding PipelineFileDisplayName}"
TextAlignment="Left"
TextTrimming="CharacterEllipsis"
ToolTip="{Binding PipelineFileDisplayName}" />
</Grid> </Grid>
</Border> </Border>
@@ -90,117 +121,108 @@
BorderThickness="0" BorderThickness="0"
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}" ItemContainerStyle="{StaticResource PipelineNodeItemStyle}"
ItemsSource="{Binding PipelineNodes}" ItemsSource="{Binding PipelineNodes}"
KeyboardNavigation.TabNavigation="Continue"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"> SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid x:Name="NodeRoot" MinHeight="48"> <Border
<Grid.ColumnDefinitions> x:Name="NodeContainer"
<ColumnDefinition Width="44" /> Margin="2"
<ColumnDefinition Width="*" /> Padding="2"
<ColumnDefinition Width="Auto" /> Background="Transparent"
</Grid.ColumnDefinitions> BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="3">
<Grid x:Name="NodeRoot" MinHeight="48">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="44" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Line <Line
x:Name="TopLine" x:Name="TopLine"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Top" VerticalAlignment="Top"
Stroke="#5B9BD5" Stroke="#5B9BD5"
StrokeThickness="2" StrokeThickness="2"
X1="0" X1="0"
X2="0" X2="0"
Y1="0" Y1="0"
Y2="10" /> Y2="10" />
<Line <Line
HorizontalAlignment="Center" x:Name="BottomLine"
VerticalAlignment="Bottom" HorizontalAlignment="Center"
Stroke="#5B9BD5" VerticalAlignment="Bottom"
StrokeThickness="2" Stroke="#5B9BD5"
X1="0" StrokeThickness="2"
X2="0" X1="0"
Y1="0" X2="0"
Y2="14" /> Y1="0"
Y2="14" />
<Border <Border
Grid.Column="0" x:Name="IconBorder"
Width="28" Grid.Column="0"
Height="28" Width="28"
HorizontalAlignment="Center" Height="28"
VerticalAlignment="Center"
Background="#E8F0FE"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
CornerRadius="4">
<TextBlock
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
FontSize="13" Background="#E8F0FE"
Text="{Binding IconPath}" /> BorderBrush="#5B9BD5"
</Border> BorderThickness="1.5"
CornerRadius="4">
<TextBlock
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="13"
Text="{Binding IconPath}" />
</Border>
<TextBlock <StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
Grid.Column="1" <TextBlock
Margin="6,0,0,0" x:Name="NodeTitle"
VerticalAlignment="Center" FontFamily="Microsoft YaHei UI"
FontFamily="Microsoft YaHei UI" FontSize="12"
FontSize="12" Text="{Binding DisplayName}" />
Text="{Binding DisplayName}" /> <TextBlock
x:Name="NodeState"
<StackPanel Margin="0,2,0,0"
x:Name="NodeActions" FontFamily="Microsoft YaHei UI"
Grid.Column="2" FontSize="10"
Margin="0,0,4,0" Foreground="#6E6E6E"
VerticalAlignment="Center" Text="已启用" />
Orientation="Horizontal" </StackPanel>
Visibility="Collapsed"> </Grid>
<Button </Border>
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>
<DataTemplate.Triggers> <DataTemplate.Triggers>
<DataTrigger Binding="{Binding Order}" Value="0"> <DataTrigger Binding="{Binding Order}" Value="0">
<Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" /> <Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" />
</DataTrigger> </DataTrigger>
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True"> <DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" /> <Setter TargetName="NodeContainer" Property="Background" Value="{StaticResource DisabledNodeBg}" />
</Trigger> <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>
</DataTemplate.Triggers> </DataTemplate.Triggers>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
@@ -224,6 +246,16 @@
Foreground="#555" Foreground="#555"
Text="参数配置" /> Text="参数配置" />
<ItemsControl ItemsSource="{Binding SelectedNode.Parameters}"> <ItemsControl ItemsSource="{Binding SelectedNode.Parameters}">
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid Margin="0,3"> <Grid Margin="0,3">
@@ -239,6 +271,7 @@
Text="{Binding DisplayName}" Text="{Binding DisplayName}"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis" />
<TextBox <TextBox
x:Name="TextValueEditor"
Grid.Column="1" Grid.Column="1"
Padding="4,2" Padding="4,2"
BorderBrush="#CDCBCB" BorderBrush="#CDCBCB"
@@ -250,15 +283,56 @@
<Style TargetType="TextBox"> <Style TargetType="TextBox">
<Setter Property="BorderBrush" Value="#CDCBCB" /> <Setter Property="BorderBrush" Value="#CDCBCB" />
<Setter Property="Background" Value="White" /> <Setter Property="Background" Value="White" />
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsTextInput}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding IsValueValid}" Value="False"> <DataTrigger Binding="{Binding IsValueValid}" Value="False">
<Setter Property="BorderBrush" Value="Red" /> <Setter Property="BorderBrush" Value="#D9534F" />
<Setter Property="Background" Value="#FFF0F0" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</TextBox.Style> </TextBox.Style>
</TextBox> </TextBox>
<ComboBox
Grid.Column="1"
MinHeight="24"
Padding="4,1"
BorderBrush="#CDCBCB"
BorderThickness="1"
FontFamily="Microsoft YaHei UI"
FontSize="11"
ItemsSource="{Binding Options}"
SelectedItem="{Binding SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasOptions}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<CheckBox
Grid.Column="1"
VerticalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="11"
IsChecked="{Binding BoolValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<CheckBox.Style>
<Style TargetType="CheckBox">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsBool}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
</CheckBox>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
@@ -268,16 +342,38 @@
<Border <Border
Grid.Row="4" Grid.Row="4"
Padding="6,4" Height="24"
Background="#F5F5F5" Padding="6,0"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="0,1,0,0"> BorderThickness="0,1,0,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F5F5F5" />
<Setter Property="BorderBrush" Value="{StaticResource PanelBorder}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
<Setter Property="Background" Value="#FFF1F1" />
<Setter Property="BorderBrush" Value="#D9534F" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock <TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource CsdFont}" FontFamily="{StaticResource CsdFont}"
FontSize="11" FontSize="11"
Foreground="#555"
Text="{Binding StatusMessage, StringFormat='Status: {0}'}" Text="{Binding StatusMessage, StringFormat='Status: {0}'}"
TextTrimming="CharacterEllipsis" /> TextTrimming="CharacterEllipsis">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#555" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
<Setter Property="Foreground" Value="#A12A2A" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border> </Border>
</Grid> </Grid>
</Border> </Border>
@@ -2,15 +2,27 @@ using Prism.Ioc;
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
namespace XplorePlane.Views namespace XplorePlane.Views
{ {
public partial class PipelineEditorView : UserControl public partial class PipelineEditorView : UserControl
{ {
private const string PipelineNodeDragFormat = "PipelineNodeDrag";
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private Point _dragStartPoint;
private bool _isInternalDragging;
private bool _suppressClickToggle;
private PipelineNodeViewModel _draggedNode;
public PipelineEditorView() public PipelineEditorView()
{ {
InitializeComponent(); InitializeComponent();
@@ -35,50 +47,252 @@ namespace XplorePlane.Views
} }
} }
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
DataContext?.GetType().Name);
PipelineListBox.AllowDrop = true; PipelineListBox.AllowDrop = true;
PipelineListBox.Drop += OnOperatorDropped; PipelineListBox.Focusable = true;
PipelineListBox.Drop -= OnListBoxDrop;
PipelineListBox.Drop += OnListBoxDrop;
PipelineListBox.DragOver -= OnDragOver;
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;
_suppressClickToggle = true;
var data = new DataObject(PipelineNodeDragFormat, _draggedNode);
DragDrop.DoDragDrop(PipelineListBox, data, DragDropEffects.Move);
Dispatcher.BeginInvoke(new Action(() =>
{
_suppressClickToggle = false;
ResetDragState();
}), DispatcherPriority.Background);
}
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) private void OnDragOver(object sender, DragEventArgs e)
{ {
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat) if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
? DragDropEffects.Copy {
: DragDropEffects.None; e.Effects = DragDropEffects.Copy;
}
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
{
e.Effects = DragDropEffects.Move;
}
else
{
e.Effects = DragDropEffects.None;
}
e.Handled = true; e.Handled = true;
} }
private void OnOperatorDropped(object sender, DragEventArgs e) private void OnListBoxDrop(object sender, DragEventArgs e)
{ {
if (DataContext is not IPipelineEditorHostViewModel vm) if (DataContext is not IPipelineEditorHostViewModel vm)
{ {
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel"); ResetDragState();
return; return;
} }
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat)) if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
{ {
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat); OnOperatorDropped(vm, e);
return; }
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; var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
if (string.IsNullOrWhiteSpace(operatorKey)) if (string.IsNullOrWhiteSpace(operatorKey))
{ {
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空"); _logger?.Warn("Drop 触发,但 OperatorKey 为空");
return; return;
} }
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
operatorKey, vm.PipelineNodes.Count);
vm.AddOperatorCommand.Execute(operatorKey); 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 insertionIndex = GetDropInsertionIndex(e.GetPosition(PipelineListBox), vm.PipelineNodes.Count);
var newIndex = insertionIndex > oldIndex ? insertionIndex - 1 : insertionIndex;
newIndex = Math.Max(0, Math.Min(newIndex, vm.PipelineNodes.Count - 1));
if (oldIndex == newIndex)
{
return;
}
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs
{
OldIndex = oldIndex,
NewIndex = newIndex
});
}
private int GetDropInsertionIndex(Point position, int itemCount)
{
var item = GetItemAtPosition(position);
if (item == null)
{
return itemCount;
}
var targetIndex = PipelineListBox.ItemContainerGenerator.IndexFromContainer(item);
if (targetIndex < 0)
{
return itemCount;
}
var itemTop = item.TranslatePoint(new Point(0, 0), PipelineListBox).Y;
var itemMid = itemTop + (item.ActualHeight / 2);
return position.Y > itemMid ? targetIndex + 1 : targetIndex;
}
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 ListBoxItem)
{
return false;
}
if (current is ButtonBase || current is TextBoxBase || current is ScrollBar)
{
return true;
}
current = VisualTreeHelper.GetParent(current);
}
return false;
}
private void ResetDragState()
{
_isInternalDragging = false;
_draggedNode = null;
} }
} }
} }
+3 -10
View File
@@ -469,21 +469,14 @@
Grid.ColumnSpan="3" Grid.ColumnSpan="3"
Margin="0"> Margin="0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition x:Name="NavColumn" Width="0" />
<ColumnDefinition Width="{Binding ViewportPanelWidth}" /> <ColumnDefinition Width="{Binding ViewportPanelWidth}" />
<ColumnDefinition Width="{Binding ImagePanelWidth}" /> <ColumnDefinition Width="{Binding ImagePanelWidth}" />
<ColumnDefinition Width="350" /> <ColumnDefinition Width="350" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
<views:NavigationPanelView
x:Name="NavigationPanel"
Grid.Column="0"
Visibility="Collapsed" />
<!-- 中间: 2D Viewport --> <!-- 中间: 2D Viewport -->
<Border <Border
Grid.Column="1" Grid.Column="0"
BorderBrush="#DDDDDD" BorderBrush="#DDDDDD"
BorderThickness="1,0,1,0"> BorderThickness="1,0,1,0">
<views:ViewportPanelView /> <views:ViewportPanelView />
@@ -491,14 +484,14 @@
<!-- 中间: 图像 --> <!-- 中间: 图像 -->
<Border <Border
Grid.Column="2" Grid.Column="1"
BorderBrush="#DDDDDD" BorderBrush="#DDDDDD"
BorderThickness="0,0,1,0"> BorderThickness="0,0,1,0">
<views:ImagePanelView /> <views:ImagePanelView />
</Border> </Border>
<!-- 右侧: 属性面板 --> <!-- 右侧: 属性面板 -->
<Grid Grid.Column="3"> <Grid Grid.Column="2">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="350*" /> <ColumnDefinition Width="350*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -13,12 +13,5 @@ namespace XplorePlane.Views
InitializeComponent(); InitializeComponent();
DataContext = viewModel; DataContext = viewModel;
} }
private void AccountingNumberFormatButton_Click(object sender, RoutedEventArgs e)
{
bool show = NavigationPanel.Visibility != Visibility.Visible;
NavigationPanel.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
NavColumn.Width = show ? new GridLength(180) : new GridLength(0);
}
} }
} }
@@ -4,7 +4,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="600" d:DesignHeight="424"
d:DesignWidth="400" d:DesignWidth="400"
mc:Ignorable="d"> mc:Ignorable="d">
<Grid Background="#FFFFFF"> <Grid Background="#FFFFFF">
@@ -48,12 +48,15 @@
<Border <Border
Grid.Row="2" Grid.Row="2"
Background="#000000" Background="#000000"
Padding="8,4"> Height="24"
<TextBlock FontSize="12"> Padding="8,0">
<TextBlock
VerticalAlignment="Center"
FontSize="12">
<Run Foreground="#FFFFFF" Text="{Binding CameraStatusText, Mode=OneWay}" /> <Run Foreground="#FFFFFF" Text="{Binding CameraStatusText, Mode=OneWay}" />
<Run Text=" " /> <Run Text=" " />
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" /> <Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
</TextBlock> </TextBlock>
</Border> </Border>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -28,10 +28,6 @@
ImageSource="{Binding ImageSource}" ImageSource="{Binding ImageSource}"
Background="White" /> Background="White" />
<!-- 图像信息栏 -->
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
<TextBlock Margin="4,2" FontSize="11" Foreground="#666666"
Text="{Binding ImageInfo}" />
</Border>
</Grid> </Grid>
</UserControl> </UserControl>