- 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
28 KiB
原生树无法扩展,自定义别名树面板就是唯一可靠的载体。在那个面板里,你可以完全自由地定制右键菜单,比如: 表格 菜单项 功能 编辑别名 就地改名 清除别名 恢复原名 在视图中聚焦 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 视图/内置树选中后,别名树自动展开并高亮。 延迟加载:大模型下只加载可见节点,展开时才填充子节点。
- 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));
}
}
- 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(); } }
- AliasTreePane.cs(Pane 外壳)
using Autodesk.Navisworks.Api.Plugins;
namespace AliasTree { public class AliasTreePane : Pane { public AliasTreePane() { this.Content = new AliasTreeControl(); } } }
- AliasTreeControl.xaml(界面布局)
<!-- 别名树 -->
<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>
- 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< _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<<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();
}
}
}
- 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 _usedNames = new HashSet();
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.dllAutodesk.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 即可恢复所有别名。