From a9ccc057d39841bc14d7c7b19f7de308374a405c Mon Sep 17 00:00:00 2001
From: tian <11429339@qq.com>
Date: Sat, 14 Feb 2026 22:22:51 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9Clashdetective=E8=BF=9B?=
=?UTF-8?q?=E5=BA=A6=E6=9D=A1=E4=B8=8D=E5=B1=95=E7=A4=BA=E7=99=BE=E5=88=86?=
=?UTF-8?q?=E6=AF=94=EF=BC=8C=E4=BB=A5=E5=8F=8A=E4=B8=8D=E8=83=BD=E5=8F=96?=
=?UTF-8?q?=E6=B6=88=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B=E8=BF=9B=E5=BA=A6?=
=?UTF-8?q?=E6=9D=A1=E5=85=B3=E9=97=AD=E4=B8=BB=E7=AA=97=E5=8F=A3=E5=A4=B1?=
=?UTF-8?q?=E7=84=A6=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
doc/design/2026/NavisworksAPI使用方法.md | 217 ++++++++++++++++++
src/Core/Animation/PathAnimationManager.cs | 15 +-
.../Collision/ClashDetectiveIntegration.cs | 203 +++++++++++++++-
.../ViewModels/AnimationControlViewModel.cs | 10 +
4 files changed, 433 insertions(+), 12 deletions(-)
diff --git a/doc/design/2026/NavisworksAPI使用方法.md b/doc/design/2026/NavisworksAPI使用方法.md
index 6dd0b65..1f3923c 100644
--- a/doc/design/2026/NavisworksAPI使用方法.md
+++ b/doc/design/2026/NavisworksAPI使用方法.md
@@ -4218,3 +4218,220 @@ public static class ViewpointHelper
**文档版本**:2026.2
**最后更新**:基于 ViewpointHelper 重构经验(聚焦功能实现)
**适用版本**:Navisworks Manage 2026
+
+
+## 15. Progress 进度条与窗口焦点管理 ⚠️ 重要
+
+### 15.1 问题现象
+
+在使用 `Application.BeginProgress` 创建进度条后,如果在 `finally` 块中关闭进度条并立即显示消息对话框,会出现以下问题:
+
+1. **焦点丢失**:关闭进度条后,Navisworks 主窗口失去焦点,Windows 将焦点交给其他应用程序
+2. **消息框被遮盖**:消息对话框显示在其他应用程序后面,用户需要点击任务栏才能看到
+3. **用户体验差**:用户以为程序卡死或无响应
+
+### 15.2 根本原因
+
+Navisworks 的 `Progress` 是一个**应用程序级别的模态对话框**:
+
+```
+进度条关闭时:
+1. Windows 焦点管理器需要决定下一个获得焦点的窗口
+2. 如果 Navisworks 主窗口之前被进度条遮盖/禁用
+3. 关闭后焦点可能没有正确回到主窗口
+4. Windows 可能将焦点交给其他应用程序(如资源管理器、浏览器等)
+```
+
+### 15.3 错误做法
+
+```csharp
+// ❌ 错误:先关闭进度条,后显示消息框
+try
+{
+ // 执行长时间操作...
+ ShowMessage("操作完成"); // 先显示消息
+}
+finally
+{
+ Application.EndProgress(); // 后关闭进度条
+}
+// 结果:消息框显示在进度条后面,进度条关闭时焦点丢失
+```
+
+### 15.4 正确做法
+
+**解决方案:在关闭进度条之前,先显式激活 Navisworks 主窗口**
+
+```csharp
+// ✅ 正确:先激活主窗口,再关闭进度条,最后显示消息框
+using System.Runtime.InteropServices;
+
+public class ClashDetectiveIntegration
+{
+ // Windows API: 设置前台窗口
+ [DllImport("user32.dll")]
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
+
+ public void LongRunningOperation()
+ {
+ Progress progress = Application.BeginProgress("正在处理...", "请稍候");
+
+ try
+ {
+ // 执行长时间操作...
+ RunCollisionDetection(progress);
+ }
+ finally
+ {
+ // 🔥 关键:先激活 Navisworks 主窗口
+ try
+ {
+ var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
+ if (mainWindow != null)
+ {
+ SetForegroundWindow(mainWindow.Handle);
+ }
+ }
+ catch { /* 忽略激活失败 */ }
+
+ // 然后关闭进度条
+ Application.EndProgress();
+ }
+
+ // 最后显示消息对话框(在主窗口激活后)
+ ShowCancellationMessage();
+ }
+
+ private void ShowCancellationMessage()
+ {
+ // 使用主窗口作为父窗口
+ var mainWindow = System.Windows.Forms.Form.FromHandle(
+ Autodesk.Navisworks.Api.Application.Gui.MainWindow.Handle);
+
+ System.Windows.Forms.MessageBox.Show(
+ mainWindow, // 指定父窗口
+ "操作已取消。",
+ "取消",
+ System.Windows.Forms.MessageBoxButtons.OK,
+ System.Windows.Forms.MessageBoxIcon.Information);
+ }
+}
+```
+
+### 15.5 实际应用案例
+
+**场景:碰撞检测取消后的处理**
+
+```csharp
+public bool CreateAllAnimationCollisionTests(...)
+{
+ // 重置取消标志
+ _wasLastTestCanceled = false;
+
+ Progress progress = Application.BeginProgress("碰撞检测数据分析中,请稍候...");
+ bool wasCanceled = false;
+
+ try
+ {
+ var result = RunClashDetectiveTestsAndSaveToDatabase(..., progress);
+ wasCanceled = result.wasCanceled;
+
+ if (wasCanceled)
+ {
+ // 设置取消标志
+ _wasLastTestCanceled = true;
+
+ // 清空结果
+ ClearCurrentTestCache();
+
+ // 恢复物体状态
+ PathAnimationManager.GetInstance().RestoreAnimatedObjectState(animatedObject);
+
+ // 重置动画完成标志
+ ResetAnimationCompletedFlag(routeId);
+ }
+ }
+ finally
+ {
+ // 🔥 关键:先激活 Navisworks 主窗口(避免关闭进度条后丢失焦点)
+ try
+ {
+ var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
+ if (mainWindow != null)
+ {
+ SetForegroundWindow(mainWindow.Handle);
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Debug($"[ClashDetective] 激活主窗口失败: {ex.Message}");
+ }
+
+ // 关闭进度条
+ Application.EndProgress();
+ }
+
+ // 如果取消,在 finally 之后显示提示消息
+ if (wasCanceled)
+ {
+ ShowCancellationMessage();
+ return false;
+ }
+
+ return true;
+}
+```
+
+### 15.6 关键要点总结
+
+| 步骤 | 操作 | 目的 |
+|------|------|------|
+| 1 | `SetForegroundWindow(mainWindow.Handle)` | 显式激活 Navisworks 主窗口 |
+| 2 | `Application.EndProgress()` | 关闭进度条 |
+| 3 | `MessageBox.Show(mainWindow, ...)` | 显示消息对话框(使用父窗口) |
+
+**为什么这个顺序有效?**
+
+1. **激活主窗口**:在进度条关闭前,先让主窗口成为活动窗口
+2. **关闭进度条**:进度条关闭时,焦点会回到已经激活的主窗口
+3. **显示消息框**:消息框作为主窗口的子窗口,自然显示在最前面
+
+### 15.7 常见错误
+
+```csharp
+// ❌ 错误1:先显示消息框,后关闭进度条
+try { }
+finally
+{
+ ShowMessage("完成"); // 消息框在进度条前显示
+ Application.EndProgress(); // 关闭进度条时消息框也消失
+}
+
+// ❌ 错误2:只关闭进度条,不激活主窗口
+try { }
+finally
+{
+ Application.EndProgress(); // 焦点丢失
+}
+ShowMessage("完成"); // 消息框显示在其他程序后面
+
+// ❌ 错误3:使用延时等待
+finally
+{
+ Application.EndProgress();
+ Thread.Sleep(100); // 不必要的延时
+ Application.DoEvents();
+}
+```
+
+### 15.8 最佳实践
+
+1. **始终使用 `SetForegroundWindow`** 在关闭进度条前激活主窗口
+2. **指定父窗口** 在 `MessageBox.Show` 中使用 `mainWindow` 参数
+3. **正确处理异常** 激活失败不应影响主要功能
+4. **保持代码简洁** 不需要 Thread.Sleep 等延时操作
+5. **测试验证** 在取消操作后验证焦点是否正确回到 Navisworks
+
+---
+
+*最后更新:2026-02-14 - 添加了 Progress 进度条焦点管理的实践经验*
diff --git a/src/Core/Animation/PathAnimationManager.cs b/src/Core/Animation/PathAnimationManager.cs
index eac9181..622c62e 100644
--- a/src/Core/Animation/PathAnimationManager.cs
+++ b/src/Core/Animation/PathAnimationManager.cs
@@ -1681,10 +1681,11 @@ namespace NavisworksTransport.Core.Animation
var oldCursor = System.Windows.Input.Mouse.OverrideCursor;
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
+ bool testCompleted = false;
try
{
// 🔥 使用用户设置的检测容差(米)
- ClashDetectiveIntegration.Instance.CreateAllAnimationCollisionTests(
+ testCompleted = ClashDetectiveIntegration.Instance.CreateAllAnimationCollisionTests(
_allCollisionResults,
_detectionTolerance,
_currentRouteId,
@@ -1704,8 +1705,16 @@ namespace NavisworksTransport.Core.Animation
System.Windows.Input.Mouse.OverrideCursor = oldCursor;
}
- _completedCollisionTests.Add(_currentAnimationHash); // 记录此配置已完成碰撞检测
- LogManager.Info($"碰撞测试汇总已创建并记录,此配置后续播放将跳过碰撞检测");
+ // 只有在检测成功完成(未被取消)的情况下才记录为已完成
+ if (testCompleted)
+ {
+ _completedCollisionTests.Add(_currentAnimationHash); // 记录此配置已完成碰撞检测
+ LogManager.Info($"碰撞测试汇总已创建并记录,此配置后续播放将跳过碰撞检测");
+ }
+ else
+ {
+ LogManager.Info("碰撞检测被取消,不记录为已完成,允许下次重新检测");
+ }
}
else if (_completedCollisionTests.Contains(_currentAnimationHash))
{
diff --git a/src/Core/Collision/ClashDetectiveIntegration.cs b/src/Core/Collision/ClashDetectiveIntegration.cs
index dca3958..66ce261 100644
--- a/src/Core/Collision/ClashDetectiveIntegration.cs
+++ b/src/Core/Collision/ClashDetectiveIntegration.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Runtime.InteropServices;
using Autodesk.Navisworks.Api;
using Autodesk.Navisworks.Api.Clash;
using NavisworksTransport.Core;
@@ -15,6 +16,10 @@ namespace NavisworksTransport
///
public class ClashDetectiveIntegration
{
+ // Windows API: 设置前台窗口
+ [DllImport("user32.dll")]
+ private static extern bool SetForegroundWindow(IntPtr hWnd);
+
private static ClashDetectiveIntegration _instance;
private DocumentClash _documentClash;
@@ -71,6 +76,17 @@ namespace NavisworksTransport
private string _currentTestName;
+ // 🔥 检测取消标志
+ private bool _wasLastTestCanceled = false;
+
+ ///
+ /// 上次检测是否被取消
+ ///
+ public bool WasLastTestCanceled
+ {
+ get { return _wasLastTestCanceled; }
+ }
+
///
/// 单例实例
///
@@ -663,8 +679,8 @@ namespace NavisworksTransport
///
/// 运行ClashDetective测试并保存到数据库(公共方法,供批处理和非批处理调用)
///
- /// 碰撞分组、主测试、确认的碰撞数量
- public (ClashResultGroup collisionGroup, ClashTest addedMainTest, int confirmedCount) RunClashDetectiveTestsAndSaveToDatabase(
+ /// 碰撞分组、主测试、确认的碰撞数量、是否被取消
+ public (ClashResultGroup collisionGroup, ClashTest addedMainTest, int confirmedCount, bool wasCanceled) RunClashDetectiveTestsAndSaveToDatabase(
List precomputedCollisions,
double detectionGap,
string routeId,
@@ -719,7 +735,7 @@ namespace NavisworksTransport
if (addedMainTest == null)
{
LogManager.Error("[分组测试] 无法获取添加后的主测试对象");
- return (null, null, 0);
+ return (null, null, 0, false);
}
// 如果没有有效碰撞,直接保存空记录并返回
@@ -745,7 +761,7 @@ namespace NavisworksTransport
precomputedCollisionCount
);
- return (null, addedMainTest, 0);
+ return (null, addedMainTest, 0, false);
}
// 第二步:创建分组并添加碰撞结果(应用智能去重)
@@ -781,11 +797,17 @@ namespace NavisworksTransport
int skippedCount = 0;
int testCount = 0; // 检测计数器,用于进度日志
int totalTests = validCollisions.Count; // 总检测次数(预估)
+ int totalGroups = groupedCollisions.Count; // 总组数
var doc = Application.ActiveDocument;
+ // 初始化进度条
+ progress?.Update(0.0);
+
// 收集所有临时测试,用于批量删除
var tempTestsToRemove = new List();
+ bool wasCanceled = false;
+
// 2. 遍历每一组
for (int groupIndex = 0; groupIndex < groupedCollisions.Count; groupIndex++)
{
@@ -793,9 +815,17 @@ namespace NavisworksTransport
if (progress != null && progress.IsCanceled)
{
LogManager.Info($"[分组测试] 用户取消操作,已处理 {groupIndex}/{groupedCollisions.Count} 组");
+ wasCanceled = true;
break;
}
+ // 更新进度条(每5组更新一次,避免过于频繁)
+ if (groupIndex % 5 == 0 || groupIndex == groupedCollisions.Count - 1)
+ {
+ double progressPercent = (double)groupIndex / totalGroups * 0.7; // 组进度占70%
+ progress?.Update(progressPercent);
+ }
+
var group = groupedCollisions[groupIndex];
// 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞
@@ -898,12 +928,20 @@ namespace NavisworksTransport
_documentClash.TestsData.TestsRunTest(addedTempTest);
- // 增加检测计数并打印进度日志(每100次)
+ // 增加检测计数并打印进度日志(每50次更新一次进度条,每100次打印日志)
testCount++;
+ if (testCount % 50 == 0)
+ {
+ // 基于总检测次数计算进度(组进度占70%,检测进度占30%)
+ double groupProgress = (double)(groupIndex + 1) / totalGroups * 0.7;
+ double testProgress = (double)testCount / totalTests * 0.3;
+ double totalProgress = Math.Min(groupProgress + testProgress, 0.99);
+ progress?.Update(totalProgress);
+ }
if (testCount % 100 == 0)
{
- double progressPercent = (double)testCount / totalTests * 100;
- LogManager.Info($"[ClashDetective进度] 已执行 {testCount}/{totalTests} 次检测 ({progressPercent:F1}%)");
+ double logProgressPercent = (double)testCount / totalTests * 100;
+ LogManager.Info($"[ClashDetective进度] 已执行 {testCount}/{totalTests} 次检测 ({logProgressPercent:F1}%)");
}
var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
@@ -966,6 +1004,12 @@ namespace NavisworksTransport
}
}
+ // 如果用户取消,直接返回,不执行后续操作(临时测试将在下方统一清理)
+ if (wasCanceled)
+ {
+ LogManager.Info($"[分组测试] 检测已被用户取消,跳过结果处理和保存");
+ }
+
// 🔥 优化4:批量删除所有临时测试
if (tempTestsToRemove.Count > 0)
{
@@ -983,6 +1027,15 @@ namespace NavisworksTransport
}
}
+ // 如果用户取消,直接返回,不执行后续操作
+ if (wasCanceled)
+ {
+ return (null, addedMainTest, confirmedCount, true);
+ }
+
+ // 完成进度条
+ progress?.Update(1.0);
+
LogManager.Info($"[分组测试] ClashDetective检测完成: 执行 {testCount} 次检测, 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点");
// 第三步:处理碰撞结果
@@ -1067,7 +1120,7 @@ namespace NavisworksTransport
LogManager.Info($"[ClashDetective] 结果已保存到数据库");
// 第四步:将分组添加到主测试(由调用方完成)
- return (collisionGroup, addedMainTest, confirmedCount);
+ return (collisionGroup, addedMainTest, confirmedCount, false);
}
///
@@ -1084,7 +1137,11 @@ namespace NavisworksTransport
/// 虚拟车辆宽度(米)
/// 虚拟车辆高度(米)
/// 路径点列表(用于测试完成后恢复物体位置)
- public void CreateAllAnimationCollisionTests(
+ ///
+ /// 创建并运行ClashDetective碰撞测试
+ ///
+ /// true = 检测完成,false = 用户取消
+ public bool CreateAllAnimationCollisionTests(
List precomputedCollisions,
double detectionGap,
string routeId,
@@ -1097,6 +1154,9 @@ namespace NavisworksTransport
double virtualVehicleHeight,
List pathPoints = null)
{
+ // 重置取消标志
+ _wasLastTestCanceled = false;
+
try
{
LogManager.Info($"=== 使用预计算碰撞数据创建ClashDetective测试(容差: {detectionGap}米)===");
@@ -1129,6 +1189,7 @@ namespace NavisworksTransport
ClashTest addedMainTest = null;
ClashResultGroup collisionGroup = null;
int confirmedCount = 0;
+ bool wasCanceled = false;
try
{
@@ -1149,12 +1210,61 @@ namespace NavisworksTransport
collisionGroup = result.collisionGroup;
addedMainTest = result.addedMainTest;
confirmedCount = result.confirmedCount;
+ wasCanceled = result.wasCanceled;
+
+ // 检查是否被取消
+ if (wasCanceled)
+ {
+ LogManager.Info("[CreateAllAnimationCollisionTests] 用户取消了碰撞检测");
+
+ // 设置取消标志
+ _wasLastTestCanceled = true;
+
+ // 清空当前测试结果,避免取消后显示旧结果
+ ClearCurrentTestCache();
+ _currentTestName = null;
+ _clashDetectiveCollisionCount = 0;
+
+ // 恢复物体状态
+ if (animatedObject != null && IsModelItemValid(animatedObject))
+ {
+ PathAnimationManager.GetInstance().RestoreAnimatedObjectState(animatedObject);
+ }
+
+ // 重置动画完成标志,允许重新运行动画
+ ResetAnimationCompletedFlag(routeId);
+ }
}
finally
{
+ // 先激活 Navisworks 主窗口(避免关闭进度条后丢失焦点)
+ try
+ {
+ var mainWindow = Autodesk.Navisworks.Api.Application.Gui.MainWindow;
+ if (mainWindow != null)
+ {
+ // 使用 Windows API 激活窗口
+ SetForegroundWindow(mainWindow.Handle);
+ LogManager.Debug("[ClashDetective] 已激活 Navisworks 主窗口");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Debug($"[ClashDetective] 激活主窗口失败: {ex.Message}");
+ }
+
+ // 关闭进度条
Application.EndProgress();
}
+ // 如果取消,显示提示消息后返回
+ if (wasCanceled)
+ {
+ // 在 finally 之后显示消息对话框
+ ShowCancellationMessage();
+ return false; // 返回取消状态
+ }
+
// UI操作:将分组添加到主测试
if (collisionGroup != null && addedMainTest != null && collisionGroup.Children.Count > 0)
{
@@ -1199,6 +1309,8 @@ namespace NavisworksTransport
{
LogManager.Error($"动画结束后创建测试失败: {ex.Message}");
}
+
+ return !_wasLastTestCanceled; // 返回是否成功完成(未被取消)
}
///
@@ -1248,6 +1360,67 @@ namespace NavisworksTransport
}
}
+ ///
+ /// 显示碰撞检测取消提示消息
+ ///
+ private void ShowCancellationMessage()
+ {
+ try
+ {
+ // 获取 Navisworks 主窗口句柄作为父窗口
+ var mainWindow = System.Windows.Forms.Form.FromHandle(
+ Autodesk.Navisworks.Api.Application.Gui.MainWindow.Handle);
+
+ // 使用 Windows Forms 显示提示框,指定父窗口保持焦点
+ System.Windows.Forms.MessageBox.Show(
+ mainWindow,
+ "碰撞检测已取消。\n\n您可以重新运行动画来重新检测碰撞。",
+ "检测取消",
+ System.Windows.Forms.MessageBoxButtons.OK,
+ System.Windows.Forms.MessageBoxIcon.Information);
+
+ LogManager.Info("[ClashDetective] 已显示取消提示消息");
+ }
+ catch (Exception ex)
+ {
+ // 如果获取父窗口失败,使用默认方式
+ System.Windows.Forms.MessageBox.Show(
+ "碰撞检测已取消。\n\n您可以重新运行动画来重新检测碰撞。",
+ "检测取消",
+ System.Windows.Forms.MessageBoxButtons.OK,
+ System.Windows.Forms.MessageBoxIcon.Information);
+
+ LogManager.Info("[ClashDetective] 已显示取消提示消息(无父窗口)");
+ }
+ }
+
+ ///
+ /// 重置动画完成标志,允许重新运行动画进行碰撞检测
+ ///
+ /// 路径ID
+ private void ResetAnimationCompletedFlag(string routeId)
+ {
+ try
+ {
+ // 从已完成检测集合中移除该路径的哈希,允许重新检测
+ var animManager = PathAnimationManager.GetInstance();
+ if (animManager != null)
+ {
+ // 先尝试清除当前动画配置的检测记录
+ animManager.ForceRecreateCollisionTest();
+
+ // 同时清除所有记录(确保万无一失)
+ PathAnimationManager.ClearAllCollisionTestRecords();
+
+ LogManager.Info($"[ClashDetective] 已重置动画完成标志,允许重新检测。路径ID: {routeId}");
+ }
+ }
+ catch (Exception ex)
+ {
+ LogManager.Error($"重置动画完成标志失败: {ex.Message}");
+ }
+ }
+
///
/// 检查ModelItem是否仍然有效
///
@@ -1352,6 +1525,18 @@ namespace NavisworksTransport
}
}
+ ///
+ /// 清空取消标志
+ ///
+ public void ClearWasLastTestCanceled()
+ {
+ if (_wasLastTestCanceled)
+ {
+ LogManager.Info("[ClashDetective] 清空取消标志");
+ _wasLastTestCanceled = false;
+ }
+ }
+
///
/// 清空当前测试名称
///
diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
index d40bdff..0b59860 100644
--- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
+++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs
@@ -1802,6 +1802,16 @@ namespace NavisworksTransport.UI.WPF.ViewModels
CanStopAnimation = false;
UpdateMainStatus("动画已完成");
UpdateMediaControlProperties(); // 更新媒体控制属性
+
+ // 检查检测是否被取消
+ if (_clashIntegration?.WasLastTestCanceled == true)
+ {
+ LogManager.Info("[动画完成] 检测已被取消,跳过报告生成和高亮");
+ // 重置取消标志
+ _clashIntegration.ClearWasLastTestCanceled();
+ break;
+ }
+
// 先清除所有碰撞高亮(包括预计算和ClashDetective)
ModelHighlightHelper.ClearCollisionHighlights();
// 生成碰撞报告(无论有无碰撞)并等待完成