phase 2: VM + TreeView skeleton for alias tree

- AliasNodeViewModel: INotifyPropertyChanged, alias/display/editing state
- AliasTreeIcons.xaml: vector icon geometry + toolbar/button styles
- AliasTreeControl.xaml: TreeView with icon toolbar, status bar, context menu
- AliasTreeControl.xaml.cs: tree building, lazy loading, bidir selection sync,
  inline editing, alias highlight (via ModelHighlightHelper)
- Both projects compile successfully
This commit is contained in:
tian 2026-05-31 12:49:14 +08:00
parent 3f985aa235
commit f3392ddbf6
5 changed files with 1185 additions and 0 deletions

View File

@ -218,6 +218,9 @@
<Compile Include="src\UI\WPF\Views\LogisticsControlPanel.xaml.cs">
<DependentUpon>LogisticsControlPanel.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\AliasTreeControl.xaml.cs">
<DependentUpon>AliasTreeControl.xaml</DependentUpon>
</Compile>
<Compile Include="src\UI\WPF\Views\ModelSettingsView.xaml.cs">
<DependentUpon>ModelSettingsView.xaml</DependentUpon>
</Compile>
@ -288,6 +291,7 @@
<Compile Include="src\UI\WPF\ViewModels\ModelSettingsViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\AnimationControlViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\PathEditingViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\AliasNodeViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\RailAssemblyWorkflowContext.cs" />
<Compile Include="src\UI\WPF\ViewModels\SystemManagementViewModel.cs" />
<Compile Include="src\UI\WPF\ViewModels\CollisionReportViewModel.cs" />
@ -391,6 +395,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\AliasTreeControl.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Views\EditCoordinatesWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@ -508,6 +516,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="src\UI\WPF\Resources\AliasTreeIcons.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets" Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\WinFX\3.0\Microsoft.WinFX.targets')" />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />

View File

@ -0,0 +1,136 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ── 别名树工具栏图标 ── -->
<!-- 保存图标(磁盘) -->
<Geometry x:Key="SaveIconGeometry">
M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z
</Geometry>
<!-- 导出图标(向上箭头出盒子) -->
<Geometry x:Key="ExportIconGeometry">
M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z
</Geometry>
<!-- 导入图标(向下箭头入盒子) -->
<Geometry x:Key="ImportIconGeometry">
M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z
</Geometry>
<!-- 自动生成图标(齿轮) -->
<Geometry x:Key="AutoGenIconGeometry">
M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12
A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12
C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95
21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.97 19.05,5.05L16.56,6.05
C16.04,5.65 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2
H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.65 7.44,6.05
L4.95,5.05C4.73,4.97 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22
2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65
4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73
C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.95C7.96,18.35 8.5,18.68
9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82
14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.35 16.56,17.95L19.05,18.95
C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78
21.54,14.63L19.43,12.97Z
</Geometry>
<!-- 清除图标(垃圾桶) -->
<Geometry x:Key="ClearIconGeometry">
M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z
</Geometry>
<!-- 批量编辑图标(列表+铅笔) -->
<Geometry x:Key="BatchEditIconGeometry">
M3,17.25V21H6.75L17.81,9.94L14.06,6.19L3,17.25ZM20.71,7.04C21.1,6.65
21.1,6.02 20.71,5.63L18.37,3.29C17.98,2.9 17.35,2.9 16.96,3.29
L15.13,5.12L18.88,8.87L20.71,7.04Z
</Geometry>
<!-- 聚焦图标(瞄准镜) -->
<Geometry x:Key="FocusIconGeometry">
M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52
22,12C22,6.48 17.52,2 12,2ZM12,20C7.59,20 4,16.41 4,12C4,7.59
7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20ZM12,9
C10.34,9 9,10.34 9,12C9,13.66 10.34,15 12,15C13.66,15 15,13.66
15,12C15,10.34 13.66,9 12,9Z
</Geometry>
<!-- 铅笔编辑图标 -->
<Geometry x:Key="EditIconGeometry">
M3,17.25V21H6.75L17.81,9.94L14.06,6.19L3,17.25ZM20.71,7.04C21.1,6.65
21.1,6.02 20.71,5.63L18.37,3.29C17.98,2.9 17.35,2.9 16.96,3.29
L15.13,5.12L18.88,8.87L20.71,7.04Z
</Geometry>
<!-- 刷新图标 -->
<Geometry x:Key="RefreshIconGeometry">
M17.65,6.35C16.2,4.9 14.21,4 12,4C7.58,4 4.01,7.58 4.01,12
C4.01,16.42 7.58,20 12,20C15.73,20 18.84,17.45 19.73,14H17.65
C16.83,16.33 14.61,18 12,18C8.69,18 6,15.31 6,12C6,8.69 8.69,6
12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z
</Geometry>
<!-- ── 别名树工具栏按钮样式 ── -->
<!-- 图标按钮:紧凑 24×24透明背景悬停蓝色高亮 -->
<Style x:Key="AliasToolbarButtonStyle" TargetType="Button">
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Margin" Value="1,0"/>
<Setter Property="Padding" Value="2"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="#FF666666"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="ToolTipService.ShowOnDisabled" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="border"
Background="{TemplateBinding Background}"
BorderThickness="0"
CornerRadius="2">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="border" Property="Background" Value="#FFD4E7FF"/>
<Setter Property="Foreground" Value="#FF2B579A"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="border" Property="Background" Value="#FF4472C4"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="#FFBBBBBB"/>
<Setter Property="Opacity" Value="0.5"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 紧凑分隔线 -->
<Style x:Key="AliasToolbarSeparatorStyle" TargetType="Separator">
<Setter Property="Width" Value="1"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Margin" Value="3,0"/>
<Setter Property="Background" Value="#FFCCCCCC"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- ── 别名树 TreeViewItem 样式 ── -->
<Style x:Key="AliasTreeViewItemStyle" TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="2,1"/>
</Style>
</ResourceDictionary>

View File

@ -0,0 +1,108 @@
using System;
using System.ComponentModel;
namespace NavisworksTransport.UI.WPF.ViewModels
{
/// <summary>
/// 别名树单节点 ViewModel。
/// 支持 INotifyPropertyChanged 以驱动 TreeView 实时更新。
/// </summary>
public class AliasNodeViewModel : INotifyPropertyChanged
{
private string _displayName;
private string _originalName;
private string _alias;
private bool _isEditing;
private bool _isOrphan;
private bool _hasAlias;
private int _childCount;
/// <summary>节点的序列化标识 key对应 AliasNodeIdentity.ToKey()</summary>
public string NodeKey { get; set; }
/// <summary>当前显示文本(别名优先,无别名回退到原名)</summary>
public string DisplayName
{
get => _displayName;
set { _displayName = value; OnPropertyChanged(nameof(DisplayName)); }
}
/// <summary>Navisworks 原始名称(只读)</summary>
public string OriginalName
{
get => _originalName;
set { _originalName = value; OnPropertyChanged(nameof(OriginalName)); }
}
/// <summary>用户别名</summary>
public string Alias
{
get => _alias;
set
{
_alias = value;
HasAlias = !string.IsNullOrEmpty(value);
// 更新显示文本
DisplayName = HasAlias ? value : (OriginalName ?? "?");
OnPropertyChanged(nameof(Alias));
}
}
/// <summary>是否处于就地编辑状态</summary>
public bool IsEditing
{
get => _isEditing;
set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); }
}
/// <summary>是否孤儿节点(模型结构变化后无法定位)</summary>
public bool IsOrphan
{
get => _isOrphan;
set { _isOrphan = value; OnPropertyChanged(nameof(IsOrphan)); }
}
/// <summary>是否有别名</summary>
public bool HasAlias
{
get => _hasAlias;
set { _hasAlias = value; OnPropertyChanged(nameof(HasAlias)); }
}
/// <summary>子节点数量(用于展开箭头)</summary>
public int ChildCount
{
get => _childCount;
set { _childCount = value; OnPropertyChanged(nameof(ChildCount)); OnPropertyChanged(nameof(HasChildren)); }
}
/// <summary>是否有子节点</summary>
public bool HasChildren => ChildCount > 0;
/// <summary>子节点集合</summary>
public System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel> Children { get; }
= new System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>();
/// <summary>父节点引用(用于回溯路径)</summary>
public AliasNodeViewModel Parent { get; set; }
public AliasNodeViewModel()
{
}
/// <summary>
/// 刷新显示文本(别名变更新调用)
/// </summary>
public void RefreshDisplay()
{
DisplayName = HasAlias ? Alias : (OriginalName ?? "?");
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -0,0 +1,213 @@
<UserControl x:Class="NavisworksTransport.UI.WPF.Views.AliasTreeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:NavisworksTransport.UI.WPF.ViewModels"
xmlns:converters="clr-namespace:NavisworksTransport.UI.WPF.Converters">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="../Resources/NavisworksStyles.xaml"/>
<ResourceDictionary Source="../Resources/AliasTreeIcons.xaml"/>
</ResourceDictionary.MergedDictionaries>
<!-- Converters -->
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<!-- 别名树 TreeView 的模板 -->
<HierarchicalDataTemplate DataType="{x:Type vm:AliasNodeViewModel}"
ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="0,1"
VerticalAlignment="Center">
<!-- 别名前缀标记 ◆ -->
<TextBlock Text="◆ " FontSize="11"
Visibility="{Binding HasAlias, Converter={StaticResource BoolToVisibilityConverter}}"
Foreground="#FF1565C0" VerticalAlignment="Center"/>
<!-- 无别名时显示灰色斜体原名 -->
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center"
FontSize="12" TextTrimming="CharacterEllipsis" MaxWidth="200">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#FF888888"/>
<Setter Property="FontStyle" Value="Italic"/>
<Style.Triggers>
<DataTrigger Binding="{Binding HasAlias}" Value="True">
<Setter Property="Foreground" Value="#FF1565C0"/>
<Setter Property="FontStyle" Value="Normal"/>
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
<DataTrigger Binding="{Binding IsOrphan}" Value="True">
<Setter Property="TextDecorations" Value="Strikethrough"/>
<Setter Property="Foreground" Value="#FFAAAAAA"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- 编辑文本框IsEditing=true 时显示) -->
<TextBox Text="{Binding Alias, UpdateSourceTrigger=PropertyChanged}"
Width="150" VerticalAlignment="Center"
Visibility="Collapsed" FontSize="12"
KeyDown="OnAliasTextBoxKeyDown"
LostFocus="OnAliasTextBoxLostFocus"
Background="#FFFFF8DC"
BorderBrush="#FF1565C0" BorderThickness="1">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsEditing}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<!-- 铅笔编辑按钮(鼠标悬停显示) -->
<Button Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="编辑别名"
Tag="{Binding}"
Click="OnEditClick"
Width="18" Height="18"
Margin="2,0,0,0">
<Path Data="{StaticResource EditIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="14" Height="14" Stretch="Uniform"/>
</Button>
</StackPanel>
</HierarchicalDataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<DockPanel>
<!-- ── 顶部工具栏 ── -->
<Border DockPanel.Dock="Top"
Background="#FFF5F5F5"
BorderBrush="#FFDDDDDD"
BorderThickness="0,0,0,1"
Padding="4,2">
<DockPanel>
<!-- 当前选中节点名 -->
<TextBlock x:Name="TbCurrentNode"
DockPanel.Dock="Left"
Text="当前: 无"
VerticalAlignment="Center"
Margin="2,0,6,0"
FontSize="11"
Foreground="#FF666666"
MaxWidth="120"
TextTrimming="CharacterEllipsis"/>
<!-- 图标工具栏 -->
<StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
<Button x:Name="BtnSave" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="保存别名 (Ctrl+S)"
IsEnabled="False"
Click="OnSaveCurrentClick">
<Path Data="{StaticResource SaveIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
<Button x:Name="BtnAutoGen" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="自动生成别名"
Click="OnAutoGenClick">
<Path Data="{StaticResource AutoGenIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
<Button x:Name="BtnExport" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="导出别名 (JSON)"
Click="OnExportClick">
<Path Data="{StaticResource ExportIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Button x:Name="BtnImport" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="导入别名 (JSON)"
Click="OnImportClick">
<Path Data="{StaticResource ImportIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Separator Style="{StaticResource AliasToolbarSeparatorStyle}"/>
<Button x:Name="BtnClearAll" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="清除全部别名"
Click="OnClearAllClick">
<Path Data="{StaticResource ClearIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
<Button x:Name="BtnRefresh" Style="{StaticResource AliasToolbarButtonStyle}"
ToolTip="刷新树结构"
Click="OnRefreshClick">
<Path Data="{StaticResource RefreshIconGeometry}"
Fill="{Binding RelativeSource={RelativeSource AncestorType=Button}, Path=Foreground}"
Width="16" Height="16" Stretch="Uniform"/>
</Button>
</StackPanel>
<!-- 别名输入框 -->
<TextBox x:Name="TxtAliasInput"
DockPanel.Dock="Right"
Width="100"
Height="20"
FontSize="11"
Margin="0,0,4,0"
ToolTip="输入别名后回车保存"
KeyDown="OnAliasInputKeyDown"/>
</DockPanel>
</Border>
<!-- ── 别名树 ── -->
<TreeView x:Name="AliasTreeView"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
Background="White"
BorderThickness="0"
ItemContainerStyle="{StaticResource AliasTreeViewItemStyle}">
<TreeView.ContextMenu>
<ContextMenu>
<MenuItem Header="编辑别名" Click="OnCtxEditClick">
<MenuItem.Icon>
<Path Data="{StaticResource EditIconGeometry}"
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
Margin="0,0,4,0"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="批量编辑子节点..." Click="OnCtxBatchEditClick">
<MenuItem.Icon>
<Path Data="{StaticResource BatchEditIconGeometry}"
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
Margin="0,0,4,0"/>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="清除别名" Click="OnCtxClearClick"/>
<Separator/>
<MenuItem Header="在视图中聚焦" Click="OnCtxFocusClick">
<MenuItem.Icon>
<Path Data="{StaticResource FocusIconGeometry}"
Fill="#FF666666" Width="14" Height="14" Stretch="Uniform"
Margin="0,0,4,0"/>
</MenuItem.Icon>
</MenuItem>
<Separator/>
<MenuItem Header="复制原名" Click="OnCtxCopyNameClick"/>
</ContextMenu>
</TreeView.ContextMenu>
</TreeView>
<!-- ── 底部状态栏 ── -->
<StatusBar DockPanel.Dock="Bottom"
Background="#FFF8F8F8"
BorderBrush="#FFDDDDDD"
BorderThickness="0,1,0,0"
Height="20"
Visibility="Collapsed">
<StatusBarItem x:Name="StatusBarItem">
<TextBlock x:Name="TbStatus" FontSize="10" Foreground="#FF666666"/>
</StatusBarItem>
</StatusBar>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,716 @@
using NavisworksTransport.Core.AliasTree;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using NavisApplication = Autodesk.Navisworks.Api.Application;
namespace NavisworksTransport.UI.WPF.Views
{
/// <summary>
/// 别名导航树面板 — 核心 UI 逻辑
/// </summary>
public partial class AliasTreeControl : UserControl
{
// ============================================================
// 状态
// ============================================================
private AliasDataStore _dataStore;
private Dictionary<string, AliasNodeViewModel> _nodeMap = new Dictionary<string, AliasNodeViewModel>();
private bool _isInternalSelection = false;
private Autodesk.Navisworks.Api.ModelItem _currentSelectionItem;
private bool _built = false;
// ============================================================
// 生命周期
// ============================================================
public AliasTreeControl()
{
InitializeComponent();
// 将 AliasTreeView 的 items 源指向根节点集合
var rootNodes = new System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>();
AliasTreeView.ItemsSource = rootNodes;
// 文档切换事件
NavisApplication.ActiveDocumentChanged += (s, e) => RebuildTree();
NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; };
HookSelectionEvents();
}
/// <summary>
/// 设置数据存储(由外部在 PathDatabase 就绪后调用)
/// </summary>
public void SetDataStore(AliasDataStore store)
{
_dataStore = store;
RebuildTree();
}
/// <summary>
/// 清理资源
/// </summary>
public void Cleanup()
{
UnhookSelectionEvents();
_dataStore = null;
_nodeMap.Clear();
}
// ============================================================
// 选择事件(双向联动)
// ============================================================
private bool _selectionHooked = false;
private void HookSelectionEvents()
{
if (_selectionHooked) return;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc != null)
{
doc.CurrentSelection.Changed += OnNavisSelectionChanged;
_selectionHooked = true;
}
}
catch { /* 文档未就绪,跳过 */ }
}
private void UnhookSelectionEvents()
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc != null)
{
doc.CurrentSelection.Changed -= OnNavisSelectionChanged;
}
}
catch { }
_selectionHooked = false;
}
/// <summary>
/// 别名树选中 → 同步到 Navisworks 选择
/// </summary>
private void OnTreeViewSelected(object sender, RoutedEventArgs e)
{
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
return;
if (node.IsOrphan) return; // 孤儿节点不允许选中
_isInternalSelection = true;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc != null)
{
var coll = new Autodesk.Navisworks.Api.ModelItemCollection();
var item = FindModelItemByNodeKey(node.NodeKey);
if (item != null)
{
coll.Add(item);
doc.CurrentSelection.Clear();
doc.CurrentSelection.CopyFrom(coll);
}
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 选中同步失败: {ex.Message}");
}
finally
{
_isInternalSelection = false;
}
}
/// <summary>
/// Navisworks 选择变化 → 同步到别名树高亮
/// </summary>
private void OnNavisSelectionChanged(object sender, EventArgs e)
{
if (_isInternalSelection) return;
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
var selected = doc.CurrentSelection.SelectedItems;
if (selected.Count == 1)
{
var item = selected.First();
_currentSelectionItem = item;
TbCurrentNode.Text = $"当前: {item.DisplayName}";
BtnSave.IsEnabled = true;
// 在树中展开并高亮
ExpandAndSelect(item);
}
else
{
_currentSelectionItem = null;
TbCurrentNode.Text = selected.Count > 1 ? $"当前: ({selected.Count} 个选中)" : "当前: 无";
BtnSave.IsEnabled = false;
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 选择变化处理失败: {ex.Message}");
}
}));
}
// ============================================================
// 树构建
// ============================================================
private void RebuildTree()
{
if (_built) return;
_built = true;
try
{
var rootNodes = AliasTreeView.ItemsSource as System.Collections.ObjectModel.ObservableCollection<AliasNodeViewModel>;
if (rootNodes == null) return;
rootNodes.Clear();
_nodeMap.Clear();
var doc = NavisApplication.ActiveDocument;
if (doc == null || doc.Models == null) return;
// 全量加载别名到内存
Dictionary<string, string> aliasMap = null;
if (_dataStore != null)
aliasMap = _dataStore.LoadAll();
// 遍历所有 root items
foreach (var model in doc.Models)
{
var rootItem = model.RootItem;
if (rootItem != null)
{
var node = BuildNode(rootItem, aliasMap);
if (node != null)
rootNodes.Add(node);
}
}
// 重新挂载选择事件(可能文档刚切换)
HookSelectionEvents();
LogManager.Info($"[别名树] 树构建完成,共 {_nodeMap.Count} 个节点");
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 构建失败: {ex.Message}");
}
}
/// <summary>
/// 递归构建节点树(主线程调用)
/// </summary>
private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
Dictionary<string, string> aliasMap)
{
if (modelItem == null) return null;
var identity = AliasNodeIdentity.FromModelItem(modelItem);
string key = identity.ToKey();
string alias = null;
aliasMap?.TryGetValue(key, out alias);
var node = new AliasNodeViewModel
{
NodeKey = key,
OriginalName = modelItem.DisplayName ?? "?",
Alias = alias,
ChildCount = modelItem.Children.Count()
};
_nodeMap[key] = node;
// 子节点(第一层不懒加载,展开时在 Expanded 事件中再填充)
// 但先加一个占位节点让 TreeView 显示展开箭头
if (node.ChildCount > 0)
{
node.Children.Add(new AliasNodeViewModel
{
DisplayName = "加载中...",
OriginalName = "加载中..."
});
}
return node;
}
/// <summary>
/// 懒加载子节点(在 Expanded 事件中触发)
/// </summary>
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> aliasMap)
{
// 检查是否只有占位节点
if (parentNode.Children.Count != 1 || parentNode.Children[0].OriginalName != "加载中...")
return;
parentNode.Children.Clear();
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
// 从 nodeKey 反推出 ModelItem
var item = FindModelItemByNodeKey(parentNode.NodeKey);
if (item == null) return;
foreach (var childItem in item.Children)
{
var childNode = BuildNode(childItem, aliasMap);
if (childNode != null)
{
childNode.Parent = parentNode;
parentNode.Children.Add(childNode);
}
}
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 懒加载子节点失败: {ex.Message}");
}
}
// ============================================================
// 从 NodeKey 回溯定位 ModelItem
// ============================================================
private Autodesk.Navisworks.Api.ModelItem FindModelItemByNodeKey(string key)
{
if (!AliasNodeIdentity.TryParseKey(key, out var identity))
return null;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return null;
// 沿 HierarchicalPath 匹配 DisplayName
string[] parts = identity.HierarchicalPath.Split('/');
// parts[0] 是模型名/根节点名
// 遍历 Models 找匹配的根
foreach (var model in doc.Models)
{
var current = model.RootItem;
if (current == null) continue;
// 检查根节点
if (parts.Length > 0 && current.DisplayName != parts[0])
continue;
for (int i = 1; i < parts.Length; i++)
{
string targetName = parts[i];
Autodesk.Navisworks.Api.ModelItem matchedChild = null;
foreach (var child in current.Children)
{
if (child.DisplayName == targetName)
{
matchedChild = child;
break;
}
}
if (matchedChild == null) return null;
current = matchedChild;
}
// 到了叶子层级,用 SiblingIndex 在同名兄弟中定位
if (identity.SiblingIndex > 0)
{
int idx = 0;
foreach (var child in current.Children)
{
if (child.DisplayName == parts.Last())
{
if (idx == identity.SiblingIndex)
return child;
idx++;
}
}
return null;
}
return current;
}
}
catch { }
return null;
}
// ============================================================
// 展开并高亮指定 ModelItem
// ============================================================
private void ExpandAndSelect(Autodesk.Navisworks.Api.ModelItem item)
{
var identity = AliasNodeIdentity.FromModelItem(item);
string key = identity.ToKey();
if (!_nodeMap.TryGetValue(key, out var targetNode))
return;
// 收集从根到目标的路径
var path = new List<AliasNodeViewModel>();
var current = targetNode;
while (current != null)
{
path.Insert(0, current);
current = current.Parent;
}
// 逐层展开
ItemsControl currentContainer = AliasTreeView;
foreach (var node in path)
{
// 触发懒加载
var aliasMap = _dataStore?.LoadAll();
LazyLoadChildren(node, aliasMap);
if (currentContainer is TreeViewItem tvi)
{
tvi.IsExpanded = true;
tvi.UpdateLayout();
}
var container = currentContainer.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
if (container == null) break;
currentContainer = container;
}
// 高亮目标
if (currentContainer is TreeViewItem targetTvi && targetTvi.DataContext is AliasNodeViewModel target
&& target.NodeKey == key)
{
targetTvi.IsSelected = true;
targetTvi.BringIntoView();
}
}
// ============================================================
// 事件处理:工具栏
// ============================================================
private void OnSaveCurrentClick(object sender, RoutedEventArgs e)
{
if (_currentSelectionItem == null || _dataStore == null) return;
string input = TxtAliasInput.Text?.Trim();
if (string.IsNullOrEmpty(input)) return;
var identity = AliasNodeIdentity.FromModelItem(_currentSelectionItem);
_dataStore.SetAlias(identity, input);
TxtAliasInput.Clear();
// 刷新树中对应节点
RefreshNodeDisplay(identity.ToKey());
}
private void OnAliasInputKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter && BtnSave.IsEnabled)
OnSaveCurrentClick(null, null);
}
private void OnAutoGenClick(object sender, RoutedEventArgs e)
{
// 阶段 3 实现
SetStatus("自动生成尚未实现");
}
private void OnExportClick(object sender, RoutedEventArgs e)
{
// 阶段 5 实现
SetStatus("导出尚未实现");
}
private void OnImportClick(object sender, RoutedEventArgs e)
{
// 阶段 5 实现
SetStatus("导入尚未实现");
}
private void OnClearAllClick(object sender, RoutedEventArgs e)
{
if (_dataStore == null) return;
var result = System.Windows.MessageBox.Show(
"确定要清除全部别名吗?此操作不可撤销。",
"清除全部别名",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
_dataStore.ClearAll();
RebuildTree();
SetStatus("已清除全部别名");
}
}
private void OnRefreshClick(object sender, RoutedEventArgs e)
{
_built = false;
RebuildTree();
SetStatus("树已刷新");
}
// ============================================================
// 事件处理:内联编辑
// ============================================================
private void OnEditClick(object sender, RoutedEventArgs e)
{
if (!(sender is Button btn) || !(btn.Tag is AliasNodeViewModel node))
return;
// 清除所有编辑状态
ClearAllEditing();
node.IsEditing = true;
Dispatcher.BeginInvoke(new Action(() =>
{
var container = AliasTreeView.ItemContainerGenerator.ContainerFromItem(node) as TreeViewItem;
if (container == null) return;
var txt = FindVisualChild<TextBox>(container);
if (txt != null)
{
txt.Focus();
txt.SelectAll();
}
}), System.Windows.Threading.DispatcherPriority.Render);
}
private void OnAliasTextBoxKeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
CommitEdit(sender as TextBox);
}
else if (e.Key == Key.Escape)
{
CancelEdit(sender as TextBox);
}
}
private void OnAliasTextBoxLostFocus(object sender, RoutedEventArgs e)
{
CommitEdit(sender as TextBox);
}
private void CommitEdit(TextBox textBox)
{
if (textBox?.DataContext is AliasNodeViewModel node)
{
string newAlias = textBox.Text?.Trim();
if (_dataStore != null)
{
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.SetAlias(identity, newAlias);
}
node.Alias = newAlias;
node.IsEditing = false;
// 清除别名高亮 / 应用别名高亮
ApplyAliasHighlight(node.NodeKey, node.HasAlias);
}
}
private void CancelEdit(TextBox textBox)
{
if (textBox?.DataContext is AliasNodeViewModel node)
{
node.RefreshDisplay();
node.IsEditing = false;
}
}
private void ClearAllEditing()
{
foreach (var kvp in _nodeMap)
{
kvp.Value.IsEditing = false;
}
}
// ============================================================
// 事件处理:右键菜单
// ============================================================
private AliasNodeViewModel GetSelectedNode()
{
return AliasTreeView.SelectedItem as AliasNodeViewModel;
}
private void OnCtxEditClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null) return;
OnEditClick(new Button { Tag = node }, null);
}
private void OnCtxBatchEditClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null) return;
// 阶段 3 实现批量对话框
SetStatus($"批量编辑 {node.NodeKey} 的子节点 — 尚未实现");
}
private void OnCtxClearClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null || _dataStore == null) return;
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.DeleteAlias(identity);
node.Alias = null;
ApplyAliasHighlight(node.NodeKey, false);
}
private void OnCtxFocusClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null || node.IsOrphan) return;
var item = FindModelItemByNodeKey(node.NodeKey);
if (item == null) return;
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item };
doc.CurrentSelection.Clear();
doc.CurrentSelection.CopyFrom(coll);
}
catch (Exception ex)
{
LogManager.Error($"[别名树] 聚焦失败: {ex.Message}");
}
}
private void OnCtxCopyNameClick(object sender, RoutedEventArgs e)
{
var node = GetSelectedNode();
if (node == null) return;
try
{
Clipboard.SetText(node.OriginalName);
}
catch { }
}
// ============================================================
// 3D 高亮
// ============================================================
private void ApplyAliasHighlight(string nodeKey, bool highlight)
{
var item = FindModelItemByNodeKey(nodeKey);
if (item == null) return;
var coll = new Autodesk.Navisworks.Api.ModelItemCollection { item };
if (highlight)
{
ModelHighlightHelper.HighlightItems("AliasHighlight", coll);
}
else
{
ModelHighlightHelper.ClearCategory("AliasHighlight");
}
}
// ============================================================
// 辅助
// ============================================================
private void RefreshNodeDisplay(string key)
{
if (_nodeMap.TryGetValue(key, out var node))
{
if (_dataStore != null)
{
AliasNodeIdentity.TryParseKey(key, out var identity);
node.Alias = _dataStore.GetAlias(identity);
}
node.RefreshDisplay();
}
}
private void SetStatus(string message)
{
TbStatus.Text = message;
StatusBarItem.Visibility = Visibility.Visible;
// 3 秒后自动隐藏
var timer = new System.Windows.Threading.DispatcherTimer
{
Interval = TimeSpan.FromSeconds(3)
};
timer.Tick += (s, e2) =>
{
timer.Stop();
StatusBarItem.Visibility = Visibility.Collapsed;
};
timer.Start();
}
private static T FindVisualChild<T>(DependencyObject parent) where T : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var child = VisualTreeHelper.GetChild(parent, i);
if (child is T t) return t;
var result = FindVisualChild<T>(child);
if (result != null) return result;
}
return null;
}
// ============================================================
// TreeView 子节点懒加载
// ============================================================
private void OnTreeViewItemExpanded(object sender, RoutedEventArgs e)
{
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
return;
var aliasMap = _dataStore?.LoadAll();
LazyLoadChildren(node, aliasMap);
}
private void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
OnTreeViewSelected(sender, e);
}
}
}