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

28 KiB
Raw Blame History

原生树无法扩展,自定义别名树面板就是唯一可靠的载体。在那个面板里,你可以完全自由地定制右键菜单,比如: 表格 菜单项 功能 编辑别名 就地改名 清除别名 恢复原名 在视图中聚焦 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));
}

}

  1. 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(); } }

  1. AliasTreePane.csPane 外壳)

using Autodesk.Navisworks.Api.Plugins;

namespace AliasTree { public class AliasTreePane : Pane { public AliasTreePane() { this.Content = new AliasTreeControl(); } } }

  1. 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>
  1. 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();
    }
}

}

  1. 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.dll
Autodesk.Navisworks.Automation.dll
位置通常在:C:\Program Files\Autodesk\Navisworks Manage 2024\
输出路径 编译后,将生成的 DLL 与 XAML/BAML 一起放入:
%APPDATA%\Autodesk Navisworks Manage 2024\Plugins\AliasTree\
启动 重启 NavisworksView → Docking Windows 中找到 别名导航树

使用流程(假设一个"八个楼层"的模型节点) 打开插件面板dock 到内置 Selection Tree 旁边。 在内置树或 3D 视图中点选一个名叫"楼层"的节点。 别名树自动高亮该节点,顶部工具栏显示原名。 任选一种方式打别名: 鼠标悬停别名树节点 → 点 ✎ 铅笔 → 就地输入"8F-办公层" → 回车。 看 3D 视图确认位置 → 在顶部输入框敲"8F-办公层" → 点 ✓ 保存。 右键节点 → 编辑别名。 如果八个子节点全叫"楼层",可点 自动生成 按钮,一键生成 楼层/楼层_001 … 楼层/楼层_007你再精修成 1F大堂、2F办公 等。 工作完成后 导出 CSV下次打开 Navisworks 时 导入 CSV 即可恢复所有别名。