From f3392ddbf6a5c59f83eeee7694f084b0d0554228 Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sun, 31 May 2026 12:49:14 +0800
Subject: [PATCH] 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
---
TransportPlugin.csproj | 12 +
src/UI/WPF/Resources/AliasTreeIcons.xaml | 136 ++++
src/UI/WPF/ViewModels/AliasNodeViewModel.cs | 108 +++
src/UI/WPF/Views/AliasTreeControl.xaml | 213 ++++++
src/UI/WPF/Views/AliasTreeControl.xaml.cs | 716 ++++++++++++++++++++
5 files changed, 1185 insertions(+)
create mode 100644 src/UI/WPF/Resources/AliasTreeIcons.xaml
create mode 100644 src/UI/WPF/ViewModels/AliasNodeViewModel.cs
create mode 100644 src/UI/WPF/Views/AliasTreeControl.xaml
create mode 100644 src/UI/WPF/Views/AliasTreeControl.xaml.cs
diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj
index 63376ee..3a7251b 100644
--- a/TransportPlugin.csproj
+++ b/TransportPlugin.csproj
@@ -218,6 +218,9 @@
LogisticsControlPanel.xaml
+
+ AliasTreeControl.xaml
+
ModelSettingsView.xaml
@@ -288,6 +291,7 @@
+
@@ -391,6 +395,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
Designer
MSBuild:Compile
@@ -508,6 +516,10 @@
Designer
MSBuild:Compile
+
+ Designer
+ MSBuild:Compile
+
diff --git a/src/UI/WPF/Resources/AliasTreeIcons.xaml b/src/UI/WPF/Resources/AliasTreeIcons.xaml
new file mode 100644
index 0000000..aee4b81
--- /dev/null
+++ b/src/UI/WPF/Resources/AliasTreeIcons.xaml
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+ M15,9H5V5H15M15,19H5V15H15M17,3H3V21H17V16L21,12L17,8V3Z
+
+
+
+
+ M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z
+
+
+
+
+ M5,10H19V12H5M12,5L5,12H9V21H15V12H19L12,5Z
+
+
+
+
+ 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
+
+
+
+
+ M6,19V7H18V19H6M9,4H15L14.5,3H9.5L9,4M19,5V7H5V5H8L8.5,4H15.5L16,5H19Z
+
+
+
+
+ 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
+
+
+
+
+ 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
+
+
+
+
+ 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
+
+
+
+
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/ViewModels/AliasNodeViewModel.cs b/src/UI/WPF/ViewModels/AliasNodeViewModel.cs
new file mode 100644
index 0000000..fd02a01
--- /dev/null
+++ b/src/UI/WPF/ViewModels/AliasNodeViewModel.cs
@@ -0,0 +1,108 @@
+using System;
+using System.ComponentModel;
+
+namespace NavisworksTransport.UI.WPF.ViewModels
+{
+ ///
+ /// 别名树单节点 ViewModel。
+ /// 支持 INotifyPropertyChanged 以驱动 TreeView 实时更新。
+ ///
+ 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;
+
+ /// 节点的序列化标识 key(对应 AliasNodeIdentity.ToKey())
+ public string NodeKey { get; set; }
+
+ /// 当前显示文本(别名优先,无别名回退到原名)
+ public string DisplayName
+ {
+ get => _displayName;
+ set { _displayName = value; OnPropertyChanged(nameof(DisplayName)); }
+ }
+
+ /// Navisworks 原始名称(只读)
+ public string OriginalName
+ {
+ get => _originalName;
+ set { _originalName = value; OnPropertyChanged(nameof(OriginalName)); }
+ }
+
+ /// 用户别名
+ public string Alias
+ {
+ get => _alias;
+ set
+ {
+ _alias = value;
+ HasAlias = !string.IsNullOrEmpty(value);
+ // 更新显示文本
+ DisplayName = HasAlias ? value : (OriginalName ?? "?");
+ OnPropertyChanged(nameof(Alias));
+ }
+ }
+
+ /// 是否处于就地编辑状态
+ public bool IsEditing
+ {
+ get => _isEditing;
+ set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); }
+ }
+
+ /// 是否孤儿节点(模型结构变化后无法定位)
+ public bool IsOrphan
+ {
+ get => _isOrphan;
+ set { _isOrphan = value; OnPropertyChanged(nameof(IsOrphan)); }
+ }
+
+ /// 是否有别名
+ public bool HasAlias
+ {
+ get => _hasAlias;
+ set { _hasAlias = value; OnPropertyChanged(nameof(HasAlias)); }
+ }
+
+ /// 子节点数量(用于展开箭头)
+ public int ChildCount
+ {
+ get => _childCount;
+ set { _childCount = value; OnPropertyChanged(nameof(ChildCount)); OnPropertyChanged(nameof(HasChildren)); }
+ }
+
+ /// 是否有子节点
+ public bool HasChildren => ChildCount > 0;
+
+ /// 子节点集合
+ public System.Collections.ObjectModel.ObservableCollection Children { get; }
+ = new System.Collections.ObjectModel.ObservableCollection();
+
+ /// 父节点引用(用于回溯路径)
+ public AliasNodeViewModel Parent { get; set; }
+
+ public AliasNodeViewModel()
+ {
+ }
+
+ ///
+ /// 刷新显示文本(别名变更新调用)
+ ///
+ public void RefreshDisplay()
+ {
+ DisplayName = HasAlias ? Alias : (OriginalName ?? "?");
+ }
+
+ public event PropertyChangedEventHandler PropertyChanged;
+
+ protected virtual void OnPropertyChanged(string propertyName)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+ }
+}
diff --git a/src/UI/WPF/Views/AliasTreeControl.xaml b/src/UI/WPF/Views/AliasTreeControl.xaml
new file mode 100644
index 0000000..be35ca8
--- /dev/null
+++ b/src/UI/WPF/Views/AliasTreeControl.xaml
@@ -0,0 +1,213 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UI/WPF/Views/AliasTreeControl.xaml.cs b/src/UI/WPF/Views/AliasTreeControl.xaml.cs
new file mode 100644
index 0000000..a4c7bd7
--- /dev/null
+++ b/src/UI/WPF/Views/AliasTreeControl.xaml.cs
@@ -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
+{
+ ///
+ /// 别名导航树面板 — 核心 UI 逻辑
+ ///
+ public partial class AliasTreeControl : UserControl
+ {
+ // ============================================================
+ // 状态
+ // ============================================================
+
+ private AliasDataStore _dataStore;
+ private Dictionary _nodeMap = new Dictionary();
+ 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();
+ AliasTreeView.ItemsSource = rootNodes;
+
+ // 文档切换事件
+ NavisApplication.ActiveDocumentChanged += (s, e) => RebuildTree();
+ NavisApplication.ActiveDocumentChanging += (s, e) => { _built = false; };
+
+ HookSelectionEvents();
+ }
+
+ ///
+ /// 设置数据存储(由外部在 PathDatabase 就绪后调用)
+ ///
+ public void SetDataStore(AliasDataStore store)
+ {
+ _dataStore = store;
+ RebuildTree();
+ }
+
+ ///
+ /// 清理资源
+ ///
+ 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;
+ }
+
+ ///
+ /// 别名树选中 → 同步到 Navisworks 选择
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Navisworks 选择变化 → 同步到别名树高亮
+ ///
+ 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;
+ if (rootNodes == null) return;
+
+ rootNodes.Clear();
+ _nodeMap.Clear();
+
+ var doc = NavisApplication.ActiveDocument;
+ if (doc == null || doc.Models == null) return;
+
+ // 全量加载别名到内存
+ Dictionary 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}");
+ }
+ }
+
+ ///
+ /// 递归构建节点树(主线程调用)
+ ///
+ private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
+ Dictionary 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;
+ }
+
+ ///
+ /// 懒加载子节点(在 Expanded 事件中触发)
+ ///
+ private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary 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();
+ 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(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(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(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);
+ }
+ }
+}