From ae845bc57172d27e3c6aac277d0a16c4e8be32f4 Mon Sep 17 00:00:00 2001 From: tian <11429339@qq.com> Date: Mon, 23 Feb 2026 00:25:17 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A3=80=E6=9F=A5=E5=B9=B6=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=BC=B9=E5=87=BA=E7=AA=97=E5=8F=A3=E3=80=81=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=A1=86=E3=80=81=E6=B6=88=E6=81=AF=E6=A1=86=EF=BC=8C=E6=9C=AA?= =?UTF-8?q?=E8=AE=BE=E7=BD=AEowner=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 137 +------ TransportPlugin.csproj | 1 + doc/guide/design_principles.md | 370 ++++++++++++++++++ doc/guide/user_guide.md | 11 +- .../ViewModels/AnimationControlViewModel.cs | 19 +- .../ViewModels/CollisionReportViewModel.cs | 6 +- .../ViewModels/SystemManagementViewModel.cs | 84 +--- src/UI/WPF/Views/AboutDialog.xaml | 3 +- src/UI/WPF/Views/CollisionAnalysisDialog.xaml | 1 + src/UI/WPF/Views/CollisionReportDialog.xaml | 1 + src/UI/WPF/Views/ConfigEditorDialog.xaml | 1 + .../Views/CoordinateSystemResultDialog.xaml | 3 +- src/UI/WPF/Views/HelpDialog.xaml | 3 +- .../WPF/Views/LogisticsControlPanel.xaml.cs | 13 +- src/UI/WPF/Views/PathConfigDialog.xaml | 3 +- src/UI/WPF/Views/PathEditingView.xaml.cs | 58 +-- src/UI/WPF/Views/PathSelectionDialog.xaml | 3 +- src/UI/WPF/Views/TimeTagDialog.xaml | 1 + src/Utils/DialogHelper.cs | 215 ++++++++++ 19 files changed, 644 insertions(+), 289 deletions(-) create mode 100644 src/Utils/DialogHelper.cs diff --git a/AGENTS.md b/AGENTS.md index 456fd34..d5d4b7e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,31 +57,6 @@ - 构建 Release 配置的 x64 平台 - 输出目录: `bin\x64\Release\` -### 手动构建 - -```bash -# 设置MSBuild路径 -set MSBUILD_PATH="C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" - -# 构建主项目 -%MSBUILD_PATH% "TransportPlugin.csproj" /p:Configuration=Release /p:Platform=x64 - -# 构建测试项目 -%MSBUILD_PATH% "NavisworksTransport.UnitTests.csproj" /p:Configuration=Release /p:Platform=x64 -``` - -### 运行测试 - -```bash -./run-unit-tests.bat -``` - -该脚本将: - -1. 构建主项目 -2. 构建测试项目 -3. 使用 VSTest.Console 运行单元测试 - ### 部署插件 ```bash @@ -325,6 +300,7 @@ double distance = GeometryHelper.Distance(pointA, pointB); ``` **复用检查清单:** + - 需要单位转换?→ 使用 `UnitsConverter` - 需要几何计算?→ 使用 `GeometryHelper` - 需要日志记录?→ 使用 `LogManager` @@ -450,102 +426,19 @@ private void OnStatusChanged(string status) ### WPF UI开发注意事项 -#### 必须检查:XAML资源引用有效性 +#### XAML资源引用原则 -**问题**:使用未在XAML中定义的Converter/Style资源会导致窗口无法显示 +**原则**:所有 XAML 中引用的资源(Converter、Style、Brush 等)必须在当前文件或合并的资源字典中有定义。 -```xml - -Visibility="{Binding HasItems, Converter={StaticResource InverseBoolToVisibilityConverter}}" +**约束**: - -Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}" -``` +- 禁止复制其他文件的 XAML 代码而不检查资源定义 +- 每添加一个 `{StaticResource xxx}` 引用,立即确认 `x:Key="xxx"` 存在 +- 小步增量开发,每步验证窗口可正常打开 -**项目中已定义的资源(AnimationControlView.xaml)**: +**关键错误**:资源键名拼写错误、未定义的 Converter、错误的外部资源字典路径 -| 资源名 | 类型 | 说明 | -|--------|------|------| -| `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性,支持Inverse参数 | - -**XAML开发工作流程(必须遵守)**: - -1. **每添加一个资源引用,立即确认其存在** - - ``` - ❌ 错误做法: - - 复制其他文件的XAML代码 - - 一次性写大量XAML再测试 - - ✅ 正确做法: - - 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义 - - 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在 - ``` - -2. **资源定义位置优先级**: - - 本文件 `` 或 `` 内定义 - - 引用的外部资源字典(如 `NavisworksStyles.xaml`) - - 必须在 XAML 顶部检查 `MergedDictionaries` 是否正确合并 - -3. **新增XAML文件时必做检查清单**: - - ``` - □ 所有 {StaticResource xxx} 引用都有定义 - □ 所有 xmlns:local 命名空间映射正确 - □ 所有 ValueConverter 在 xaml.cs 中已实现 - □ 合并的资源字典路径正确(pack://application:,,,/...) - □ 编译通过后立即运行测试,验证窗口能正常打开 - ``` - -4. **常见错误模式与修正**: - - | 错误写法 | 问题 | 正确写法 | - |---------|------|---------| - | `InverseBoolToVisibilityConverter` | 未定义 | `BoolToVisibilityConverter` + `ConverterParameter=Inverse` | - | `VisibilityConverter` | 未定义 | `BoolToVisibilityConverter` | - | `BooleanToVisibilityConverter` (键名) | 引用时使用 `BoolToVisibilityConverter` | 键名和引用保持一致 | - | 拼写错误的Style名 | 找不到资源 | 检查资源字典中的精确定义 | - -5. **调试XAML资源错误**: - - 错误信息:`无法在资源字典中找到名为 xxx 的资源` - - 定位方法:从堆栈跟踪找到最后加载的XAML元素 - - 快速修复:临时注释掉报错元素,添加简化版本测试 - - 预防措施:小步增量开发,每步验证窗口可正常显示 - -#### AI助手强制执行机制(防止再次犯错) - -**写XAML前,必须大声说出以下承诺:** -> "我承诺:本次XAML开发将严格遵守渐进式验证原则,每添加一个资源引用立即验证,绝不一次性写大量代码。" - -**编写过程中的强制检查点:** - -| 步骤 | 强制动作 | 验证命令/方法 | -|-----|---------|--------------| -| 1. 写完XAML结构 | 列出所有 `{StaticResource xxx}` | `grep -n "StaticResource" File.xaml` | -| 2. 检查每个引用 | 在文件中搜索资源定义 | 确认 `x:Key="xxx"` 存在 | -| 3. 检查Converter | 确认 xaml.cs 中有实现 | `grep "class.*Converter.*:" File.xaml.cs` | -| 4. 首次编译 | 必须通过 | `compile.bat` | -| 5. 运行时验证 | 窗口能正常打开无异常 | 实际运行测试 | - -**如果违反规则的后果:** - -- 必须立即停止当前工作 -- 回滚到上一个可运行的版本 -- 重新开始,严格按照检查点执行 - -**自检话术(每次保存前自问):** - -``` -□ 我是否复制了其他文件的XAML代码而没有检查资源? -□ 我是否一次性写了超过50行XAML才测试? -□ 我能否确定每个 {StaticResource} 都在当前文件或合并字典中定义? -□ 如果现在运行,窗口会崩溃吗? -``` - -**历史错误记录(警示):** - -- 2025-02-14: PathAnalysisDialog.xaml 使用了 `BoolToVisibilityConverter` 但定义的是 `BooleanToVisibilityConverter`,导致窗口崩溃 -- 根本原因:没有逐条执行检查清单,过度自信 +**参考**:详细检查清单和常见错误模式见 `doc/guide/design_principles.md` 第16节 #### UI色彩规范 - Material Design @@ -576,6 +469,17 @@ Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConvert 3. 使用 `Color.FromByteRGB(r, g, b)` 定义颜色(Navisworks API) 4. 保持透明度一致:通行空间类用 0.8-0.9,碰撞类用不透明 +#### 对话框置顶原则 + +**原则**:在Navisworks插件环境中,所有模态对话框必须确保置顶显示,避免被主窗口遮挡。 + +**约束**: + +- 所有模态对话框必须在 XAML 中设置 `Topmost="True"` +- 使用 `DialogHelper` 工具类统一处理 Owner 设置(`src/Utils/DialogHelper.cs`) + +**参考**:详细方案见 `doc/guide/design_principles.md` 第15节、第17节 + ## 配置系统 配置文件使用 TOML 格式,默认配置位于 `default_config.toml`: @@ -723,6 +627,7 @@ namespace NavisworksTransport.UnitTests - **API文档**: `doc/navisworks_api/NET/documentation/NET API.chm` - **COM API文档**: `doc/navisworks_api/COM/documentation/NavisWorksCOM.chm` +- **设计原则**: `doc/guide/design_principles.md`(线程安全、对话框置顶等设计模式) - **设计文档**: `doc/design/2026/` - **架构设计**: `doc/architecture/` - **迁移指南**: `doc/migration/`(2017到2026的API变更) diff --git a/TransportPlugin.csproj b/TransportPlugin.csproj index 6ffd61f..dd98d2b 100644 --- a/TransportPlugin.csproj +++ b/TransportPlugin.csproj @@ -316,6 +316,7 @@ + diff --git a/doc/guide/design_principles.md b/doc/guide/design_principles.md index 4aa2f0b..f05e188 100644 --- a/doc/guide/design_principles.md +++ b/doc/guide/design_principles.md @@ -1775,4 +1775,374 @@ C# 7.3 功能总结 C# 版本兼容性表 | .NET Framework | 默认 C# 版本 | 最高支持 C# 版本 | |----------------|-------------|-----------------| | 4.6.1 | C# 7.0 | C# 7.3 | | 4.7 | C# 7.0 | C# 7.3 | | 4.7.1 | C# 7.1 | C# 7.3 | | 4.7.2 | C# 7.3 | C# 7.3 | | 4.8 | C# 7.3 | C# 7.3 | | 4.8.1 | C# 7.3 | C# 7.3 | +## 15. WPF对话框置顶问题(窗口Z-Order) + +### 问题描述 + +在Navisworks插件开发中,WPF对话框可能会出现在主窗口背后,导致用户无法操作。这个问题在处理大模型时尤为明显: + +- **小模型**:动画生成速度快,对话框弹出时主窗口仍处于活动状态 +- **大模型**:预计算耗时较长,用户可能点击主窗口,导致窗口焦点/Z-Order变化。对话框弹出时不在最前面 + +### 根本原因 + +1. **Navisworks是Win32应用程序**,WPF作为插件嵌入其中,`Application.Current.MainWindow` 不可靠 +2. **Owner属性未正确设置**时,`WindowStartupLocation="CenterOwner"` 无法正常工作 +3. **异步操作耗时**导致窗口焦点在对话框显示前发生变化 + +### 解决方案 + +#### 15.1 XAML层解决方案(推荐) + +在对话框XAML中添加 `Topmost="True"`,确保窗口始终置顶: + +```xml + + +``` + +**参考实现**:`GenerateNavigationMapDialog.xaml`、`EditRotationWindow.xaml`、`AerialHeightDialog.xaml` + +#### 15.2 代码层解决方案(备用) + +在ViewModel中动态查找Owner窗口: + +```csharp +// ✅ 正确:多重备用方案查找Owner窗口 +private void ShowDialogWithOwner(Window dialog) +{ + // 方案1:尝试使用MainWindow + var mainWindow = System.Windows.Application.Current?.MainWindow; + if (mainWindow != null && mainWindow.IsVisible) + { + try + { + dialog.Owner = mainWindow; + } + catch (InvalidOperationException) + { + LogManager.Debug("[对话框] 无法设置MainWindow为Owner,尝试备用方案"); + } + } + + // 方案2:遍历所有窗口找活动窗口 + if (dialog.Owner == null && System.Windows.Application.Current != null) + { + foreach (Window window in System.Windows.Application.Current.Windows) + { + if (window.IsActive && window.IsLoaded) + { + try + { + dialog.Owner = window; + LogManager.Debug($"[对话框] 设置活动窗口为Owner: {window.Title}"); + break; + } + catch (InvalidOperationException) + { + // 继续尝试下一个窗口 + } + } + } + } + + dialog.ShowDialog(); +} +``` + +#### 15.3 MessageBox的Owner设置 + +```csharp +// ✅ 正确:为MessageBox传入owner参数 +Window owner = System.Windows.Application.Current?.MainWindow; +if (owner == null || !owner.IsVisible) +{ + foreach (Window window in System.Windows.Application.Current?.Windows ?? new WindowCollection()) + { + if (window.IsActive) + { + owner = window; + break; + } + } +} + +var result = System.Windows.MessageBox.Show( + owner, // 关键:传入owner确保MessageBox置顶 + message, + "检测配置已存在", + System.Windows.MessageBoxButton.YesNo, + System.Windows.MessageBoxImage.Question); +``` + +#### 15.4 Code-Behind中的Owner设置 + +当在UserControl中显示对话框时,使用 `Window.GetWindow(this)`: + +```csharp +// ✅ 正确:从UserControl获取父窗口 +private void ShowHelpDialog() +{ + var helpDialog = new Views.HelpDialog + { + Owner = Window.GetWindow(this) // 关键:获取当前UserControl所在的Window + }; + helpDialog.ShowDialog(); +} +``` + +**参考实现**:`LogisticsControlPanel.xaml.cs`、`PathEditingView.xaml.cs` + +### 实际修复案例 + +#### 案例1:CollisionAnalysisDialog(预计算碰撞分析) + +**问题**:打开大模型点击生成动画后,碰撞结果分析窗口藏在主窗口背后,无法点击 + +**修复**: +1. XAML添加 `Topmost="True"` +2. ViewModel添加备用Owner查找逻辑 + +```xml + + +``` + +```csharp +// AnimationControlViewModel.cs - ShowCollisionAnalysisDialog方法 +// 添加备用Owner查找逻辑(详见15.2节) +``` + +#### 案例2:重复配置提示MessageBox + +**问题**:检测配置重复提示的MessageBox也可能被主窗口遮挡 + +**修复**: +```csharp +// ShowDuplicateConfigDialog方法 +var result = System.Windows.MessageBox.Show( + owner, // 传入owner参数 + message, + "检测配置已存在", + System.Windows.MessageBoxButton.YesNo, + System.Windows.MessageBoxImage.Question); +``` + +### 设计原则 + +| 场景 | 推荐方案 | 说明 | +|------|---------|------| +| 自定义WPF对话框 | `Topmost="True"` | 最简单可靠 | +| 需要居中对齐 | `Topmost="True"` + 动态设置Owner | 兼顾置顶和位置 | +| MessageBox | 传入owner参数 | 系统对话框特殊处理 | +| UserControl内调用 | `Window.GetWindow(this)` | 获取正确的父窗口 | + +### 检查清单 + +- [ ] 所有模态对话框是否设置了 `Topmost="True"`? +- [ ] 需要居中的对话框是否正确设置了Owner? +- [ ] MessageBox是否传入了owner参数? +- [ ] UserControl中显示对话框是否使用了 `Window.GetWindow(this)`? +- [ ] 是否有多重备用方案处理Owner查找失败? + +### 常见陷阱 + +1. **仅依赖 `Application.Current.MainWindow`** - 在Navisworks环境中不可靠 +2. **只设置 `WindowStartupLocation="CenterOwner"` 但不设置Owner** - 窗口位置不可预测 +3. **忽略异步操作的时序问题** - 耗时操作后窗口焦点可能已变化 +4. **MessageBox不传入owner** - 系统对话框也可能被遮挡 + +## 16. XAML资源引用检查清单 + +### 问题描述 + +使用未在XAML中定义的Converter/Style资源会导致窗口无法显示,这是WPF开发中常见的运行时错误。 + +### 错误示例 + +```xml + +Visibility="{Binding HasItems, Converter={StaticResource InverseBoolToVisibilityConverter}}" + + +Visibility="{Binding HasItems, Converter={StaticResource BoolToVisibilityConverter}, ConverterParameter=Inverse}" +``` + +### 项目中已定义的资源 + +| 资源名 | 类型 | 说明 | +|--------|------|------| +| `BoolToVisibilityConverter` | BoolToVisibilityConverter | 布尔转可见性,支持Inverse参数 | + +### 开发工作流程 + +1. **每添加一个资源引用,立即确认其存在** + + ``` + ❌ 错误做法: + - 复制其他文件的XAML代码 + - 一次性写大量XAML再测试 + + ✅ 正确做法: + - 每写一行 {StaticResource xxx},立即检查 xxx 是否已定义 + - 使用 Ctrl+F 搜索 xxx 确认在当前文件或合并字典中存在 + ``` + +2. **资源定义位置优先级**: + - 本文件 `` 或 `` 内定义 + - 引用的外部资源字典(如 `NavisworksStyles.xaml`) + - 必须在 XAML 顶部检查 `MergedDictionaries` 是否正确合并 + +3. **新增XAML文件时必做检查清单**: + + ``` + □ 所有 {StaticResource xxx} 引用都有定义 + □ 所有 xmlns:local 命名空间映射正确 + □ 所有 ValueConverter 在 xaml.cs 中已实现 + □ 合并的资源字典路径正确(pack://application:,,,/...) + □ 编译通过后立即运行测试,验证窗口能正常打开 + ``` + +4. **常见错误模式与修正**: + + | 错误写法 | 问题 | 正确写法 | + |---------|------|---------| + | `InverseBoolToVisibilityConverter` | 未定义 | `BoolToVisibilityConverter` + `ConverterParameter=Inverse` | + | `VisibilityConverter` | 未定义 | `BoolToVisibilityConverter` | + | `BooleanToVisibilityConverter` (键名) | 引用时使用 `BoolToVisibilityConverter` | 键名和引用保持一致 | + | 拼写错误的Style名 | 找不到资源 | 检查资源字典中的精确定义 | + +5. **调试XAML资源错误**: + - 错误信息:`无法在资源字典中找到名为 xxx 的资源` + - 定位方法:从堆栈跟踪找到最后加载的XAML元素 + - 快速修复:临时注释掉报错元素,添加简化版本测试 + - 预防措施:小步增量开发,每步验证窗口可正常显示 + +### 历史错误记录 + +- **2025-02-14**: `PathAnalysisDialog.xaml` 使用了 `BoolToVisibilityConverter` 但定义的是 `BooleanToVisibilityConverter`,导致窗口崩溃 +- **根本原因**:没有逐条执行检查清单,过度自信 + +### AI助手强制检查点 + +| 步骤 | 强制动作 | 验证方法 | +|-----|---------|---------| +| 1. 写完XAML结构 | 列出所有 `{StaticResource xxx}` | `grep -n "StaticResource" File.xaml` | +| 2. 检查每个引用 | 在文件中搜索资源定义 | 确认 `x:Key="xxx"` 存在 | +| 3. 检查Converter | 确认 xaml.cs 中有实现 | `grep "class.*Converter.*:" File.xaml.cs` | +| 4. 首次编译 | 必须通过 | `compile.bat` | +| 5. 运行时验证 | 窗口能正常打开 | 实际运行测试 | + +--- + +## 17. 对话框辅助类(DialogHelper) + +### 问题描述 + +项目中多处重复代码用于设置对话框 Owner 和确保窗口置顶。需要统一封装,减少重复代码。 + +### DialogHelper 工具类 + +`src/Utils/DialogHelper.cs` 提供了统一的对话框处理方法: + +```csharp +// 使用方式1:Code-Behind 中从 UserControl 获取 Owner +var dialog = new MyDialog(); +DialogHelper.SetOwnerFromUserControl(dialog, this); +dialog.ShowDialog(); + +// 使用方式2:ViewModel 中自动查找 Owner +var dialog = new MyDialog(); +DialogHelper.SetOwnerSafely(dialog); +dialog.ShowDialog(); + +// 使用方式3:一键显示对话框(自动处理 Owner) +var result = DialogHelper.ShowDialog(new MyDialog(), this); + +// 使用方式4:MessageBox 确保置顶 +var result = DialogHelper.ShowMessageBox( + "确认删除?", + "提示", + MessageBoxButton.YesNo, + MessageBoxImage.Question); +``` + +### 方法说明 + +| 方法 | 场景 | 说明 | +|------|------|------| +| `SetOwnerFromUserControl` | Code-Behind | 从 UserControl 获取父窗口 | +| `FindOwnerWindow` | ViewModel | 查找合适的 Owner(MainWindow -> 活动窗口) | +| `SetOwnerSafely` | ViewModel | 安全设置 Owner,带异常处理 | +| `ShowDialog` | 通用 | 自动设置 Owner 并显示对话框 | +| `ShowMessageBox` | 通用 | 显示置顶 MessageBox | +| `SetWin32Owner` | 特殊 | 设置 Win32 父窗口(Navisworks 主窗口) | + +### 迁移示例 + +**旧代码(重复且冗长)**: +```csharp +// Code-Behind +var dialog = new HelpDialog(); +try +{ + var parentWindow = Window.GetWindow(this); + if (parentWindow != null) + { + dialog.Owner = parentWindow; + } +} +catch (Exception ownerEx) +{ + LogManager.Warning($"设置Owner失败: {ownerEx.Message}"); +} +dialog.ShowDialog(); +``` + +```csharp +// ViewModel +var dialog = new MyDialog(); +var mainWindow = System.Windows.Application.Current?.MainWindow; +if (mainWindow != null && mainWindow.IsVisible) +{ + try + { + dialog.Owner = mainWindow; + } + catch (InvalidOperationException) + { + // 备用方案... + foreach (Window window in System.Windows.Application.Current.Windows) + { + // ... + } + } +} +dialog.ShowDialog(); +``` + +**新代码(简洁统一)**: +```csharp +// Code-Behind +DialogHelper.ShowDialog(new HelpDialog(), this); + +// ViewModel +DialogHelper.ShowDialog(new MyDialog()); +``` + +### 设计原则 + +1. **代码复用优先**:使用 `DialogHelper` 替代重复的 Owner 设置代码 +2. **渐进式降级**:自动尝试多种方式获取 Owner(MainWindow -> 活动窗口 -> null) +3. **静默失败**:设置 Owner 失败时不影响对话框显示(仅记录日志) +4. **Topmost 仍是主要手段**:XAML 中仍需设置 `Topmost="True"` + +--- + *本文档将随着项目发展持续更新,确保设计指导的有效性和实用性。* diff --git a/doc/guide/user_guide.md b/doc/guide/user_guide.md index 6abd53b..8720eb1 100644 --- a/doc/guide/user_guide.md +++ b/doc/guide/user_guide.md @@ -1,7 +1,5 @@ - - -### 生成动画测试流程 +# 生成动画测试流程 用户点击"生成动画" ↓ @@ -19,8 +17,9 @@ CheckExistingDetectionRecord 用户生成动画 → 检测到相同配置的历史记录 → 弹出对话框询问 ↓ -用户选择"使用历史记录" +用户选择"使用历史记录" ↓ + 1. 设置 IsUsingHistoryRecord = true 2. 在历史列表中选中该记录 3. 自动滚动到该记录 @@ -30,7 +29,7 @@ CheckExistingDetectionRecord ↓ ├── true → 跳过 ClashDetective 测试,记录日志 └── false → 正常执行 ClashDetective 测试 - + 生成动画 ↓ 检查动画帧缓存 @@ -39,4 +38,4 @@ CheckExistingDetectionRecord ↓ 检查配置是否已存在(_animationHashToRecordId) ├── 存在 → 提示用户选择使用历史记录或重新生成 - └── 不存在 → 创建新记录,继续正常流程 \ No newline at end of file + └── 不存在 → 创建新记录,继续正常流程 diff --git a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs index 6986e42..f62e11c 100644 --- a/src/UI/WPF/ViewModels/AnimationControlViewModel.cs +++ b/src/UI/WPF/ViewModels/AnimationControlViewModel.cs @@ -3036,22 +3036,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels try { var dialog = new CollisionAnalysisDialog(hotspots, totalCollisions); - - // 安全地设置 Owner,确保窗口已显示 - var mainWindow = System.Windows.Application.Current?.MainWindow; - if (mainWindow != null && mainWindow.IsVisible) - { - try - { - dialog.Owner = mainWindow; - } - catch (InvalidOperationException) - { - // Owner 设置失败,继续显示对话框(无 Owner) - LogManager.Debug("[碰撞分析] 无法设置对话框 Owner,将以独立窗口显示"); - } - } - + DialogHelper.SetOwnerSafely(dialog); var result = dialog.ShowDialog(); if (result == null) @@ -3404,7 +3389,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels $"• 【是】直接使用历史记录(跳过重复检测)\n" + $"• 【否】重新生成新记录"; - var result = System.Windows.MessageBox.Show( + var result = DialogHelper.ShowMessageBox( message, "检测配置已存在", System.Windows.MessageBoxButton.YesNo, diff --git a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs index c95e89e..37e97bc 100644 --- a/src/UI/WPF/ViewModels/CollisionReportViewModel.cs +++ b/src/UI/WPF/ViewModels/CollisionReportViewModel.cs @@ -1247,11 +1247,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 设置当前值,默认优先使用当前视窗尺寸 dialog.SetInitialValues(ScreenshotWidthSetting, ScreenshotHeightSetting, ScreenshotFormatSetting, preferCurrentViewport: true); - var mainWindow = System.Windows.Application.Current.MainWindow; - if (mainWindow != null && mainWindow.IsLoaded) - { - dialog.Owner = mainWindow; - } + DialogHelper.SetOwnerSafely(dialog); if (dialog.ShowDialog() == true) { diff --git a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs index 5dbac2f..52bf942 100644 --- a/src/UI/WPF/ViewModels/SystemManagementViewModel.cs +++ b/src/UI/WPF/ViewModels/SystemManagementViewModel.cs @@ -619,33 +619,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 创建并显示日志查看器对话框 var logViewerDialog = new NavisworksTransport.UI.WPF.Views.LogViewerDialog(); - // 尝试设置窗口所有者,使用更安全的方式 - try - { - // 查找当前活动的主窗口 - var mainWindow = System.Windows.Application.Current.MainWindow; - if (mainWindow != null && mainWindow.IsLoaded) - { - logViewerDialog.Owner = mainWindow; - } - else - { - // 如果主窗口不可用,查找当前活动的窗口 - foreach (System.Windows.Window window in System.Windows.Application.Current.Windows) - { - if (window.IsActive && window.IsLoaded) - { - logViewerDialog.Owner = window; - break; - } - } - } - } - catch (Exception ownerEx) - { - // 如果设置Owner失败,记录警告但继续显示窗口 - LogManager.Warning($"设置日志查看器Owner失败: {ownerEx.Message}"); - } + // 设置窗口所有者 + DialogHelper.SetOwnerSafely(logViewerDialog); // 显示对话框(使用Show而不是ShowDialog以避免阻塞) logViewerDialog.Show(); @@ -678,32 +653,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels // 创建并显示配置编辑器对话框 var configEditorDialog = new NavisworksTransport.UI.WPF.Views.ConfigEditorDialog(); - // 尝试设置窗口所有者 - try - { - var mainWindow = System.Windows.Application.Current.MainWindow; - if (mainWindow != null && mainWindow.IsLoaded) - { - configEditorDialog.Owner = mainWindow; - } - else - { - foreach (System.Windows.Window window in System.Windows.Application.Current.Windows) - { - if (window.IsActive && window.IsLoaded) - { - configEditorDialog.Owner = window; - break; - } - } - } - } - catch (Exception ownerEx) - { - LogManager.Warning($"设置配置编辑器Owner失败: {ownerEx.Message}"); - } - - // 显示对话框 + // 设置窗口所有者并显示对话框 + DialogHelper.SetOwnerSafely(configEditorDialog); bool? result = configEditorDialog.ShowDialog(); if (result == true) @@ -1726,31 +1677,8 @@ namespace NavisworksTransport.UI.WPF.ViewModels ResultText = result }; - // 尝试设置对话框所有者 - try - { - var mainWindow = System.Windows.Application.Current?.MainWindow; - if (mainWindow != null && mainWindow.IsLoaded) - { - resultDialog.Owner = mainWindow; - } - else - { - foreach (System.Windows.Window window in System.Windows.Application.Current.Windows) - { - if (window.IsActive && window.IsLoaded) - { - resultDialog.Owner = window; - break; - } - } - } - } - catch (Exception ex) - { - LogManager.Warning($"[坐标系探索] 设置视图状态失败: {ex.Message}"); - } - + // 设置对话框所有者并显示 + DialogHelper.SetOwnerSafely(resultDialog); resultDialog.ShowDialog(); UpdateMainStatus("坐标系探索完成"); diff --git a/src/UI/WPF/Views/AboutDialog.xaml b/src/UI/WPF/Views/AboutDialog.xaml index 27edb48..d1d144e 100644 --- a/src/UI/WPF/Views/AboutDialog.xaml +++ b/src/UI/WPF/Views/AboutDialog.xaml @@ -18,7 +18,8 @@ NavisworksTransport 关于对话框 - 采用与主界面一致的Navisworks 2026 Height="400" Width="500" ResizeMode="NoResize" - WindowStartupLocation="CenterOwner"> + WindowStartupLocation="CenterOwner" + Topmost="True"> diff --git a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml index b4f8232..8eed1aa 100644 --- a/src/UI/WPF/Views/CollisionAnalysisDialog.xaml +++ b/src/UI/WPF/Views/CollisionAnalysisDialog.xaml @@ -8,6 +8,7 @@ Height="720" Width="720" WindowStartupLocation="CenterOwner" ResizeMode="CanResize" + Topmost="True" Background="White"> diff --git a/src/UI/WPF/Views/CollisionReportDialog.xaml b/src/UI/WPF/Views/CollisionReportDialog.xaml index 8152a56..30a6adb 100644 --- a/src/UI/WPF/Views/CollisionReportDialog.xaml +++ b/src/UI/WPF/Views/CollisionReportDialog.xaml @@ -21,6 +21,7 @@ NavisworksTransport 碰撞检测报告对话框 - 采用与主界面一致的Nav Width="900" ResizeMode="CanResize" WindowStartupLocation="CenterOwner" + Topmost="True" MinHeight="800" MinWidth="600"> diff --git a/src/UI/WPF/Views/ConfigEditorDialog.xaml b/src/UI/WPF/Views/ConfigEditorDialog.xaml index 57f8d2b..5c582f7 100644 --- a/src/UI/WPF/Views/ConfigEditorDialog.xaml +++ b/src/UI/WPF/Views/ConfigEditorDialog.xaml @@ -21,6 +21,7 @@ NavisworksTransport 配置编辑器对话框 - 采用与主界面一致的Navisw Width="900" ResizeMode="CanResize" WindowStartupLocation="CenterOwner" + Topmost="True" MinHeight="500" MinWidth="700"> diff --git a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml index 7c8b254..0143941 100644 --- a/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml +++ b/src/UI/WPF/Views/CoordinateSystemResultDialog.xaml @@ -4,7 +4,8 @@ Title="坐标系探索结果" Height="600" Width="700" WindowStartupLocation="CenterOwner" - ResizeMode="CanResize"> + ResizeMode="CanResize" + Topmost="True"> diff --git a/src/UI/WPF/Views/HelpDialog.xaml b/src/UI/WPF/Views/HelpDialog.xaml index 2126724..f0c8a11 100644 --- a/src/UI/WPF/Views/HelpDialog.xaml +++ b/src/UI/WPF/Views/HelpDialog.xaml @@ -18,7 +18,8 @@ NavisworksTransport 帮助对话框 - 采用与主界面一致的Navisworks 2026 Height="500" Width="650" ResizeMode="NoResize" - WindowStartupLocation="CenterOwner"> + WindowStartupLocation="CenterOwner" + Topmost="True"> diff --git a/src/UI/WPF/Views/LogisticsControlPanel.xaml.cs b/src/UI/WPF/Views/LogisticsControlPanel.xaml.cs index 2b4e908..088df62 100644 --- a/src/UI/WPF/Views/LogisticsControlPanel.xaml.cs +++ b/src/UI/WPF/Views/LogisticsControlPanel.xaml.cs @@ -3,6 +3,7 @@ using System.Windows; using System.Windows.Controls; using NavisworksTransport.UI.WPF.ViewModels; using NavisworksTransport.UI.WPF.Views; +using NavisworksTransport.Utils; using NavisApplication = Autodesk.Navisworks.Api.Application; namespace NavisworksTransport.UI.WPF @@ -328,11 +329,7 @@ namespace NavisworksTransport.UI.WPF { GlobalExceptionHandler.SafeExecute(() => { - var helpDialog = new Views.HelpDialog - { - Owner = Window.GetWindow(this) - }; - helpDialog.ShowDialog(); + DialogHelper.ShowDialog(new Views.HelpDialog(), this); }, "显示帮助"); } @@ -343,11 +340,7 @@ namespace NavisworksTransport.UI.WPF { GlobalExceptionHandler.SafeExecute(() => { - var aboutDialog = new Views.AboutDialog - { - Owner = Window.GetWindow(this) - }; - aboutDialog.ShowDialog(); + DialogHelper.ShowDialog(new Views.AboutDialog(), this); }, "显示关于"); } diff --git a/src/UI/WPF/Views/PathConfigDialog.xaml b/src/UI/WPF/Views/PathConfigDialog.xaml index 58d23fd..d12cb87 100644 --- a/src/UI/WPF/Views/PathConfigDialog.xaml +++ b/src/UI/WPF/Views/PathConfigDialog.xaml @@ -18,7 +18,8 @@ Height="700" Width="750" WindowStartupLocation="CenterScreen" - ResizeMode="CanResize"> + ResizeMode="CanResize" + Topmost="True"> diff --git a/src/UI/WPF/Views/PathEditingView.xaml.cs b/src/UI/WPF/Views/PathEditingView.xaml.cs index 5d6c001..c80fe75 100644 --- a/src/UI/WPF/Views/PathEditingView.xaml.cs +++ b/src/UI/WPF/Views/PathEditingView.xaml.cs @@ -6,6 +6,7 @@ using NavisworksTransport.UI.WPF.ViewModels; using NavisworksTransport.UI.WPF.Views; using NavisworksTransport.Core.Services; using NavisworksTransport.Core.Models; +using NavisworksTransport.Utils; namespace NavisworksTransport.UI.WPF.Views { @@ -93,21 +94,7 @@ namespace NavisworksTransport.UI.WPF.Views // 创建并显示时标对话框 var dlg = new TimeTagDialog(_timeTagService, route); - - // 设置父窗口 - try - { - var parentWindow = Window.GetWindow(this); - if (parentWindow != null) - { - dlg.Owner = parentWindow; - } - } - catch (Exception ownerEx) - { - LogManager.Warning($"设置Owner失败: {ownerEx.Message}"); - } - + DialogHelper.SetOwnerFromUserControl(dlg, this); dlg.ShowDialog(); LogManager.Info($"时标对话框关闭: {route.Name}"); } @@ -158,25 +145,8 @@ namespace NavisworksTransport.UI.WPF.Views var dialog = new GenerateNavigationMapDialog(); - // 在Navisworks环境中,尝试找到合适的父窗口 - try - { - // 尝试设置Owner为当前WPF窗口的顶级父窗口 - var parentWindow = Window.GetWindow(this); - if (parentWindow != null) - { - dialog.Owner = parentWindow; - LogManager.Info("设置GenerateNavigationMapDialog的Owner为当前窗口"); - } - else - { - LogManager.Info("未找到父窗口,将以独立窗口方式显示GenerateNavigationMapDialog"); - } - } - catch (Exception ownerEx) - { - LogManager.Warning($"设置Owner失败,将以独立窗口显示: {ownerEx.Message}"); - } + // 设置父窗口 + DialogHelper.SetOwnerFromUserControl(dialog, this); if (dialog.ShowDialog() == true && dialog.IsConfirmed) { @@ -272,24 +242,8 @@ namespace NavisworksTransport.UI.WPF.Views { _boundsWindow = new ModelItemBoundsWindow(); - // 在Navisworks环境中,尝试找到合适的父窗口 - try - { - var parentWindow = Window.GetWindow(this); - if (parentWindow != null) - { - _boundsWindow.Owner = parentWindow; - LogManager.Info("设置ModelItemBoundsWindow的Owner为当前窗口"); - } - else - { - LogManager.Info("未找到父窗口,将以独立窗口方式显示ModelItemBoundsWindow"); - } - } - catch (Exception ownerEx) - { - LogManager.Warning($"设置Owner失败,将以独立窗口显示: {ownerEx.Message}"); - } + // 设置父窗口(非模态窗口) + DialogHelper.SetOwnerFromUserControl(_boundsWindow, this); // 使用 Show() 而非 ShowDialog(),允许窗口一直开着 _boundsWindow.Show(); diff --git a/src/UI/WPF/Views/PathSelectionDialog.xaml b/src/UI/WPF/Views/PathSelectionDialog.xaml index ec900fe..efc6529 100644 --- a/src/UI/WPF/Views/PathSelectionDialog.xaml +++ b/src/UI/WPF/Views/PathSelectionDialog.xaml @@ -16,7 +16,8 @@ Height="500" Width="800" WindowStartupLocation="CenterScreen" - ResizeMode="CanResize"> + ResizeMode="CanResize" + Topmost="True"> diff --git a/src/UI/WPF/Views/TimeTagDialog.xaml b/src/UI/WPF/Views/TimeTagDialog.xaml index 30314a7..3546256 100644 --- a/src/UI/WPF/Views/TimeTagDialog.xaml +++ b/src/UI/WPF/Views/TimeTagDialog.xaml @@ -7,6 +7,7 @@ Height="750" Width="1100" WindowStartupLocation="CenterOwner" ResizeMode="CanResize" + Topmost="True" x:Name="root"> diff --git a/src/Utils/DialogHelper.cs b/src/Utils/DialogHelper.cs new file mode 100644 index 0000000..9f9f762 --- /dev/null +++ b/src/Utils/DialogHelper.cs @@ -0,0 +1,215 @@ +using System; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Interop; +using Autodesk.Navisworks.Api; + +namespace NavisworksTransport.Utils +{ + /// + /// 对话框辅助类 - 统一处理对话框 Owner 设置和置顶显示 + /// + public static class DialogHelper + { + /// + /// 从 UserControl 获取父窗口并设置为对话框 Owner + /// + /// 要显示的对话框 + /// 当前 UserControl + /// 是否成功设置 Owner + public static bool SetOwnerFromUserControl(Window dialog, UserControl userControl) + { + if (dialog == null || userControl == null) + return false; + + try + { + var parentWindow = Window.GetWindow(userControl); + if (parentWindow != null) + { + dialog.Owner = parentWindow; + return true; + } + } + catch (Exception ex) + { + LogManager.Warning($"[DialogHelper] 设置 Owner 失败: {ex.Message}"); + } + + return false; + } + + /// + /// 在 ViewModel 中查找合适的 Owner 窗口 + /// 策略:MainWindow -> 活动窗口 -> null + /// + /// 找到的窗口,或 null + public static Window FindOwnerWindow() + { + try + { + // 方案1:尝试 MainWindow + var mainWindow = System.Windows.Application.Current?.MainWindow; + if (mainWindow != null && (mainWindow.IsVisible || mainWindow.IsLoaded)) + { + return mainWindow; + } + } + catch (Exception ex) + { + LogManager.Debug($"[DialogHelper] 获取 MainWindow 失败: {ex.Message}"); + } + + // 方案2:遍历查找活动窗口 + try + { + if (System.Windows.Application.Current != null) + { + foreach (Window window in System.Windows.Application.Current.Windows) + { + if (window.IsActive && window.IsLoaded) + { + return window; + } + } + } + } + catch (Exception ex) + { + LogManager.Debug($"[DialogHelper] 遍历窗口失败: {ex.Message}"); + } + + return null; + } + + /// + /// 安全地设置对话框 Owner(ViewModel 场景) + /// + /// 要设置的对话框 + /// 是否成功设置 + public static bool SetOwnerSafely(Window dialog) + { + if (dialog == null) + return false; + + if (dialog.Owner != null) + return true; // 已设置 + + var owner = FindOwnerWindow(); + if (owner != null) + { + try + { + dialog.Owner = owner; + LogManager.Debug($"[DialogHelper] 设置 Owner: {owner.Title}"); + return true; + } + catch (InvalidOperationException ex) + { + LogManager.Warning($"[DialogHelper] 设置 Owner 失败: {ex.Message}"); + } + } + + return false; + } + + /// + /// 显示对话框(自动处理 Owner 设置) + /// + /// 要显示的对话框 + /// 可选的 UserControl,用于获取 Owner + /// 对话框结果 + public static bool? ShowDialog(Window dialog, UserControl userControl = null) + { + if (dialog == null) + throw new ArgumentNullException(nameof(dialog)); + + // 如果提供了 UserControl,优先使用它获取 Owner + if (userControl != null) + { + SetOwnerFromUserControl(dialog, userControl); + } + else + { + // 否则尝试自动查找 + SetOwnerSafely(dialog); + } + + return dialog.ShowDialog(); + } + + /// + /// 显示 MessageBox 并确保置顶 + /// + /// 消息内容 + /// 标题 + /// 按钮类型 + /// 图标 + /// MessageBox 结果 + public static MessageBoxResult ShowMessageBox( + string messageBoxText, + string caption, + MessageBoxButton button = MessageBoxButton.OK, + MessageBoxImage icon = MessageBoxImage.Information) + { + var owner = FindOwnerWindow(); + + if (owner != null) + { + return MessageBox.Show(owner, messageBoxText, caption, button, icon); + } + + return MessageBox.Show(messageBoxText, caption, button, icon); + } + + /// + /// 获取 Navisworks 主窗口句柄(Win32) + /// + /// 窗口句柄,或 IntPtr.Zero + public static IntPtr GetNavisworksMainWindowHandle() + { + try + { + var mainWindow = Autodesk.Navisworks.Api.Application.Gui?.MainWindow; + if (mainWindow != null) + { + return mainWindow.Handle; + } + } + catch (Exception ex) + { + LogManager.Debug($"[DialogHelper] 获取 Navisworks 主窗口句柄失败: {ex.Message}"); + } + + return IntPtr.Zero; + } + + /// + /// 设置 Win32 父窗口(用于需要强制置顶到 Navisworks 主窗口的场景) + /// + /// 对话框 + /// 是否成功 + public static bool SetWin32Owner(Window dialog) + { + if (dialog == null) + return false; + + var handle = GetNavisworksMainWindowHandle(); + if (handle == IntPtr.Zero) + return false; + + try + { + var helper = new WindowInteropHelper(dialog); + helper.Owner = handle; + return true; + } + catch (Exception ex) + { + LogManager.Warning($"[DialogHelper] 设置 Win32 Owner 失败: {ex.Message}"); + } + + return false; + } + } +}