NavisworksTransport/doc/design/2026/节点重命名参考方案.md
tian 3f985aa235 phase 1: data layer for alias tree
- AliasNodeIdentity: stable composite node identifier (path + sibling index)
- AliasDataStore: CRUD operations on SQLite AliasMap table
- PathDatabase: add AliasMap table creation + index
- Unit tests: 30 tests for identity parsing/equality, store CRUD/batch/import
- Both projects compile successfully
2026-05-31 12:46:33 +08:00

724 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

原生树无法扩展,自定义别名树面板就是唯一可靠的载体。在那个面板里,你可以完全自由地定制右键菜单,比如:
表格 菜单项 功能
编辑别名 就地改名
清除别名 恢复原名
在视图中聚焦 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 个文件。核心思路:
AliasTreePaneNavisworks DockPane 外壳。
AliasTreeControlWPF 别名树面板支持悬停铅笔编辑、顶部快捷栏、右键菜单、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<<AliasNode> Children { get; set; } = new List<<AliasNode>();
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.csPane 外壳)
using Autodesk.Navisworks.Api.Plugins;
namespace AliasTree
{
public class AliasTreePane : Pane
{
public AliasTreePane()
{
this.Content = new AliasTreeControl();
}
}
}
4. AliasTreeControl.xaml界面布局
<UserControl x:Class="AliasTree.AliasTreeControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AliasTree">
<DockPanel>
<!-- 顶部快捷栏 -->
<ToolBar DockPanel.Dock="Top" Height="32">
<TextBlock Text="当前选中:" VerticalAlignment="Center" Margin="4,0"/>
<TextBlock x:Name="TbCurrentName" Text="无" VerticalAlignment="Center"
FontWeight="Bold" Margin="4,0" MaxWidth="150" TextTrimming="CharacterEllipsis"/>
<Separator/>
<TextBox x:Name="TxtQuickAlias" Width="120" ToolTip="输入别名后回车"
KeyDown="TxtQuickAlias_KeyDown"/>
<Button Content="✓ 保存" Click="BtnQuickSave_Click" ToolTip="为当前选中节点保存别名"/>
<Separator/>
<Button Content="导出CSV" Click="BtnExport_Click"/>
<Button Content="导入CSV" Click="BtnImport_Click"/>
<Separator/>
<Button Content="自动生成" Click="BtnAutoGen_Click" ToolTip="按父节点路径自动生成别名"/>
</ToolBar>
<!-- 别名树 -->
<TreeView x:Name="AliasTreeView"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:AliasNode}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="0,1">
<!-- 显示模式:无别名时灰色斜体显示原名 -->
<TextBlock Text="{Binding DisplayText}" VerticalAlignment="Center" MinWidth="60">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="Black"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Alias}" Value="">
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
<DataTrigger Binding="{Binding Alias}" Value="{x:Null}">
<Setter Property="Foreground" Value="Gray"/>
<Setter Property="FontStyle" Value="Italic"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- 编辑模式默认隐藏IsEditing=True 时显示 -->
<TextBox Text="{Binding Alias, UpdateSourceTrigger=PropertyChanged}"
Width="120" Visibility="Collapsed" VerticalAlignment="Center"
KeyDown="AliasTextBox_KeyDown" LostFocus="AliasTextBox_LostFocus">
<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 Content="✎" Margin="4,0,0,0" Padding="2,0"
Click="BtnEdit_Click" Tag="{Binding}" VerticalAlignment="Center">
<Button.Style>
<Style TargetType="Button">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=TreeViewItem}, Path=IsMouseOver}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="False"/>
<EventSetter Event="Expanded" Handler="TreeViewItem_Expanded"/>
<EventSetter Event="Selected" Handler="TreeViewItem_Selected"/>
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="编辑别名" Click="CtxEdit_Click"/>
<MenuItem Header="清除别名" Click="CtxClear_Click"/>
<Separator/>
<MenuItem Header="在视图中聚焦" Click="CtxFocus_Click"/>
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
</DockPanel>
</UserControl>
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<<Guid, string> _aliasMap = new Dictionary<<Guid, string>();
private List<<AliasNode> _rootNodes = new List<<AliasNode>();
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<<AliasNode>();
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<<AliasNode> nodes, Guid guid, List<<AliasNode> 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<TextBox>(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<string> { "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<<AliasNode> 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<<AliasNode> nodes)
{
foreach (var n in nodes)
{
n.IsEditing = false;
ClearAllEditing(n.Children);
}
}
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;
}
private static string[] ParseCsvLine(string line)
{
var result = new List<string>();
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<<Guid, string> _result = new Dictionary<<Guid, string>();
private HashSet<string> _usedNames = new HashSet<string>();
public Dictionary<<Guid, string> 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`<br>`Autodesk.Navisworks.Automation.dll`<br>位置通常在:`C:\Program Files\Autodesk\Navisworks Manage 2024\` |
| **输出路径** | 编译后,将生成的 DLL 与 XAML/BAML 一起放入:<br>`%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 即可恢复所有别名。