NavisworksTransport/doc/design/2026/NavisworksAPI使用方法.md

1493 lines
47 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.

# 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<ModelItem> 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<ModelItem> 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<ModelItem> selectedItems, string filePath)
{
var document = Application.ActiveDocument;
var nodesToKeepVisible = new HashSet<ModelItem>();
// 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<bool> 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<ModelItem> 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<ModelItem> 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<ModelItem> 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<string, string> 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` 相关示例 - 高级功能示例