# Navisworks API 使用方法指南 基于真实官方示例的正确API用法总结 ## 参考示例来源 基于以下官方示例文件的真实API用法: - `C:\Users\Tellme\apps\NavisworksTransport\doc\navisworks_api\NET\examples\PlugIns\SearchComparisonPlugIn\SearchComparisonPlugIn.cs` - `C:\Users\Tellme\apps\NavisworksTransport\doc\navisworks_api\NET\examples\PlugIns\Examiner\Examiner.cs` ## 1. 模型遍历和节点访问 ### 1.1 正确的遍历方式 ```csharp // ✅ 正确:获取所有模型项 IEnumerable allItems = Application.ActiveDocument.Models.RootItemDescendantsAndSelf; // ✅ 正确:遍历特定模型的所有项 foreach (Model model in document.Models) { foreach (ModelItem item in model.RootItem.DescendantsAndSelf) { // 处理每个模型项 } } // ✅ 正确:只获取顶级节点 foreach (Model model in document.Models) { foreach (ModelItem topLevelItem in model.RootItem.Children) { // 处理顶级节点 } } ``` ### 1.2 获取子节点 ```csharp // ✅ 正确:获取某个节点的所有后代 var childItems = selectedItem.DescendantsAndSelf.Where(x => x != selectedItem); // ✅ 正确:只获取直接子节点 foreach (ModelItem child in parentItem.Children) { // 处理直接子节点 } ``` ### 1.3 遍历祖先节点 ```csharp // ✅ 正确:向上遍历父节点链 var current = selectedItem.Parent; while (current != null) { // 处理祖先节点 current = current.Parent; } ``` ## 2. 搜索和查询 ### 2.1 使用LINQ查询 ```csharp // ✅ 正确:使用LINQ查询模型项 IEnumerable results = Application.ActiveDocument.Models.RootItemDescendantsAndSelf .Where(x => x.HasGeometry && !x.IsHidden && x.ClassDisplayName.ToLower().Contains("wall")); ``` ### 2.2 使用Search类 ```csharp // ✅ 正确:使用Search API Search search = new Search(); // 添加搜索条件 search.SearchConditions.Add( SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry)); search.SearchConditions.Add( SearchCondition.HasPropertyByName(PropertyCategoryNames.Item, DataPropertyNames.ItemHidden) .EqualValue(VariantData.FromBoolean(false))); // 设置搜索范围 search.Selection.SelectAll(); search.Locations = SearchLocations.DescendantsAndSelf; // 执行搜索 ModelItemCollection results = search.FindAll(document, false); ``` ### 2.3 迭代遍历(性能对比) ```csharp // ✅ 可用但性能较低:迭代方法 ModelItemCollection searchResults = new ModelItemCollection(); foreach (ModelItem modelItem in Application.ActiveDocument.Models.CreateCollectionFromRootItems().DescendantsAndSelf) { if (modelItem.HasGeometry && !modelItem.IsHidden) searchResults.Add(modelItem); } ``` ## 3. 选择操作 ### 3.1 操作当前选择 ```csharp // ✅ 正确:获取当前选择 var currentSelection = document.CurrentSelection.SelectedItems; // ✅ 正确:清空选择 document.CurrentSelection.Clear(); // ✅ 正确:添加到选择 document.CurrentSelection.Add(modelItem); // ✅ 正确:复制集合到选择 document.CurrentSelection.CopyFrom(modelItems); ``` ## 4. 可见性控制 ### 4.1 隐藏和显示 ```csharp // ✅ 正确:隐藏项目 ModelItemCollection itemsToHide = new ModelItemCollection(); itemsToHide.Add(modelItem); document.Models.SetHidden(itemsToHide, true); // ✅ 正确:显示项目 document.Models.SetHidden(itemsToHide, false); // ✅ 正确:检查是否隐藏 if (modelItem.IsHidden) { // 项目被隐藏 } ``` ## 5. 文件导出 ### 5.1 基本文件保存 ```csharp // ✅ 正确:保存NWD文件 document.SaveFile(filePath); // ✅ 正确:指定版本保存 document.SaveFile(filePath, DocumentFileVersion.Current); ``` ### 5.2 ExportToNwd API ```csharp // ✅ 正确:使用ExportToNwd导出 var exportOptions = new NwdExportOptions(); exportOptions.ExcludeHiddenItems = true; // 只导出可见项目 exportOptions.EmbedXrefs = false; exportOptions.PreventObjectPropertyExport = false; document.ExportToNwd(saveFilePath, exportOptions); ``` ## 6. 性能最佳实践 ### 6.1 避免的做法 ```csharp // ❌ 错误:使用不存在的API // SearchCondition.HasAncestor(items) // 这个API不存在 // ❌ 错误:深度递归遍历 // void RecursiveTraversal(ModelItem item) // 大模型中可能导致堆栈溢出 ``` ### 6.2 推荐的做法 ```csharp // ✅ 推荐:使用内置的DescendantsAndSelf var allDescendants = rootItem.DescendantsAndSelf; // ✅ 推荐:使用LINQ进行高效查询 var filteredItems = allItems.Where(x => x.HasGeometry); // ✅ 推荐:批量操作而不是逐个操作 ModelItemCollection batchItems = new ModelItemCollection(); // 添加所有需要处理的项目 document.Models.SetHidden(batchItems, true); // 一次性操作 ``` ## 7. 完整示例:多选节点导出 ```csharp public void ExportSelectedNodes(List selectedItems, string filePath) { var document = Application.ActiveDocument; var nodesToKeepVisible = new HashSet(); // 1. 收集需要保持可见的节点 foreach (var selectedItem in selectedItems) { // 添加选中节点本身 nodesToKeepVisible.Add(selectedItem); // 添加所有祖先节点 var current = selectedItem.Parent; while (current != null) { nodesToKeepVisible.Add(current); current = current.Parent; } // 添加所有子节点(可选) var childItems = selectedItem.DescendantsAndSelf.Where(x => x != selectedItem); foreach (ModelItem child in childItems) { nodesToKeepVisible.Add(child); } } // 2. 收集顶级节点并决定隐藏哪些 var itemsToHide = new ModelItemCollection(); foreach (Model model in document.Models) { foreach (ModelItem topLevelItem in model.RootItem.Children) { bool shouldKeep = false; // 检查是否包含选中节点 foreach (var selectedItem in selectedItems) { var current = selectedItem; while (current != null) { if (current == topLevelItem) { shouldKeep = true; break; } current = current.Parent; } if (shouldKeep) break; } if (!shouldKeep) { itemsToHide.Add(topLevelItem); } } } // 3. 执行隐藏和导出 try { document.Models.SetHidden(itemsToHide, true); var exportOptions = new NwdExportOptions(); exportOptions.ExcludeHiddenItems = true; document.ExportToNwd(filePath, exportOptions); } finally { // 4. 恢复可见性 document.Models.SetHidden(itemsToHide, false); } } ``` ## 8. 线程安全 - 关键重要 ### 8.1 Navisworks API 线程安全要求 **核心原则:所有 Navisworks API 调用必须在主 UI 线程(STA 线程)中执行** ```csharp // ❌ 错误:在后台线程中调用 Navisworks API await Task.Run(() => { var document = Application.ActiveDocument; // 可能崩溃 document.ExportToNwd(path, options); // 会崩溃 }); // ✅ 正确:使用 Dispatcher.Invoke 确保主线程执行 await Task.Run(() => { System.Windows.Application.Current.Dispatcher.Invoke(() => { var document = Application.ActiveDocument; document.ExportToNwd(path, options); // 安全执行 }); }); ``` ### 8.2 实际案例:分层导出修复 **问题场景**:`SimplifiedModelSplitterManager.ExportLayerToNwd` 方法通过后台线程调用时崩溃 ```csharp // ❌ 问题代码:导致崩溃 public bool ExportLayerToNwd(...) { var document = NavisApplication.ActiveDocument; document.ExportToNwd(outputPath, exportOptions); // 后台线程崩溃 } ``` **修复方案**:使用 Dispatcher.Invoke 包装所有 API 调用 ```csharp // ✅ 修复代码:线程安全 public bool ExportLayerToNwd(...) { bool exportResult = false; Exception exportException = null; // 确保在主线程中执行所有 Navisworks API 调用 System.Windows.Application.Current.Dispatcher.Invoke(() => { try { var document = NavisApplication.ActiveDocument; // 保存可见性状态 var originalVisibilityState = SaveCurrentVisibilityState(document); try { // 隐藏不需要的项目 var itemsToHide = GetItemsToHide(...); document.Models.SetHidden(itemsToHide, true); // 创建导出选项 var exportOptions = new NwdExportOptions { ExcludeHiddenItems = true, EmbedXrefs = false, PreventObjectPropertyExport = false }; // 在主线程中安全执行导出 document.ExportToNwd(outputPath, exportOptions); exportResult = true; } finally { // 恢复可见性状态 RestoreVisibilityState(document, originalVisibilityState); } } catch (Exception ex) { exportException = ex; } }); if (exportException != null) throw exportException; return exportResult; } ``` ### 8.3 线程安全检查和诊断 ```csharp // ✅ 检查当前线程状态 var apartmentState = System.Threading.Thread.CurrentThread.GetApartmentState(); LogManager.Info($"当前线程状态: {apartmentState}"); // 应该是 STA if (apartmentState != System.Threading.ApartmentState.STA) { LogManager.Warning("警告:不在STA线程中,API调用可能失败"); } // ✅ 验证是否在主线程中 bool isMainThread = System.Windows.Application.Current.Dispatcher.CheckAccess(); if (!isMainThread) { LogManager.Warning("警告:不在主线程中,需要使用Dispatcher.Invoke"); } ``` ### 8.4 常见线程安全问题和解决方案 | 问题场景 | 症状 | 解决方案 | |---------|------|---------| | 后台线程调用 API | 程序崩溃,无错误信息 | 使用 `Dispatcher.Invoke()` | | Command.ExecuteAsync() | Task 中的 API 调用崩溃 | 在 Task 内部使用 Dispatcher | | 异步方法调用 API | 间歇性崩溃 | 检查执行线程,确保主线程 | | Timer 中调用 API | 定时器触发时崩溃 | Timer 回调使用 Dispatcher | ### 8.5 最佳实践模式 ```csharp // ✅ 推荐模式:安全的异步 Navisworks API 调用 public async Task SafeNavisworksOperationAsync() { // 1. 后台准备数据 var preparedData = await Task.Run(() => { // 在后台线程中进行数据准备(不涉及 Navisworks API) return PrepareDataSafely(); }); // 2. 主线程执行 API 调用 bool result = false; await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { // 所有 Navisworks API 调用都在主线程中 var document = Application.ActiveDocument; result = document.SomeNavisworksOperation(preparedData); }); return result; } // ✅ 推荐模式:批量 API 操作 public void BatchNavisworksOperations(List items) { System.Windows.Application.Current.Dispatcher.Invoke(() => { var document = Application.ActiveDocument; // 批量操作,避免多次线程切换 var itemCollection = new ModelItemCollection(); foreach (var item in items) { itemCollection.Add(item); } // 一次性完成所有操作 document.Models.SetHidden(itemCollection, true); document.CurrentSelection.CopyFrom(itemCollection); }); } ``` ## 9. 常用属性和方法速查 ### ModelItem 常用属性 - `HasGeometry` - 是否有几何体 - `IsHidden` - 是否隐藏 - `IsRequired` - 是否必需 - `IsInsert` - 是否为插入对象 - `IsLayer` - 是否为图层 - `DisplayName` - 显示名称 - `ClassName` - 类名 - `ClassDisplayName` - 类显示名称 - `Parent` - 父节点 - `Children` - 子节点集合 - `DescendantsAndSelf` - 所有后代节点(包括自己) ### Document 常用方法 - `SaveFile(string path)` - 保存文件 - `ExportToNwd(string path, NwdExportOptions options)` - 导出NWD - `CurrentSelection` - 当前选择 - `Models` - 模型集合 ### Models 常用方法 - `SetHidden(ModelItemCollection items, bool hidden)` - 设置隐藏状态 - `SetRequired(ModelItemCollection items, bool required)` - 设置必需状态 - `RootItemDescendantsAndSelf` - 所有根项目的后代 ## 10. 错误避免指南 1. **线程安全是第一要务**:所有 Navisworks API 调用必须在主 UI 线程中执行 2. **不要使用不存在的API**:如 `SearchCondition.HasAncestor` 3. **避免深度递归**:使用内置的 `DescendantsAndSelf` 代替手写递归 4. **批量操作**:使用 `ModelItemCollection` 进行批量设置,而不是逐个操作 5. **正确的命名空间**:确保引用 `using Autodesk.Navisworks.Api;` 6. **异常处理**:文件操作和API调用要适当处理异常 7. **资源清理**:隐藏操作后要恢复原始状态 8. **线程状态检查**:在关键操作前验证线程状态(STA) 9. **Dispatcher 模式**:后台线程中需要调用 API 时,始终使用 Dispatcher.Invoke ## 11. Transform 变换操作 ### 11.1 Transform 相关 API 概念 **核心概念**: - `ModelItem.Transform` - 返回设计文件中的原始变换,只读属性 - `OverridePermanentTransform()` - 应用增量变换(与现有变换叠加) - `ResetPermanentTransform()` - 清除所有增量变换,恢复到设计文件原始位置 ### 11.2 Transform 操作的正确用法 ```csharp // ✅ 获取物体的原始Transform(设计文件中的位置) Transform3D originalTransform = modelItem.Transform; // ✅ 应用增量变换(累积变换) var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { modelItem }; doc.Models.OverridePermanentTransform(modelItems, newTransform, false); // ✅ 重置到原始位置(清除所有增量变换) doc.Models.ResetPermanentTransform(modelItems); ``` ### 11.3 Transform 操作的关键区别 | API方法 | 作用 | 使用场景 | 注意事项 | |---------|------|---------|---------| | `ModelItem.Transform` | 获取原始变换 | 记录物体初始位置 | 只读属性,返回设计文件位置 | | `OverridePermanentTransform()` | 应用增量变换 | 动画中移动物体 | 与现有变换累积,不是绝对位置 | | `ResetPermanentTransform()` | 重置到原始位置 | 清除所有移动,恢复初始状态 | 忽略所有之前的变换 | ### 11.4 实际应用案例 **案例1:动画系统中的Transform管理** ```csharp // 动画开始时记录原始位置 private Transform3D _originalTransform; public void StartAnimation(ModelItem animatedObject) { // 记录原始Transform _originalTransform = animatedObject.Transform; // 移动到路径起点(增量变换) var startTransform = Transform3D.CreateTranslation(startPosition); var modelItems = new ModelItemCollection { animatedObject }; doc.Models.OverridePermanentTransform(modelItems, startTransform, false); } public void ResetAnimation() { // 动画结束后,使用原始Transform恢复位置 var modelItems = new ModelItemCollection { _animatedObject }; doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false); } ``` **案例2:用户手动位置恢复** ```csharp public void RestoreToOriginalPosition(ModelItem selectedObject) { // 不需要记录Transform,直接重置到设计文件原始位置 var doc = Application.ActiveDocument; var modelItems = new ModelItemCollection { selectedObject }; // 清除所有增量变换,恢复到设计文件原始位置 doc.Models.ResetPermanentTransform(modelItems); } ``` ### 11.5 常见Transform问题和解决方案 **问题1:位置恢复有偏移** ```csharp // ❌ 错误:使用增量变换恢复位置 doc.Models.OverridePermanentTransform(modelItems, originalTransform, false); // 问题:如果物体已经被移动过,这会导致累积偏移 // ✅ 正确:重置到原始位置 doc.Models.ResetPermanentTransform(modelItems); // 结果:直接恢复到设计文件中的原始位置,无偏移 ``` **问题2:动画结束后位置不准确** ```csharp // ✅ 动画系统应该记录原始Transform并使用增量恢复 private Transform3D _originalTransform; // 动画开始时 _originalTransform = animatedObject.Transform; // 动画结束时恢复 doc.Models.OverridePermanentTransform(modelItems, _originalTransform, false); ``` **问题3:记录Transform但不使用** ```csharp // ❌ 不必要:记录Transform但使用Reset private Transform3D _originalTransform; _originalTransform = selectedItem.Transform; // 记录了但不使用 doc.Models.ResetPermanentTransform(modelItems); // 直接重置 // ✅ 简化:直接重置,无需记录 doc.Models.ResetPermanentTransform(modelItems); ``` ### 11.6 Transform 最佳实践 1. **选择合适的恢复方式**: - 动画系统:使用 `OverridePermanentTransform` + 原始Transform - 用户操作:使用 `ResetPermanentTransform` 直接重置 2. **避免不必要的Transform记录**: - 如果只需要恢复到设计文件原始位置,使用 `ResetPermanentTransform` - 只有需要恢复到特定中间状态时才记录Transform 3. **理解增量vs绝对变换**: - `OverridePermanentTransform` 是增量的,会与现有变换叠加 - `ResetPermanentTransform` 是绝对的,清除所有变换 4. **线程安全**: - 所有Transform操作都必须在主UI线程中执行 - 使用 `Dispatcher.Invoke` 确保线程安全 ## 12. Item属性和自定义属性访问 基于官方示例的正确属性访问方法总结。 ### 12.1 NET API 属性访问方法 #### 基础属性访问(基于 Examiner.cs) ```csharp // ✅ 通过PropertyCategories查找特定分类 var category = item.PropertyCategories.FindCategoryByDisplayName("Material"); // ✅ 访问基础属性 string displayName = item.DisplayName; string className = item.ClassName; string classDisplayName = item.ClassDisplayName; bool hasGeometry = item.HasGeometry; bool isHidden = item.IsHidden; bool isRequired = item.IsRequired; // ✅ 通过LINQ查询特定属性的项目 IEnumerable itemsWithMaterial = Application.ActiveDocument.Models.RootItemDescendantsAndSelf .Where(x => x.PropertyCategories.FindCategoryByDisplayName("Material") != null); ``` #### 高级属性搜索(基于 SearchComparisonPlugIn.cs) ```csharp // ✅ 使用预定义属性分类和属性名进行搜索 Search search = new Search(); // 搜索有几何体的项目 search.SearchConditions.Add( SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry)); // 搜索非隐藏的项目 search.SearchConditions.Add( SearchCondition.HasPropertyByName(PropertyCategoryNames.Item, DataPropertyNames.ItemHidden) .EqualValue(VariantData.FromBoolean(false))); // 设置搜索范围并执行 search.Selection.SelectAll(); search.Locations = SearchLocations.DescendantsAndSelf; ModelItemCollection results = search.FindAll(document, false); ``` #### 属性分类和属性名常量 ```csharp // ✅ 使用预定义常量访问标准属性 // PropertyCategoryNames 包含: // - PropertyCategoryNames.Item (项目属性) // - PropertyCategoryNames.Geometry (几何属性) // - PropertyCategoryNames.Material (材质属性) // DataPropertyNames 包含: // - DataPropertyNames.ItemHidden (隐藏状态) // - DataPropertyNames.ItemRequired (必需状态) // 等等... ``` ### 12.2 COM API 属性访问方法(基于 AutoUserPropsExample.cs) #### 获取和遍历属性 ```csharp // ✅ 获取选中对象的属性节点 InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2; if (selection.Paths().Count > 0) { InwGUIPropertyNode2 propertyNode = m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2; // 遍历所有属性分类 foreach (InwGUIAttribute2 guiAttribute in propertyNode.GUIAttributes()) { Console.WriteLine($"分类: {guiAttribute.ClassName}"); Console.WriteLine($"显示名: {guiAttribute.ClassUserName}"); Console.WriteLine($"用户自定义: {guiAttribute.UserDefined}"); // 遍历分类中的所有属性 foreach (InwOaProperty property in guiAttribute.Properties()) { string propertyName = property.name; // 内部名称 string displayName = property.UserName; // 显示名称 string value = property.value; // 属性值 Console.WriteLine($" {displayName}({propertyName}) = {value}"); } } } ``` #### 添加自定义属性 ```csharp // ✅ 创建新的自定义属性 private void AddCustomProperty(string categoryName, string internalName, string value) { InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2; if (selection.Paths().Count > 0) { InwGUIPropertyNode2 propertyNode = m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2; // 创建属性容器 InwOaPropertyVec propertyVector = m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaPropertyVec); // 创建单个属性 InwOaProperty property = m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaProperty); property.name = "CustomProperty1"; // 内部名称 property.UserName = "自定义属性1"; // 显示名称 property.value = value; // 属性值 // 添加到容器 propertyVector.Properties().Add(property); // 设置到对象上 propertyNode.SetUserDefined(0, categoryName, internalName, propertyVector); } } // ✅ 使用示例 AddCustomProperty("自定义分类", "Custom_Category", "自定义值"); ``` #### 修改现有自定义属性 ```csharp // ✅ 正确的修改方法(基于实际修复经验和官方AutoUserPropsExample示例) private void UpdateCustomProperty(string categoryName, string internalName, string newValue) { InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2; if (selection.Paths().Count > 0) { InwGUIPropertyNode2 propertyNode = m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2; // 🔍 关键第一步:查找现有属性分类的正确索引 int existingIndex = GetFloorAttributeIndex(propertyNode, categoryName); // 创建新的属性内容 InwOaPropertyVec propertyVector = m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaPropertyVec); InwOaProperty property = m_state.ObjectFactory(nwEObjectType.eObjectType_nwOaProperty); property.name = "Floor_Level"; property.UserName = "楼层"; property.value = newValue; propertyVector.Properties().Add(property); if (existingIndex >= 0) { // 🎯 存在属性时:使用正确的索引进行更新 // 注意:SetUserDefined的索引是从1开始的 int updateIndex = existingIndex + 1; propertyNode.SetUserDefined(updateIndex, categoryName, internalName, propertyVector); } else { // 🆕 不存在属性时:使用索引0创建新属性分类 propertyNode.SetUserDefined(0, categoryName, internalName, propertyVector); } } } // ✅ 查找现有属性分类索引的正确方法 private int GetFloorAttributeIndex(InwGUIPropertyNode2 propertyNode, string categoryName) { int userDefinedIndex = 0; // 用户定义属性的索引计数器 foreach (InwGUIAttribute2 attribute in propertyNode.GUIAttributes()) { if (attribute.UserDefined) { // ⚠️ 关键修复:只需要匹配ClassUserName,不需要匹配ClassName // ClassName是系统生成的(如"LcOaPropOverrideCat"),不是我们控制的 if (attribute.ClassUserName == categoryName) { return userDefinedIndex; // 返回在用户定义属性中的索引位置 } userDefinedIndex++; } } return -1; // 未找到返回-1 } // ✅ 使用示例 - 正确的更新方式 // 第一次设置:创建新属性 UpdateCustomProperty("分层信息", "Floor_Category", "F1"); // 第二次设置:找到并更新现有属性 UpdateCustomProperty("分层信息", "Floor_Category", "F2"); // 第三次设置:继续更新同一个属性 UpdateCustomProperty("分层信息", "Floor_Category", "F3"); ``` **关键要点**(基于实际修复经验): - **动态索引查找**:不能使用硬编码索引,必须动态查找现有属性的位置 - **只匹配ClassUserName**:`ClassName`是系统生成的标识符(如`LcOaPropOverrideCat`),我们无法控制 - **索引转换规则**:查找返回的是0基索引,`SetUserDefined`使用的是1基索引 - **创建vs更新**:index=0表示创建新属性,index>0表示更新指定位置的属性 #### 删除自定义属性 ```csharp // ✅ 删除指定的自定义属性分类 private void RemoveCustomProperty() { InwOpSelection2 selection = m_state.CurrentSelection as InwOpSelection2; if (selection.Paths().Count > 0) { InwGUIPropertyNode2 propertyNode = m_state.GetGUIPropertyNode(selection.Paths()[1], true) as InwGUIPropertyNode2; // 删除索引为0的用户自定义属性分类 propertyNode.RemoveUserDefined(0); } } ``` ### 12.3 两种API的选择建议 | 操作类型 | 推荐API | 理由 | |---------|---------|------| | 搜索和过滤 | NET API | LINQ查询更灵活,性能更好 | | 读取标准属性 | NET API | 类型安全,代码简洁 | | 添加/修改自定义属性 | COM API | 提供完整的属性操作能力 | | 删除自定义属性 | COM API | NET API不支持属性删除 | | 批量属性操作 | NET API | 支持ModelItemCollection批量操作 | ### 12.4 属性操作最佳实践 ```csharp // ✅ 推荐模式:结合两种API的优势 public void ProcessItemsWithCustomProperties() { // 1. 使用NET API进行搜索和过滤 var itemsWithGeometry = Application.ActiveDocument.Models.RootItemDescendantsAndSelf .Where(item => item.HasGeometry && !item.IsHidden) .ToList(); // 2. 对每个项目使用COM API添加自定义属性 foreach (ModelItem item in itemsWithGeometry) { // 选中当前项目 Application.ActiveDocument.CurrentSelection.Clear(); Application.ActiveDocument.CurrentSelection.Add(item); // 使用COM API添加自定义属性 AddCustomProperty("物流信息", "Logistics_Info", $"处理时间: {DateTime.Now}"); } } ``` ### 12.5 常见属性操作示例 ```csharp // ✅ 检查项目是否有特定属性分类 public bool HasPropertyCategory(ModelItem item, string categoryName) { return item.PropertyCategories.FindCategoryByDisplayName(categoryName) != null; } // ✅ 获取项目的所有属性信息(用于调试) public string GetItemPropertyInfo(ModelItem item) { var sb = new StringBuilder(); sb.AppendLine($"项目: {item.DisplayName}"); foreach (PropertyCategory category in item.PropertyCategories) { sb.AppendLine($" 分类: {category.DisplayName}"); foreach (DataProperty property in category.Properties) { sb.AppendLine($" {property.DisplayName}: {property.Value}"); } } return sb.ToString(); } // ✅ 基于属性值进行复杂搜索 public List FindItemsByPropertyValue(string categoryName, string propertyName, string searchValue) { return Application.ActiveDocument.Models.RootItemDescendantsAndSelf .Where(item => { var category = item.PropertyCategories.FindCategoryByDisplayName(categoryName); if (category == null) return false; var property = category.Properties.FirstOrDefault(p => p.DisplayName == propertyName); return property != null && property.Value.ToString().Contains(searchValue); }) .ToList(); } ``` ### 12.6 Item属性的可编辑性分析 根据官方示例和Navisworks API设计,**大部分标准Item属性是只读的**,但有**少数属性可以编辑**。 #### 可编辑的Item属性 **1. 可见性和状态属性(通过Models API编辑)** ```csharp // ✅ 可以修改:隐藏状态 document.Models.SetHidden(modelItemCollection, true); // ✅ 可以修改:必需状态 document.Models.SetRequired(modelItemCollection, true); // ✅ 可以修改:颜色覆盖 document.Models.OverridePermanentColor(modelItemCollection, Color.Red); // ✅ 可以修改:透明度覆盖 document.Models.OverridePermanentTransparency(modelItemCollection, 0.5); // ✅ 可以修改:Transform变换 document.Models.OverridePermanentTransform(modelItemCollection, transform, false); ``` **2. 基于官方Examiner.cs示例的确认** ```csharp // 官方示例中的这些操作证实了这些属性是可修改的 document.Models.SetRequired(items, (required == SearchForm.ChangeDecision.Yes)); document.Models.SetHidden(items, (hidden == SearchForm.ChangeDecision.Yes)); document.Models.OverridePermanentColor(items, overrideColor); document.Models.OverridePermanentTransparency(items, overrideTransparencyValue); ``` #### 只读的Item属性 **1. 基础标识属性(只读)** ```csharp // ❌ 只读:无法修改,由原始CAD文件决定 item.DisplayName // 显示名称 item.ClassName // 类名 item.ClassDisplayName // 类显示名称 item.HasGeometry // 几何体标志 item.IsInsert // 插入对象标志 item.IsLayer // 图层标志 ``` **2. 结构关系属性(只读)** ```csharp // ❌ 只读:结构关系由模型文件决定,无法通过API修改 item.Parent // 父节点 item.Children // 子节点 item.Ancestors // 祖先节点 item.Descendants // 后代节点 ``` **3. 几何和材质属性(只读)** ```csharp // ❌ 只读:由原始CAD文件决定 item.Geometry // 几何信息 item.BoundingBox() // 包围盒 item.PropertyCategories // 标准属性分类(但可通过COM API添加自定义分类) ``` #### 特殊情况:自定义属性完全可编辑 通过COM API可以**完全控制自定义属性**: ```csharp // ✅ 完全可编辑:自定义属性 // 添加新的自定义属性 propertyNode.SetUserDefined(index, categoryName, internalName, propertyVector); // 修改现有自定义属性(重新设置) propertyNode.SetUserDefined(index, categoryName, internalName, newPropertyVector); // 删除自定义属性 propertyNode.RemoveUserDefined(index); ``` #### 物流插件的实际应用策略 **1. 使用可编辑的标准属性进行状态管理** ```csharp // 物流分类可见性管理 document.Models.SetHidden(obstacleItems, true); // 隐藏障碍物 document.Models.SetHidden(loadingZoneItems, false); // 显示装卸区 // 路径可视化 document.Models.OverridePermanentColor(pathItems, pathColor); // 路径颜色标记 document.Models.OverridePermanentColor(selectedItems, highlightColor); // 选中高亮 // 动画和移动 document.Models.OverridePermanentTransform(movableItems, newTransform, false); // 物体移动动画 ``` **2. 使用自定义属性存储业务数据** ```csharp // 存储物流分类信息 AddCustomProperty("物流信息", "Logistics_Category", "装卸区"); AddCustomProperty("物流信息", "Access_Level", "高优先级"); AddCustomProperty("物流信息", "Capacity", "1000kg"); // 存储路径规划数据 AddCustomProperty("路径信息", "Path_ID", "Path_001"); AddCustomProperty("路径信息", "Path_Length", "15.5m"); AddCustomProperty("路径信息", "Travel_Time", "120s"); // 存储碰撞检测结果 AddCustomProperty("碰撞信息", "Collision_Status", "Safe"); AddCustomProperty("碰撞信息", "Last_Check", DateTime.Now.ToString()); ``` **3. 只读属性用于查询和智能分析** ```csharp // 基于只读属性进行智能空间分析 var suitableForStorage = items.Where(x => x.HasGeometry && x.ClassDisplayName.Contains("Room") && !x.IsHidden && x.BoundingBox().Max.Z > 3.0); // 高度足够的房间 // 基于结构关系进行逻辑分组 var floorItems = selectedFloor.DescendantsAndSelf .Where(x => x.HasGeometry); ``` **4. 综合应用示例:物流节点标记** ```csharp public void MarkAsLogisticsNode(ModelItem item, string category, Dictionary properties) { var itemCollection = new ModelItemCollection { item }; // 1. 使用标准可编辑属性设置可视化 Color categoryColor = GetCategoryColor(category); document.Models.OverridePermanentColor(itemCollection, categoryColor); document.Models.SetRequired(itemCollection, true); // 标记为重要 // 2. 使用自定义属性存储详细信息 document.CurrentSelection.Clear(); document.CurrentSelection.Add(item); AddCustomProperty("物流分类", "Category", category); foreach (var kvp in properties) { AddCustomProperty("物流属性", kvp.Key, kvp.Value); } // 3. 基于只读属性进行验证 if (!item.HasGeometry) { LogManager.Warning($"警告:{item.DisplayName} 没有几何体,可能不适合作为物流节点"); } } ``` #### 属性编辑最佳实践 | 用途 | 推荐方法 | API类型 | 特点 | |------|----------|---------|------| | 状态标记 | SetHidden, SetRequired | NET API | 影响显示和选择 | | 可视化 | OverridePermanentColor | NET API | 临时视觉效果 | | 空间变换 | OverridePermanentTransform | NET API | 支持动画 | | 业务数据 | SetUserDefined | COM API | 持久化存储 | | 查询分析 | 只读属性 + LINQ | NET API | 高性能筛选 | ### 12.7 属性操作注意事项 1. **线程安全**:所有属性操作都必须在主UI线程中执行 2. **选择状态**:COM API属性操作需要先选中目标对象 3. **性能考虑**:大批量属性读取时,NET API性能更佳 4. **属性持久化**:自定义属性会保存在NWD文件中,标准属性覆盖是临时的 5. **属性索引**:COM API中的用户自定义属性使用索引管理 6. **错误处理**:属性不存在时API会返回null,需要检查 7. **属性类型限制**:只读属性无法通过API修改,只能通过可编辑API间接影响 8. **覆盖vs原始**:OverridePermanent系列方法是覆盖原始属性,可以恢复 ### 12.8 COM API 自定义属性重要修复经验 基于实际生产环境中发现的问题和修复经验,这里记录COM API操作自定义属性时的关键要点。 #### 🚨 常见陷阱:重复创建属性分类 **问题现象**:对同一对象多次设置自定义属性时,会创建多个同名的属性分类,而不是更新现有属性。 **错误做法**: ```csharp // ❌ 错误:硬编码索引会导致重复创建 propertyNode.SetUserDefined(1, "分层信息", "Floor_Category", propertyVector); ``` **正确做法**: ```csharp // ✅ 正确:动态查找现有属性的索引 int existingIndex = GetFloorAttributeIndex(propertyNode, "分层信息"); if (existingIndex >= 0) { // 更新现有属性(注意索引转换:查找用0基,设置用1基) propertyNode.SetUserDefined(existingIndex + 1, "分层信息", "Floor_Category", propertyVector); } else { // 创建新属性 propertyNode.SetUserDefined(0, "分层信息", "Floor_Category", propertyVector); } ``` #### 🔍 ClassName vs ClassUserName 的区别 这是导致重复创建问题的根本原因: **ClassUserName**:用户定义的显示名称,由我们控制 ```csharp // 我们定义的显示名称 attribute.ClassUserName == "分层信息" ``` **ClassName**:系统生成的内部标识符,我们无法控制 ```csharp // 系统生成的内部名称,每次可能不同 attribute.ClassName == "LcOaPropOverrideCat" // 系统生成,不可预测 ``` **修复要点**: ```csharp // ❌ 错误:同时匹配两个条件会导致找不到现有属性 if (attribute.UserDefined && attribute.ClassUserName == FLOOR_CATEGORY && attribute.ClassName == FLOOR_CATEGORY_INTERNAL) // ✅ 正确:只匹配用户显示名称 if (attribute.UserDefined && attribute.ClassUserName == FLOOR_CATEGORY) ``` #### 📝 索引管理的正确方式 **索引计算规则**: 1. 遍历所有属性,只计算 `UserDefined == true` 的属性 2. 查找方法返回的是**0基索引**(第一个用户属性是0) 3. `SetUserDefined`方法使用的是**1基索引**(第一个用户属性是1) 4. `RemoveUserDefined`方法也使用**1基索引** ```csharp // ✅ 正确的索引转换 int foundIndex = GetFloorAttributeIndex(propertyNode, categoryName); // 返回0, 1, 2... if (foundIndex >= 0) { // SetUserDefined 需要1基索引 int setIndex = foundIndex + 1; // 转换为 1, 2, 3... propertyNode.SetUserDefined(setIndex, categoryName, internalName, propertyVector); // RemoveUserDefined 也需要1基索引 int removeIndex = foundIndex + 1; // 转换为 1, 2, 3... propertyNode.RemoveUserDefined(removeIndex); } ``` #### 🛠️ 完整的修复模板 ```csharp public class CustomPropertyManager { // ✅ 设置自定义属性的正确方式 public bool SetCustomProperty(ModelItem item, string categoryName, string internalName, string propertyName, string displayName, string value) { return ExecuteWithUIThread(() => { var state = ComApiBridge.State; var comPath = ComApiBridge.ToInwOaPath(item); var propertyNode = (ComApi.InwGUIPropertyNode2)state.GetGUIPropertyNode(comPath, false); // 🔍 关键:动态查找现有属性索引 int existingIndex = FindCustomPropertyIndex(propertyNode, categoryName); // 创建属性内容 var propertyCategory = (ComApi.InwOaPropertyVec)state.ObjectFactory( ComApi.nwEObjectType.eObjectType_nwOaPropertyVec, null, null); var property = (ComApi.InwOaProperty)state.ObjectFactory( ComApi.nwEObjectType.eObjectType_nwOaProperty, null, null); property.name = propertyName; property.UserName = displayName; property.value = value; propertyCategory.Properties().Add(property); if (existingIndex >= 0) { // 🎯 更新现有属性(索引+1) propertyNode.SetUserDefined(existingIndex + 1, categoryName, internalName, propertyCategory); LogManager.Info($"更新现有属性分类 '{categoryName}' (index={existingIndex + 1})"); } else { // 🆕 创建新属性(索引0) propertyNode.SetUserDefined(0, categoryName, internalName, propertyCategory); LogManager.Info($"创建新属性分类 '{categoryName}' (index=0)"); } return true; }); } // ✅ 查找自定义属性索引的正确方式 private int FindCustomPropertyIndex(ComApi.InwGUIPropertyNode2 propertyNode, string categoryName) { int userDefinedIndex = 0; foreach (ComApi.InwGUIAttribute2 attribute in propertyNode.GUIAttributes()) { if (attribute.UserDefined) { // ⚠️ 关键:只匹配ClassUserName,不匹配ClassName if (attribute.ClassUserName == categoryName) { return userDefinedIndex; } userDefinedIndex++; } } return -1; } // ✅ 清除自定义属性的正确方式 public bool ClearCustomProperty(ModelItem item, string categoryName) { return ExecuteWithUIThread(() => { var state = ComApiBridge.State; var comPath = ComApiBridge.ToInwOaPath(item); var propertyNode = (ComApi.InwGUIPropertyNode2)state.GetGUIPropertyNode(comPath, false); int existingIndex = FindCustomPropertyIndex(propertyNode, categoryName); if (existingIndex >= 0) { // 🗑️ 删除属性(索引+1) propertyNode.RemoveUserDefined(existingIndex + 1); LogManager.Info($"删除属性分类 '{categoryName}' (removed index={existingIndex + 1})"); return true; } else { LogManager.Info($"属性分类 '{categoryName}' 不存在,无需删除"); return false; } }); } } ``` #### 📋 问题诊断检查列表 当遇到属性重复创建问题时,检查以下要点: - [ ] 是否使用了硬编码索引(如固定使用索引1) - [ ] 是否同时匹配了`ClassUserName`和`ClassName` - [ ] 是否正确进行了索引转换(0基→1基) - [ ] 是否在主UI线程中执行COM API调用 - [ ] 日志中是否显示"未找到现有属性分类"但实际存在 #### 🎯 修复验证方法 **测试步骤**: 1. 选择一个对象,设置自定义属性(如"F1") 2. 再次选择同一对象,设置不同值(如"F2") 3. 检查属性面板,应该只有一个属性分类 4. 重复步骤2,设置第三个值(如"F3") 5. 确认仍然只有一个属性分类,值为最新设置的值 **日志验证**: ``` [FloorAttributeManager] 找到楼层属性分类,索引为: 0 [FloorAttributeManager] ✅ 成功更新现有楼层属性分类 (index=1) ``` #### 💡 经验总结 1. **COM API的ClassUserName是关键**:这是我们控制的显示名称,用于匹配现有属性 2. **ClassName不可靠**:系统生成的内部标识符,每次可能不同 3. **索引转换至关重要**:查找用0基,设置/删除用1基 4. **动态索引查找必不可少**:硬编码索引是重复创建问题的根源 5. **详细日志帮助调试**:记录索引查找和转换过程,便于问题定位 ## 13. 缓存刷新和状态同步 ⚠️ 重要 ### 13.1 缓存刷新的必要性 在某些API操作(特别是自定义属性的删除)后,Navisworks内部缓存可能与实际状态不同步,导致: - 搜索结果不准确 - 属性面板显示异常 - 后续API调用出现意外行为 ### 13.2 安全的缓存刷新方法 ✅ 基于实际生产环境的测试和验证,以下是推荐的缓存刷新方法: ```csharp // ✅ 最安全的缓存刷新方法:空集合的SetHidden操作 public static void SafeCacheRefresh() { try { var document = NavisApplication.ActiveDocument; if (document?.Models != null) { // 使用空集合的SetHidden操作触发缓存更新 // 这是一个几乎零开销的"伪操作" var emptyCollection = new ModelItemCollection(); document.Models.SetHidden(emptyCollection, false); LogManager.WriteLog("✅ 已执行轻量级缓存刷新"); } } catch (Exception ex) { LogManager.WriteLog($"⚠️ 缓存刷新失败,但不影响主要操作: {ex.Message}"); } } ``` ### 13.3 危险的缓存刷新方法 ❌ **⚠️ 避免使用以下方法,已确认会导致程序崩溃:** ```csharp // ❌ 危险:会导致程序崩溃 document.Models.ResetAllTemporaryMaterials(); // ❌ 危险:重载过重,可能影响性能 document.ActiveView.RequestDelayedRedraw(ViewRedrawRequests.All); // ❌ 危险:可能干扰用户界面状态 document.Models.ResetOverriddenTransparency(allItems); ``` ### 13.4 何时需要缓存刷新 **必须刷新的操作**: - COM API删除自定义属性后 - 大批量修改模型可见性后 - 复杂的Transform操作后 **可选刷新的操作**: - 添加自定义属性(通常自动同步) - 简单的颜色或透明度覆盖 - 单个对象的操作 ### 13.5 实践模式:操作后刷新 ```csharp // ✅ 推荐模式:操作 + 刷新 + 验证 public int RemoveLogisticsAttributes(ModelItemCollection items) { int successCount = 0; try { // 1. 执行主要操作 foreach (var item in items) { // COM API删除操作... successCount++; } } catch (Exception ex) { LogManager.WriteLog($"操作失败: {ex.Message}"); } // 2. 如果有成功操作,执行缓存刷新 if (successCount > 0) { SafeCacheRefresh(); // 使用安全的刷新方法 } return successCount; } ``` ### 13.6 缓存问题的诊断 **常见症状**: - 删除属性后,搜索仍能找到已删除的属性 - 属性面板显示的内容与API返回不一致 - 连续相同操作的结果不同 **诊断代码**: ```csharp // ✅ 验证缓存同步状态 public bool VerifyCacheSync(ModelItem item, string categoryName) { // 通过NET API检查 bool netApiResult = HasPropertyCategory(item, categoryName); // 通过COM API检查 bool comApiResult = HasPropertyCategoryViaCom(item, categoryName); if (netApiResult != comApiResult) { LogManager.Warning($"缓存不同步检测: NET={netApiResult}, COM={comApiResult}"); SafeCacheRefresh(); return false; } return true; } ``` ### 13.7 多线程环境下的缓存刷新 ```csharp // ✅ 线程安全的缓存刷新 public async Task SafeCacheRefreshAsync() { await System.Windows.Application.Current.Dispatcher.InvokeAsync(() => { SafeCacheRefresh(); // 确保在主线程中执行 }); } ``` ### 13.8 缓存刷新最佳实践 | 场景 | 刷新时机 | 刷新方法 | 必要性 | |------|----------|----------|--------| | 删除自定义属性 | 操作完成后 | 空集合SetHidden | 必须 | | 批量隐藏对象 | 操作完成后 | 可选择不刷新 | 可选 | | Transform动画 | 动画结束后 | 空集合SetHidden | 推荐 | | 颜色覆盖 | 通常不需要 | 无 | 不需要 | **关键原则**: 1. **安全第一**:只使用验证过的安全方法 2. **按需刷新**:不是所有操作都需要缓存刷新 3. **异常处理**:缓存刷新失败不应该影响主要功能 4. **线程安全**:在主UI线程中执行刷新操作 ## 14. 参考官方示例 强烈建议查看以下官方示例了解更多用法: - `SearchComparisonPlugIn.cs` - 搜索性能对比和属性搜索 - `Examiner.cs` - LINQ查询和属性过滤示例 - `AutoUserPropsExample.cs` - COM API自定义属性操作 - `BasicDockPanePlugin.cs` - 基础插件结构 - `DatabaseDockPane/Models.cs` - 数据库操作示例 - `ClashDetective` 相关示例 - 高级功能示例