原生树无法扩展,自定义别名树面板就是唯一可靠的载体。在那个面板里,你可以完全自由地定制右键菜单,比如: 表格 菜单项 功能 编辑别名 就地改名 清除别名 恢复原名 在视图中聚焦 3D 定位 批量设置前缀 对当前节点及其子节点统一加前缀 导出此分支 只导出当前节点下的别名 CSV 实际工作流画面 想象你的屏幕布局: plain ┌─────────────────┬──────────────────┐ │ 内置 Selection │ 3D 模型视图 │ │ Tree │ │ │ (原始名字) │ [框选操作区] │ │ │ │ ├─────────────────┤ │ │ 别名导航树 │ │ │ (自定义名字) │ │ │ │ │ └─────────────────┴──────────────────┘ 找东西:用户在别名树里看到"二楼-空调水管-123",一眼认出。 点一下:别名树高亮,3D 视图聚焦到那根管子,内置 Selection Tree 也同步跳转到对应节点。 反过来:用户在 3D 视图里框选一根管子,内置树亮了,别名树也自动展开高亮显示"二楼-空调水管-123"。 用户完全不需要记住原始命名规则,两个面板互相印证。 一个小提醒:首次加载性能 如果模型有 10万+ 节点,且你需要一次性把所有别名加载进字典,建议在插件初始化时做一个后台线程 + 进度条: csharp // 异步构建别名字典,避免 UI 卡死 private async void BuildAliasMapAsync() { await Task.Run(() => { var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; // 遍历所有节点生成 Guid->Alias 映射 // ... }); RefreshTree(); } 结论 这个"双面板联动"方案是性价比最高的落地方式: 没有破坏 Navisworks 原生结构(稳定、升级兼容) 没有重写复杂树控件(100 多行代码搞定) ** alias 数据完全由你掌控**(Excel、数据库、规则生成均可) 用户体验几乎等同于原生支持别名 下面是可直接编译的完整方案,共 6 个文件。核心思路: AliasTreePane:Navisworks DockPane 外壳。 AliasTreeControl:WPF 别名树面板,支持悬停铅笔编辑、顶部快捷栏、右键菜单、CSV 导入导出、自动生成前缀别名。 双向联动:点击别名树选中模型;在 3D 视图/内置树选中后,别名树自动展开并高亮。 延迟加载:大模型下只加载可见节点,展开时才填充子节点。 1. AliasNode.cs(数据模型) using Autodesk.Navisworks.Api; using System; using System.Collections.Generic; using System.ComponentModel; namespace AliasTree { public class AliasNode : INotifyPropertyChanged { public ModelItem ModelItem { get; set; } public Guid Guid => ModelItem?.InstanceGuid ?? Guid.Empty; private string _alias; public string Alias { get => _alias; set { _alias = value; OnPropertyChanged(nameof(Alias)); OnPropertyChanged(nameof(DisplayText)); } } private bool _isEditing; public bool IsEditing { get => _isEditing; set { _isEditing = value; OnPropertyChanged(nameof(IsEditing)); } } public string DisplayText => string.IsNullOrEmpty(Alias) ? (ModelItem?.DisplayName ?? "未命名") : Alias; public List< Children { get; set; } = new List<(); public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } } 2. AliasTreePlugin.cs(插件入口) using Autodesk.Navisworks.Api.Plugins; namespace AliasTree { [Plugin("AliasTreePane", "YourCompany", DisplayName = "别名导航树")] [DockPanePlugin(400, 600, FixedSize = false)] public class AliasTreePlugin : DockPanePlugin { public override Pane CreatePane() => new AliasTreePane(); } } 3. AliasTreePane.cs(Pane 外壳) using Autodesk.Navisworks.Api.Plugins; namespace AliasTree { public class AliasTreePane : Pane { public AliasTreePane() { this.Content = new AliasTreeControl(); } } } 4. AliasTreeControl.xaml(界面布局) 5. AliasTreeControl.xaml.cs(核心逻辑) using Autodesk.Navisworks.Api; using Microsoft.Win32; 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; namespace AliasTree { public partial class AliasTreeControl : UserControl { private Dictionary< _aliasMap = new Dictionary<(); private List< _rootNodes = new List<(); public AliasTreeControl() { InitializeComponent(); AliasTreeView.ItemsSource = _rootNodes; Autodesk.Navisworks.Api.Application.MainDocumentChanged += (s, e) => { HookSelection(); RefreshTree(); }; HookSelection(); RefreshTree(); } private void HookSelection() { var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc != null) doc.CurrentSelection.Changed += OnNavisSelectionChanged; } // ==================== 树构建与延迟加载 ==================== private void RefreshTree() { _rootNodes.Clear(); var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc == null) return; foreach (var root in doc.Models.RootItems) { var node = CreateAliasNode(root, true); if (node != null) _rootNodes.Add(node); } } private AliasNode CreateAliasNode(ModelItem item, bool lazyLoad) { if (item == null) return null; var node = new AliasNode { ModelItem = item, Alias = GetAlias(item.InstanceGuid) }; if (item.Children.Any()) { if (lazyLoad) { // 添加占位节点,使 TreeView 显示展开箭头 node.Children.Add(new AliasNode { ModelItem = null, Alias = "Loading..." }); } else { foreach (var child in item.Children) { var childNode = CreateAliasNode(child, true); if (childNode != null) node.Children.Add(childNode); } } } return node; } private void TreeViewItem_Expanded(object sender, RoutedEventArgs e) { if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNode node) { // 检查是否是占位节点(尚未加载真实子节点) if (node.Children.Count == 1 && node.Children[0].ModelItem == null) { node.Children.Clear(); if (node.ModelItem != null) { foreach (var child in node.ModelItem.Children) { var childNode = CreateAliasNode(child, true); if (childNode != null) node.Children.Add(childNode); } } } } } // ==================== 双向联动 ==================== private void TreeViewItem_Selected(object sender, RoutedEventArgs e) { if (e.OriginalSource is TreeViewItem tvi && tvi.DataContext is AliasNode node) { if (node.ModelItem == null) return; var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc == null) return; var coll = new ModelItemCollection(); coll.Add(node.ModelItem); doc.CurrentSelection.SelectedItems = coll; } } private void OnNavisSelectionChanged(object sender, EventArgs e) { var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc == null) return; var selected = doc.CurrentSelection.SelectedItems; Dispatcher.BeginInvoke(new Action(() => { if (selected.Count == 1) { var item = selected.First(); TbCurrentName.Text = item.DisplayName; TbCurrentName.Tag = item; ExpandAndHighlight(item.InstanceGuid); } else { TbCurrentName.Text = selected.Count > 1 ? $"({selected.Count} 个选中)" : "无"; TbCurrentName.Tag = null; } })); } private void ExpandAndHighlight(Guid guid) { // 找到路径 var path = new List<(); if (!FindPath(_rootNodes, guid, path)) return; // 逐层展开并加载数据 ItemsControl current = AliasTreeView; foreach (var node in path) { // 强制加载延迟加载的节点数据 if (node.Children.Count == 1 && node.Children[0].ModelItem == null) { node.Children.Clear(); if (node.ModelItem != null) { foreach (var child in node.ModelItem.Children) { var childNode = CreateAliasNode(child, true); if (childNode != null) node.Children.Add(childNode); } } } if (current is TreeViewItem tvi) { tvi.IsExpanded = true; tvi.UpdateLayout(); } current = (TreeViewItem)current.ItemContainerGenerator.ContainerFromItem(node); if (current == null) break; // 虚拟化下可能未生成,中断 } // 高亮目标 if (current is TreeViewItem target && target.DataContext is AliasNode targetNode && targetNode.Guid == guid) { target.IsSelected = true; target.BringIntoView(); } } private bool FindPath(List< nodes, Guid guid, List< path) { foreach (var n in nodes) { path.Add(n); if (n.Guid == guid) return true; if (FindPath(n.Children, guid, path)) return true; path.RemoveAt(path.Count - 1); } return false; } // ==================== 内联编辑(铅笔图标) ==================== private void BtnEdit_Click(object sender, RoutedEventArgs e) { if ((sender as Button)?.Tag is not AliasNode node) return; ClearAllEditing(_rootNodes); 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 AliasTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) (sender as TextBox)?.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next)); } private void AliasTextBox_LostFocus(object sender, RoutedEventArgs e) { if ((sender as TextBox)?.DataContext is not AliasNode node) return; SaveAlias(node.Guid, node.Alias); node.IsEditing = false; node.Alias = GetAlias(node.Guid); // 同步最终值 } // ==================== 右键菜单 ==================== private void CtxEdit_Click(object sender, RoutedEventArgs e) { if (AliasTreeView.SelectedItem is AliasNode node) BtnEdit_Click(new Button { Tag = node }, null); } private void CtxClear_Click(object sender, RoutedEventArgs e) { if (AliasTreeView.SelectedItem is not AliasNode node) return; SaveAlias(node.Guid, null); node.Alias = null; } private void CtxFocus_Click(object sender, RoutedEventArgs e) { if (AliasTreeView.SelectedItem is not AliasNode node || node.ModelItem == null) return; var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc == null) return; var coll = new ModelItemCollection(); coll.Add(node.ModelItem); doc.CurrentSelection.SelectedItems = coll; doc.ActiveView.FocusOnItem(node.ModelItem, false); } // ==================== 顶部快捷栏 ==================== private void TxtQuickAlias_KeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.Enter) BtnQuickSave_Click(null, null); } private void BtnQuickSave_Click(object sender, RoutedEventArgs e) { if (TbCurrentName.Tag is not ModelItem item) return; if (string.IsNullOrWhiteSpace(TxtQuickAlias.Text)) return; SaveAlias(item.InstanceGuid, TxtQuickAlias.Text.Trim()); TxtQuickAlias.Clear(); RefreshNodeDisplay(item.InstanceGuid); } // ==================== 自动生成别名 ==================== private void BtnAutoGen_Click(object sender, RoutedEventArgs e) { var doc = Autodesk.Navisworks.Api.Application.ActiveDocument; if (doc == null) return; var gen = new AliasGenerator(); var generated = gen.Generate(doc.Models.RootItems.FirstOrDefault(), "/"); foreach (var kv in generated) _aliasMap[kv.Key] = kv.Value; RefreshTree(); } // ==================== CSV 导入导出 ==================== private void BtnExport_Click(object sender, RoutedEventArgs e) { var dlg = new SaveFileDialog { Filter = "CSV 文件|*.csv", Title = "导出别名映射" }; if (dlg.ShowDialog() != true) return; var lines = new List { "Guid,OriginalName,Alias" }; foreach (var kv in _aliasMap) { var item = Autodesk.Navisworks.Api.Application.ActiveDocument?.Models.FindItemByGuid(kv.Key); string orig = item?.DisplayName ?? ""; lines.Add($"{kv.Key},\"{orig}\",\"{kv.Value}\""); } File.WriteAllLines(dlg.FileName, lines); MessageBox.Show($"已导出 {_aliasMap.Count} 条别名记录。", "导出完成", MessageBoxButton.OK, MessageBoxImage.Information); } private void BtnImport_Click(object sender, RoutedEventArgs e) { var dlg = new OpenFileDialog { Filter = "CSV 文件|*.csv", Title = "导入别名映射" }; if (dlg.ShowDialog() != true) return; int count = 0; foreach (var line in File.ReadLines(dlg.FileName).Skip(1)) { var parts = ParseCsvLine(line); if (parts.Length >= 3 && Guid.TryParse(parts[0], out var guid)) { string alias = parts[2].Trim('"'); if (!string.IsNullOrWhiteSpace(alias)) { _aliasMap[guid] = alias; count++; } } } RefreshTree(); MessageBox.Show($"已导入 {count} 条别名记录。", "导入完成", MessageBoxButton.OK, MessageBoxImage.Information); } // ==================== 辅助方法 ==================== private string GetAlias(Guid guid) => _aliasMap.ContainsKey(guid) ? _aliasMap[guid] : null; private void SaveAlias(Guid guid, string alias) { if (string.IsNullOrWhiteSpace(alias)) { if (_aliasMap.ContainsKey(guid)) _aliasMap.Remove(guid); } else { _aliasMap[guid] = alias; } } private void RefreshNodeDisplay(Guid guid) { var node = FindNode(_rootNodes, guid); if (node != null) node.Alias = GetAlias(guid); } private AliasNode FindNode(List< nodes, Guid guid) { foreach (var n in nodes) { if (n.Guid == guid) return n; var found = FindNode(n.Children, guid); if (found != null) return found; } return null; } private void ClearAllEditing(List< nodes) { foreach (var n in nodes) { n.IsEditing = false; ClearAllEditing(n.Children); } } 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; } private static string[] ParseCsvLine(string line) { var result = new List(); bool inQuotes = false; var sb = new System.Text.StringBuilder(); foreach (char c in line) { if (c == '"') { inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { result.Add(sb.ToString()); sb.Clear(); } else { sb.Append(c); } } result.Add(sb.ToString()); return result.ToArray(); } } } 6. AliasGenerator.cs(批量自动生成别名) using Autodesk.Navisworks.Api; using System; using System.Collections.Generic; namespace AliasTree { public class AliasGenerator { private Dictionary< _result = new Dictionary<(); private HashSet _usedNames = new HashSet(); public Dictionary< Generate(ModelItem root, string separator = "/") { _result.Clear(); _usedNames.Clear(); if (root != null) Traverse(root, "", separator, 0); return _result; } private void Traverse(ModelItem item, string parentPath, string sep, int depth) { string rawName = item.DisplayName?.Trim() ?? "未命名"; // 一级节点保持原名;二级开始加父前缀 string alias = depth == 0 ? rawName : (string.IsNullOrEmpty(parentPath) ? rawName : $"{parentPath}{sep}{rawName}"); string uniqueAlias = MakeUnique(alias); _result[item.InstanceGuid] = uniqueAlias; // 下一级前缀:最多保留最近一级父名,避免路径无限膨胀 string nextPrefix = depth < 1 ? uniqueAlias : rawName; foreach (var child in item.Children) Traverse(child, nextPrefix, sep, depth + 1); } private string MakeUnique(string name) { if (!_usedNames.Contains(name)) { _usedNames.Add(name); return name; } int idx = 1; string candidate; do { candidate = $"{name}_{idx:D3}"; idx++; } while (_usedNames.Contains(candidate)); _usedNames.Add(candidate); return candidate; } } } 编译与部署说明 | 项目 | 内容 | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **项目类型** | Visual Studio → C# → 类库(.NET Framework) | | **目标框架** | .NET Framework 4.8(与 Navisworks 版本匹配) | | **引用 DLL** | `Autodesk.Navisworks.Api.dll`
`Autodesk.Navisworks.Automation.dll`
位置通常在:`C:\Program Files\Autodesk\Navisworks Manage 2024\` | | **输出路径** | 编译后,将生成的 DLL 与 XAML/BAML 一起放入:
`%APPDATA%\Autodesk Navisworks Manage 2024\Plugins\AliasTree\` | | **启动** | 重启 Navisworks,在 **View → Docking Windows** 中找到 **别名导航树** | 使用流程(假设一个"八个楼层"的模型节点) 打开插件面板,dock 到内置 Selection Tree 旁边。 在内置树或 3D 视图中点选一个名叫"楼层"的节点。 别名树自动高亮该节点,顶部工具栏显示原名。 任选一种方式打别名: 鼠标悬停别名树节点 → 点 ✎ 铅笔 → 就地输入"8F-办公层" → 回车。 看 3D 视图确认位置 → 在顶部输入框敲"8F-办公层" → 点 ✓ 保存。 右键节点 → 编辑别名。 如果八个子节点全叫"楼层",可点 自动生成 按钮,一键生成 楼层/楼层_001 … 楼层/楼层_007,你再精修成 1F大堂、2F办公 等。 工作完成后 导出 CSV,下次打开 Navisworks 时 导入 CSV 即可恢复所有别名。