1493 lines
47 KiB
Markdown
1493 lines
47 KiB
Markdown
# 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` 相关示例 - 高级功能示例
|