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(); // 生成碰撞报告(无论有无碰撞)并等待完成