树形节点图标直接复用项目现有图标资源;调整CNC编辑界面大小

This commit is contained in:
zhengxuan.zhang
2026-04-21 02:41:18 +08:00
parent 3b4b794dd0
commit d9d3e31e57
7 changed files with 933 additions and 269 deletions
@@ -205,6 +205,32 @@ namespace XplorePlane.Services.Cnc
return updated;
}
/// <inheritdoc />
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<CncNode>(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;
}
/// <inheritdoc />
public async Task SaveAsync(CncProgram program, string filePath)
{
+4 -12
View File
@@ -4,36 +4,28 @@ using XplorePlane.Models;
namespace XplorePlane.Services.Cnc
{
/// <summary>
/// CNC 程序管理服务接口,负责程序的创建、节点编辑、序列化/反序列化和文件读写
/// CNC program management service interface for creation, node editing, serialization and file I/O
/// CNC program management service interface.
/// </summary>
public interface ICncProgramService
{
/// <summary>创建空的 CNC 程序 | Create an empty CNC program</summary>
CncProgram CreateProgram(string name);
/// <summary>根据节点类型创建节点(从 IAppStateService 捕获设备状态)| Create a node by type (captures device state from IAppStateService)</summary>
CncNode CreateNode(CncNodeType type);
/// <summary>在指定索引之后插入节点并重新编号 | Insert a node after the given index and renumber</summary>
CncProgram InsertNode(CncProgram program, int afterIndex, CncNode node);
/// <summary>移除指定索引的节点并重新编号 | Remove the node at the given index and renumber</summary>
CncProgram RemoveNode(CncProgram program, int index);
/// <summary>将节点从旧索引移动到新索引并重新编号 | Move a node from old index to new index and renumber</summary>
CncProgram MoveNode(CncProgram program, int oldIndex, int newIndex);
/// <summary>将 CNC 程序保存到 .xp 文件 | Save CNC program to .xp file</summary>
CncProgram UpdateNode(CncProgram program, int index, CncNode node);
Task SaveAsync(CncProgram program, string filePath);
/// <summary>从 .xp 文件加载 CNC 程序 | Load CNC program from .xp file</summary>
Task<CncProgram> LoadAsync(string filePath);
/// <summary>将 CNC 程序序列化为 JSON 字符串 | Serialize CNC program to JSON string</summary>
string Serialize(CncProgram program);
/// <summary>从 JSON 字符串反序列化 CNC 程序 | Deserialize CNC program from JSON string</summary>
CncProgram Deserialize(string json);
}
}
}
+93 -152
View File
@@ -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
{
/// <summary>
/// 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.
/// </summary>
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<CncNodeViewModel> _nodes;
private ObservableCollection<CncNodeViewModel> _treeNodes;
private CncNodeViewModel _selectedNode;
private bool _isModified;
private string _programName;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
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<CncEditorViewModel>();
_nodes = new ObservableCollection<CncNodeViewModel>();
_treeNodes = new ObservableCollection<CncNodeViewModel>();
// ── 节点插入命令 | 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<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(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 ──────────────────────────────────────────
/// <summary>节点列表 | Node list</summary>
public ObservableCollection<CncNodeViewModel> Nodes
{
get => _nodes;
set => SetProperty(ref _nodes, value);
private set => SetProperty(ref _nodes, value);
}
public ObservableCollection<CncNodeViewModel> TreeNodes
{
get => _treeNodes;
private set => SetProperty(ref _treeNodes, value);
}
/// <summary>当前选中的节点 | Currently selected node</summary>
public CncNodeViewModel SelectedNode
{
get => _selectedNode;
set => SetProperty(ref _selectedNode, value);
set
{
if (SetProperty(ref _selectedNode, value))
{
RaisePropertyChanged(nameof(HasSelection));
}
}
}
/// <summary>程序是否已修改 | Whether the program has been modified</summary>
public bool HasSelection => SelectedNode != null;
public bool IsModified
{
get => _isModified;
set => SetProperty(ref _isModified, value);
}
/// <summary>当前程序名称 | Current program name</summary>
public string ProgramName
{
get => _programName;
set => SetProperty(ref _programName, value);
}
// ── 节点插入命令 | Node insertion commands ──────────────────────
/// <summary>插入参考点命令 | Insert reference point command</summary>
public DelegateCommand InsertReferencePointCommand { get; }
/// <summary>插入保存节点(含图像)命令 | Insert save node with image command</summary>
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
/// <summary>插入保存节点命令 | Insert save node command</summary>
public DelegateCommand InsertSaveNodeCommand { get; }
/// <summary>插入保存位置命令 | Insert save position command</summary>
public DelegateCommand InsertSavePositionCommand { get; }
/// <summary>插入检测模块命令 | Insert inspection module command</summary>
public DelegateCommand InsertInspectionModuleCommand { get; }
/// <summary>插入检测标记命令 | Insert inspection marker command</summary>
public DelegateCommand InsertInspectionMarkerCommand { get; }
/// <summary>插入停顿对话框命令 | Insert pause dialog command</summary>
public DelegateCommand InsertPauseDialogCommand { get; }
/// <summary>插入等待延时命令 | Insert wait delay command</summary>
public DelegateCommand InsertWaitDelayCommand { get; }
/// <summary>插入完成程序命令 | Insert complete program command</summary>
public DelegateCommand InsertCompleteProgramCommand { get; }
// ── 节点编辑命令 | Node editing commands ────────────────────────
/// <summary>删除选中节点命令 | Delete selected node command</summary>
public DelegateCommand DeleteNodeCommand { get; }
/// <summary>上移节点命令 | Move node up command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
/// <summary>下移节点命令 | Move node down command</summary>
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
// ── 文件操作命令 | File operation commands ──────────────────────
/// <summary>保存程序命令 | Save program command</summary>
public DelegateCommand SaveProgramCommand { get; }
/// <summary>加载程序命令 | Load program command</summary>
public DelegateCommand LoadProgramCommand { get; }
/// <summary>新建程序命令 | New program command</summary>
public DelegateCommand NewProgramCommand { get; }
/// <summary>导出 CSV 命令 | Export CSV command</summary>
public DelegateCommand ExportCsvCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
/// 插入指定类型的节点到选中节点之后
/// Insert a node of the specified type after the selected node
/// </summary>
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);
}
}
/// <summary>
/// 删除选中节点 | Delete the selected node
/// </summary>
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);
}
}
/// <summary>
/// 判断是否可以删除节点(至少保留 1 个节点)
/// Determines whether delete is allowed (at least 1 node must remain)
/// </summary>
private bool CanExecuteDeleteNode()
{
return SelectedNode != null
@@ -223,9 +172,6 @@ namespace XplorePlane.ViewModels.Cnc
&& _currentProgram.Nodes.Count > 1;
}
/// <summary>
/// 上移节点 | Move node up
/// </summary>
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);
}
}
/// <summary>
/// 下移节点 | Move node down
/// </summary>
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);
}
}
/// <summary>
/// 保存当前程序到文件 | Save current program to file
/// </summary>
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");
}
}
/// <summary>
/// 从文件加载程序 | Load program from file
/// </summary>
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");
}
}
/// <summary>
/// 创建新程序 | Create a new program
/// </summary>
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);
}
/// <summary>
/// 导出当前程序为 CSV 文件 | Export current program to CSV file
/// </summary>
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");
}
}
/// <summary>
/// CSV 字段转义:含逗号、引号或换行时用双引号包裹
/// Escape CSV field: wrap with double quotes if it contains comma, quote, or newline
/// </summary>
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 ───────────────────────────────────
/// <summary>
/// 编辑操作后的统一处理:刷新节点、标记已修改、发布变更事件
/// Unified post-edit handling: refresh nodes, mark modified, publish change event
/// </summary>
private void OnProgramEdited()
{
IsModified = true;
@@ -431,34 +336,70 @@ namespace XplorePlane.ViewModels.Cnc
PublishProgramChanged();
}
/// <summary>
/// 从 _currentProgram.Nodes 重建 Nodes 集合
/// Rebuild the Nodes collection from _currentProgram.Nodes
/// </summary>
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<CncNodeViewModel>();
var rootNodes = new List<CncNodeViewModel>();
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<CncNodeViewModel>(flatNodes);
TreeNodes = new ObservableCollection<CncNodeViewModel>(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;
}
/// <summary>
/// 通过 IEventAggregator 发布 CNC 程序变更事件
/// Publish CNC program changed event via IEventAggregator
/// </summary>
private void PublishProgramChanged()
{
_eventAggregator
+491 -50
View File
@@ -1,89 +1,530 @@
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using XplorePlane.Models;
namespace XplorePlane.ViewModels.Cnc
{
/// <summary>
/// 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.
/// </summary>
public class CncNodeViewModel : BindableBase
{
private int _index;
private string _name;
private CncNodeType _nodeType;
private readonly Action<CncNodeViewModel, CncNode> _modelChangedCallback;
private CncNode _model;
private string _icon;
private bool _isExpanded = true;
/// <summary>
/// 构造函数,从 CncNode 模型初始化 ViewModel
/// Constructor that initializes the ViewModel from a CncNode model
/// </summary>
public CncNodeViewModel(CncNode model)
public CncNodeViewModel(CncNode model, Action<CncNodeViewModel, CncNode> 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<CncNodeViewModel>();
}
/// <summary>底层 CNC 节点模型(只读)| Underlying CNC node model (read-only)</summary>
public CncNode Model { get; }
public ObservableCollection<CncNodeViewModel> Children { get; }
/// <summary>节点在程序中的索引 | Node index in the program</summary>
public int Index
{
get => _index;
set => SetProperty(ref _index, value);
}
public CncNode Model => _model;
public Guid Id => _model.Id;
public int Index => _model.Index;
/// <summary>节点显示名称 | Node display name</summary>
public string Name
{
get => _name;
set => SetProperty(ref _name, value);
get => _model.Name;
set => UpdateModel(_model with { Name = value ?? string.Empty });
}
/// <summary>节点类型 | Node type</summary>
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 });
}
}
}
/// <summary>节点图标路径 | Node icon path</summary>
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();
}
/// <summary>
/// 根据节点类型返回对应的图标路径
/// Returns the icon path for the given node type
/// </summary>
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
}
}
}
}
+1 -1
View File
@@ -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 编辑器已切换到主界面图像区域");
}
+286 -52
View File
@@ -1,27 +1,26 @@
<UserControl
<UserControl
x:Class="XplorePlane.Views.Cnc.CncPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:XplorePlane.Views.Cnc"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:views="clr-namespace:XplorePlane.Views"
d:DesignHeight="700"
d:DesignWidth="544"
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
d:DesignHeight="760"
d:DesignWidth="1040"
prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<SolidColorBrush x:Key="PanelBg" Color="White" />
<SolidColorBrush x:Key="PanelBorder" Color="#cdcbcb" />
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<Style x:Key="CncNodeItemStyle" TargetType="ListBoxItem">
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
<SolidColorBrush x:Key="TreeItemHover" Color="#F5F9FF" />
<SolidColorBrush x:Key="TreeItemSelected" Color="#E8F0FE" />
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
@@ -34,57 +33,84 @@
<Setter Property="FontSize" Value="11" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="Padding" Value="0" />
<Setter Property="Margin" Value="0" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</UserControl.Resources>
<Border
MinWidth="550"
Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="280" MinWidth="240" />
<ColumnDefinition Width="1" />
<ColumnDefinition Width="*" MinWidth="300" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" MinWidth="230" />
<ColumnDefinition Width="1" />
<ColumnDefinition Width="*" MinWidth="260" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!--树行节点显示-->
<ListBox
x:Name="CncNodeListBox"
Grid.Column="0"
<Border
Grid.Row="0"
Padding="10,8"
BorderBrush="{StaticResource SeparatorBrush}"
BorderThickness="0,0,0,1">
<StackPanel>
<TextBlock
FontFamily="Microsoft YaHei UI"
FontSize="14"
FontWeight="SemiBold"
Text="{Binding ProgramName, TargetNullValue=CNC Program}" />
<TextBlock
Margin="0,4,0,0"
FontFamily="Microsoft YaHei UI"
FontSize="11"
Foreground="#666"
Text="模块节点下会自动归类显示标记/等待/消息节点" />
</StackPanel>
</Border>
<TreeView
x:Name="CncTreeView"
Grid.Row="1"
Background="Transparent"
BorderThickness="0"
ItemContainerStyle="{StaticResource CncNodeItemStyle}"
ItemsSource="{Binding Nodes}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="NodeRoot" MinHeight="40">
ItemsSource="{Binding TreeNodes}"
SelectedItemChanged="CncTreeView_SelectedItemChanged">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
<Grid x:Name="NodeRoot" MinHeight="42">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="34" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Width="28"
Height="28"
Width="26"
Height="26"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="#E8F0FE"
BorderBrush="#5B9BD5"
BorderThickness="1.5"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="4">
<Image
Width="20"
Height="20"
Width="18"
Height="18"
Source="{Binding Icon}"
Stretch="Uniform" />
</Border>
@@ -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 @@
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Rectangle
Grid.Column="1"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<views:PipelineEditorView Grid.Column="2" />
</HierarchicalDataTemplate>
</TreeView.Resources>
<TreeView.ItemContainerStyle>
<Style BasedOn="{StaticResource TreeItemStyle}" TargetType="TreeViewItem" />
</TreeView.ItemContainerStyle>
</TreeView>
</Grid>
<Rectangle
Grid.Column="1"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
<Grid Margin="18,18,22,18">
<Grid.Resources>
<Style x:Key="EditorTitle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
<Style x:Key="LabelStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Foreground" Value="#666" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style x:Key="EditorBox" TargetType="TextBox">
<Setter Property="Height" Value="30" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="Margin" Value="0,0,0,12" />
<Setter Property="BorderBrush" Value="#CFCFCF" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
</Style>
<Style x:Key="EditorCheck" TargetType="CheckBox">
<Setter Property="Margin" Value="0,4,0,12" />
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
</Style>
</Grid.Resources>
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
<UniformGrid Columns="2" Margin="0,0,0,12">
<StackPanel Margin="0,0,12,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
</StackPanel>
</UniformGrid>
<GroupBox
Margin="0,0,0,12"
Header="运动参数"
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
<UniformGrid Margin="12" Columns="2">
<StackPanel Margin="0,0,12,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,12,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel Margin="0,0,12,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</UniformGrid>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="射线源"
Visibility="{Binding SelectedNode.IsReferencePoint, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电流 (uA)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Current, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="采集参数"
Visibility="{Binding SelectedNode.IsSaveNode, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="采集参数"
Visibility="{Binding SelectedNode.IsSaveNodeWithImage, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="图像文件" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ImageFileName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="检测模块"
Visibility="{Binding SelectedNode.IsInspectionModule, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<TextBlock Style="{StaticResource LabelStyle}" Text="Pipeline 名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.PipelineName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="标记参数"
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerType, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerX" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerX, UpdateSourceTrigger=LostFocus}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerY" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerY, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="消息弹窗"
Visibility="{Binding SelectedNode.IsPauseDialog, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<TextBlock Style="{StaticResource LabelStyle}" Text="标题" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DialogTitle, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Style="{StaticResource LabelStyle}" Text="消息内容" />
<TextBox
MinHeight="80"
Margin="0,0,0,12"
Padding="8,6"
AcceptsReturn="True"
Text="{Binding SelectedNode.DialogMessage, UpdateSourceTrigger=PropertyChanged}"
TextWrapping="Wrap" />
</StackPanel>
</GroupBox>
<GroupBox
Margin="0,0,0,12"
Header="等待参数"
Visibility="{Binding SelectedNode.IsWaitDelay, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="12">
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
</StackPanel>
</GroupBox>
</StackPanel>
<Border
Padding="24"
Background="#FAFAFA"
BorderBrush="#E6E6E6"
BorderThickness="1"
CornerRadius="6"
Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Invert}">
<StackPanel>
<TextBlock
FontFamily="Microsoft YaHei UI"
FontSize="15"
FontWeight="SemiBold"
Text="未选择节点" />
<TextBlock
Margin="0,8,0,0"
FontFamily="Microsoft YaHei UI"
Foreground="#666"
Text="从左侧树中选择一个节点后,这里会显示并允许编辑该节点的参数。" />
</StackPanel>
</Border>
</Grid>
</ScrollViewer>
</Grid>
</Border>
</UserControl>
+32 -2
View File
@@ -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<object> e)
{
if (DataContext is CncEditorViewModel viewModel)
{
viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
}
}
}
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var invert = string.Equals(parameter as string, "Invert", StringComparison.OrdinalIgnoreCase);
var isVisible = value != null;
if (invert)
{
isVisible = !isVisible;
}
return isVisible ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
}