4 Commits

Author SHA1 Message Date
zhengxuan.zhang f64a0f7b31 优化CNC编辑为2列布局 2026-04-24 11:18:44 +08:00
zhengxuan.zhang dbfa09a9fd 检测模块_0 成连续编号 2026-04-24 10:57:21 +08:00
zhengxuan.zhang f3e77562b1 高亮节点、取消前面的[n]序号、参考位置,保存位置,参数区为只读 2026-04-24 10:44:22 +08:00
zhengxuan.zhang 9911566675 增加节点右键菜单功能,支持上方插入和下方插入 2026-04-24 01:58:11 +08:00
6 changed files with 654 additions and 217 deletions
+13 -2
View File
@@ -364,9 +364,20 @@ namespace XplorePlane.Services.Cnc
private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes) private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes)
{ {
var result = new List<CncNode>(nodes.Count); var result = new List<CncNode>(nodes.Count);
int referencePointNumber = 0;
int savePositionNumber = 0;
int inspectionModuleNumber = 0;
for (int i = 0; i < nodes.Count; i++) for (int i = 0; i < nodes.Count; i++)
{ {
result.Add(nodes[i] with { Index = i }); var indexedNode = nodes[i] with { Index = i };
result.Add(indexedNode switch
{
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
_ => indexedNode
});
} }
return result.AsReadOnly(); return result.AsReadOnly();
} }
@@ -414,7 +425,7 @@ namespace XplorePlane.Services.Cnc
private SavePositionNode CreateSavePositionNode(Guid id, int index) private SavePositionNode CreateSavePositionNode(Guid id, int index)
{ {
return new SavePositionNode( return new SavePositionNode(
id, index, $"保存位置_{index}", id, index, $"检测位置_{index}",
MotionState: _appStateService.MotionState); MotionState: _appStateService.MotionState);
} }
private double TryReadCurrent() private double TryReadCurrent()
@@ -36,6 +36,8 @@ namespace XplorePlane.ViewModels.Cnc
private string _programName; private string _programName;
private string _programDisplayName = "新建检测程序.xp"; private string _programDisplayName = "新建检测程序.xp";
private Guid? _preferredSelectedNodeId; private Guid? _preferredSelectedNodeId;
private Guid? _pendingInsertAnchorNodeId;
private bool _pendingInsertAfterAnchor;
public CncEditorViewModel( public CncEditorViewModel(
ICncProgramService cncProgramService, ICncProgramService cncProgramService,
@@ -69,6 +71,8 @@ namespace XplorePlane.ViewModels.Cnc
.ObservesProperty(() => SelectedNode); .ObservesProperty(() => SelectedNode);
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp); MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown); MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync()); SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync()); LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
@@ -144,6 +148,8 @@ namespace XplorePlane.ViewModels.Cnc
public DelegateCommand DeleteNodeCommand { get; } public DelegateCommand DeleteNodeCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; } public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertAboveCommand { get; }
public DelegateCommand<CncNodeViewModel> PrepareInsertBelowCommand { get; }
public DelegateCommand SaveProgramCommand { get; } public DelegateCommand SaveProgramCommand { get; }
public DelegateCommand LoadProgramCommand { get; } public DelegateCommand LoadProgramCommand { get; }
public DelegateCommand NewProgramCommand { get; } public DelegateCommand NewProgramCommand { get; }
@@ -162,6 +168,7 @@ namespace XplorePlane.ViewModels.Cnc
int afterIndex = ResolveInsertAfterIndex(nodeType); int afterIndex = ResolveInsertAfterIndex(nodeType);
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node); _currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
_preferredSelectedNodeId = node.Id; _preferredSelectedNodeId = node.Id;
ClearPendingInsertAnchor();
OnProgramEdited(); OnProgramEdited();
_logger.Info("Inserted node: Type={NodeType}", nodeType); _logger.Info("Inserted node: Type={NodeType}", nodeType);
@@ -179,21 +186,24 @@ namespace XplorePlane.ViewModels.Cnc
try try
{ {
int deletedIndex = SelectedNode.Index;
if (SelectedNode.IsSavePosition) if (SelectedNode.IsSavePosition)
{ {
var nodes = _currentProgram.Nodes.ToList(); var nodes = _currentProgram.Nodes.ToList();
int startIndex = SelectedNode.Index; int startIndex = deletedIndex;
int endIndex = GetSavePositionBlockEndIndex(startIndex); int endIndex = GetSavePositionBlockEndIndex(startIndex);
nodes.RemoveRange(startIndex, endIndex - startIndex + 1); nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
_currentProgram = ReplaceProgramNodes(nodes); _currentProgram = ReplaceProgramNodes(nodes);
} }
else else
{ {
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index); _currentProgram = _cncProgramService.RemoveNode(_currentProgram, deletedIndex);
} }
OnProgramEdited(); OnProgramEdited();
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index); ClearPendingInsertAnchorIfMissing();
_logger.Info("Deleted node at index: {Index}", deletedIndex);
} }
catch (ArgumentOutOfRangeException ex) catch (ArgumentOutOfRangeException ex)
{ {
@@ -305,6 +315,7 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = Path.GetFileName(dlg.FileName); ProgramDisplayName = Path.GetFileName(dlg.FileName);
IsModified = false; IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes(); RefreshNodes();
} }
catch (Exception ex) catch (Exception ex)
@@ -320,6 +331,7 @@ namespace XplorePlane.ViewModels.Cnc
ProgramName = _currentProgram.Name; ProgramName = _currentProgram.Name;
ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name); ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name);
IsModified = false; IsModified = false;
ClearPendingInsertAnchor();
RefreshNodes(); RefreshNodes();
} }
@@ -404,6 +416,8 @@ namespace XplorePlane.ViewModels.Cnc
private void RefreshNodes() private void RefreshNodes()
{ {
NormalizeDefaultNodeNamesInCurrentProgram();
var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id; var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded); var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
@@ -454,6 +468,31 @@ namespace XplorePlane.ViewModels.Cnc
_preferredSelectedNodeId = null; _preferredSelectedNodeId = null;
} }
private void NormalizeDefaultNodeNamesInCurrentProgram()
{
if (_currentProgram?.Nodes == null || _currentProgram.Nodes.Count == 0)
{
return;
}
var normalizedNodes = ApplyDefaultNodeNames(_currentProgram.Nodes);
bool changed = false;
for (int i = 0; i < normalizedNodes.Count; i++)
{
if (!Equals(normalizedNodes[i], _currentProgram.Nodes[i]))
{
changed = true;
break;
}
}
if (changed)
{
_currentProgram = _currentProgram with { Nodes = normalizedNodes };
}
}
private int ResolveInsertAfterIndex(CncNodeType nodeType) private int ResolveInsertAfterIndex(CncNodeType nodeType)
{ {
if (_currentProgram == null || _currentProgram.Nodes.Count == 0) if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
@@ -461,8 +500,19 @@ namespace XplorePlane.ViewModels.Cnc
return -1; return -1;
} }
if (TryResolvePendingInsertAfterIndex(nodeType, out int pendingAfterIndex))
{
return pendingAfterIndex;
}
if (!IsSavePositionChild(nodeType)) if (!IsSavePositionChild(nodeType))
{ {
int? currentSavePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
if (currentSavePositionIndex.HasValue)
{
return GetSavePositionBlockEndIndex(currentSavePositionIndex.Value);
}
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1; return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
} }
@@ -475,6 +525,75 @@ namespace XplorePlane.ViewModels.Cnc
return GetSavePositionBlockEndIndex(savePositionIndex.Value); return GetSavePositionBlockEndIndex(savePositionIndex.Value);
} }
private void SetPendingInsertAnchor(CncNodeViewModel nodeVm, bool insertAfter)
{
if (_currentProgram == null || nodeVm == null)
{
return;
}
_pendingInsertAnchorNodeId = nodeVm.Id;
_pendingInsertAfterAnchor = insertAfter;
SelectedNode = nodeVm;
}
private bool TryResolvePendingInsertAfterIndex(CncNodeType nodeType, out int afterIndex)
{
afterIndex = -1;
if (!_pendingInsertAnchorNodeId.HasValue || _currentProgram == null || IsSavePositionChild(nodeType))
{
return false;
}
int anchorIndex = FindNodeIndexById(_pendingInsertAnchorNodeId.Value);
if (anchorIndex < 0)
{
ClearPendingInsertAnchor();
return false;
}
afterIndex = _pendingInsertAfterAnchor ? anchorIndex : anchorIndex - 1;
return true;
}
private int FindNodeIndexById(Guid nodeId)
{
if (_currentProgram?.Nodes == null)
{
return -1;
}
for (int i = 0; i < _currentProgram.Nodes.Count; i++)
{
if (_currentProgram.Nodes[i].Id == nodeId)
{
return i;
}
}
return -1;
}
private void ClearPendingInsertAnchor()
{
_pendingInsertAnchorNodeId = null;
_pendingInsertAfterAnchor = false;
}
private void ClearPendingInsertAnchorIfMissing()
{
if (!_pendingInsertAnchorNodeId.HasValue)
{
return;
}
if (FindNodeIndexById(_pendingInsertAnchorNodeId.Value) < 0)
{
ClearPendingInsertAnchor();
}
}
private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown) private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown)
{ {
int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index); int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index);
@@ -627,10 +746,7 @@ namespace XplorePlane.ViewModels.Cnc
private CncProgram ReplaceProgramNodes(List<CncNode> nodes) private CncProgram ReplaceProgramNodes(List<CncNode> nodes)
{ {
var renumberedNodes = nodes var renumberedNodes = ApplyDefaultNodeNames(nodes);
.Select((node, index) => node with { Index = index })
.ToList()
.AsReadOnly();
return _currentProgram with return _currentProgram with
{ {
@@ -639,6 +755,28 @@ namespace XplorePlane.ViewModels.Cnc
}; };
} }
private static IReadOnlyList<CncNode> ApplyDefaultNodeNames(IReadOnlyList<CncNode> nodes)
{
var result = new List<CncNode>(nodes.Count);
int referencePointNumber = 0;
int savePositionNumber = 0;
int inspectionModuleNumber = 0;
for (int i = 0; i < nodes.Count; i++)
{
var indexedNode = nodes[i] with { Index = i };
result.Add(indexedNode switch
{
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
_ => indexedNode
});
}
return result.AsReadOnly();
}
private static bool IsSavePositionChild(CncNodeType type) private static bool IsSavePositionChild(CncNodeType type)
{ {
return type is CncNodeType.InspectionMarker return type is CncNodeType.InspectionMarker
@@ -37,6 +37,8 @@ namespace XplorePlane.ViewModels.Cnc
set => UpdateModel(_model with { Name = value ?? string.Empty }); set => UpdateModel(_model with { Name = value ?? string.Empty });
} }
public bool IsReadOnlyNodeProperties => IsReferencePoint || IsSavePosition;
public CncNodeType NodeType => _model.NodeType; public CncNodeType NodeType => _model.NodeType;
public string NodeTypeDisplay => NodeType.ToString(); public string NodeTypeDisplay => NodeType.ToString();
+1 -1
View File
@@ -21,7 +21,7 @@ namespace XplorePlane.ViewModels
{ {
public class MainViewModel : BindableBase public class MainViewModel : BindableBase
{ {
private const double CncEditorHostWidth = 710d; private const double CncEditorHostWidth = 502d;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IContainerProvider _containerProvider; private readonly IContainerProvider _containerProvider;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
+40 -76
View File
@@ -9,21 +9,20 @@
xmlns:views="clr-namespace:XplorePlane.Views" xmlns:views="clr-namespace:XplorePlane.Views"
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc" xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
d:DesignHeight="760" d:DesignHeight="760"
d:DesignWidth="702" d:DesignWidth="502"
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<local:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" />
<local:BoolToDisplayTextConverter x:Key="BoolToDisplayTextConverter" />
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" /> <local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<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="#E5E5E5" /> <SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" /> <SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
<SolidColorBrush x:Key="TreeParentBg" Color="#F6F8FB" />
<SolidColorBrush x:Key="TreeChildBg" Color="#FBFCFE" />
<SolidColorBrush x:Key="TreeAccentBrush" Color="#2E6FA3" />
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#C7D4DF" /> <SolidColorBrush x:Key="TreeChildLineBrush" Color="#C7D4DF" />
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily> <FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
@@ -58,6 +57,19 @@
<Setter Property="FontSize" Value="11" /> <Setter Property="FontSize" Value="11" />
</Style> </Style>
<Style x:Key="DisplayValueLabel" TargetType="Label">
<Setter Property="Height" Value="28" />
<Setter Property="Padding" Value="8,3" />
<Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="Background" Value="#F7F8FA" />
<Setter Property="BorderBrush" Value="#D9DDE3" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="Foreground" Value="#333333" />
</Style>
<Style x:Key="EditorCheck" TargetType="CheckBox"> <Style x:Key="EditorCheck" TargetType="CheckBox">
<Setter Property="Margin" Value="0,2,0,8" /> <Setter Property="Margin" Value="0,2,0,8" />
<Setter Property="FontFamily" Value="{StaticResource UiFont}" /> <Setter Property="FontFamily" Value="{StaticResource UiFont}" />
@@ -86,8 +98,8 @@
</UserControl.Resources> </UserControl.Resources>
<Border <Border
Width="702" Width="502"
MinWidth="702" MinWidth="502"
HorizontalAlignment="Left" HorizontalAlignment="Left"
Background="{StaticResource PanelBg}" Background="{StaticResource PanelBg}"
BorderBrush="{StaticResource PanelBorder}" BorderBrush="{StaticResource PanelBorder}"
@@ -97,9 +109,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="200" /> <ColumnDefinition Width="200" />
<ColumnDefinition Width="1" /> <ColumnDefinition Width="1" />
<ColumnDefinition Width="250" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="1" />
<ColumnDefinition Width="250" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0"> <Grid Grid.Column="0">
@@ -233,24 +243,15 @@
Stretch="Uniform" /> Stretch="Uniform" />
</Border> </Border>
<StackPanel <TextBlock
Grid.Column="2" Grid.Column="2"
Margin="3,0,0,0" Margin="3,0,0,0"
VerticalAlignment="Center"
Orientation="Horizontal">
<TextBlock
VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#888888"
Text="{Binding Index, StringFormat='[{0}] '}" />
<TextBlock
VerticalAlignment="Center" VerticalAlignment="Center"
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="11" FontSize="11"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="{Binding Name}" /> Text="{Binding Name}"
</StackPanel> TextTrimming="CharacterEllipsis" />
<StackPanel <StackPanel
x:Name="NodeActions" x:Name="NodeActions"
@@ -299,7 +300,10 @@
Width="1" Width="1"
Fill="{StaticResource SeparatorBrush}" /> Fill="{StaticResource SeparatorBrush}" />
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto"> <Grid Grid.Column="2">
<ScrollViewer
x:Name="NodePropertyEditor"
VerticalScrollBarVisibility="Auto">
<Grid Margin="10"> <Grid Margin="10">
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}"> <StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" /> <TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
@@ -310,11 +314,11 @@
<UniformGrid Margin="0,0,0,8" Columns="2"> <UniformGrid Margin="0,0,0,8" Columns="2">
<StackPanel Margin="0,0,6,0"> <StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" /> <TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" /> <Label Style="{StaticResource DisplayValueLabel}" Content="{Binding SelectedNode.Index, Mode=OneWay}" />
</StackPanel> </StackPanel>
<StackPanel> <StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" /> <TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" /> <Label Style="{StaticResource DisplayValueLabel}" Content="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
</StackPanel> </StackPanel>
</UniformGrid> </UniformGrid>
@@ -403,16 +407,6 @@
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox
Style="{StaticResource CompactGroupBox}"
Header="检测模块"
Visibility="{Binding SelectedNode.IsInspectionModule, Converter={StaticResource BoolToVisibilityConverter}}">
<StackPanel Margin="8,8,8,6">
<TextBlock Style="{StaticResource LabelStyle}" Text="Pipeline 名称" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.PipelineName, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</GroupBox>
<GroupBox <GroupBox
Style="{StaticResource CompactGroupBox}" Style="{StaticResource CompactGroupBox}"
Header="检测标记" Header="检测标记"
@@ -459,14 +453,22 @@
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
</StackPanel> </StackPanel>
</Grid>
</ScrollViewer>
<views:PipelineEditorView
x:Name="InspectionModulePipelineEditor"
Margin="0"
Visibility="{Binding EditorVisibility}" />
<Border <Border
Padding="12" x:Name="NodePropertyEmptyState"
Margin="12"
Padding="16"
Background="#FAFAFA" Background="#FAFAFA"
BorderBrush="#E6E6E6" BorderBrush="#E6E6E6"
BorderThickness="1" BorderThickness="1"
CornerRadius="6" CornerRadius="6">
Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Invert}">
<StackPanel> <StackPanel>
<TextBlock <TextBlock
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
@@ -479,45 +481,7 @@
FontFamily="{StaticResource UiFont}" FontFamily="{StaticResource UiFont}"
FontSize="10" FontSize="10"
Foreground="#666666" Foreground="#666666"
Text="从左侧树中选择一个节点后,这里会显示可编辑的参数。" Text="从左侧树中选择一个节点后,这里会显示对应的参数或检测流程。"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</Grid>
</ScrollViewer>
<Rectangle
Grid.Column="3"
Width="1"
Fill="{StaticResource SeparatorBrush}" />
<Grid Grid.Column="4">
<views:PipelineEditorView
x:Name="InspectionModulePipelineEditor"
Visibility="{Binding EditorVisibility}" />
<Border
x:Name="InspectionModulePipelineEmptyState"
Margin="12"
Padding="16"
Background="#FAFAFA"
BorderBrush="#E6E6E6"
BorderThickness="1"
CornerRadius="6"
Visibility="{Binding EmptyStateVisibility}">
<StackPanel>
<TextBlock
FontFamily="{StaticResource UiFont}"
FontSize="13"
FontWeight="SemiBold"
Text="未选择检测模块"
TextWrapping="Wrap" />
<TextBlock
Margin="0,6,0,0"
FontFamily="{StaticResource UiFont}"
FontSize="10"
Foreground="#666666"
Text="请选择一个检测模块节点后,在这里拖拽算子并配置参数。"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</Border> </Border>
+323 -1
View File
@@ -1,10 +1,13 @@
using Prism.Ioc; using Prism.Ioc;
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data; using System.Windows.Data;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Media;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.ViewModels.Cnc; using XplorePlane.ViewModels.Cnc;
@@ -16,7 +19,17 @@ namespace XplorePlane.Views.Cnc
/// </summary> /// </summary>
public partial class CncPageView : UserControl public partial class CncPageView : UserControl
{ {
private static readonly Brush SelectedNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E7F0F7"));
private static readonly Brush SelectedNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CB9D1"));
private static readonly Brush HoverNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F6FAFC"));
private static readonly Brush HoverNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D7E4EE"));
private static readonly Brush SelectedNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F4E79"));
private static readonly Brush DefaultNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#202020"));
private static readonly Brush TransparentBrush = Brushes.Transparent;
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel; private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
private readonly Dictionary<TextBox, Label> _textDisplayLabels = new();
private readonly Dictionary<CheckBox, Label> _checkDisplayLabels = new();
public CncPageView() public CncPageView()
{ {
@@ -56,12 +69,18 @@ namespace XplorePlane.Views.Cnc
logger); logger);
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel; InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
InspectionModulePipelineEmptyState.DataContext = _inspectionModulePipelineViewModel;
} }
catch (Exception) catch (Exception)
{ {
// keep page usable even if pipeline editor host setup fails // keep page usable even if pipeline editor host setup fails
} }
CncTreeView.ContextMenuOpening -= CncTreeView_ContextMenuOpening;
CncTreeView.ContextMenuOpening += CncTreeView_ContextMenuOpening;
CncTreeView.LayoutUpdated -= CncTreeView_LayoutUpdated;
CncTreeView.LayoutUpdated += CncTreeView_LayoutUpdated;
UpdateNodeVisualState();
UpdatePropertyEditorState();
} }
private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
@@ -70,6 +89,9 @@ namespace XplorePlane.Views.Cnc
{ {
viewModel.SelectedNode = e.NewValue as CncNodeViewModel; viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
} }
UpdateNodeVisualState();
UpdatePropertyEditorState();
} }
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e) private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
@@ -83,6 +105,280 @@ namespace XplorePlane.Views.Cnc
viewModel.DeleteNodeCommand.Execute(); viewModel.DeleteNodeCommand.Execute();
e.Handled = true; e.Handled = true;
} }
private void CncTreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
if (DataContext is not CncEditorViewModel viewModel)
{
return;
}
var position = Mouse.GetPosition(CncTreeView);
var hit = VisualTreeHelper.HitTest(CncTreeView, position);
var treeViewItem = FindAncestor<TreeViewItem>(hit?.VisualHit);
if (treeViewItem?.DataContext is not CncNodeViewModel nodeVm)
{
CncTreeView.ContextMenu = null;
return;
}
viewModel.SelectedNode = nodeVm;
UpdateNodeVisualState();
UpdatePropertyEditorState();
CncTreeView.ContextMenu = new ContextMenu
{
Items =
{
new MenuItem
{
Header = "\u5728\u4E0A\u65B9\u63D2\u5165\u4F4D\u7F6E",
Command = viewModel.PrepareInsertAboveCommand,
CommandParameter = nodeVm
},
new MenuItem
{
Header = "\u5728\u4E0B\u65B9\u63D2\u5165\u4F4D\u7F6E",
Command = viewModel.PrepareInsertBelowCommand,
CommandParameter = nodeVm
}
}
};
}
private void CncTreeView_LayoutUpdated(object sender, EventArgs e)
{
HideInlineDeleteButtons();
UpdateNodeVisualState();
UpdatePropertyEditorState();
}
private void HideInlineDeleteButtons()
{
foreach (var button in FindVisualDescendants<Button>(CncTreeView))
{
if (button.ToolTip is string)
{
button.Visibility = Visibility.Collapsed;
button.IsHitTestVisible = false;
}
}
}
private void UpdateNodeVisualState()
{
foreach (var item in FindVisualDescendants<TreeViewItem>(CncTreeView))
{
if (item.DataContext is not CncNodeViewModel)
{
continue;
}
var card = FindNodeCard(item);
if (card == null)
{
continue;
}
if (item.IsSelected)
{
card.Background = SelectedNodeBackground;
card.BorderBrush = SelectedNodeBorder;
ApplyNodeTextForeground(card, SelectedNodeForeground);
}
else if (card.IsMouseOver)
{
card.Background = HoverNodeBackground;
card.BorderBrush = HoverNodeBorder;
ApplyNodeTextForeground(card, DefaultNodeForeground);
}
else
{
card.Background = TransparentBrush;
card.BorderBrush = TransparentBrush;
ApplyNodeTextForeground(card, DefaultNodeForeground);
}
}
}
private void UpdatePropertyEditorState()
{
if (DataContext is not CncEditorViewModel viewModel)
{
return;
}
var propertyEditorRoot = FindPropertyEditorRoot();
if (propertyEditorRoot == null)
{
return;
}
bool isReadOnlyNode = viewModel.SelectedNode?.IsReadOnlyNodeProperties == true;
bool showNodeProperties = viewModel.SelectedNode != null && !viewModel.SelectedNode.IsInspectionModule;
NodePropertyEditor.Visibility = showNodeProperties ? Visibility.Visible : Visibility.Collapsed;
NodePropertyEmptyState.Visibility = viewModel.SelectedNode == null ? Visibility.Visible : Visibility.Collapsed;
foreach (var textBox in FindVisualDescendants<TextBox>(propertyEditorRoot))
{
var bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty);
string bindingPath = bindingExpression?.ParentBinding?.Path?.Path ?? string.Empty;
if (string.IsNullOrWhiteSpace(bindingPath))
{
continue;
}
bool alwaysDisplay = bindingPath is "SelectedNode.Index" or "SelectedNode.NodeTypeDisplay";
var label = EnsureTextDisplayLabel(textBox, bindingPath);
bool showLabel = alwaysDisplay || isReadOnlyNode;
textBox.IsReadOnly = showLabel;
textBox.Visibility = showLabel ? Visibility.Collapsed : Visibility.Visible;
label.Visibility = showLabel ? Visibility.Visible : Visibility.Collapsed;
}
foreach (var checkBox in FindVisualDescendants<CheckBox>(propertyEditorRoot))
{
var bindingExpression = checkBox.GetBindingExpression(ToggleButton.IsCheckedProperty);
string bindingPath = bindingExpression?.ParentBinding?.Path?.Path ?? string.Empty;
if (string.IsNullOrWhiteSpace(bindingPath))
{
continue;
}
var label = EnsureCheckDisplayLabel(checkBox, bindingPath);
checkBox.IsEnabled = !isReadOnlyNode;
checkBox.Visibility = isReadOnlyNode ? Visibility.Collapsed : Visibility.Visible;
label.Visibility = isReadOnlyNode ? Visibility.Visible : Visibility.Collapsed;
}
}
private Label EnsureTextDisplayLabel(TextBox textBox, string bindingPath)
{
if (_textDisplayLabels.TryGetValue(textBox, out var existingLabel))
{
return existingLabel;
}
var label = new Label
{
Style = TryFindResource("DisplayValueLabel") as Style,
Visibility = Visibility.Collapsed
};
label.SetBinding(ContentProperty, new Binding(bindingPath));
InsertCompanionControl(textBox, label);
_textDisplayLabels[textBox] = label;
return label;
}
private Label EnsureCheckDisplayLabel(CheckBox checkBox, string bindingPath)
{
if (_checkDisplayLabels.TryGetValue(checkBox, out var existingLabel))
{
return existingLabel;
}
var label = new Label
{
Style = TryFindResource("DisplayValueLabel") as Style,
Visibility = Visibility.Collapsed
};
label.SetBinding(ContentProperty, new Binding(bindingPath)
{
Converter = TryFindResource("BoolToDisplayTextConverter") as IValueConverter
});
InsertCompanionControl(checkBox, label);
_checkDisplayLabels[checkBox] = label;
return label;
}
private static void InsertCompanionControl(Control sourceControl, Control companionControl)
{
if (VisualTreeHelper.GetParent(sourceControl) is not Panel panel)
{
return;
}
if (panel.Children.Contains(companionControl))
{
return;
}
int index = panel.Children.IndexOf(sourceControl);
panel.Children.Insert(index + 1, companionControl);
}
private static void ApplyNodeTextForeground(Border card, Brush foreground)
{
foreach (var textBlock in FindVisualDescendants<TextBlock>(card))
{
if (textBlock.Visibility == Visibility.Visible)
{
textBlock.Foreground = foreground;
}
}
}
private DependencyObject FindPropertyEditorRoot()
{
return NodePropertyEditor;
}
private static Border FindNodeCard(DependencyObject root)
{
foreach (var border in FindVisualDescendants<Border>(root))
{
if (border.DataContext is CncNodeViewModel && border.CornerRadius.TopLeft == 4 && border.BorderThickness.Left == 1)
{
return border;
}
}
return null;
}
private static T FindAncestor<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var current = dependencyObject;
while (current != null)
{
if (current is T match)
{
return match;
}
current = VisualTreeHelper.GetParent(current);
}
return null;
}
private static System.Collections.Generic.IEnumerable<T> FindVisualDescendants<T>(DependencyObject root) where T : DependencyObject
{
if (root == null)
{
yield break;
}
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(root, i);
if (child is T typed)
{
yield return typed;
}
foreach (var descendant in FindVisualDescendants<T>(child))
{
yield return descendant;
}
}
}
} }
public class NullToVisibilityConverter : IValueConverter public class NullToVisibilityConverter : IValueConverter
@@ -104,4 +400,30 @@ namespace XplorePlane.Views.Cnc
throw new NotSupportedException(); throw new NotSupportedException();
} }
} }
public class InverseBooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is true ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
public class BoolToDisplayTextConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is true ? "\u662F" : "\u5426";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
} }