diff --git a/XplorePlane/Services/Cnc/CncProgramService.cs b/XplorePlane/Services/Cnc/CncProgramService.cs
index eabad59..f4fa478 100644
--- a/XplorePlane/Services/Cnc/CncProgramService.cs
+++ b/XplorePlane/Services/Cnc/CncProgramService.cs
@@ -205,6 +205,32 @@ namespace XplorePlane.Services.Cnc
return updated;
}
+ ///
+ public CncProgram UpdateNode(CncProgram program, int index, CncNode node)
+ {
+ ArgumentNullException.ThrowIfNull(program);
+ ArgumentNullException.ThrowIfNull(node);
+
+ if (index < 0 || index >= program.Nodes.Count)
+ throw new ArgumentOutOfRangeException(nameof(index),
+ $"Index out of range: {index}, Count={program.Nodes.Count}");
+
+ var nodes = new List(program.Nodes)
+ {
+ [index] = node with { Index = index }
+ };
+
+ var updated = program with
+ {
+ Nodes = nodes.AsReadOnly(),
+ UpdatedAt = DateTime.UtcNow
+ };
+
+ _logger.Info("Updated node: Index={Index}, Type={NodeType}, Program={ProgramName}",
+ index, node.NodeType, program.Name);
+ return updated;
+ }
+
///
public async Task SaveAsync(CncProgram program, string filePath)
{
diff --git a/XplorePlane/Services/Cnc/ICncProgramService.cs b/XplorePlane/Services/Cnc/ICncProgramService.cs
index 8429f96..db54c03 100644
--- a/XplorePlane/Services/Cnc/ICncProgramService.cs
+++ b/XplorePlane/Services/Cnc/ICncProgramService.cs
@@ -4,36 +4,28 @@ using XplorePlane.Models;
namespace XplorePlane.Services.Cnc
{
///
- /// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
- /// CNC program management service interface for creation, node editing, serialization and file I/O
+ /// CNC program management service interface.
///
public interface ICncProgramService
{
- /// 创建空的 CNC 程序 | Create an empty CNC program
CncProgram CreateProgram(string name);
- /// 根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)
CncNode CreateNode(CncNodeType type);
- /// 在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
- /// 移除指定索引的节点并重新编号 | Remove the node at the given index and renumber
CncProgram RemoveNode(CncProgram program, int index);
- /// 将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
- /// 将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file
+ CncProgram UpdateNode(CncProgram program, int index, CncNode node);
+
Task SaveAsync(CncProgram program, string filePath);
- /// 从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file
Task LoadAsync(string filePath);
- /// 将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string
string Serialize(CncProgram program);
- /// 从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string
CncProgram Deserialize(string json);
}
-}
\ No newline at end of file
+}
diff --git a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
index c1cdc9c..0b3590f 100644
--- a/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
+++ b/XplorePlane/ViewModels/Cnc/CncEditorViewModel.cs
@@ -3,6 +3,7 @@ using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
@@ -18,27 +19,21 @@ using XplorePlane.Services.Cnc;
namespace XplorePlane.ViewModels.Cnc
{
///
- /// CNC 编辑器 ViewModel,管理 CNC 程序的节点列表、编辑操作和文件操作
- /// CNC editor ViewModel that manages the node list, editing operations and file operations
+ /// CNC editor ViewModel that manages the node tree, editing operations and file operations.
///
public class CncEditorViewModel : BindableBase
{
private readonly ICncProgramService _cncProgramService;
- private readonly IAppStateService _appStateService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
- // 当前 CNC 程序 | Current CNC program
private CncProgram _currentProgram;
-
private ObservableCollection _nodes;
+ private ObservableCollection _treeNodes;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
- ///
- /// 构造函数 | Constructor
- ///
public CncEditorViewModel(
ICncProgramService cncProgramService,
IAppStateService appStateService,
@@ -46,13 +41,13 @@ namespace XplorePlane.ViewModels.Cnc
ILoggerService logger)
{
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
- _appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
+ ArgumentNullException.ThrowIfNull(appStateService);
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule();
_nodes = new ObservableCollection();
+ _treeNodes = new ObservableCollection();
- // ── 节点插入命令 | Node insertion commands ──
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
InsertSaveNodeCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNode));
@@ -63,116 +58,78 @@ namespace XplorePlane.ViewModels.Cnc
InsertWaitDelayCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.WaitDelay));
InsertCompleteProgramCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.CompleteProgram));
- // ── 节点编辑命令 | Node editing commands ──
DeleteNodeCommand = new DelegateCommand(ExecuteDeleteNode, CanExecuteDeleteNode)
.ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand(ExecuteMoveNodeDown);
- // ── 文件操作命令 | File operation commands ──
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
NewProgramCommand = new DelegateCommand(ExecuteNewProgram);
ExportCsvCommand = new DelegateCommand(ExecuteExportCsv);
- _logger.Info("CncEditorViewModel 已初始化 | CncEditorViewModel initialized");
+ _logger.Info("CncEditorViewModel initialized");
}
- // ── 属性 | Properties ──────────────────────────────────────────
-
- /// 节点列表 | Node list
public ObservableCollection Nodes
{
get => _nodes;
- set => SetProperty(ref _nodes, value);
+ private set => SetProperty(ref _nodes, value);
+ }
+
+ public ObservableCollection TreeNodes
+ {
+ get => _treeNodes;
+ private set => SetProperty(ref _treeNodes, value);
}
- /// 当前选中的节点 | Currently selected node
public CncNodeViewModel SelectedNode
{
get => _selectedNode;
- set => SetProperty(ref _selectedNode, value);
+ set
+ {
+ if (SetProperty(ref _selectedNode, value))
+ {
+ RaisePropertyChanged(nameof(HasSelection));
+ }
+ }
}
- /// 程序是否已修改 | Whether the program has been modified
+ public bool HasSelection => SelectedNode != null;
+
public bool IsModified
{
get => _isModified;
set => SetProperty(ref _isModified, value);
}
- /// 当前程序名称 | Current program name
public string ProgramName
{
get => _programName;
set => SetProperty(ref _programName, value);
}
- // ── 节点插入命令 | Node insertion commands ──────────────────────
-
- /// 插入参考点命令 | Insert reference point command
public DelegateCommand InsertReferencePointCommand { get; }
-
- /// 插入保存节点(含图像)命令 | Insert save node with image command
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
-
- /// 插入保存节点命令 | Insert save node command
public DelegateCommand InsertSaveNodeCommand { get; }
-
- /// 插入保存位置命令 | Insert save position command
public DelegateCommand InsertSavePositionCommand { get; }
-
- /// 插入检测模块命令 | Insert inspection module command
public DelegateCommand InsertInspectionModuleCommand { get; }
-
- /// 插入检测标记命令 | Insert inspection marker command
public DelegateCommand InsertInspectionMarkerCommand { get; }
-
- /// 插入停顿对话框命令 | Insert pause dialog command
public DelegateCommand InsertPauseDialogCommand { get; }
-
- /// 插入等待延时命令 | Insert wait delay command
public DelegateCommand InsertWaitDelayCommand { get; }
-
- /// 插入完成程序命令 | Insert complete program command
public DelegateCommand InsertCompleteProgramCommand { get; }
-
- // ── 节点编辑命令 | Node editing commands ────────────────────────
-
- /// 删除选中节点命令 | Delete selected node command
public DelegateCommand DeleteNodeCommand { get; }
-
- /// 上移节点命令 | Move node up command
public DelegateCommand MoveNodeUpCommand { get; }
-
- /// 下移节点命令 | Move node down command
public DelegateCommand MoveNodeDownCommand { get; }
-
- // ── 文件操作命令 | File operation commands ──────────────────────
-
- /// 保存程序命令 | Save program command
public DelegateCommand SaveProgramCommand { get; }
-
- /// 加载程序命令 | Load program command
public DelegateCommand LoadProgramCommand { get; }
-
- /// 新建程序命令 | New program command
public DelegateCommand NewProgramCommand { get; }
-
- /// 导出 CSV 命令 | Export CSV command
public DelegateCommand ExportCsvCommand { get; }
- // ── 命令执行方法 | Command execution methods ────────────────────
-
- ///
- /// 插入指定类型的节点到选中节点之后
- /// Insert a node of the specified type after the selected node
- ///
private void ExecuteInsertNode(CncNodeType nodeType)
{
if (_currentProgram == null)
{
- _logger.Warn("无法插入节点:当前无程序 | Cannot insert node: no current program");
ExecuteNewProgram();
}
@@ -183,18 +140,14 @@ namespace XplorePlane.ViewModels.Cnc
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
OnProgramEdited();
- _logger.Info("已插入节点 | Inserted node: Type={NodeType}", nodeType);
+ _logger.Info("Inserted node: Type={NodeType}", nodeType);
}
catch (InvalidOperationException ex)
{
- // 重复插入 CompleteProgram 等业务规则异常 | Business rule exceptions like duplicate CompleteProgram
- _logger.Warn("插入节点被阻止 | Node insertion blocked: {Message}", ex.Message);
+ _logger.Warn("Node insertion blocked: {Message}", ex.Message);
}
}
- ///
- /// 删除选中节点 | Delete the selected node
- ///
private void ExecuteDeleteNode()
{
if (_currentProgram == null || SelectedNode == null)
@@ -204,18 +157,14 @@ namespace XplorePlane.ViewModels.Cnc
{
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
OnProgramEdited();
- _logger.Info("已删除节点 | Deleted node at index: {Index}", SelectedNode.Index);
+ _logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
}
catch (ArgumentOutOfRangeException ex)
{
- _logger.Warn("删除节点失败 | Delete node failed: {Message}", ex.Message);
+ _logger.Warn("Delete node failed: {Message}", ex.Message);
}
}
- ///
- /// 判断是否可以删除节点(至少保留 1 个节点)
- /// Determines whether delete is allowed (at least 1 node must remain)
- ///
private bool CanExecuteDeleteNode()
{
return SelectedNode != null
@@ -223,9 +172,6 @@ namespace XplorePlane.ViewModels.Cnc
&& _currentProgram.Nodes.Count > 1;
}
- ///
- /// 上移节点 | Move node up
- ///
private void ExecuteMoveNodeUp(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index <= 0)
@@ -235,17 +181,13 @@ namespace XplorePlane.ViewModels.Cnc
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index - 1);
OnProgramEdited();
- _logger.Info("已上移节点 | Moved node up: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index - 1);
}
catch (ArgumentOutOfRangeException ex)
{
- _logger.Warn("上移节点失败 | Move node up failed: {Message}", ex.Message);
+ _logger.Warn("Move node up failed: {Message}", ex.Message);
}
}
- ///
- /// 下移节点 | Move node down
- ///
private void ExecuteMoveNodeDown(CncNodeViewModel nodeVm)
{
if (_currentProgram == null || nodeVm == null || nodeVm.Index >= _currentProgram.Nodes.Count - 1)
@@ -255,22 +197,18 @@ namespace XplorePlane.ViewModels.Cnc
{
_currentProgram = _cncProgramService.MoveNode(_currentProgram, nodeVm.Index, nodeVm.Index + 1);
OnProgramEdited();
- _logger.Info("已下移节点 | Moved node down: {OldIndex} -> {NewIndex}", nodeVm.Index, nodeVm.Index + 1);
}
catch (ArgumentOutOfRangeException ex)
{
- _logger.Warn("下移节点失败 | Move node down failed: {Message}", ex.Message);
+ _logger.Warn("Move node down failed: {Message}", ex.Message);
}
}
- ///
- /// 保存当前程序到文件 | Save current program to file
- ///
private async Task ExecuteSaveProgramAsync()
{
if (_currentProgram == null)
{
- _logger.Warn("无法保存:当前无程序 | Cannot save: no current program");
+ _logger.Warn("Cannot save: no current program");
return;
}
@@ -289,17 +227,13 @@ namespace XplorePlane.ViewModels.Cnc
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
IsModified = false;
- _logger.Info("程序已保存 | Program saved: {FilePath}", dlg.FileName);
}
catch (Exception ex)
{
- _logger.Error(ex, "保存程序失败 | Failed to save program");
+ _logger.Error(ex, "Failed to save program");
}
}
- ///
- /// 从文件加载程序 | Load program from file
- ///
private async Task ExecuteLoadProgramAsync()
{
try
@@ -318,35 +252,27 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
- _logger.Info("程序已加载 | Program loaded: {ProgramName}", _currentProgram.Name);
}
catch (Exception ex)
{
- _logger.Error(ex, "加载程序失败 | Failed to load program");
+ _logger.Error(ex, "Failed to load program");
}
}
- ///
- /// 创建新程序 | Create a new program
- ///
private void ExecuteNewProgram()
{
- var name = string.IsNullOrWhiteSpace(ProgramName) ? "新程序" : ProgramName;
+ var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
_currentProgram = _cncProgramService.CreateProgram(name);
ProgramName = _currentProgram.Name;
IsModified = false;
RefreshNodes();
- _logger.Info("已创建新程序 | Created new program: {ProgramName}", name);
}
- ///
- /// 导出当前程序为 CSV 文件 | Export current program to CSV file
- ///
private void ExecuteExportCsv()
{
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
{
- _logger.Warn("无法导出 CSV:当前无程序或节点为空 | Cannot export CSV: no program or empty nodes");
+ _logger.Warn("Cannot export CSV: no program or empty nodes");
return;
}
@@ -364,7 +290,6 @@ namespace XplorePlane.ViewModels.Cnc
return;
var sb = new StringBuilder();
- // CSV 表头 | CSV header
sb.AppendLine("Index,NodeType,Name,XM,YM,ZT,ZD,TiltD,Dist,Voltage_kV,Current_uA,Power_W,RayOn,DetectorConnected,FrameRate,Resolution,ImageFile,MarkerType,MarkerX,MarkerY,DialogTitle,DialogMessage,DelayMs,Pipeline");
var inv = CultureInfo.InvariantCulture;
@@ -374,23 +299,14 @@ namespace XplorePlane.ViewModels.Cnc
var row = node switch
{
ReferencePointNode rp => $"{rp.Index},{rp.NodeType},{Esc(rp.Name)},{rp.XM.ToString(inv)},{rp.YM.ToString(inv)},{rp.ZT.ToString(inv)},{rp.ZD.ToString(inv)},{rp.TiltD.ToString(inv)},{rp.Dist.ToString(inv)},{rp.Voltage.ToString(inv)},{rp.Current.ToString(inv)},,{rp.IsRayOn},,,,,,,,,,",
-
SaveNodeWithImageNode sni => $"{sni.Index},{sni.NodeType},{Esc(sni.Name)},{sni.MotionState.XM.ToString(inv)},{sni.MotionState.YM.ToString(inv)},{sni.MotionState.ZT.ToString(inv)},{sni.MotionState.ZD.ToString(inv)},{sni.MotionState.TiltD.ToString(inv)},{sni.MotionState.Dist.ToString(inv)},{sni.RaySourceState.Voltage.ToString(inv)},,{sni.RaySourceState.Power.ToString(inv)},{sni.RaySourceState.IsOn},{sni.DetectorState.IsConnected},{sni.DetectorState.FrameRate.ToString(inv)},{Esc(sni.DetectorState.Resolution)},{Esc(sni.ImageFileName)},,,,,,",
-
SaveNodeNode sn => $"{sn.Index},{sn.NodeType},{Esc(sn.Name)},{sn.MotionState.XM.ToString(inv)},{sn.MotionState.YM.ToString(inv)},{sn.MotionState.ZT.ToString(inv)},{sn.MotionState.ZD.ToString(inv)},{sn.MotionState.TiltD.ToString(inv)},{sn.MotionState.Dist.ToString(inv)},{sn.RaySourceState.Voltage.ToString(inv)},,{sn.RaySourceState.Power.ToString(inv)},{sn.RaySourceState.IsOn},{sn.DetectorState.IsConnected},{sn.DetectorState.FrameRate.ToString(inv)},{Esc(sn.DetectorState.Resolution)},,,,,,,",
-
SavePositionNode sp => $"{sp.Index},{sp.NodeType},{Esc(sp.Name)},{sp.MotionState.XM.ToString(inv)},{sp.MotionState.YM.ToString(inv)},{sp.MotionState.ZT.ToString(inv)},{sp.MotionState.ZD.ToString(inv)},{sp.MotionState.TiltD.ToString(inv)},{sp.MotionState.Dist.ToString(inv)},,,,,,,,,,,,,,",
-
InspectionModuleNode im => $"{im.Index},{im.NodeType},{Esc(im.Name)},,,,,,,,,,,,,,,,,,,{Esc(im.Pipeline?.Name ?? string.Empty)}",
-
InspectionMarkerNode mk => $"{mk.Index},{mk.NodeType},{Esc(mk.Name)},,,,,,,,,,,,,,{Esc(mk.MarkerType)},{mk.MarkerX.ToString(inv)},{mk.MarkerY.ToString(inv)},,,",
-
PauseDialogNode pd => $"{pd.Index},{pd.NodeType},{Esc(pd.Name)},,,,,,,,,,,,,,,,,{Esc(pd.DialogTitle)},{Esc(pd.DialogMessage)},,",
-
WaitDelayNode wd => $"{wd.Index},{wd.NodeType},{Esc(wd.Name)},,,,,,,,,,,,,,,,,,,{wd.DelayMilliseconds},",
-
CompleteProgramNode cp => $"{cp.Index},{cp.NodeType},{Esc(cp.Name)},,,,,,,,,,,,,,,,,,,,",
-
_ => $"{node.Index},{node.NodeType},{Esc(node.Name)},,,,,,,,,,,,,,,,,,,,"
};
@@ -398,18 +314,13 @@ namespace XplorePlane.ViewModels.Cnc
}
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
- _logger.Info("CSV 已导出 | CSV exported: {FilePath}, 节点数={Count}", dlg.FileName, _currentProgram.Nodes.Count);
}
catch (Exception ex)
{
- _logger.Error(ex, "导出 CSV 失败 | Failed to export CSV");
+ _logger.Error(ex, "Failed to export CSV");
}
}
- ///
- /// CSV 字段转义:含逗号、引号或换行时用双引号包裹
- /// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
- ///
private static string Esc(string value)
{
if (string.IsNullOrEmpty(value)) return string.Empty;
@@ -418,12 +329,6 @@ namespace XplorePlane.ViewModels.Cnc
return value;
}
- // ── 辅助方法 | Helper methods ───────────────────────────────────
-
- ///
- /// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
- /// Unified post-edit handling: refresh nodes, mark modified, publish change event
- ///
private void OnProgramEdited()
{
IsModified = true;
@@ -431,34 +336,70 @@ namespace XplorePlane.ViewModels.Cnc
PublishProgramChanged();
}
- ///
- /// 从 _currentProgram.Nodes 重建 Nodes 集合
- /// Rebuild the Nodes collection from _currentProgram.Nodes
- ///
- private void RefreshNodes()
+ private void HandleNodeModelChanged(CncNodeViewModel nodeVm, CncNode updatedNode)
{
- Nodes.Clear();
-
- if (_currentProgram?.Nodes == null)
+ if (_currentProgram == null)
return;
- foreach (var node in _currentProgram.Nodes)
- {
- Nodes.Add(new CncNodeViewModel(node));
- }
-
- // 尝试保持选中状态 | Try to preserve selection
- if (SelectedNode != null)
- {
- var match = Nodes.FirstOrDefault(n => n.Index == SelectedNode.Index);
- SelectedNode = match ?? Nodes.LastOrDefault();
- }
+ _currentProgram = _cncProgramService.UpdateNode(_currentProgram, nodeVm.Index, updatedNode);
+ IsModified = true;
+ ProgramName = _currentProgram.Name;
+ PublishProgramChanged();
+ }
+
+ private void RefreshNodes()
+ {
+ var selectedId = SelectedNode?.Id;
+ var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
+
+ var flatNodes = new List();
+ var rootNodes = new List();
+ CncNodeViewModel currentModule = null;
+
+ if (_currentProgram?.Nodes != null)
+ {
+ foreach (var node in _currentProgram.Nodes)
+ {
+ var vm = new CncNodeViewModel(node, HandleNodeModelChanged)
+ {
+ IsExpanded = expansionState.TryGetValue(node.Id, out var isExpanded) ? isExpanded : true
+ };
+
+ flatNodes.Add(vm);
+
+ if (vm.IsInspectionModule)
+ {
+ rootNodes.Add(vm);
+ currentModule = vm;
+ continue;
+ }
+
+ if (currentModule != null && IsModuleChild(vm.NodeType))
+ {
+ currentModule.Children.Add(vm);
+ continue;
+ }
+
+ rootNodes.Add(vm);
+ currentModule = null;
+ }
+ }
+
+ Nodes = new ObservableCollection(flatNodes);
+ TreeNodes = new ObservableCollection(rootNodes);
+
+ SelectedNode = selectedId.HasValue
+ ? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
+ : Nodes.LastOrDefault();
+ }
+
+ private static bool IsModuleChild(CncNodeType type)
+ {
+ return type is CncNodeType.InspectionMarker
+ or CncNodeType.PauseDialog
+ or CncNodeType.WaitDelay;
}
- ///
- /// 通过 IEventAggregator 发布 CNC 程序变更事件
- /// Publish CNC program changed event via IEventAggregator
- ///
private void PublishProgramChanged()
{
_eventAggregator
diff --git a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs
index 4fa752b..886ba15 100644
--- a/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs
+++ b/XplorePlane/ViewModels/Cnc/CncNodeViewModel.cs
@@ -1,89 +1,530 @@
using Prism.Mvvm;
+using System;
+using System.Collections.ObjectModel;
using XplorePlane.Models;
namespace XplorePlane.ViewModels.Cnc
{
///
- /// CNC 节点 ViewModel,将 CncNode 模型封装为可绑定的 WPF ViewModel
- /// CNC node ViewModel that wraps a CncNode model into a bindable WPF ViewModel
+ /// CNC node ViewModel with editable properties and tree children.
///
public class CncNodeViewModel : BindableBase
{
- private int _index;
- private string _name;
- private CncNodeType _nodeType;
+ private readonly Action _modelChangedCallback;
+ private CncNode _model;
private string _icon;
+ private bool _isExpanded = true;
- ///
- /// 构造函数,从 CncNode 模型初始化 ViewModel
- /// Constructor that initializes the ViewModel from a CncNode model
- ///
- public CncNodeViewModel(CncNode model)
+ public CncNodeViewModel(CncNode model, Action modelChangedCallback)
{
- Model = model;
- _index = model.Index;
- _name = model.Name;
- _nodeType = model.NodeType;
+ _model = model ?? throw new ArgumentNullException(nameof(model));
+ _modelChangedCallback = modelChangedCallback ?? throw new ArgumentNullException(nameof(modelChangedCallback));
_icon = GetIconForNodeType(model.NodeType);
+ Children = new ObservableCollection();
}
- /// 底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)
- public CncNode Model { get; }
+ public ObservableCollection Children { get; }
- /// 节点在程序中的索引 | Node index in the program
- public int Index
- {
- get => _index;
- set => SetProperty(ref _index, value);
- }
+ public CncNode Model => _model;
+
+ public Guid Id => _model.Id;
+
+ public int Index => _model.Index;
- /// 节点显示名称 | Node display name
public string Name
{
- get => _name;
- set => SetProperty(ref _name, value);
+ get => _model.Name;
+ set => UpdateModel(_model with { Name = value ?? string.Empty });
}
- /// 节点类型 | Node type
- public CncNodeType NodeType
+ public CncNodeType NodeType => _model.NodeType;
+
+ public string NodeTypeDisplay => NodeType.ToString();
+
+ public string Icon
{
- get => _nodeType;
+ get => _icon;
+ private set => SetProperty(ref _icon, value);
+ }
+
+ public bool IsExpanded
+ {
+ get => _isExpanded;
+ set => SetProperty(ref _isExpanded, value);
+ }
+
+ public bool HasChildren => Children.Count > 0;
+
+ public bool IsReferencePoint => _model is ReferencePointNode;
+ public bool IsSaveNode => _model is SaveNodeNode;
+ public bool IsSaveNodeWithImage => _model is SaveNodeWithImageNode;
+ public bool IsSavePosition => _model is SavePositionNode;
+ public bool IsInspectionModule => _model is InspectionModuleNode;
+ public bool IsInspectionMarker => _model is InspectionMarkerNode;
+ public bool IsPauseDialog => _model is PauseDialogNode;
+ public bool IsWaitDelay => _model is WaitDelayNode;
+ public bool IsCompleteProgram => _model is CompleteProgramNode;
+ public bool IsMotionSnapshotNode => _model is ReferencePointNode or SaveNodeNode or SaveNodeWithImageNode or SavePositionNode;
+
+ public double XM
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.XM,
+ SaveNodeNode sn => sn.MotionState.XM,
+ SaveNodeWithImageNode sni => sni.MotionState.XM,
+ SavePositionNode sp => sp.MotionState.XM,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.XM);
+ }
+
+ public double YM
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.YM,
+ SaveNodeNode sn => sn.MotionState.YM,
+ SaveNodeWithImageNode sni => sni.MotionState.YM,
+ SavePositionNode sp => sp.MotionState.YM,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.YM);
+ }
+
+ public double ZT
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.ZT,
+ SaveNodeNode sn => sn.MotionState.ZT,
+ SaveNodeWithImageNode sni => sni.MotionState.ZT,
+ SavePositionNode sp => sp.MotionState.ZT,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.ZT);
+ }
+
+ public double ZD
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.ZD,
+ SaveNodeNode sn => sn.MotionState.ZD,
+ SaveNodeWithImageNode sni => sni.MotionState.ZD,
+ SavePositionNode sp => sp.MotionState.ZD,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.ZD);
+ }
+
+ public double TiltD
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.TiltD,
+ SaveNodeNode sn => sn.MotionState.TiltD,
+ SaveNodeWithImageNode sni => sni.MotionState.TiltD,
+ SavePositionNode sp => sp.MotionState.TiltD,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.TiltD);
+ }
+
+ public double Dist
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.Dist,
+ SaveNodeNode sn => sn.MotionState.Dist,
+ SaveNodeWithImageNode sni => sni.MotionState.Dist,
+ SavePositionNode sp => sp.MotionState.Dist,
+ _ => 0d
+ };
+ set => UpdateMotion(value, MotionAxis.Dist);
+ }
+
+ public bool IsRayOn
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.IsRayOn,
+ SaveNodeNode sn => sn.RaySourceState.IsOn,
+ SaveNodeWithImageNode sni => sni.RaySourceState.IsOn,
+ _ => false
+ };
+ set => UpdateRaySource(isOn: value);
+ }
+
+ public double Voltage
+ {
+ get => _model switch
+ {
+ ReferencePointNode rp => rp.Voltage,
+ SaveNodeNode sn => sn.RaySourceState.Voltage,
+ SaveNodeWithImageNode sni => sni.RaySourceState.Voltage,
+ _ => 0d
+ };
+ set => UpdateRaySource(voltage: value);
+ }
+
+ public double Current
+ {
+ get => _model is ReferencePointNode rp ? rp.Current : 0d;
set
{
- if (SetProperty(ref _nodeType, value))
+ if (_model is ReferencePointNode rp)
{
- // 类型变更时自动更新图标 | Auto-update icon when type changes
- Icon = GetIconForNodeType(value);
+ UpdateModel(rp with { Current = value });
}
}
}
- /// 节点图标路径 | Node icon path
- public string Icon
+ public double Power
{
- get => _icon;
- set => SetProperty(ref _icon, value);
+ get => _model switch
+ {
+ SaveNodeNode sn => sn.RaySourceState.Power,
+ SaveNodeWithImageNode sni => sni.RaySourceState.Power,
+ _ => 0d
+ };
+ set => UpdateRaySource(power: value);
+ }
+
+ public bool DetectorConnected
+ {
+ get => _model switch
+ {
+ SaveNodeNode sn => sn.DetectorState.IsConnected,
+ SaveNodeWithImageNode sni => sni.DetectorState.IsConnected,
+ _ => false
+ };
+ set => UpdateDetector(isConnected: value);
+ }
+
+ public bool DetectorAcquiring
+ {
+ get => _model switch
+ {
+ SaveNodeNode sn => sn.DetectorState.IsAcquiring,
+ SaveNodeWithImageNode sni => sni.DetectorState.IsAcquiring,
+ _ => false
+ };
+ set => UpdateDetector(isAcquiring: value);
+ }
+
+ public double FrameRate
+ {
+ get => _model switch
+ {
+ SaveNodeNode sn => sn.DetectorState.FrameRate,
+ SaveNodeWithImageNode sni => sni.DetectorState.FrameRate,
+ _ => 0d
+ };
+ set => UpdateDetector(frameRate: value);
+ }
+
+ public string Resolution
+ {
+ get => _model switch
+ {
+ SaveNodeNode sn => sn.DetectorState.Resolution,
+ SaveNodeWithImageNode sni => sni.DetectorState.Resolution,
+ _ => string.Empty
+ };
+ set => UpdateDetector(resolution: value ?? string.Empty);
+ }
+
+ public string ImageFileName
+ {
+ get => _model is SaveNodeWithImageNode sni ? sni.ImageFileName : string.Empty;
+ set
+ {
+ if (_model is SaveNodeWithImageNode sni)
+ {
+ UpdateModel(sni with { ImageFileName = value ?? string.Empty });
+ }
+ }
+ }
+
+ public string PipelineName
+ {
+ get => _model is InspectionModuleNode im ? im.Pipeline?.Name ?? string.Empty : string.Empty;
+ set
+ {
+ if (_model is InspectionModuleNode im)
+ {
+ var pipeline = im.Pipeline ?? new PipelineModel();
+ pipeline.Name = value ?? string.Empty;
+ UpdateModel(im with { Pipeline = pipeline });
+ }
+ }
+ }
+
+ public string MarkerType
+ {
+ get => _model is InspectionMarkerNode mk ? mk.MarkerType : string.Empty;
+ set
+ {
+ if (_model is InspectionMarkerNode mk)
+ {
+ UpdateModel(mk with { MarkerType = value ?? string.Empty });
+ }
+ }
+ }
+
+ public double MarkerX
+ {
+ get => _model is InspectionMarkerNode mk ? mk.MarkerX : 0d;
+ set
+ {
+ if (_model is InspectionMarkerNode mk)
+ {
+ UpdateModel(mk with { MarkerX = value });
+ }
+ }
+ }
+
+ public double MarkerY
+ {
+ get => _model is InspectionMarkerNode mk ? mk.MarkerY : 0d;
+ set
+ {
+ if (_model is InspectionMarkerNode mk)
+ {
+ UpdateModel(mk with { MarkerY = value });
+ }
+ }
+ }
+
+ public string DialogTitle
+ {
+ get => _model is PauseDialogNode pd ? pd.DialogTitle : string.Empty;
+ set
+ {
+ if (_model is PauseDialogNode pd)
+ {
+ UpdateModel(pd with { DialogTitle = value ?? string.Empty });
+ }
+ }
+ }
+
+ public string DialogMessage
+ {
+ get => _model is PauseDialogNode pd ? pd.DialogMessage : string.Empty;
+ set
+ {
+ if (_model is PauseDialogNode pd)
+ {
+ UpdateModel(pd with { DialogMessage = value ?? string.Empty });
+ }
+ }
+ }
+
+ public int DelayMilliseconds
+ {
+ get => _model is WaitDelayNode wd ? wd.DelayMilliseconds : 0;
+ set
+ {
+ if (_model is WaitDelayNode wd)
+ {
+ UpdateModel(wd with { DelayMilliseconds = value });
+ }
+ }
+ }
+
+ public void ReplaceModel(CncNode model)
+ {
+ _model = model ?? throw new ArgumentNullException(nameof(model));
+ Icon = GetIconForNodeType(model.NodeType);
+ RaiseAllPropertiesChanged();
}
- ///
- /// 根据节点类型返回对应的图标路径
- /// Returns the icon path for the given node type
- ///
public static string GetIconForNodeType(CncNodeType nodeType)
{
return nodeType switch
{
- CncNodeType.ReferencePoint => "/Resources/Icons/cnc_reference_point.png",
- CncNodeType.SaveNodeWithImage => "/Resources/Icons/cnc_save_with_image.png",
- CncNodeType.SaveNode => "/Resources/Icons/cnc_save_node.png",
- CncNodeType.SavePosition => "/Resources/Icons/cnc_save_position.png",
- CncNodeType.InspectionModule => "/Resources/Icons/cnc_inspection_module.png",
- CncNodeType.InspectionMarker => "/Resources/Icons/cnc_inspection_marker.png",
- CncNodeType.PauseDialog => "/Resources/Icons/cnc_pause_dialog.png",
- CncNodeType.WaitDelay => "/Resources/Icons/cnc_wait_delay.png",
- CncNodeType.CompleteProgram => "/Resources/Icons/cnc_complete_program.png",
- _ => "/Resources/Icons/cnc_default.png",
+ CncNodeType.ReferencePoint => "/Assets/Icons/reference.png",
+ CncNodeType.SaveNodeWithImage => "/Assets/Icons/saveall.png",
+ CncNodeType.SaveNode => "/Assets/Icons/save.png",
+ CncNodeType.SavePosition => "/Assets/Icons/add-pos.png",
+ CncNodeType.InspectionModule => "/Assets/Icons/Module.png",
+ CncNodeType.InspectionMarker => "/Assets/Icons/mark.png",
+ CncNodeType.PauseDialog => "/Assets/Icons/message.png",
+ CncNodeType.WaitDelay => "/Assets/Icons/wait.png",
+ CncNodeType.CompleteProgram => "/Assets/Icons/finish.png",
+ _ => "/Assets/Icons/cnc.png",
};
}
+
+ private void UpdateMotion(double value, MotionAxis axis)
+ {
+ switch (_model)
+ {
+ case ReferencePointNode rp:
+ UpdateModel(axis switch
+ {
+ MotionAxis.XM => rp with { XM = value },
+ MotionAxis.YM => rp with { YM = value },
+ MotionAxis.ZT => rp with { ZT = value },
+ MotionAxis.ZD => rp with { ZD = value },
+ MotionAxis.TiltD => rp with { TiltD = value },
+ MotionAxis.Dist => rp with { Dist = value },
+ _ => rp
+ });
+ break;
+ case SaveNodeNode sn:
+ UpdateModel(sn with { MotionState = UpdateMotionState(sn.MotionState, axis, value) });
+ break;
+ case SaveNodeWithImageNode sni:
+ UpdateModel(sni with { MotionState = UpdateMotionState(sni.MotionState, axis, value) });
+ break;
+ case SavePositionNode sp:
+ UpdateModel(sp with { MotionState = UpdateMotionState(sp.MotionState, axis, value) });
+ break;
+ }
+ }
+
+ private void UpdateRaySource(bool? isOn = null, double? voltage = null, double? current = null, double? power = null)
+ {
+ switch (_model)
+ {
+ case ReferencePointNode rp:
+ UpdateModel(rp with
+ {
+ IsRayOn = isOn ?? rp.IsRayOn,
+ Voltage = voltage ?? rp.Voltage,
+ Current = current ?? rp.Current
+ });
+ break;
+ case SaveNodeNode sn:
+ UpdateModel(sn with
+ {
+ RaySourceState = sn.RaySourceState with
+ {
+ IsOn = isOn ?? sn.RaySourceState.IsOn,
+ Voltage = voltage ?? sn.RaySourceState.Voltage,
+ Power = power ?? sn.RaySourceState.Power
+ }
+ });
+ break;
+ case SaveNodeWithImageNode sni:
+ UpdateModel(sni with
+ {
+ RaySourceState = sni.RaySourceState with
+ {
+ IsOn = isOn ?? sni.RaySourceState.IsOn,
+ Voltage = voltage ?? sni.RaySourceState.Voltage,
+ Power = power ?? sni.RaySourceState.Power
+ }
+ });
+ break;
+ }
+ }
+
+ private void UpdateDetector(bool? isConnected = null, bool? isAcquiring = null, double? frameRate = null, string resolution = null)
+ {
+ switch (_model)
+ {
+ case SaveNodeNode sn:
+ UpdateModel(sn with
+ {
+ DetectorState = sn.DetectorState with
+ {
+ IsConnected = isConnected ?? sn.DetectorState.IsConnected,
+ IsAcquiring = isAcquiring ?? sn.DetectorState.IsAcquiring,
+ FrameRate = frameRate ?? sn.DetectorState.FrameRate,
+ Resolution = resolution ?? sn.DetectorState.Resolution
+ }
+ });
+ break;
+ case SaveNodeWithImageNode sni:
+ UpdateModel(sni with
+ {
+ DetectorState = sni.DetectorState with
+ {
+ IsConnected = isConnected ?? sni.DetectorState.IsConnected,
+ IsAcquiring = isAcquiring ?? sni.DetectorState.IsAcquiring,
+ FrameRate = frameRate ?? sni.DetectorState.FrameRate,
+ Resolution = resolution ?? sni.DetectorState.Resolution
+ }
+ });
+ break;
+ }
+ }
+
+ private static MotionState UpdateMotionState(MotionState state, MotionAxis axis, double value)
+ {
+ return axis switch
+ {
+ MotionAxis.XM => state with { XM = value },
+ MotionAxis.YM => state with { YM = value },
+ MotionAxis.ZT => state with { ZT = value },
+ MotionAxis.ZD => state with { ZD = value },
+ MotionAxis.TiltD => state with { TiltD = value },
+ MotionAxis.Dist => state with { Dist = value },
+ _ => state
+ };
+ }
+
+ private void UpdateModel(CncNode updatedModel)
+ {
+ _model = updatedModel ?? throw new ArgumentNullException(nameof(updatedModel));
+ RaiseAllPropertiesChanged();
+ _modelChangedCallback(this, updatedModel);
+ }
+
+ private void RaiseAllPropertiesChanged()
+ {
+ RaisePropertyChanged(nameof(Model));
+ RaisePropertyChanged(nameof(Id));
+ RaisePropertyChanged(nameof(Index));
+ RaisePropertyChanged(nameof(Name));
+ RaisePropertyChanged(nameof(NodeType));
+ RaisePropertyChanged(nameof(NodeTypeDisplay));
+ RaisePropertyChanged(nameof(Icon));
+ RaisePropertyChanged(nameof(IsReferencePoint));
+ RaisePropertyChanged(nameof(IsSaveNode));
+ RaisePropertyChanged(nameof(IsSaveNodeWithImage));
+ RaisePropertyChanged(nameof(IsSavePosition));
+ RaisePropertyChanged(nameof(IsInspectionModule));
+ RaisePropertyChanged(nameof(IsInspectionMarker));
+ RaisePropertyChanged(nameof(IsPauseDialog));
+ RaisePropertyChanged(nameof(IsWaitDelay));
+ RaisePropertyChanged(nameof(IsCompleteProgram));
+ RaisePropertyChanged(nameof(IsMotionSnapshotNode));
+ RaisePropertyChanged(nameof(XM));
+ RaisePropertyChanged(nameof(YM));
+ RaisePropertyChanged(nameof(ZT));
+ RaisePropertyChanged(nameof(ZD));
+ RaisePropertyChanged(nameof(TiltD));
+ RaisePropertyChanged(nameof(Dist));
+ RaisePropertyChanged(nameof(IsRayOn));
+ RaisePropertyChanged(nameof(Voltage));
+ RaisePropertyChanged(nameof(Current));
+ RaisePropertyChanged(nameof(Power));
+ RaisePropertyChanged(nameof(DetectorConnected));
+ RaisePropertyChanged(nameof(DetectorAcquiring));
+ RaisePropertyChanged(nameof(FrameRate));
+ RaisePropertyChanged(nameof(Resolution));
+ RaisePropertyChanged(nameof(ImageFileName));
+ RaisePropertyChanged(nameof(PipelineName));
+ RaisePropertyChanged(nameof(MarkerType));
+ RaisePropertyChanged(nameof(MarkerX));
+ RaisePropertyChanged(nameof(MarkerY));
+ RaisePropertyChanged(nameof(DialogTitle));
+ RaisePropertyChanged(nameof(DialogMessage));
+ RaisePropertyChanged(nameof(DelayMilliseconds));
+ }
+
+ private enum MotionAxis
+ {
+ XM,
+ YM,
+ ZT,
+ ZD,
+ TiltD,
+ Dist
+ }
}
-}
\ No newline at end of file
+}
diff --git a/XplorePlane/ViewModels/Main/MainViewModel.cs b/XplorePlane/ViewModels/Main/MainViewModel.cs
index f3a633e..3891463 100644
--- a/XplorePlane/ViewModels/Main/MainViewModel.cs
+++ b/XplorePlane/ViewModels/Main/MainViewModel.cs
@@ -227,7 +227,7 @@ namespace XplorePlane.ViewModels
private void ShowCncEditor()
{
ImagePanelContent = _cncPageView;
- ImagePanelWidth = new GridLength(430);
+ ImagePanelWidth = new GridLength(720);
_isCncEditorMode = true;
_logger.Info("CNC 编辑器已切换到主界面图像区域");
}
diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml b/XplorePlane/Views/Cnc/CncPageView.xaml
index 10a3fd8..db3b2f1 100644
--- a/XplorePlane/Views/Cnc/CncPageView.xaml
+++ b/XplorePlane/Views/Cnc/CncPageView.xaml
@@ -1,27 +1,26 @@
-
+
+
+
- Microsoft YaHei UI
-
-
+
+
+
+
+
-
-
-
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
-
-
+ ItemsSource="{Binding TreeNodes}"
+ SelectedItemChanged="CncTreeView_SelectedItemChanged">
+
+
+
-
+
@@ -121,7 +147,7 @@
Background="Transparent"
BorderBrush="#cdcbcb"
BorderThickness="1"
- Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
+ Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="▲"
Cursor="Hand"
@@ -134,7 +160,7 @@
Background="Transparent"
BorderBrush="#cdcbcb"
BorderThickness="1"
- Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
+ Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
CommandParameter="{Binding}"
Content="▼"
Cursor="Hand"
@@ -147,7 +173,7 @@
Background="Transparent"
BorderBrush="#E05050"
BorderThickness="1"
- Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
+ Command="{Binding DataContext.DeleteNodeCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
Content="✕"
Cursor="Hand"
FontSize="10"
@@ -159,17 +185,225 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml.cs b/XplorePlane/Views/Cnc/CncPageView.xaml.cs
index 172345e..af635de 100644
--- a/XplorePlane/Views/Cnc/CncPageView.xaml.cs
+++ b/XplorePlane/Views/Cnc/CncPageView.xaml.cs
@@ -1,7 +1,9 @@
-using System;
+using Prism.Ioc;
+using System;
+using System.Globalization;
using System.Windows;
using System.Windows.Controls;
-using Prism.Ioc;
+using System.Windows.Data;
using XplorePlane.ViewModels.Cnc;
namespace XplorePlane.Views.Cnc
@@ -31,5 +33,33 @@ namespace XplorePlane.Views.Cnc
}
}
}
+
+ private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs