已合并 PR 36: 调整CNC为两列布局
 1、CNC界面及交互优化,包括插入节点,节点排序,编辑,算子是否启用 2、当流程图异常时,状态提醒 3、优化树形结构 4、CNC数据源考虑
This commit is contained in:
@@ -106,5 +106,5 @@ dotnet build XplorePlane.sln -c Release
|
||||
- [x] 相机实时影像集成(连接、采集、Live View、像素坐标显示)
|
||||
- [x] 相机参数设置对话框(曝光、增益、分辨率、像素格式)
|
||||
- [x] 主界面硬件栏相机设置按钮
|
||||
- [ ] 打通与硬件层的调用流程
|
||||
- [ ] 打通与图像层的调用流程
|
||||
- [x] 打通与硬件层的调用流程
|
||||
- [x] 打通与图像层的调用流程
|
||||
|
||||
@@ -0,0 +1,505 @@
|
||||
# CNC 多检测结果归档与报告取数说明
|
||||
|
||||
## 1. 目标
|
||||
|
||||
为 CNC 执行结果提供一套适合报告模块直接取数的归档结构。
|
||||
|
||||
设计目标:
|
||||
|
||||
- 以“一次工件检测实例”作为主归档单位
|
||||
- 同时保留到“检测节点级别”的明细
|
||||
- 支持保存:
|
||||
- CNC 程序名
|
||||
- 工件号 / 序列号
|
||||
- 检测节点信息
|
||||
- 节点使用的 Pipeline / 配方快照
|
||||
- 原图、节点输入图、节点最终结果图
|
||||
- 节点输出的多个数值结果
|
||||
- 节点判定和整次实例判定
|
||||
- 方便后续报告模块直接读取,不依赖运行时最新配方
|
||||
|
||||
当前实现采用:
|
||||
|
||||
- SQLite 保存结构化索引数据
|
||||
- 文件系统保存图片资产和 `manifest.json`
|
||||
|
||||
---
|
||||
|
||||
## 2. 总体设计
|
||||
|
||||
一次检测实例会生成:
|
||||
|
||||
1. 一组数据库记录
|
||||
2. 一组文件目录和图像文件
|
||||
3. 一份 `manifest.json` 快照文件
|
||||
|
||||
归档核心对象:
|
||||
|
||||
- `InspectionRunRecord`
|
||||
- 一次完整检测实例
|
||||
- `InspectionNodeResult`
|
||||
- 一个 CNC 检测节点的结果
|
||||
- `InspectionMetricResult`
|
||||
- 节点输出的数值结果
|
||||
- `InspectionAssetRecord`
|
||||
- 图像或附件索引
|
||||
- `PipelineExecutionSnapshot`
|
||||
- 节点执行时使用的 Pipeline 快照
|
||||
|
||||
默认图片保留策略:
|
||||
|
||||
- 整次实例原图
|
||||
- 每个节点的输入图
|
||||
- 每个节点的最终结果图
|
||||
|
||||
不默认保存每个算法步骤的中间图。
|
||||
|
||||
---
|
||||
|
||||
## 3. 文件存储结构
|
||||
|
||||
### 3.1 根目录
|
||||
|
||||
默认根目录:
|
||||
|
||||
```text
|
||||
%AppData%\XplorePlane\InspectionResults
|
||||
```
|
||||
|
||||
每次检测实例按日期和 `RunId` 分层:
|
||||
|
||||
```text
|
||||
Results/{yyyy}/{MM}/{dd}/{RunId}/
|
||||
```
|
||||
|
||||
### 3.2 示例目录
|
||||
|
||||
假设:
|
||||
|
||||
- `ProgramName = NewCncProgram`
|
||||
- `WorkpieceId = QFN_1`
|
||||
- `SerialNumber = SN-001`
|
||||
- `RunId = 7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a`
|
||||
- 检测节点共 2 个
|
||||
|
||||
则文件结构大致为:
|
||||
|
||||
```text
|
||||
%AppData%\XplorePlane\InspectionResults\
|
||||
└─ Results\
|
||||
└─ 2026\
|
||||
└─ 04\
|
||||
└─ 21\
|
||||
└─ 7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a\
|
||||
├─ manifest.json
|
||||
├─ run\
|
||||
│ └─ source.bmp
|
||||
└─ nodes\
|
||||
├─ 001_检测节点1\
|
||||
│ ├─ input.bmp
|
||||
│ └─ result_overlay.bmp
|
||||
└─ 002_检测节点2\
|
||||
└─ result_overlay.bmp
|
||||
```
|
||||
|
||||
### 3.3 文件说明
|
||||
|
||||
- `run/source.bmp`
|
||||
- 本次工件检测实例的原始输入图
|
||||
- `nodes/001_检测节点1/input.bmp`
|
||||
- 节点 1 输入图
|
||||
- `nodes/001_检测节点1/result_overlay.bmp`
|
||||
- 节点 1 最终结果图
|
||||
- `nodes/002_检测节点2/result_overlay.bmp`
|
||||
- 节点 2 最终结果图
|
||||
- `manifest.json`
|
||||
- 本次检测完整快照,便于离线查看、调试和导出
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库表设计
|
||||
|
||||
当前实现包含 5 张主表。
|
||||
|
||||
### 4.1 `inspection_runs`
|
||||
|
||||
用途:保存一次完整检测实例的主记录。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `run_id` | `TEXT` | 主键,检测实例 ID,GUID |
|
||||
| `program_name` | `TEXT` | CNC 程序名 |
|
||||
| `workpiece_id` | `TEXT` | 工件号 |
|
||||
| `serial_number` | `TEXT` | 序列号 |
|
||||
| `started_at` | `TEXT` | 开始时间,ISO 8601 |
|
||||
| `completed_at` | `TEXT` | 结束时间,ISO 8601,可空 |
|
||||
| `overall_pass` | `INTEGER` | 整体判定,`0/1` |
|
||||
| `source_image_path` | `TEXT` | 原图相对路径 |
|
||||
| `result_root_path` | `TEXT` | 本次结果包根目录相对路径 |
|
||||
| `node_count` | `INTEGER` | 节点数量 |
|
||||
|
||||
样例数据:
|
||||
|
||||
| run_id | program_name | workpiece_id | serial_number | started_at | completed_at | overall_pass | source_image_path | result_root_path | node_count |
|
||||
|---|---|---|---|---|---|---:|---|---|---:|
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `NewCncProgram` | `QFN_1` | `SN-001` | `2026-04-21T10:00:00.0000000Z` | `2026-04-21T10:00:03.2000000Z` | `0` | `Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a/run/source.bmp` | `Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `2` |
|
||||
|
||||
### 4.2 `inspection_node_results`
|
||||
|
||||
用途:保存一次检测实例中的节点级结果。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `run_id` | `TEXT` | 所属检测实例 ID |
|
||||
| `node_id` | `TEXT` | 节点 ID,GUID |
|
||||
| `node_index` | `INTEGER` | 节点序号 |
|
||||
| `node_name` | `TEXT` | 节点名称 |
|
||||
| `pipeline_id` | `TEXT` | Pipeline ID,GUID |
|
||||
| `pipeline_name` | `TEXT` | Pipeline 名称 |
|
||||
| `pipeline_version_hash` | `TEXT` | Pipeline 快照 hash |
|
||||
| `node_pass` | `INTEGER` | 节点判定,`0/1` |
|
||||
| `source_image_path` | `TEXT` | 节点输入图相对路径 |
|
||||
| `result_image_path` | `TEXT` | 节点结果图相对路径 |
|
||||
| `status` | `TEXT` | 节点状态:`Succeeded / Failed / PartialSuccess / AssetMissing` |
|
||||
| `duration_ms` | `INTEGER` | 节点耗时 |
|
||||
|
||||
样例数据:
|
||||
|
||||
| run_id | node_id | node_index | node_name | pipeline_name | pipeline_version_hash | node_pass | source_image_path | result_image_path | status | duration_ms |
|
||||
|---|---|---:|---|---|---|---:|---|---|---|---:|
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `1` | `检测节点1` | `Recipe-A` | `A1B2C3...` | `1` | `Results/2026/04/21/.../nodes/001_检测节点1/input.bmp` | `Results/2026/04/21/.../nodes/001_检测节点1/result_overlay.bmp` | `Succeeded` | `135` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `22222222-2222-2222-2222-222222222222` | `2` | `检测节点2` | `Recipe-B` | `D4E5F6...` | `0` | `` | `Results/2026/04/21/.../nodes/002_检测节点2/result_overlay.bmp` | `Failed` | `240` |
|
||||
|
||||
### 4.3 `inspection_metric_results`
|
||||
|
||||
用途:保存节点输出的结构化数值结果。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `run_id` | `TEXT` | 所属检测实例 ID |
|
||||
| `node_id` | `TEXT` | 所属节点 ID |
|
||||
| `metric_key` | `TEXT` | 指标 key |
|
||||
| `metric_name` | `TEXT` | 指标名称 |
|
||||
| `metric_value` | `REAL` | 指标值 |
|
||||
| `unit` | `TEXT` | 单位 |
|
||||
| `lower_limit` | `REAL` | 下限,可空 |
|
||||
| `upper_limit` | `REAL` | 上限,可空 |
|
||||
| `is_pass` | `INTEGER` | 单指标判定,`0/1` |
|
||||
| `display_order` | `INTEGER` | 展示顺序 |
|
||||
|
||||
样例数据:
|
||||
|
||||
| run_id | node_id | metric_key | metric_name | metric_value | unit | lower_limit | upper_limit | is_pass | display_order |
|
||||
|---|---|---|---|---:|---|---:|---:|---:|---:|
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `bridge.rate` | `Bridge Rate` | `0.12` | `%` | | `0.2` | `1` | `1` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `void.area` | `Void Area` | `5.6` | `px` | | `8` | `1` | `2` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `22222222-2222-2222-2222-222222222222` | `solder.height` | `Solder Height` | `1.7` | `mm` | `1.8` | | `0` | `1` |
|
||||
|
||||
### 4.4 `inspection_assets`
|
||||
|
||||
用途:保存文件资产索引。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `run_id` | `TEXT` | 所属检测实例 ID |
|
||||
| `node_id` | `TEXT` | 所属节点 ID,可空 |
|
||||
| `asset_type` | `TEXT` | 资产类型 |
|
||||
| `relative_path` | `TEXT` | 相对路径 |
|
||||
| `file_format` | `TEXT` | 文件格式 |
|
||||
| `width` | `INTEGER` | 宽度 |
|
||||
| `height` | `INTEGER` | 高度 |
|
||||
|
||||
约定的 `asset_type`:
|
||||
|
||||
- `RunSourceImage`
|
||||
- `NodeInputImage`
|
||||
- `NodeResultImage`
|
||||
|
||||
样例数据:
|
||||
|
||||
| run_id | node_id | asset_type | relative_path | file_format | width | height |
|
||||
|---|---|---|---|---|---:|---:|
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | | `RunSourceImage` | `Results/2026/04/21/.../run/source.bmp` | `bmp` | `0` | `0` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `NodeInputImage` | `Results/2026/04/21/.../nodes/001_检测节点1/input.bmp` | `bmp` | `0` | `0` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `NodeResultImage` | `Results/2026/04/21/.../nodes/001_检测节点1/result_overlay.bmp` | `bmp` | `0` | `0` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `22222222-2222-2222-2222-222222222222` | `NodeResultImage` | `Results/2026/04/21/.../nodes/002_检测节点2/result_overlay.bmp` | `bmp` | `0` | `0` |
|
||||
|
||||
### 4.5 `pipeline_execution_snapshots`
|
||||
|
||||
用途:保存节点执行时的 Pipeline 快照,避免后续配方修改影响历史报告。
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `run_id` | `TEXT` | 所属检测实例 ID |
|
||||
| `node_id` | `TEXT` | 所属节点 ID |
|
||||
| `pipeline_name` | `TEXT` | Pipeline 名称 |
|
||||
| `pipeline_definition_json` | `TEXT` | Pipeline 序列化 JSON |
|
||||
| `pipeline_hash` | `TEXT` | Pipeline JSON 的 SHA-256 |
|
||||
|
||||
样例数据:
|
||||
|
||||
| run_id | node_id | pipeline_name | pipeline_hash |
|
||||
|---|---|---|---|
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `11111111-1111-1111-1111-111111111111` | `Recipe-A` | `A1B2C3...` |
|
||||
| `7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a` | `22222222-2222-2222-2222-222222222222` | `Recipe-B` | `D4E5F6...` |
|
||||
|
||||
---
|
||||
|
||||
## 5. `manifest.json` 示例
|
||||
|
||||
每次 `CompleteRunAsync` 后,会在结果包目录下生成 `manifest.json`。
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"Run": {
|
||||
"RunId": "7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"ProgramName": "NewCncProgram",
|
||||
"WorkpieceId": "QFN_1",
|
||||
"SerialNumber": "SN-001",
|
||||
"StartedAt": "2026-04-21T10:00:00.0000000Z",
|
||||
"CompletedAt": "2026-04-21T10:00:03.2000000Z",
|
||||
"OverallPass": false,
|
||||
"SourceImagePath": "Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a/run/source.bmp",
|
||||
"ResultRootPath": "Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"NodeCount": 2
|
||||
},
|
||||
"Nodes": [
|
||||
{
|
||||
"RunId": "7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"NodeId": "11111111-1111-1111-1111-111111111111",
|
||||
"NodeIndex": 1,
|
||||
"NodeName": "检测节点1",
|
||||
"PipelineName": "Recipe-A",
|
||||
"PipelineVersionHash": "A1B2C3...",
|
||||
"NodePass": true,
|
||||
"SourceImagePath": "Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a/nodes/001_检测节点1/input.bmp",
|
||||
"ResultImagePath": "Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a/nodes/001_检测节点1/result_overlay.bmp",
|
||||
"Status": 0,
|
||||
"DurationMs": 135
|
||||
},
|
||||
{
|
||||
"RunId": "7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"NodeId": "22222222-2222-2222-2222-222222222222",
|
||||
"NodeIndex": 2,
|
||||
"NodeName": "检测节点2",
|
||||
"PipelineName": "Recipe-B",
|
||||
"PipelineVersionHash": "D4E5F6...",
|
||||
"NodePass": false,
|
||||
"SourceImagePath": "",
|
||||
"ResultImagePath": "Results/2026/04/21/7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a/nodes/002_检测节点2/result_overlay.bmp",
|
||||
"Status": 1,
|
||||
"DurationMs": 240
|
||||
}
|
||||
],
|
||||
"Metrics": [
|
||||
{
|
||||
"RunId": "7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"NodeId": "11111111-1111-1111-1111-111111111111",
|
||||
"MetricKey": "bridge.rate",
|
||||
"MetricName": "Bridge Rate",
|
||||
"MetricValue": 0.12,
|
||||
"Unit": "%",
|
||||
"LowerLimit": null,
|
||||
"UpperLimit": 0.2,
|
||||
"IsPass": true,
|
||||
"DisplayOrder": 1
|
||||
},
|
||||
{
|
||||
"RunId": "7d7d8d7d-1234-4567-89ab-9f0e1d2c3b4a",
|
||||
"NodeId": "22222222-2222-2222-2222-222222222222",
|
||||
"MetricKey": "solder.height",
|
||||
"MetricName": "Solder Height",
|
||||
"MetricValue": 1.7,
|
||||
"Unit": "mm",
|
||||
"LowerLimit": 1.8,
|
||||
"UpperLimit": null,
|
||||
"IsPass": false,
|
||||
"DisplayOrder": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `manifest.json` 是文件侧的完整快照
|
||||
- SQLite 是主索引
|
||||
- 报告模块可以优先查库,再按需读取文件
|
||||
|
||||
---
|
||||
|
||||
## 6. 报告模块如何取数
|
||||
|
||||
### 6.1 首页列表 / 历史查询
|
||||
|
||||
建议通过:
|
||||
|
||||
- `inspection_runs`
|
||||
- 必要时联查 `inspection_node_results`
|
||||
|
||||
可支持筛选条件:
|
||||
|
||||
- 时间范围
|
||||
- 程序名
|
||||
- 工件号
|
||||
- 序列号
|
||||
- Pipeline 名称
|
||||
|
||||
### 6.2 单份报告生成
|
||||
|
||||
建议按 `RunId` 调用:
|
||||
|
||||
- `GetRunDetailAsync(runId)`
|
||||
|
||||
得到:
|
||||
|
||||
- `Run`
|
||||
- `Nodes`
|
||||
- `Metrics`
|
||||
- `Assets`
|
||||
- `PipelineSnapshots`
|
||||
|
||||
即可直接组装报告:
|
||||
|
||||
- 报告首页
|
||||
- 工件号、序列号、程序名、开始/结束时间、整体判定
|
||||
- 节点章节
|
||||
- 节点名称
|
||||
- 配方名
|
||||
- 结果图
|
||||
- 关键指标值
|
||||
- 节点判定
|
||||
- 追溯信息
|
||||
- Pipeline 快照 hash
|
||||
- 原图路径
|
||||
- 结果图路径
|
||||
|
||||
### 6.3 离线导出
|
||||
|
||||
若后续需要将单个检测实例导出给第三方或留档,可直接复制:
|
||||
|
||||
- 整个 `Results/{yyyy}/{MM}/{dd}/{RunId}` 目录
|
||||
|
||||
这样会同时带走:
|
||||
|
||||
- `manifest.json`
|
||||
- 原图
|
||||
- 节点图
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前实现接口
|
||||
|
||||
当前服务接口:
|
||||
|
||||
- `BeginRunAsync(...)`
|
||||
- 创建实例记录和结果目录
|
||||
- `AppendNodeResultAsync(...)`
|
||||
- 写入节点结果、指标、图片索引、Pipeline 快照
|
||||
- `CompleteRunAsync(...)`
|
||||
- 回填结束时间、整体判定,并写出 `manifest.json`
|
||||
- `QueryRunsAsync(...)`
|
||||
- 查询检测实例列表
|
||||
- `GetRunDetailAsync(...)`
|
||||
- 查询单个实例的完整报告数据
|
||||
|
||||
当前 DI 注册:
|
||||
|
||||
- `IInspectionResultStore -> InspectionResultStore`
|
||||
|
||||
---
|
||||
|
||||
## 8. 设计约束与说明
|
||||
|
||||
### 8.1 为什么不直接扩展 `MeasurementDataService`
|
||||
|
||||
因为原有 `MeasurementRecord` 只适合:
|
||||
|
||||
- 单值统计
|
||||
- 简单的 pass/fail 汇总
|
||||
|
||||
它不适合承载:
|
||||
|
||||
- 多节点
|
||||
- 多图像
|
||||
- 多指标
|
||||
- Pipeline 快照
|
||||
- 报告导出
|
||||
|
||||
所以当前设计中:
|
||||
|
||||
- `MeasurementDataService` 继续保留给旧统计用途
|
||||
- `InspectionResultStore` 作为报告归档主通道
|
||||
|
||||
### 8.2 为什么图片不直接进 SQLite
|
||||
|
||||
因为图片数据量大,直接入库会带来:
|
||||
|
||||
- 数据库膨胀
|
||||
- 查询性能下降
|
||||
- 迁移和备份成本变高
|
||||
|
||||
因此使用:
|
||||
|
||||
- SQLite 存索引
|
||||
- 文件系统存图片
|
||||
|
||||
这是更适合报告场景的折中方案。
|
||||
|
||||
### 8.3 为什么要保存 Pipeline 快照
|
||||
|
||||
因为报告要可追溯。
|
||||
|
||||
如果只保存 `PipelineName`,后续配方被修改后,历史报告就无法复原当时的真实算法链。
|
||||
因此需要保存:
|
||||
|
||||
- `pipeline_definition_json`
|
||||
- `pipeline_hash`
|
||||
|
||||
---
|
||||
|
||||
## 9. 后续可扩展方向
|
||||
|
||||
后续可继续扩展:
|
||||
|
||||
1. 增加 `manifest.json` 中的设备上下文
|
||||
- 运动位置
|
||||
- 射线源状态
|
||||
- 探测器状态
|
||||
|
||||
2. 增加中间图可选保留策略
|
||||
- 默认关闭
|
||||
- 调试模式开启
|
||||
|
||||
3. 增加结果导出包
|
||||
- ZIP 打包
|
||||
- 单份报告 PDF
|
||||
|
||||
4. 增加报告模板字段映射
|
||||
- 将 `MetricKey` 映射到报告模板占位符
|
||||
|
||||
5. 增加数据清理策略
|
||||
- 保留天数
|
||||
- 自动清理旧图片
|
||||
- 保留数据库索引或同时删除
|
||||
|
||||
---
|
||||
|
||||
## 10. 结论
|
||||
|
||||
当前这套归档设计的核心特点是:
|
||||
|
||||
- 以“检测实例”为主组织数据
|
||||
- 以“检测节点”为明细展开
|
||||
- 以“结构化指标 + 图片文件 + Pipeline 快照”支撑报告
|
||||
- 通过 SQLite 和文件系统混合存储兼顾查询效率和图片落盘
|
||||
|
||||
对于后续报告模块,这套结构已经可以直接支持:
|
||||
|
||||
- 历史列表查询
|
||||
- 单次检测报告生成
|
||||
- 结果图展示
|
||||
- 节点级指标展示
|
||||
- 历史结果可追溯
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
public class PipelineModel
|
||||
public class PipelineModel //流程图
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; } = string.Empty;
|
||||
@@ -13,7 +13,7 @@ namespace XplorePlane.Models
|
||||
public List<PipelineNodeModel> Nodes { get; set; } = new();
|
||||
}
|
||||
|
||||
public class PipelineNodeModel
|
||||
public class PipelineNodeModel //节点
|
||||
{
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string OperatorKey { get; set; } = string.Empty;
|
||||
|
||||
@@ -364,9 +364,20 @@ namespace XplorePlane.Services.Cnc
|
||||
private static IReadOnlyList<CncNode> RenumberNodes(List<CncNode> nodes)
|
||||
{
|
||||
var result = new List<CncNode>(nodes.Count);
|
||||
int referencePointNumber = 0;
|
||||
int savePositionNumber = 0;
|
||||
int inspectionModuleNumber = 0;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
result.Add(nodes[i] with { Index = i });
|
||||
var indexedNode = nodes[i] with { Index = i };
|
||||
result.Add(indexedNode switch
|
||||
{
|
||||
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
||||
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
|
||||
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
|
||||
_ => indexedNode
|
||||
});
|
||||
}
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
@@ -414,7 +425,7 @@ namespace XplorePlane.Services.Cnc
|
||||
private SavePositionNode CreateSavePositionNode(Guid id, int index)
|
||||
{
|
||||
return new SavePositionNode(
|
||||
id, index, $"保存位置_{index}",
|
||||
id, index, $"检测位置_{index}",
|
||||
MotionState: _appStateService.MotionState);
|
||||
}
|
||||
private double TryReadCurrent()
|
||||
|
||||
@@ -37,11 +37,10 @@ namespace XplorePlane.Services
|
||||
if (enabledNodes.Count == 0)
|
||||
return source;
|
||||
|
||||
// 大图像预览缩放
|
||||
var current = ScaleForPreview(source);
|
||||
|
||||
int total = enabledNodes.Count;
|
||||
for (int step = 0; step < total; step++)
|
||||
var total = enabledNodes.Count;
|
||||
for (var step = 0; step < total; step++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -53,15 +52,14 @@ namespace XplorePlane.Services
|
||||
|
||||
if (invalidParameters.Count > 0)
|
||||
{
|
||||
var invalidParameterText = string.Join("、", invalidParameters);
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.DisplayName}' 存在无效参数:{string.Join("、", invalidParameters)}",
|
||||
$"算子 '{node.DisplayName}' 存在无效参数:{invalidParameterText}",
|
||||
node.Order,
|
||||
node.OperatorKey);
|
||||
}
|
||||
|
||||
var parameters = node.Parameters
|
||||
.Where(p => p.IsValueValid)
|
||||
.ToDictionary(p => p.Name, p => p.Value);
|
||||
var parameters = node.Parameters.ToDictionary(p => p.Name, p => p.Value);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -69,9 +67,12 @@ namespace XplorePlane.Services
|
||||
current, node.OperatorKey, parameters, null, cancellationToken);
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.OperatorKey}' 返回了空图像",
|
||||
node.Order, node.OperatorKey);
|
||||
$"算子 '{node.DisplayName}' 返回了空图像",
|
||||
node.Order,
|
||||
node.OperatorKey);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
@@ -85,7 +86,9 @@ namespace XplorePlane.Services
|
||||
{
|
||||
throw new PipelineExecutionException(
|
||||
$"算子 '{node.DisplayName}' 执行失败:{ex.Message}",
|
||||
node.Order, node.OperatorKey, ex);
|
||||
node.Order,
|
||||
node.OperatorKey,
|
||||
ex);
|
||||
}
|
||||
|
||||
progress?.Report(new PipelineProgress(step + 1, total, node.DisplayName));
|
||||
@@ -102,7 +105,7 @@ namespace XplorePlane.Services
|
||||
if (source.PixelWidth <= UhdThreshold && source.PixelHeight <= UhdThreshold)
|
||||
return source;
|
||||
|
||||
double scale = (double)PreviewMaxHeight / source.PixelHeight;
|
||||
var scale = (double)PreviewMaxHeight / source.PixelHeight;
|
||||
if (source.PixelWidth * scale > UhdThreshold)
|
||||
scale = (double)UhdThreshold / source.PixelWidth;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace XplorePlane.Services
|
||||
if (!Directory.Exists(directory))
|
||||
return Array.Empty<PipelineModel>();
|
||||
|
||||
var files = Directory.GetFiles(directory, "*.imw");
|
||||
var files = Directory.GetFiles(directory, "*.xpm");
|
||||
var results = new List<PipelineModel>();
|
||||
|
||||
foreach (var file in files)
|
||||
@@ -108,4 +108,4 @@ namespace XplorePlane.Services
|
||||
throw new UnauthorizedAccessException($"不允许路径遍历:{directory}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,14 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private CncProgram _currentProgram;
|
||||
private ObservableCollection<CncNodeViewModel> _nodes;
|
||||
private ObservableCollection<CncNodeViewModel> _treeNodes;
|
||||
private ObservableCollection<CncProgramTreeRootViewModel> _programTreeRoots;
|
||||
private CncNodeViewModel _selectedNode;
|
||||
private bool _isModified;
|
||||
private string _programName;
|
||||
private string _programDisplayName = "新建检测程序.xp";
|
||||
private Guid? _preferredSelectedNodeId;
|
||||
private Guid? _pendingInsertAnchorNodeId;
|
||||
private bool _pendingInsertAfterAnchor;
|
||||
|
||||
public CncEditorViewModel(
|
||||
ICncProgramService cncProgramService,
|
||||
@@ -48,6 +52,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
_nodes = new ObservableCollection<CncNodeViewModel>();
|
||||
_treeNodes = new ObservableCollection<CncNodeViewModel>();
|
||||
_programTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
|
||||
{
|
||||
new CncProgramTreeRootViewModel(_programDisplayName, _treeNodes)
|
||||
};
|
||||
|
||||
InsertReferencePointCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.ReferencePoint));
|
||||
InsertSaveNodeWithImageCommand = new DelegateCommand(() => ExecuteInsertNode(CncNodeType.SaveNodeWithImage));
|
||||
@@ -63,6 +71,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
.ObservesProperty(() => SelectedNode);
|
||||
MoveNodeUpCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<CncNodeViewModel>(ExecuteMoveNodeDown);
|
||||
PrepareInsertAboveCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: false));
|
||||
PrepareInsertBelowCommand = new DelegateCommand<CncNodeViewModel>(nodeVm => SetPendingInsertAnchor(nodeVm, insertAfter: true));
|
||||
|
||||
SaveProgramCommand = new DelegateCommand(async () => await ExecuteSaveProgramAsync());
|
||||
LoadProgramCommand = new DelegateCommand(async () => await ExecuteLoadProgramAsync());
|
||||
@@ -84,6 +94,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private set => SetProperty(ref _treeNodes, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<CncProgramTreeRootViewModel> ProgramTreeRoots
|
||||
{
|
||||
get => _programTreeRoots;
|
||||
private set => SetProperty(ref _programTreeRoots, value);
|
||||
}
|
||||
|
||||
public CncNodeViewModel SelectedNode
|
||||
{
|
||||
get => _selectedNode;
|
||||
@@ -110,6 +126,16 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
set => SetProperty(ref _programName, value);
|
||||
}
|
||||
|
||||
public string ProgramDisplayName
|
||||
{
|
||||
get => _programDisplayName;
|
||||
private set
|
||||
{
|
||||
if (SetProperty(ref _programDisplayName, value) && ProgramTreeRoots?.Count > 0)
|
||||
ProgramTreeRoots[0].DisplayName = value;
|
||||
}
|
||||
}
|
||||
|
||||
public DelegateCommand InsertReferencePointCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeWithImageCommand { get; }
|
||||
public DelegateCommand InsertSaveNodeCommand { get; }
|
||||
@@ -122,6 +148,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
public DelegateCommand DeleteNodeCommand { get; }
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeUpCommand { get; }
|
||||
public DelegateCommand<CncNodeViewModel> MoveNodeDownCommand { get; }
|
||||
public DelegateCommand<CncNodeViewModel> PrepareInsertAboveCommand { get; }
|
||||
public DelegateCommand<CncNodeViewModel> PrepareInsertBelowCommand { get; }
|
||||
public DelegateCommand SaveProgramCommand { get; }
|
||||
public DelegateCommand LoadProgramCommand { get; }
|
||||
public DelegateCommand NewProgramCommand { get; }
|
||||
@@ -140,6 +168,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
int afterIndex = ResolveInsertAfterIndex(nodeType);
|
||||
_currentProgram = _cncProgramService.InsertNode(_currentProgram, afterIndex, node);
|
||||
_preferredSelectedNodeId = node.Id;
|
||||
ClearPendingInsertAnchor();
|
||||
|
||||
OnProgramEdited();
|
||||
_logger.Info("Inserted node: Type={NodeType}", nodeType);
|
||||
@@ -157,21 +186,24 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
try
|
||||
{
|
||||
int deletedIndex = SelectedNode.Index;
|
||||
|
||||
if (SelectedNode.IsSavePosition)
|
||||
{
|
||||
var nodes = _currentProgram.Nodes.ToList();
|
||||
int startIndex = SelectedNode.Index;
|
||||
int startIndex = deletedIndex;
|
||||
int endIndex = GetSavePositionBlockEndIndex(startIndex);
|
||||
nodes.RemoveRange(startIndex, endIndex - startIndex + 1);
|
||||
_currentProgram = ReplaceProgramNodes(nodes);
|
||||
}
|
||||
else
|
||||
{
|
||||
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, SelectedNode.Index);
|
||||
_currentProgram = _cncProgramService.RemoveNode(_currentProgram, deletedIndex);
|
||||
}
|
||||
|
||||
OnProgramEdited();
|
||||
_logger.Info("Deleted node at index: {Index}", SelectedNode.Index);
|
||||
ClearPendingInsertAnchorIfMissing();
|
||||
_logger.Info("Deleted node at index: {Index}", deletedIndex);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException ex)
|
||||
{
|
||||
@@ -256,6 +288,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return;
|
||||
|
||||
await _cncProgramService.SaveAsync(_currentProgram, dlg.FileName);
|
||||
ProgramDisplayName = Path.GetFileName(dlg.FileName);
|
||||
IsModified = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -280,7 +313,9 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
_currentProgram = await _cncProgramService.LoadAsync(dlg.FileName);
|
||||
ProgramName = _currentProgram.Name;
|
||||
ProgramDisplayName = Path.GetFileName(dlg.FileName);
|
||||
IsModified = false;
|
||||
ClearPendingInsertAnchor();
|
||||
RefreshNodes();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -291,10 +326,12 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
private void ExecuteNewProgram()
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(ProgramName) ? "NewCncProgram" : ProgramName;
|
||||
const string name = "新建检测程序";
|
||||
_currentProgram = _cncProgramService.CreateProgram(name);
|
||||
ProgramName = _currentProgram.Name;
|
||||
ProgramDisplayName = FormatProgramDisplayName(_currentProgram.Name);
|
||||
IsModified = false;
|
||||
ClearPendingInsertAnchor();
|
||||
RefreshNodes();
|
||||
}
|
||||
|
||||
@@ -379,6 +416,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
private void RefreshNodes()
|
||||
{
|
||||
NormalizeDefaultNodeNamesInCurrentProgram();
|
||||
|
||||
var selectedId = _preferredSelectedNodeId ?? SelectedNode?.Id;
|
||||
var expansionState = Nodes.ToDictionary(node => node.Id, node => node.IsExpanded);
|
||||
|
||||
@@ -417,6 +456,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
Nodes = new ObservableCollection<CncNodeViewModel>(flatNodes);
|
||||
TreeNodes = new ObservableCollection<CncNodeViewModel>(rootNodes);
|
||||
ProgramTreeRoots = new ObservableCollection<CncProgramTreeRootViewModel>
|
||||
{
|
||||
new CncProgramTreeRootViewModel(ProgramDisplayName, TreeNodes)
|
||||
};
|
||||
|
||||
SelectedNode = selectedId.HasValue
|
||||
? Nodes.FirstOrDefault(node => node.Id == selectedId.Value) ?? Nodes.LastOrDefault()
|
||||
@@ -425,6 +468,31 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
_preferredSelectedNodeId = null;
|
||||
}
|
||||
|
||||
private void NormalizeDefaultNodeNamesInCurrentProgram()
|
||||
{
|
||||
if (_currentProgram?.Nodes == null || _currentProgram.Nodes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedNodes = ApplyDefaultNodeNames(_currentProgram.Nodes);
|
||||
|
||||
bool changed = false;
|
||||
for (int i = 0; i < normalizedNodes.Count; i++)
|
||||
{
|
||||
if (!Equals(normalizedNodes[i], _currentProgram.Nodes[i]))
|
||||
{
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
_currentProgram = _currentProgram with { Nodes = normalizedNodes };
|
||||
}
|
||||
}
|
||||
|
||||
private int ResolveInsertAfterIndex(CncNodeType nodeType)
|
||||
{
|
||||
if (_currentProgram == null || _currentProgram.Nodes.Count == 0)
|
||||
@@ -432,8 +500,19 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (TryResolvePendingInsertAfterIndex(nodeType, out int pendingAfterIndex))
|
||||
{
|
||||
return pendingAfterIndex;
|
||||
}
|
||||
|
||||
if (!IsSavePositionChild(nodeType))
|
||||
{
|
||||
int? currentSavePositionIndex = FindOwningSavePositionIndex(SelectedNode?.Index);
|
||||
if (currentSavePositionIndex.HasValue)
|
||||
{
|
||||
return GetSavePositionBlockEndIndex(currentSavePositionIndex.Value);
|
||||
}
|
||||
|
||||
return SelectedNode?.Index ?? _currentProgram.Nodes.Count - 1;
|
||||
}
|
||||
|
||||
@@ -446,6 +525,75 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
return GetSavePositionBlockEndIndex(savePositionIndex.Value);
|
||||
}
|
||||
|
||||
private void SetPendingInsertAnchor(CncNodeViewModel nodeVm, bool insertAfter)
|
||||
{
|
||||
if (_currentProgram == null || nodeVm == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingInsertAnchorNodeId = nodeVm.Id;
|
||||
_pendingInsertAfterAnchor = insertAfter;
|
||||
SelectedNode = nodeVm;
|
||||
}
|
||||
|
||||
private bool TryResolvePendingInsertAfterIndex(CncNodeType nodeType, out int afterIndex)
|
||||
{
|
||||
afterIndex = -1;
|
||||
|
||||
if (!_pendingInsertAnchorNodeId.HasValue || _currentProgram == null || IsSavePositionChild(nodeType))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int anchorIndex = FindNodeIndexById(_pendingInsertAnchorNodeId.Value);
|
||||
if (anchorIndex < 0)
|
||||
{
|
||||
ClearPendingInsertAnchor();
|
||||
return false;
|
||||
}
|
||||
|
||||
afterIndex = _pendingInsertAfterAnchor ? anchorIndex : anchorIndex - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int FindNodeIndexById(Guid nodeId)
|
||||
{
|
||||
if (_currentProgram?.Nodes == null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _currentProgram.Nodes.Count; i++)
|
||||
{
|
||||
if (_currentProgram.Nodes[i].Id == nodeId)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void ClearPendingInsertAnchor()
|
||||
{
|
||||
_pendingInsertAnchorNodeId = null;
|
||||
_pendingInsertAfterAnchor = false;
|
||||
}
|
||||
|
||||
private void ClearPendingInsertAnchorIfMissing()
|
||||
{
|
||||
if (!_pendingInsertAnchorNodeId.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (FindNodeIndexById(_pendingInsertAnchorNodeId.Value) < 0)
|
||||
{
|
||||
ClearPendingInsertAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveSavePositionChild(CncNodeViewModel nodeVm, bool moveDown)
|
||||
{
|
||||
int? parentIndex = FindOwningSavePositionIndex(nodeVm.Index);
|
||||
@@ -598,10 +746,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
private CncProgram ReplaceProgramNodes(List<CncNode> nodes)
|
||||
{
|
||||
var renumberedNodes = nodes
|
||||
.Select((node, index) => node with { Index = index })
|
||||
.ToList()
|
||||
.AsReadOnly();
|
||||
var renumberedNodes = ApplyDefaultNodeNames(nodes);
|
||||
|
||||
return _currentProgram with
|
||||
{
|
||||
@@ -610,6 +755,28 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
};
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CncNode> ApplyDefaultNodeNames(IReadOnlyList<CncNode> nodes)
|
||||
{
|
||||
var result = new List<CncNode>(nodes.Count);
|
||||
int referencePointNumber = 0;
|
||||
int savePositionNumber = 0;
|
||||
int inspectionModuleNumber = 0;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var indexedNode = nodes[i] with { Index = i };
|
||||
result.Add(indexedNode switch
|
||||
{
|
||||
ReferencePointNode referencePointNode => referencePointNode with { Name = $"\u53C2\u8003\u70B9_{referencePointNumber++}" },
|
||||
SavePositionNode savePositionNode => savePositionNode with { Name = $"\u4FDD\u5B58\u4F4D\u7F6E_{savePositionNumber++}" },
|
||||
InspectionModuleNode inspectionModuleNode => inspectionModuleNode with { Name = $"\u68C0\u6D4B\u6A21\u5757_{inspectionModuleNumber++}" },
|
||||
_ => indexedNode
|
||||
});
|
||||
}
|
||||
|
||||
return result.AsReadOnly();
|
||||
}
|
||||
|
||||
private static bool IsSavePositionChild(CncNodeType type)
|
||||
{
|
||||
return type is CncNodeType.InspectionMarker
|
||||
@@ -622,5 +789,11 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
.GetEvent<CncProgramChangedEvent>()
|
||||
.Publish(new CncProgramChangedPayload(ProgramName ?? string.Empty, IsModified));
|
||||
}
|
||||
|
||||
private static string FormatProgramDisplayName(string programName)
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(programName) ? "新建检测程序" : programName;
|
||||
return name.EndsWith(".xp", StringComparison.OrdinalIgnoreCase) ? name : $"{name}.xp";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
@@ -26,6 +26,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private CncNodeViewModel _activeModuleNode;
|
||||
private PipelineNodeViewModel _selectedNode;
|
||||
private string _statusMessage = "请选择检测模块以编辑其流水线。";
|
||||
private string _pipelineFileDisplayName = "未命名模块.xpm";
|
||||
private string _currentFilePath;
|
||||
private bool _isSynchronizing;
|
||||
|
||||
@@ -44,6 +45,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, _ => HasActiveModule);
|
||||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||||
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
|
||||
MoveNodeUpCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeUp);
|
||||
MoveNodeDownCommand = new DelegateCommand<PipelineNodeViewModel>(MoveNodeDown);
|
||||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||||
@@ -69,6 +72,14 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
private set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public bool IsStatusError => false;
|
||||
|
||||
public string PipelineFileDisplayName
|
||||
{
|
||||
get => _pipelineFileDisplayName;
|
||||
private set => SetProperty(ref _pipelineFileDisplayName, value);
|
||||
}
|
||||
|
||||
public bool HasActiveModule => _activeModuleNode?.IsInspectionModule == true;
|
||||
|
||||
public Visibility EditorVisibility => HasActiveModule ? Visibility.Visible : Visibility.Collapsed;
|
||||
@@ -79,6 +90,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
public ICommand RemoveOperatorCommand { get; }
|
||||
|
||||
public ICommand ReorderOperatorCommand { get; }
|
||||
|
||||
public ICommand ToggleOperatorEnabledCommand { get; }
|
||||
|
||||
public ICommand MoveNodeUpCommand { get; }
|
||||
|
||||
public ICommand MoveNodeDownCommand { get; }
|
||||
@@ -107,6 +122,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
_activeModuleNode = null;
|
||||
PipelineNodes.Clear();
|
||||
SelectedNode = null;
|
||||
PipelineFileDisplayName = "未命名模块.xpm";
|
||||
StatusMessage = "请选择检测模块以编辑其流水线。";
|
||||
RaiseModuleVisibilityChanged();
|
||||
RaiseCommandCanExecuteChanged();
|
||||
@@ -114,6 +130,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
_activeModuleNode = selected;
|
||||
_currentFilePath = null;
|
||||
LoadPipelineModel(_activeModuleNode.Pipeline ?? new PipelineModel
|
||||
{
|
||||
Name = _activeModuleNode.Name
|
||||
@@ -152,13 +169,10 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
|
||||
return;
|
||||
|
||||
var removedIndex = PipelineNodes.IndexOf(node);
|
||||
PipelineNodes.Remove(node);
|
||||
RenumberNodes();
|
||||
|
||||
if (SelectedNode == node)
|
||||
{
|
||||
SelectedNode = PipelineNodes.LastOrDefault();
|
||||
}
|
||||
SelectNeighborAfterRemoval(removedIndex);
|
||||
|
||||
PersistActiveModule($"已移除算子:{node.DisplayName}");
|
||||
}
|
||||
@@ -177,6 +191,26 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
PersistActiveModule($"已上移算子:{node.DisplayName}");
|
||||
}
|
||||
|
||||
private void ReorderOperator(PipelineReorderArgs args)
|
||||
{
|
||||
if (!HasActiveModule || args == null)
|
||||
return;
|
||||
|
||||
var oldIndex = args.OldIndex;
|
||||
var newIndex = args.NewIndex;
|
||||
if (oldIndex < 0 || oldIndex >= PipelineNodes.Count)
|
||||
return;
|
||||
|
||||
if (newIndex < 0 || newIndex >= PipelineNodes.Count || oldIndex == newIndex)
|
||||
return;
|
||||
|
||||
var node = PipelineNodes[oldIndex];
|
||||
PipelineNodes.Move(oldIndex, newIndex);
|
||||
RenumberNodes();
|
||||
SelectedNode = node;
|
||||
PersistActiveModule($"已调整算子顺序:{node.DisplayName}");
|
||||
}
|
||||
|
||||
private void MoveNodeDown(PipelineNodeViewModel node)
|
||||
{
|
||||
if (!HasActiveModule || node == null)
|
||||
@@ -191,6 +225,18 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
PersistActiveModule($"已下移算子:{node.DisplayName}");
|
||||
}
|
||||
|
||||
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
|
||||
{
|
||||
if (!HasActiveModule || node == null || !PipelineNodes.Contains(node))
|
||||
return;
|
||||
|
||||
node.IsEnabled = !node.IsEnabled;
|
||||
SelectedNode = node;
|
||||
PersistActiveModule(node.IsEnabled
|
||||
? $"已启用算子:{node.DisplayName}"
|
||||
: $"已停用算子:{node.DisplayName}");
|
||||
}
|
||||
|
||||
private void NewPipeline()
|
||||
{
|
||||
if (!HasActiveModule)
|
||||
@@ -199,6 +245,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
PipelineNodes.Clear();
|
||||
SelectedNode = null;
|
||||
_currentFilePath = null;
|
||||
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
|
||||
PersistActiveModule("已为当前检测模块新建空流水线。");
|
||||
}
|
||||
|
||||
@@ -217,7 +264,9 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "图像处理流水线 (*.imw)|*.imw",
|
||||
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
|
||||
DefaultExt = ".xpm",
|
||||
AddExtension = true,
|
||||
FileName = GetActivePipelineName()
|
||||
};
|
||||
|
||||
@@ -227,6 +276,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
var model = BuildPipelineModel();
|
||||
await _persistenceService.SaveAsync(model, dialog.FileName);
|
||||
_currentFilePath = dialog.FileName;
|
||||
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
|
||||
StatusMessage = $"已导出模块流水线:{Path.GetFileName(dialog.FileName)}";
|
||||
}
|
||||
|
||||
@@ -237,7 +287,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "图像处理流水线 (*.imw)|*.imw"
|
||||
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
|
||||
DefaultExt = ".xpm"
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
@@ -245,6 +296,7 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
|
||||
var model = await _persistenceService.LoadAsync(dialog.FileName);
|
||||
_currentFilePath = dialog.FileName;
|
||||
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
|
||||
LoadPipelineModel(model);
|
||||
PersistActiveModule($"已加载模块流水线:{model.Name}");
|
||||
}
|
||||
@@ -276,6 +328,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
|
||||
SelectedNode = PipelineNodes.FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(_currentFilePath))
|
||||
PipelineFileDisplayName = GetActivePipelineFileDisplayName();
|
||||
StatusMessage = HasActiveModule
|
||||
? $"正在编辑检测模块:{_activeModuleNode.Name}"
|
||||
: "请选择检测模块以编辑其流水线。";
|
||||
@@ -351,6 +405,21 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
: _activeModuleNode.Pipeline.Name;
|
||||
}
|
||||
|
||||
private string GetActivePipelineFileDisplayName()
|
||||
{
|
||||
var pipelineName = GetActivePipelineName();
|
||||
return pipelineName.EndsWith(".xpm", StringComparison.OrdinalIgnoreCase)
|
||||
? pipelineName
|
||||
: $"{pipelineName}.xpm";
|
||||
}
|
||||
|
||||
private static string FormatPipelinePath(string filePath)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(filePath)
|
||||
? "未命名模块.xpm"
|
||||
: Path.GetFullPath(filePath).Replace('\\', '/');
|
||||
}
|
||||
|
||||
private void RenumberNodes()
|
||||
{
|
||||
for (var i = 0; i < PipelineNodes.Count; i++)
|
||||
@@ -359,6 +428,20 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectNeighborAfterRemoval(int removedIndex)
|
||||
{
|
||||
if (PipelineNodes.Count == 0)
|
||||
{
|
||||
SelectedNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var nextIndex = removedIndex < PipelineNodes.Count
|
||||
? removedIndex
|
||||
: PipelineNodes.Count - 1;
|
||||
SelectedNode = PipelineNodes[nextIndex];
|
||||
}
|
||||
|
||||
private void RaiseModuleVisibilityChanged()
|
||||
{
|
||||
RaisePropertyChanged(nameof(HasActiveModule));
|
||||
|
||||
@@ -37,6 +37,8 @@ namespace XplorePlane.ViewModels.Cnc
|
||||
set => UpdateModel(_model with { Name = value ?? string.Empty });
|
||||
}
|
||||
|
||||
public bool IsReadOnlyNodeProperties => IsReferencePoint || IsSavePosition;
|
||||
|
||||
public CncNodeType NodeType => _model.NodeType;
|
||||
|
||||
public string NodeTypeDisplay => NodeType.ToString();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Prism.Mvvm;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace XplorePlane.ViewModels.Cnc
|
||||
{
|
||||
public class CncProgramTreeRootViewModel : BindableBase
|
||||
{
|
||||
private string _displayName;
|
||||
private bool _isExpanded = true;
|
||||
|
||||
public CncProgramTreeRootViewModel(string displayName, ObservableCollection<CncNodeViewModel> children)
|
||||
{
|
||||
_displayName = displayName;
|
||||
Children = children;
|
||||
}
|
||||
|
||||
public string DisplayName
|
||||
{
|
||||
get => _displayName;
|
||||
set => SetProperty(ref _displayName, value);
|
||||
}
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set => SetProperty(ref _isExpanded, value);
|
||||
}
|
||||
|
||||
public ObservableCollection<CncNodeViewModel> Children { get; }
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,18 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
string StatusMessage { get; }
|
||||
|
||||
bool IsStatusError { get; }
|
||||
|
||||
string PipelineFileDisplayName { get; }
|
||||
|
||||
ICommand AddOperatorCommand { get; }
|
||||
|
||||
ICommand RemoveOperatorCommand { get; }
|
||||
|
||||
ICommand ReorderOperatorCommand { get; }
|
||||
|
||||
ICommand ToggleOperatorEnabledCommand { get; }
|
||||
|
||||
ICommand MoveNodeUpCommand { get; }
|
||||
|
||||
ICommand MoveNodeDownCommand { get; }
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Events;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
@@ -21,6 +21,7 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
private const int MaxPipelineLength = 20;
|
||||
private const int DebounceDelayMs = 300;
|
||||
private const string DefaultPipelineFileDisplayName = "未命名模块.xpm";
|
||||
|
||||
private readonly IImageProcessingService _imageProcessingService;
|
||||
private readonly IPipelineExecutionService _executionService;
|
||||
@@ -34,7 +35,9 @@ namespace XplorePlane.ViewModels
|
||||
private string _pipelineName = "新建流水线";
|
||||
private string _selectedDevice = string.Empty;
|
||||
private bool _isExecuting;
|
||||
private bool _isStatusError;
|
||||
private string _statusMessage = string.Empty;
|
||||
private string _pipelineFileDisplayName = DefaultPipelineFileDisplayName;
|
||||
private string _currentFilePath;
|
||||
|
||||
private CancellationTokenSource _executionCts;
|
||||
@@ -59,6 +62,7 @@ namespace XplorePlane.ViewModels
|
||||
AddOperatorCommand = new DelegateCommand<string>(AddOperator, CanAddOperator);
|
||||
RemoveOperatorCommand = new DelegateCommand<PipelineNodeViewModel>(RemoveOperator);
|
||||
ReorderOperatorCommand = new DelegateCommand<PipelineReorderArgs>(ReorderOperator);
|
||||
ToggleOperatorEnabledCommand = new DelegateCommand<PipelineNodeViewModel>(ToggleOperatorEnabled);
|
||||
ExecutePipelineCommand = new DelegateCommand(async () => await ExecutePipelineAsync(), () => !IsExecuting && SourceImage != null);
|
||||
CancelExecutionCommand = new DelegateCommand(CancelExecution, () => IsExecuting);
|
||||
NewPipelineCommand = new DelegateCommand(NewPipeline);
|
||||
@@ -147,11 +151,24 @@ namespace XplorePlane.ViewModels
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public bool IsStatusError
|
||||
{
|
||||
get => _isStatusError;
|
||||
private set => SetProperty(ref _isStatusError, value);
|
||||
}
|
||||
|
||||
public string PipelineFileDisplayName
|
||||
{
|
||||
get => _pipelineFileDisplayName;
|
||||
private set => SetProperty(ref _pipelineFileDisplayName, value);
|
||||
}
|
||||
|
||||
// ── Commands ──────────────────────────────────────────────────
|
||||
|
||||
public DelegateCommand<string> AddOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineNodeViewModel> RemoveOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineReorderArgs> ReorderOperatorCommand { get; }
|
||||
public DelegateCommand<PipelineNodeViewModel> ToggleOperatorEnabledCommand { get; }
|
||||
public DelegateCommand ExecutePipelineCommand { get; }
|
||||
public DelegateCommand CancelExecutionCommand { get; }
|
||||
public DelegateCommand NewPipelineCommand { get; }
|
||||
@@ -168,6 +185,8 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
ICommand IPipelineEditorHostViewModel.AddOperatorCommand => AddOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.RemoveOperatorCommand => RemoveOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.ReorderOperatorCommand => ReorderOperatorCommand;
|
||||
ICommand IPipelineEditorHostViewModel.ToggleOperatorEnabledCommand => ToggleOperatorEnabledCommand;
|
||||
ICommand IPipelineEditorHostViewModel.MoveNodeUpCommand => MoveNodeUpCommand;
|
||||
ICommand IPipelineEditorHostViewModel.MoveNodeDownCommand => MoveNodeDownCommand;
|
||||
ICommand IPipelineEditorHostViewModel.NewPipelineCommand => NewPipelineCommand;
|
||||
@@ -186,7 +205,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
{
|
||||
StatusMessage = "算子键不能为空";
|
||||
SetInfoStatus("算子键不能为空");
|
||||
_logger.Warn("AddOperator 失败:operatorKey 为空");
|
||||
return;
|
||||
}
|
||||
@@ -197,14 +216,14 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
if (!available.Contains(operatorKey))
|
||||
{
|
||||
StatusMessage = $"算子 '{operatorKey}' 未注册";
|
||||
SetInfoStatus($"算子 '{operatorKey}' 未注册");
|
||||
_logger.Warn("AddOperator 失败:算子 {Key} 未注册", operatorKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (PipelineNodes.Count >= MaxPipelineLength)
|
||||
{
|
||||
StatusMessage = $"流水线节点数已达上限({MaxPipelineLength})";
|
||||
SetInfoStatus($"流水线节点数已达上限({MaxPipelineLength})");
|
||||
_logger.Warn("AddOperator 失败:节点数已达上限 {Max}", MaxPipelineLength);
|
||||
return;
|
||||
}
|
||||
@@ -217,9 +236,10 @@ namespace XplorePlane.ViewModels
|
||||
};
|
||||
LoadNodeParameters(node);
|
||||
PipelineNodes.Add(node);
|
||||
SelectedNode = node;
|
||||
_logger.Info("节点已添加到 PipelineNodes:{Key} ({DisplayName}),当前节点数={Count}",
|
||||
operatorKey, displayName, PipelineNodes.Count);
|
||||
StatusMessage = $"已添加算子:{displayName}";
|
||||
SetInfoStatus($"已添加算子:{displayName}");
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
@@ -227,13 +247,12 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||||
|
||||
var removedIndex = PipelineNodes.IndexOf(node);
|
||||
PipelineNodes.Remove(node);
|
||||
RenumberNodes();
|
||||
SelectNeighborAfterRemoval(removedIndex);
|
||||
|
||||
if (SelectedNode == node)
|
||||
SelectedNode = null;
|
||||
|
||||
StatusMessage = $"已移除算子:{node.DisplayName}";
|
||||
SetInfoStatus($"已移除算子:{node.DisplayName}");
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
@@ -271,6 +290,20 @@ namespace XplorePlane.ViewModels
|
||||
PipelineNodes.RemoveAt(oldIndex);
|
||||
PipelineNodes.Insert(newIndex, node);
|
||||
RenumberNodes();
|
||||
SelectedNode = node;
|
||||
SetInfoStatus($"已调整算子顺序:{node.DisplayName}");
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
private void ToggleOperatorEnabled(PipelineNodeViewModel node)
|
||||
{
|
||||
if (node == null || !PipelineNodes.Contains(node)) return;
|
||||
|
||||
node.IsEnabled = !node.IsEnabled;
|
||||
SelectedNode = node;
|
||||
SetInfoStatus(node.IsEnabled
|
||||
? $"已启用算子:{node.DisplayName}"
|
||||
: $"已停用算子:{node.DisplayName}");
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
|
||||
@@ -280,6 +313,20 @@ namespace XplorePlane.ViewModels
|
||||
PipelineNodes[i].Order = i;
|
||||
}
|
||||
|
||||
private void SelectNeighborAfterRemoval(int removedIndex)
|
||||
{
|
||||
if (PipelineNodes.Count == 0)
|
||||
{
|
||||
SelectedNode = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var nextIndex = removedIndex < PipelineNodes.Count
|
||||
? removedIndex
|
||||
: PipelineNodes.Count - 1;
|
||||
SelectedNode = PipelineNodes[nextIndex];
|
||||
}
|
||||
|
||||
private void LoadNodeParameters(PipelineNodeViewModel node)
|
||||
{
|
||||
var paramDefs = _imageProcessingService.GetProcessorParameters(node.OperatorKey);
|
||||
@@ -297,7 +344,12 @@ namespace XplorePlane.ViewModels
|
||||
vm.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ProcessorParameterVM.Value))
|
||||
{
|
||||
if (TryReportInvalidParameters())
|
||||
return;
|
||||
|
||||
TriggerDebouncedExecution();
|
||||
}
|
||||
};
|
||||
node.Parameters.Add(vm);
|
||||
}
|
||||
@@ -307,36 +359,39 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
if (SourceImage == null || IsExecuting) return;
|
||||
|
||||
if (TryReportInvalidParameters())
|
||||
return;
|
||||
|
||||
_executionCts?.Cancel();
|
||||
_executionCts = new CancellationTokenSource();
|
||||
var token = _executionCts.Token;
|
||||
|
||||
IsExecuting = true;
|
||||
StatusMessage = "正在执行流水线...";
|
||||
SetInfoStatus("正在执行流水线...");
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<PipelineProgress>(p =>
|
||||
StatusMessage = $"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})");
|
||||
SetInfoStatus($"执行中:{p.CurrentOperator} ({p.CurrentStep}/{p.TotalSteps})"));
|
||||
|
||||
var result = await _executionService.ExecutePipelineAsync(
|
||||
PipelineNodes, SourceImage, progress, token);
|
||||
|
||||
PreviewImage = result;
|
||||
StatusMessage = "流水线执行完成";
|
||||
SetInfoStatus("流水线执行完成");
|
||||
PublishPipelinePreviewUpdated(result, StatusMessage);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
StatusMessage = "流水线执行已取消";
|
||||
SetInfoStatus("流水线执行已取消");
|
||||
}
|
||||
catch (PipelineExecutionException ex)
|
||||
{
|
||||
StatusMessage = $"节点 '{ex.FailedOperatorKey}' 执行失败:{ex.Message}";
|
||||
SetErrorStatus($"执行失败:{ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"执行错误:{ex.Message}";
|
||||
SetErrorStatus($"执行错误:{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -344,6 +399,36 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReportInvalidParameters()
|
||||
{
|
||||
var firstInvalidNode = PipelineNodes
|
||||
.Where(n => n.IsEnabled)
|
||||
.OrderBy(n => n.Order)
|
||||
.FirstOrDefault(n => n.Parameters.Any(p => !p.IsValueValid));
|
||||
|
||||
if (firstInvalidNode == null)
|
||||
return false;
|
||||
|
||||
var invalidNames = firstInvalidNode.Parameters
|
||||
.Where(p => !p.IsValueValid)
|
||||
.Select(p => p.DisplayName);
|
||||
SetErrorStatus($"参数错误:算子 '{firstInvalidNode.DisplayName}' 的 {string.Join("、", invalidNames)} 输入不合理,请修正后重试。");
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetInfoStatus(string message)
|
||||
{
|
||||
IsStatusError = false;
|
||||
StatusMessage = message;
|
||||
}
|
||||
|
||||
private void SetErrorStatus(string message)
|
||||
{
|
||||
IsStatusError = true;
|
||||
StatusMessage = message;
|
||||
PublishPipelinePreviewUpdated(PreviewImage ?? SourceImage, message);
|
||||
}
|
||||
|
||||
private void LoadImage()
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
@@ -361,7 +446,7 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = $"加载图像失败:{ex.Message}";
|
||||
SetErrorStatus($"加载图像失败:{ex.Message}");
|
||||
_logger.Error(ex, "加载图像失败:{Path}", dialog.FileName);
|
||||
}
|
||||
}
|
||||
@@ -380,7 +465,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
SourceImage = bitmap;
|
||||
PreviewImage = bitmap;
|
||||
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
|
||||
SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
|
||||
PublishManualImageLoaded(bitmap, filePath);
|
||||
}
|
||||
|
||||
@@ -391,7 +476,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
SourceImage = bitmap;
|
||||
PreviewImage = bitmap;
|
||||
StatusMessage = $"已加载图像:{Path.GetFileName(filePath)}";
|
||||
SetInfoStatus($"已加载图像:{Path.GetFileName(filePath)}");
|
||||
PublishManualImageLoaded(bitmap, filePath);
|
||||
|
||||
if (runPipeline)
|
||||
@@ -419,7 +504,7 @@ namespace XplorePlane.ViewModels
|
||||
|
||||
SourceImage = payload.Image;
|
||||
PreviewImage = payload.Image;
|
||||
StatusMessage = $"已加载图像:{payload.FileName}";
|
||||
SetInfoStatus($"已加载图像:{payload.FileName}");
|
||||
}
|
||||
|
||||
private void CancelExecution()
|
||||
@@ -449,14 +534,15 @@ namespace XplorePlane.ViewModels
|
||||
PipelineName = "新建流水线";
|
||||
PreviewImage = null;
|
||||
_currentFilePath = null;
|
||||
StatusMessage = "已新建流水线";
|
||||
PipelineFileDisplayName = DefaultPipelineFileDisplayName;
|
||||
SetInfoStatus("已新建流水线");
|
||||
}
|
||||
|
||||
private async Task SavePipelineAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||||
{
|
||||
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
|
||||
SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -473,13 +559,15 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PipelineName) || PipelineName.Length > 100)
|
||||
{
|
||||
StatusMessage = "流水线名称不能为空且长度不超过 100 个字符";
|
||||
SetInfoStatus("流水线名称不能为空且长度不超过 100 个字符");
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "图像处理流水线 (*.imw)|*.imw",
|
||||
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
|
||||
DefaultExt = ".xpm",
|
||||
AddExtension = true,
|
||||
FileName = PipelineName,
|
||||
InitialDirectory = GetPipelineDirectory()
|
||||
};
|
||||
@@ -497,11 +585,12 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
var model = BuildPipelineModel();
|
||||
await _persistenceService.SaveAsync(model, filePath);
|
||||
StatusMessage = $"流水线已保存:{Path.GetFileName(filePath)}";
|
||||
PipelineFileDisplayName = FormatPipelinePath(filePath);
|
||||
SetInfoStatus($"流水线已保存:{Path.GetFileName(filePath)}");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
StatusMessage = $"保存失败:{ex.Message}";
|
||||
SetErrorStatus($"保存失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,11 +604,11 @@ namespace XplorePlane.ViewModels
|
||||
File.Delete(_currentFilePath);
|
||||
|
||||
NewPipeline();
|
||||
StatusMessage = "流水线已删除";
|
||||
SetInfoStatus("流水线已删除");
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
StatusMessage = $"删除失败:{ex.Message}";
|
||||
SetErrorStatus($"删除失败:{ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
@@ -528,7 +617,8 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "图像处理流水线 (*.imw)|*.imw",
|
||||
Filter = "XP 模块流水线 (*.xpm)|*.xpm",
|
||||
DefaultExt = ".xpm",
|
||||
InitialDirectory = GetPipelineDirectory()
|
||||
};
|
||||
|
||||
@@ -544,6 +634,7 @@ namespace XplorePlane.ViewModels
|
||||
PipelineName = model.Name;
|
||||
SelectedDevice = model.DeviceId;
|
||||
_currentFilePath = dialog.FileName;
|
||||
PipelineFileDisplayName = FormatPipelinePath(dialog.FileName);
|
||||
|
||||
foreach (var nodeModel in model.Nodes)
|
||||
{
|
||||
@@ -568,12 +659,12 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
|
||||
_logger.Info("流水线已加载:{Name},节点数={Count}", model.Name, PipelineNodes.Count);
|
||||
StatusMessage = $"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)";
|
||||
SetInfoStatus($"已加载流水线:{model.Name}({PipelineNodes.Count} 个节点)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn("加载流水线失败:{Error}", ex.Message);
|
||||
StatusMessage = $"加载失败:{ex.Message}";
|
||||
SetErrorStatus($"加载失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,5 +713,12 @@ namespace XplorePlane.ViewModels
|
||||
Directory.CreateDirectory(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
private static string FormatPipelinePath(string filePath)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(filePath)
|
||||
? DefaultPipelineFileDisplayName
|
||||
: Path.GetFullPath(filePath).Replace('\\', '/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace XplorePlane.ViewModels
|
||||
MinValue = parameter.MinValue;
|
||||
MaxValue = parameter.MaxValue;
|
||||
Options = parameter.Options;
|
||||
IsVisible = parameter.IsVisible;
|
||||
ParameterType = parameter.ValueType?.Name?.ToLowerInvariant() switch
|
||||
{
|
||||
"int32" or "int" => "int",
|
||||
@@ -34,7 +35,11 @@ namespace XplorePlane.ViewModels
|
||||
public object MinValue { get; }
|
||||
public object MaxValue { get; }
|
||||
public string[]? Options { get; }
|
||||
public bool IsVisible { get; }
|
||||
public string ParameterType { get; }
|
||||
public bool HasOptions => Options is { Length: > 0 };
|
||||
public bool IsBool => ParameterType == "bool";
|
||||
public bool IsTextInput => !IsBool && !HasOptions;
|
||||
|
||||
public bool IsValueValid
|
||||
{
|
||||
@@ -48,8 +53,40 @@ namespace XplorePlane.ViewModels
|
||||
set
|
||||
{
|
||||
var normalizedValue = NormalizeValue(value);
|
||||
if (SetProperty(ref _value, normalizedValue))
|
||||
if (!Equals(_value, normalizedValue))
|
||||
{
|
||||
_value = normalizedValue;
|
||||
ValidateValue(normalizedValue);
|
||||
RaisePropertyChanged(nameof(Value));
|
||||
RaisePropertyChanged(nameof(BoolValue));
|
||||
RaisePropertyChanged(nameof(SelectedOption));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool BoolValue
|
||||
{
|
||||
get => ParameterType == "bool" && TryConvertToBool(_value, out var boolValue) && boolValue;
|
||||
set
|
||||
{
|
||||
if (ParameterType == "bool")
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string SelectedOption
|
||||
{
|
||||
get => HasOptions
|
||||
? Convert.ToString(_value, CultureInfo.InvariantCulture) ?? string.Empty
|
||||
: string.Empty;
|
||||
set
|
||||
{
|
||||
if (HasOptions)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace XplorePlane.ViewModels
|
||||
{
|
||||
public class MainViewModel : BindableBase
|
||||
{
|
||||
private const double CncEditorHostWidth = 710d;
|
||||
private const double CncEditorHostWidth = 502d;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IContainerProvider _containerProvider;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
|
||||
@@ -9,22 +9,21 @@
|
||||
xmlns:views="clr-namespace:XplorePlane.Views"
|
||||
xmlns:vm="clr-namespace:XplorePlane.ViewModels.Cnc"
|
||||
d:DesignHeight="760"
|
||||
d:DesignWidth="702"
|
||||
d:DesignWidth="502"
|
||||
prism:ViewModelLocator.AutoWireViewModel="True"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<UserControl.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<local:InverseBooleanToVisibilityConverter x:Key="InverseBoolToVisibilityConverter" />
|
||||
<local:BoolToDisplayTextConverter x:Key="BoolToDisplayTextConverter" />
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E5E5E5" />
|
||||
<SolidColorBrush x:Key="HeaderBg" Color="#F7F7F7" />
|
||||
<SolidColorBrush x:Key="TreeParentBg" Color="#F6F8FB" />
|
||||
<SolidColorBrush x:Key="TreeChildBg" Color="#FBFCFE" />
|
||||
<SolidColorBrush x:Key="TreeAccentBrush" Color="#2E6FA3" />
|
||||
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#B9CDE0" />
|
||||
<SolidColorBrush x:Key="TreeChildLineBrush" Color="#C7D4DF" />
|
||||
<FontFamily x:Key="UiFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<Style x:Key="TreeItemStyle" TargetType="TreeViewItem">
|
||||
@@ -58,6 +57,19 @@
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="DisplayValueLabel" TargetType="Label">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Padding" Value="8,3" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
<Setter Property="Background" Value="#F7F8FA" />
|
||||
<Setter Property="BorderBrush" Value="#D9DDE3" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="Foreground" Value="#333333" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="EditorCheck" TargetType="CheckBox">
|
||||
<Setter Property="Margin" Value="0,2,0,8" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
@@ -70,11 +82,24 @@
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="TreeToolbarButton" TargetType="Button">
|
||||
<Setter Property="Height" Value="24" />
|
||||
<Setter Property="MinWidth" Value="42" />
|
||||
<Setter Property="Margin" Value="0,0,4,0" />
|
||||
<Setter Property="Padding" Value="8,0" />
|
||||
<Setter Property="Background" Value="#F8F8F8" />
|
||||
<Setter Property="BorderBrush" Value="#CFCFCF" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource UiFont}" />
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
|
||||
<Border
|
||||
Width="702"
|
||||
MinWidth="702"
|
||||
Width="502"
|
||||
MinWidth="502"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{StaticResource PanelBg}"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
@@ -84,9 +109,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="200" />
|
||||
<ColumnDefinition Width="1" />
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="1" />
|
||||
<ColumnDefinition Width="250" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
@@ -97,37 +120,81 @@
|
||||
|
||||
<Border
|
||||
Grid.Row="0"
|
||||
Padding="10,8"
|
||||
Padding="6,5"
|
||||
Background="{StaticResource HeaderBg}"
|
||||
BorderBrush="{StaticResource SeparatorBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding ProgramName, TargetNullValue=CNC编辑}"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Margin="0,3,0,0"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="10"
|
||||
Foreground="#666666"
|
||||
Text="保存位置节点下会自动显示检测标记和检测模块,清晰表达当前检测位置与检测内容的父子关系。"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<WrapPanel>
|
||||
<Button
|
||||
Command="{Binding NewProgramCommand}"
|
||||
Content="新建"
|
||||
Style="{StaticResource TreeToolbarButton}" />
|
||||
<Button
|
||||
Command="{Binding SaveProgramCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource TreeToolbarButton}" />
|
||||
<Button
|
||||
Command="{Binding LoadProgramCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource TreeToolbarButton}" />
|
||||
<Button
|
||||
Command="{Binding ExportCsvCommand}"
|
||||
Content="导出"
|
||||
Style="{StaticResource TreeToolbarButton}" />
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
|
||||
<TreeView
|
||||
x:Name="CncTreeView"
|
||||
Grid.Row="1"
|
||||
Padding="4,6"
|
||||
Padding="3,5"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
ItemsSource="{Binding TreeNodes}"
|
||||
ItemsSource="{Binding ProgramTreeRoots}"
|
||||
PreviewKeyDown="CncTreeView_PreviewKeyDown"
|
||||
SelectedItemChanged="CncTreeView_SelectedItemChanged">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type vm:CncNodeViewModel}" ItemsSource="{Binding Children}">
|
||||
<HierarchicalDataTemplate
|
||||
DataType="{x:Type vm:CncProgramTreeRootViewModel}"
|
||||
ItemContainerStyle="{StaticResource TreeItemStyle}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<Border
|
||||
x:Name="ProgramRootCard"
|
||||
Margin="0,1,0,3"
|
||||
Padding="0,2"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="1,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="12"
|
||||
Foreground="#2B8A3E"
|
||||
Text="◆" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding DisplayName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
|
||||
<Setter TargetName="ProgramRootCard" Property="Background" Value="#E7F1FB" />
|
||||
<Setter TargetName="ProgramRootCard" Property="BorderBrush" Value="#9FC6E8" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</HierarchicalDataTemplate>
|
||||
|
||||
<HierarchicalDataTemplate
|
||||
DataType="{x:Type vm:CncNodeViewModel}"
|
||||
ItemContainerStyle="{StaticResource TreeItemStyle}"
|
||||
ItemsSource="{Binding Children}">
|
||||
<Border
|
||||
x:Name="NodeCard"
|
||||
Margin="0,1,0,1"
|
||||
@@ -136,10 +203,10 @@
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4">
|
||||
<Grid x:Name="NodeRoot" MinHeight="28">
|
||||
<Grid x:Name="NodeRoot" MinHeight="23">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="18" />
|
||||
<ColumnDefinition Width="24" />
|
||||
<ColumnDefinition Width="15" />
|
||||
<ColumnDefinition Width="20" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
@@ -147,55 +214,44 @@
|
||||
<Grid Grid.Column="0">
|
||||
<Border
|
||||
x:Name="ChildStem"
|
||||
Width="2"
|
||||
Margin="8,0,0,0"
|
||||
Width="1"
|
||||
Margin="7,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Background="{StaticResource TreeChildLineBrush}"
|
||||
Visibility="Collapsed" />
|
||||
Background="{StaticResource TreeChildLineBrush}" />
|
||||
<Border
|
||||
x:Name="ChildBranch"
|
||||
Width="10"
|
||||
Height="2"
|
||||
Margin="8,0,0,0"
|
||||
Width="8"
|
||||
Height="1"
|
||||
Margin="7,0,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Background="{StaticResource TreeChildLineBrush}"
|
||||
Visibility="Collapsed" />
|
||||
Background="{StaticResource TreeChildLineBrush}" />
|
||||
</Grid>
|
||||
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Width="16"
|
||||
Height="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
CornerRadius="4">
|
||||
<Image
|
||||
Width="14"
|
||||
Height="14"
|
||||
Width="13"
|
||||
Height="13"
|
||||
Source="{Binding Icon}"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
|
||||
<StackPanel
|
||||
<TextBlock
|
||||
Grid.Column="2"
|
||||
Margin="3,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="10.5"
|
||||
Foreground="#888888"
|
||||
Text="{Binding Index, StringFormat='[{0}] '}" />
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="11.5"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Name}" />
|
||||
</StackPanel>
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
@@ -204,32 +260,6 @@
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="1,0"
|
||||
Background="White"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="↑"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
Margin="1,0"
|
||||
Background="White"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=TreeView}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="↓"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
<Button
|
||||
Width="20"
|
||||
Height="20"
|
||||
@@ -248,12 +278,13 @@
|
||||
<DataTemplate.Triggers>
|
||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="NodeCard" Property="Background" Value="#F3F7FB" />
|
||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#D7E4F1" />
|
||||
<Setter TargetName="NodeCard" Property="Background" Value="#F6FAFD" />
|
||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#DFEAF3" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsPositionChild}" Value="True">
|
||||
<Setter TargetName="ChildStem" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="ChildBranch" Property="Visibility" Value="Visible" />
|
||||
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=TreeViewItem}}" Value="True">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
<Setter TargetName="NodeCard" Property="Background" Value="#DCEEFF" />
|
||||
<Setter TargetName="NodeCard" Property="BorderBrush" Value="#71A9DB" />
|
||||
</DataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</HierarchicalDataTemplate>
|
||||
@@ -269,225 +300,188 @@
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="10">
|
||||
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
|
||||
<Grid Grid.Column="2">
|
||||
<ScrollViewer
|
||||
x:Name="NodePropertyEditor"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<Grid Margin="10">
|
||||
<StackPanel Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<TextBlock Style="{StaticResource EditorTitle}" Text="节点属性" />
|
||||
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="名称" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Name, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<UniformGrid Margin="0,0,0,8" Columns="2">
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
|
||||
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.Index, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
|
||||
<TextBox Style="{StaticResource EditorBox}" IsReadOnly="True" Text="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</UniformGrid>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="运动参数"
|
||||
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<UniformGrid Margin="8,8,8,6" Columns="2">
|
||||
<UniformGrid Margin="0,0,0,8" Columns="2">
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="索引" />
|
||||
<Label Style="{StaticResource DisplayValueLabel}" Content="{Binding SelectedNode.Index, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="类型" />
|
||||
<Label Style="{StaticResource DisplayValueLabel}" Content="{Binding SelectedNode.NodeTypeDisplay, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</UniformGrid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="射线源"
|
||||
Visibility="{Binding SelectedNode.IsReferencePoint, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电流 (uA)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Current, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="运动参数"
|
||||
Visibility="{Binding SelectedNode.IsMotionSnapshotNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<UniformGrid Margin="8,8,8,6" Columns="2">
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="XM" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.XM, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="YM" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.YM, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZT" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZT, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="ZD" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ZD, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,6,0">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="TiltD" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.TiltD, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="Dist" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Dist, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</UniformGrid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="保存参数"
|
||||
Visibility="{Binding SelectedNode.IsSaveNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="射线源"
|
||||
Visibility="{Binding SelectedNode.IsReferencePoint, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电流 (uA)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Current, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="保存参数"
|
||||
Visibility="{Binding SelectedNode.IsSaveNodeWithImage, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="图像文件" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ImageFileName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="保存参数"
|
||||
Visibility="{Binding SelectedNode.IsSaveNode, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="检测模块"
|
||||
Visibility="{Binding SelectedNode.IsInspectionModule, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="Pipeline 名称" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.PipelineName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="保存参数"
|
||||
Visibility="{Binding SelectedNode.IsSaveNodeWithImage, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="射线源开启" IsChecked="{Binding SelectedNode.IsRayOn}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="电压 (kV)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Voltage, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="功率 (W)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Power, UpdateSourceTrigger=LostFocus}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器已连接" IsChecked="{Binding SelectedNode.DetectorConnected}" />
|
||||
<CheckBox Style="{StaticResource EditorCheck}" Content="探测器正在采集" IsChecked="{Binding SelectedNode.DetectorAcquiring}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="帧率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FrameRate, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="分辨率" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Resolution, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="图像文件" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.ImageFileName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="检测标记"
|
||||
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerType, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerX" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerX, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerY" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerY, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="检测标记"
|
||||
Visibility="{Binding SelectedNode.IsInspectionMarker, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标记类型" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerType, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerX" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerX, UpdateSourceTrigger=LostFocus}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="MarkerY" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.MarkerY, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="消息弹窗"
|
||||
Visibility="{Binding SelectedNode.IsPauseDialog, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标题" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DialogTitle, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="消息内容" />
|
||||
<TextBox
|
||||
MinHeight="68"
|
||||
Margin="0,0,0,8"
|
||||
Padding="8,6"
|
||||
AcceptsReturn="True"
|
||||
BorderBrush="#CFCFCF"
|
||||
BorderThickness="1"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="11"
|
||||
Text="{Binding SelectedNode.DialogMessage, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="消息弹窗"
|
||||
Visibility="{Binding SelectedNode.IsPauseDialog, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="8,8,8,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="标题" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DialogTitle, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="消息内容" />
|
||||
<TextBox
|
||||
MinHeight="68"
|
||||
Margin="0,0,0,8"
|
||||
Padding="8,6"
|
||||
AcceptsReturn="True"
|
||||
BorderBrush="#CFCFCF"
|
||||
BorderThickness="1"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="11"
|
||||
Text="{Binding SelectedNode.DialogMessage, UpdateSourceTrigger=PropertyChanged}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="等待参数"
|
||||
Visibility="{Binding SelectedNode.IsWaitDelay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="10,8,10,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
|
||||
<Border
|
||||
Padding="12"
|
||||
Background="#FAFAFA"
|
||||
BorderBrush="#E6E6E6"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6"
|
||||
Visibility="{Binding SelectedNode, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Invert}">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="未选择节点"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="10"
|
||||
Foreground="#666666"
|
||||
Text="从左侧树中选择一个节点后,这里会显示可编辑的参数。"
|
||||
TextWrapping="Wrap" />
|
||||
<GroupBox
|
||||
Style="{StaticResource CompactGroupBox}"
|
||||
Header="等待参数"
|
||||
Visibility="{Binding SelectedNode.IsWaitDelay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<StackPanel Margin="10,8,10,6">
|
||||
<TextBlock Style="{StaticResource LabelStyle}" Text="延时 (ms)" />
|
||||
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DelayMilliseconds, UpdateSourceTrigger=LostFocus}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
|
||||
<Rectangle
|
||||
Grid.Column="3"
|
||||
Width="1"
|
||||
Fill="{StaticResource SeparatorBrush}" />
|
||||
|
||||
<Grid Grid.Column="4">
|
||||
<views:PipelineEditorView
|
||||
x:Name="InspectionModulePipelineEditor"
|
||||
Margin="0"
|
||||
Visibility="{Binding EditorVisibility}" />
|
||||
|
||||
<Border
|
||||
x:Name="InspectionModulePipelineEmptyState"
|
||||
x:Name="NodePropertyEmptyState"
|
||||
Margin="12"
|
||||
Padding="16"
|
||||
Background="#FAFAFA"
|
||||
BorderBrush="#E6E6E6"
|
||||
BorderThickness="1"
|
||||
CornerRadius="6"
|
||||
Visibility="{Binding EmptyStateVisibility}">
|
||||
CornerRadius="6">
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
Text="未选择检测模块"
|
||||
Text="未选择节点"
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
Margin="0,6,0,0"
|
||||
FontFamily="{StaticResource UiFont}"
|
||||
FontSize="10"
|
||||
Foreground="#666666"
|
||||
Text="请选择一个检测模块节点后,在这里拖拽算子并配置参数。"
|
||||
Text="从左侧树中选择一个节点后,这里会显示对应的参数或检测流程。"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using Prism.Ioc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Services;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
@@ -15,7 +19,17 @@ namespace XplorePlane.Views.Cnc
|
||||
/// </summary>
|
||||
public partial class CncPageView : UserControl
|
||||
{
|
||||
private static readonly Brush SelectedNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E7F0F7"));
|
||||
private static readonly Brush SelectedNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#9CB9D1"));
|
||||
private static readonly Brush HoverNodeBackground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#F6FAFC"));
|
||||
private static readonly Brush HoverNodeBorder = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#D7E4EE"));
|
||||
private static readonly Brush SelectedNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#1F4E79"));
|
||||
private static readonly Brush DefaultNodeForeground = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#202020"));
|
||||
private static readonly Brush TransparentBrush = Brushes.Transparent;
|
||||
|
||||
private CncInspectionModulePipelineViewModel _inspectionModulePipelineViewModel;
|
||||
private readonly Dictionary<TextBox, Label> _textDisplayLabels = new();
|
||||
private readonly Dictionary<CheckBox, Label> _checkDisplayLabels = new();
|
||||
|
||||
public CncPageView()
|
||||
{
|
||||
@@ -55,12 +69,18 @@ namespace XplorePlane.Views.Cnc
|
||||
logger);
|
||||
|
||||
InspectionModulePipelineEditor.DataContext = _inspectionModulePipelineViewModel;
|
||||
InspectionModulePipelineEmptyState.DataContext = _inspectionModulePipelineViewModel;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// keep page usable even if pipeline editor host setup fails
|
||||
}
|
||||
|
||||
CncTreeView.ContextMenuOpening -= CncTreeView_ContextMenuOpening;
|
||||
CncTreeView.ContextMenuOpening += CncTreeView_ContextMenuOpening;
|
||||
CncTreeView.LayoutUpdated -= CncTreeView_LayoutUpdated;
|
||||
CncTreeView.LayoutUpdated += CncTreeView_LayoutUpdated;
|
||||
UpdateNodeVisualState();
|
||||
UpdatePropertyEditorState();
|
||||
}
|
||||
|
||||
private void CncTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
|
||||
@@ -69,6 +89,295 @@ namespace XplorePlane.Views.Cnc
|
||||
{
|
||||
viewModel.SelectedNode = e.NewValue as CncNodeViewModel;
|
||||
}
|
||||
|
||||
UpdateNodeVisualState();
|
||||
UpdatePropertyEditorState();
|
||||
}
|
||||
|
||||
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key != Key.Delete || DataContext is not CncEditorViewModel viewModel)
|
||||
return;
|
||||
|
||||
if (!viewModel.DeleteNodeCommand.CanExecute())
|
||||
return;
|
||||
|
||||
viewModel.DeleteNodeCommand.Execute();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CncTreeView_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
if (DataContext is not CncEditorViewModel viewModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = Mouse.GetPosition(CncTreeView);
|
||||
var hit = VisualTreeHelper.HitTest(CncTreeView, position);
|
||||
var treeViewItem = FindAncestor<TreeViewItem>(hit?.VisualHit);
|
||||
if (treeViewItem?.DataContext is not CncNodeViewModel nodeVm)
|
||||
{
|
||||
CncTreeView.ContextMenu = null;
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel.SelectedNode = nodeVm;
|
||||
UpdateNodeVisualState();
|
||||
UpdatePropertyEditorState();
|
||||
|
||||
CncTreeView.ContextMenu = new ContextMenu
|
||||
{
|
||||
Items =
|
||||
{
|
||||
new MenuItem
|
||||
{
|
||||
Header = "\u5728\u4E0A\u65B9\u63D2\u5165\u4F4D\u7F6E",
|
||||
Command = viewModel.PrepareInsertAboveCommand,
|
||||
CommandParameter = nodeVm
|
||||
},
|
||||
new MenuItem
|
||||
{
|
||||
Header = "\u5728\u4E0B\u65B9\u63D2\u5165\u4F4D\u7F6E",
|
||||
Command = viewModel.PrepareInsertBelowCommand,
|
||||
CommandParameter = nodeVm
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void CncTreeView_LayoutUpdated(object sender, EventArgs e)
|
||||
{
|
||||
HideInlineDeleteButtons();
|
||||
UpdateNodeVisualState();
|
||||
UpdatePropertyEditorState();
|
||||
}
|
||||
|
||||
private void HideInlineDeleteButtons()
|
||||
{
|
||||
foreach (var button in FindVisualDescendants<Button>(CncTreeView))
|
||||
{
|
||||
if (button.ToolTip is string)
|
||||
{
|
||||
button.Visibility = Visibility.Collapsed;
|
||||
button.IsHitTestVisible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateNodeVisualState()
|
||||
{
|
||||
foreach (var item in FindVisualDescendants<TreeViewItem>(CncTreeView))
|
||||
{
|
||||
if (item.DataContext is not CncNodeViewModel)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var card = FindNodeCard(item);
|
||||
if (card == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.IsSelected)
|
||||
{
|
||||
card.Background = SelectedNodeBackground;
|
||||
card.BorderBrush = SelectedNodeBorder;
|
||||
ApplyNodeTextForeground(card, SelectedNodeForeground);
|
||||
}
|
||||
else if (card.IsMouseOver)
|
||||
{
|
||||
card.Background = HoverNodeBackground;
|
||||
card.BorderBrush = HoverNodeBorder;
|
||||
ApplyNodeTextForeground(card, DefaultNodeForeground);
|
||||
}
|
||||
else
|
||||
{
|
||||
card.Background = TransparentBrush;
|
||||
card.BorderBrush = TransparentBrush;
|
||||
ApplyNodeTextForeground(card, DefaultNodeForeground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePropertyEditorState()
|
||||
{
|
||||
if (DataContext is not CncEditorViewModel viewModel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var propertyEditorRoot = FindPropertyEditorRoot();
|
||||
if (propertyEditorRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isReadOnlyNode = viewModel.SelectedNode?.IsReadOnlyNodeProperties == true;
|
||||
bool showNodeProperties = viewModel.SelectedNode != null && !viewModel.SelectedNode.IsInspectionModule;
|
||||
|
||||
NodePropertyEditor.Visibility = showNodeProperties ? Visibility.Visible : Visibility.Collapsed;
|
||||
NodePropertyEmptyState.Visibility = viewModel.SelectedNode == null ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
foreach (var textBox in FindVisualDescendants<TextBox>(propertyEditorRoot))
|
||||
{
|
||||
var bindingExpression = textBox.GetBindingExpression(TextBox.TextProperty);
|
||||
string bindingPath = bindingExpression?.ParentBinding?.Path?.Path ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(bindingPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool alwaysDisplay = bindingPath is "SelectedNode.Index" or "SelectedNode.NodeTypeDisplay";
|
||||
var label = EnsureTextDisplayLabel(textBox, bindingPath);
|
||||
bool showLabel = alwaysDisplay || isReadOnlyNode;
|
||||
|
||||
textBox.IsReadOnly = showLabel;
|
||||
textBox.Visibility = showLabel ? Visibility.Collapsed : Visibility.Visible;
|
||||
label.Visibility = showLabel ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
foreach (var checkBox in FindVisualDescendants<CheckBox>(propertyEditorRoot))
|
||||
{
|
||||
var bindingExpression = checkBox.GetBindingExpression(ToggleButton.IsCheckedProperty);
|
||||
string bindingPath = bindingExpression?.ParentBinding?.Path?.Path ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(bindingPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var label = EnsureCheckDisplayLabel(checkBox, bindingPath);
|
||||
checkBox.IsEnabled = !isReadOnlyNode;
|
||||
checkBox.Visibility = isReadOnlyNode ? Visibility.Collapsed : Visibility.Visible;
|
||||
label.Visibility = isReadOnlyNode ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
}
|
||||
|
||||
private Label EnsureTextDisplayLabel(TextBox textBox, string bindingPath)
|
||||
{
|
||||
if (_textDisplayLabels.TryGetValue(textBox, out var existingLabel))
|
||||
{
|
||||
return existingLabel;
|
||||
}
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Style = TryFindResource("DisplayValueLabel") as Style,
|
||||
Visibility = Visibility.Collapsed
|
||||
};
|
||||
label.SetBinding(ContentProperty, new Binding(bindingPath));
|
||||
|
||||
InsertCompanionControl(textBox, label);
|
||||
_textDisplayLabels[textBox] = label;
|
||||
return label;
|
||||
}
|
||||
|
||||
private Label EnsureCheckDisplayLabel(CheckBox checkBox, string bindingPath)
|
||||
{
|
||||
if (_checkDisplayLabels.TryGetValue(checkBox, out var existingLabel))
|
||||
{
|
||||
return existingLabel;
|
||||
}
|
||||
|
||||
var label = new Label
|
||||
{
|
||||
Style = TryFindResource("DisplayValueLabel") as Style,
|
||||
Visibility = Visibility.Collapsed
|
||||
};
|
||||
label.SetBinding(ContentProperty, new Binding(bindingPath)
|
||||
{
|
||||
Converter = TryFindResource("BoolToDisplayTextConverter") as IValueConverter
|
||||
});
|
||||
|
||||
InsertCompanionControl(checkBox, label);
|
||||
_checkDisplayLabels[checkBox] = label;
|
||||
return label;
|
||||
}
|
||||
|
||||
private static void InsertCompanionControl(Control sourceControl, Control companionControl)
|
||||
{
|
||||
if (VisualTreeHelper.GetParent(sourceControl) is not Panel panel)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (panel.Children.Contains(companionControl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int index = panel.Children.IndexOf(sourceControl);
|
||||
panel.Children.Insert(index + 1, companionControl);
|
||||
}
|
||||
|
||||
private static void ApplyNodeTextForeground(Border card, Brush foreground)
|
||||
{
|
||||
foreach (var textBlock in FindVisualDescendants<TextBlock>(card))
|
||||
{
|
||||
if (textBlock.Visibility == Visibility.Visible)
|
||||
{
|
||||
textBlock.Foreground = foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DependencyObject FindPropertyEditorRoot()
|
||||
{
|
||||
return NodePropertyEditor;
|
||||
}
|
||||
|
||||
private static Border FindNodeCard(DependencyObject root)
|
||||
{
|
||||
foreach (var border in FindVisualDescendants<Border>(root))
|
||||
{
|
||||
if (border.DataContext is CncNodeViewModel && border.CornerRadius.TopLeft == 4 && border.BorderThickness.Left == 1)
|
||||
{
|
||||
return border;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static T FindAncestor<T>(DependencyObject dependencyObject) where T : DependencyObject
|
||||
{
|
||||
var current = dependencyObject;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is T match)
|
||||
{
|
||||
return match;
|
||||
}
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static System.Collections.Generic.IEnumerable<T> FindVisualDescendants<T>(DependencyObject root) where T : DependencyObject
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
int childCount = VisualTreeHelper.GetChildrenCount(root);
|
||||
for (int i = 0; i < childCount; i++)
|
||||
{
|
||||
var child = VisualTreeHelper.GetChild(root, i);
|
||||
if (child is T typed)
|
||||
{
|
||||
yield return typed;
|
||||
}
|
||||
|
||||
foreach (var descendant in FindVisualDescendants<T>(child))
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,4 +400,30 @@ namespace XplorePlane.Views.Cnc
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class InverseBooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true ? Visibility.Collapsed : Visibility.Visible;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
public class BoolToDisplayTextConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value is true ? "\u662F" : "\u5426";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
d:DesignHeight="700"
|
||||
d:DesignWidth="350"
|
||||
mc:Ignorable="d">
|
||||
@@ -13,13 +12,27 @@
|
||||
<SolidColorBrush x:Key="PanelBg" Color="White" />
|
||||
<SolidColorBrush x:Key="PanelBorder" Color="#CDCBCB" />
|
||||
<SolidColorBrush x:Key="SeparatorBrush" Color="#E0E0E0" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
|
||||
<SolidColorBrush x:Key="AccentBlue" Color="#D9ECFF" />
|
||||
<SolidColorBrush x:Key="DisabledNodeBg" Color="#F3F3F3" />
|
||||
<SolidColorBrush x:Key="DisabledNodeLine" Color="#B9B9B9" />
|
||||
<SolidColorBrush x:Key="DisabledNodeText" Color="#8A8A8A" />
|
||||
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
|
||||
|
||||
<Style x:Key="PipelineNodeItemStyle" TargetType="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Focusable" Value="False" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBlue}" />
|
||||
<Setter Property="BorderBrush" Value="#5B9BD5" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ToolbarBtn" TargetType="Button">
|
||||
@@ -43,7 +56,7 @@
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2*" MinHeight="180" />
|
||||
<RowDefinition Height="4*" MinHeight="180" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="2*" MinHeight="80" />
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -56,30 +69,48 @@
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
BorderThickness="0,1,0,1">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding NewPipelineCommand}"
|
||||
Content="新建"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="新建流水线" />
|
||||
<Button
|
||||
Command="{Binding SavePipelineCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存当前检测模块流水线" />
|
||||
<Button
|
||||
Width="60"
|
||||
Command="{Binding SaveAsPipelineCommand}"
|
||||
Content="另存为"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="导出当前检测模块流水线" />
|
||||
<Button
|
||||
Width="52"
|
||||
Command="{Binding LoadPipelineCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="将流水线模板加载到当前检测模块" />
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Command="{Binding NewPipelineCommand}"
|
||||
Content="新建"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="新建流水线" />
|
||||
<Button
|
||||
Command="{Binding SavePipelineCommand}"
|
||||
Content="保存"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="保存当前流水线" />
|
||||
<Button
|
||||
Width="60"
|
||||
Command="{Binding SaveAsPipelineCommand}"
|
||||
Content="另存为"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="另存当前流水线" />
|
||||
<Button
|
||||
Width="52"
|
||||
Command="{Binding LoadPipelineCommand}"
|
||||
Content="加载"
|
||||
Style="{StaticResource ToolbarBtn}"
|
||||
ToolTip="加载流水线" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Row="1"
|
||||
Margin="2,4,2,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="#333333"
|
||||
Text="{Binding PipelineFileDisplayName}"
|
||||
TextAlignment="Left"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
ToolTip="{Binding PipelineFileDisplayName}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -90,117 +121,108 @@
|
||||
BorderThickness="0"
|
||||
ItemContainerStyle="{StaticResource PipelineNodeItemStyle}"
|
||||
ItemsSource="{Binding PipelineNodes}"
|
||||
KeyboardNavigation.TabNavigation="Continue"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid x:Name="NodeRoot" MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border
|
||||
x:Name="NodeContainer"
|
||||
Margin="2"
|
||||
Padding="2"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="1"
|
||||
CornerRadius="3">
|
||||
<Grid x:Name="NodeRoot" MinHeight="48">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Line
|
||||
x:Name="TopLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="10" />
|
||||
<Line
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="14" />
|
||||
<Line
|
||||
x:Name="TopLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="10" />
|
||||
<Line
|
||||
x:Name="BottomLine"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
Stroke="#5B9BD5"
|
||||
StrokeThickness="2"
|
||||
X1="0"
|
||||
X2="0"
|
||||
Y1="0"
|
||||
Y2="14" />
|
||||
|
||||
<Border
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
<Border
|
||||
x:Name="IconBorder"
|
||||
Grid.Column="0"
|
||||
Width="28"
|
||||
Height="28"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
Background="#E8F0FE"
|
||||
BorderBrush="#5B9BD5"
|
||||
BorderThickness="1.5"
|
||||
CornerRadius="4">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"
|
||||
Text="{Binding IconPath}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
Margin="6,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<StackPanel
|
||||
x:Name="NodeActions"
|
||||
Grid.Column="2"
|
||||
Margin="0,0,4,0"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"
|
||||
Visibility="Collapsed">
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeUpCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="上"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="上移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.MoveNodeDownCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="下"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="下移" />
|
||||
<Button
|
||||
Width="22"
|
||||
Height="22"
|
||||
Margin="1,0"
|
||||
Background="Transparent"
|
||||
BorderBrush="#E05050"
|
||||
BorderThickness="1"
|
||||
Command="{Binding DataContext.RemoveOperatorCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="删"
|
||||
Cursor="Hand"
|
||||
FontSize="10"
|
||||
ToolTip="删除" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" Margin="6,0,0,0" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
x:Name="NodeTitle"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="12"
|
||||
Text="{Binding DisplayName}" />
|
||||
<TextBlock
|
||||
x:Name="NodeState"
|
||||
Margin="0,2,0,0"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="10"
|
||||
Foreground="#6E6E6E"
|
||||
Text="已启用" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
<DataTemplate.Triggers>
|
||||
<DataTrigger Binding="{Binding Order}" Value="0">
|
||||
<Setter TargetName="TopLine" Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<Trigger SourceName="NodeRoot" Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="NodeActions" Property="Visibility" Value="Visible" />
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
|
||||
<Setter TargetName="NodeContainer" Property="Background" Value="{StaticResource DisabledNodeBg}" />
|
||||
<Setter TargetName="NodeContainer" Property="Opacity" Value="0.78" />
|
||||
<Setter TargetName="TopLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
|
||||
<Setter TargetName="BottomLine" Property="Stroke" Value="{StaticResource DisabledNodeLine}" />
|
||||
<Setter TargetName="IconBorder" Property="BorderBrush" Value="{StaticResource DisabledNodeLine}" />
|
||||
<Setter TargetName="IconBorder" Property="Background" Value="#ECECEC" />
|
||||
<Setter TargetName="NodeTitle" Property="Foreground" Value="{StaticResource DisabledNodeText}" />
|
||||
<Setter TargetName="NodeState" Property="Text" Value="已停用" />
|
||||
<Setter TargetName="NodeState" Property="Foreground" Value="#9A6767" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True">
|
||||
<Setter TargetName="NodeContainer" Property="Background" Value="#D9ECFF" />
|
||||
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#5B9BD5" />
|
||||
</DataTrigger>
|
||||
<MultiDataTrigger>
|
||||
<MultiDataTrigger.Conditions>
|
||||
<Condition Binding="{Binding IsEnabled}" Value="False" />
|
||||
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType=ListBoxItem}, Path=IsSelected}" Value="True" />
|
||||
</MultiDataTrigger.Conditions>
|
||||
<Setter TargetName="NodeContainer" Property="Background" Value="#E6EEF7" />
|
||||
<Setter TargetName="NodeContainer" Property="BorderBrush" Value="#7E9AB6" />
|
||||
<Setter TargetName="NodeContainer" Property="Opacity" Value="1" />
|
||||
</MultiDataTrigger>
|
||||
</DataTemplate.Triggers>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
@@ -224,6 +246,16 @@
|
||||
Foreground="#555"
|
||||
Text="参数配置" />
|
||||
<ItemsControl ItemsSource="{Binding SelectedNode.Parameters}">
|
||||
<ItemsControl.ItemContainerStyle>
|
||||
<Style TargetType="ContentPresenter">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsVisible}" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ItemsControl.ItemContainerStyle>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid Margin="0,3">
|
||||
@@ -239,6 +271,7 @@
|
||||
Text="{Binding DisplayName}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBox
|
||||
x:Name="TextValueEditor"
|
||||
Grid.Column="1"
|
||||
Padding="4,2"
|
||||
BorderBrush="#CDCBCB"
|
||||
@@ -250,15 +283,56 @@
|
||||
<Style TargetType="TextBox">
|
||||
<Setter Property="BorderBrush" Value="#CDCBCB" />
|
||||
<Setter Property="Background" Value="White" />
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsTextInput}" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding IsValueValid}" Value="False">
|
||||
<Setter Property="BorderBrush" Value="Red" />
|
||||
<Setter Property="Background" Value="#FFF0F0" />
|
||||
<Setter Property="BorderBrush" Value="#D9534F" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
<ComboBox
|
||||
Grid.Column="1"
|
||||
MinHeight="24"
|
||||
Padding="4,1"
|
||||
BorderBrush="#CDCBCB"
|
||||
BorderThickness="1"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
ItemsSource="{Binding Options}"
|
||||
SelectedItem="{Binding SelectedOption, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<ComboBox.Style>
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasOptions}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ComboBox.Style>
|
||||
</ComboBox>
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="Microsoft YaHei UI"
|
||||
FontSize="11"
|
||||
IsChecked="{Binding BoolValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
|
||||
<CheckBox.Style>
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsBool}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</CheckBox.Style>
|
||||
</CheckBox>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
@@ -268,16 +342,38 @@
|
||||
|
||||
<Border
|
||||
Grid.Row="4"
|
||||
Padding="6,4"
|
||||
Background="#F5F5F5"
|
||||
BorderBrush="{StaticResource PanelBorder}"
|
||||
Height="24"
|
||||
Padding="6,0"
|
||||
BorderThickness="0,1,0,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F5F5F5" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource PanelBorder}" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
|
||||
<Setter Property="Background" Value="#FFF1F1" />
|
||||
<Setter Property="BorderBrush" Value="#D9534F" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="{StaticResource CsdFont}"
|
||||
FontSize="11"
|
||||
Foreground="#555"
|
||||
Text="{Binding StatusMessage, StringFormat='Status: {0}'}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
TextTrimming="CharacterEllipsis">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#555" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsStatusError}" Value="True">
|
||||
<Setter Property="Foreground" Value="#A12A2A" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
@@ -2,15 +2,27 @@ using Prism.Ioc;
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.ViewModels;
|
||||
|
||||
namespace XplorePlane.Views
|
||||
{
|
||||
public partial class PipelineEditorView : UserControl
|
||||
{
|
||||
private const string PipelineNodeDragFormat = "PipelineNodeDrag";
|
||||
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
private Point _dragStartPoint;
|
||||
private bool _isInternalDragging;
|
||||
private bool _suppressClickToggle;
|
||||
private PipelineNodeViewModel _draggedNode;
|
||||
|
||||
public PipelineEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -35,50 +47,252 @@ namespace XplorePlane.Views
|
||||
}
|
||||
}
|
||||
|
||||
_logger?.Info("PipelineEditorView DataContext 类型={Type}",
|
||||
DataContext?.GetType().Name);
|
||||
|
||||
PipelineListBox.AllowDrop = true;
|
||||
PipelineListBox.Drop += OnOperatorDropped;
|
||||
PipelineListBox.Focusable = true;
|
||||
PipelineListBox.Drop -= OnListBoxDrop;
|
||||
PipelineListBox.Drop += OnListBoxDrop;
|
||||
PipelineListBox.DragOver -= OnDragOver;
|
||||
PipelineListBox.DragOver += OnDragOver;
|
||||
_logger?.Debug("PipelineEditorView 原生 Drop 目标已注册");
|
||||
PipelineListBox.PreviewMouseLeftButtonDown -= OnPreviewMouseLeftButtonDown;
|
||||
PipelineListBox.PreviewMouseLeftButtonDown += OnPreviewMouseLeftButtonDown;
|
||||
PipelineListBox.PreviewMouseMove -= OnPreviewMouseMove;
|
||||
PipelineListBox.PreviewMouseMove += OnPreviewMouseMove;
|
||||
PipelineListBox.PreviewMouseLeftButtonUp -= OnPreviewMouseLeftButtonUp;
|
||||
PipelineListBox.PreviewMouseLeftButtonUp += OnPreviewMouseLeftButtonUp;
|
||||
PipelineListBox.PreviewKeyDown -= OnPreviewKeyDown;
|
||||
PipelineListBox.PreviewKeyDown += OnPreviewKeyDown;
|
||||
}
|
||||
|
||||
private void OnPreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_dragStartPoint = e.GetPosition(PipelineListBox);
|
||||
_isInternalDragging = false;
|
||||
_draggedNode = FindNodeFromOriginalSource(e.OriginalSource);
|
||||
|
||||
if (_draggedNode != null)
|
||||
{
|
||||
PipelineListBox.SelectedItem = _draggedNode;
|
||||
PipelineListBox.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPreviewMouseMove(object sender, MouseEventArgs e)
|
||||
{
|
||||
if (e.LeftButton != MouseButtonState.Pressed || _draggedNode == null || IsInteractiveChild(e.OriginalSource))
|
||||
return;
|
||||
|
||||
var position = e.GetPosition(PipelineListBox);
|
||||
var delta = position - _dragStartPoint;
|
||||
if (_isInternalDragging
|
||||
|| (Math.Abs(delta.X) < SystemParameters.MinimumHorizontalDragDistance
|
||||
&& Math.Abs(delta.Y) < SystemParameters.MinimumVerticalDragDistance))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isInternalDragging = true;
|
||||
_suppressClickToggle = true;
|
||||
var data = new DataObject(PipelineNodeDragFormat, _draggedNode);
|
||||
DragDrop.DoDragDrop(PipelineListBox, data, DragDropEffects.Move);
|
||||
Dispatcher.BeginInvoke(new Action(() =>
|
||||
{
|
||||
_suppressClickToggle = false;
|
||||
ResetDragState();
|
||||
}), DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void OnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
var vm = DataContext as IPipelineEditorHostViewModel;
|
||||
var clickedNode = FindNodeFromOriginalSource(e.OriginalSource);
|
||||
|
||||
if (_isInternalDragging)
|
||||
{
|
||||
ResetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressClickToggle)
|
||||
{
|
||||
_suppressClickToggle = false;
|
||||
ResetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (vm == null || clickedNode == null || IsInteractiveChild(e.OriginalSource))
|
||||
{
|
||||
ResetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
PipelineListBox.SelectedItem = clickedNode;
|
||||
PipelineListBox.Focus();
|
||||
vm.ToggleOperatorEnabledCommand.Execute(clickedNode);
|
||||
e.Handled = true;
|
||||
ResetDragState();
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key != Key.Delete || DataContext is not IPipelineEditorHostViewModel vm || vm.SelectedNode == null)
|
||||
return;
|
||||
|
||||
vm.RemoveOperatorCommand.Execute(vm.SelectedNode);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(OperatorToolboxView.DragFormat)
|
||||
? DragDropEffects.Copy
|
||||
: DragDropEffects.None;
|
||||
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
e.Effects = DragDropEffects.Copy;
|
||||
}
|
||||
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
|
||||
{
|
||||
e.Effects = DragDropEffects.Move;
|
||||
}
|
||||
else
|
||||
{
|
||||
e.Effects = DragDropEffects.None;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnOperatorDropped(object sender, DragEventArgs e)
|
||||
private void OnListBoxDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (DataContext is not IPipelineEditorHostViewModel vm)
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但 DataContext 不是流水线宿主 ViewModel");
|
||||
ResetDragState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
if (e.Data.GetDataPresent(OperatorToolboxView.DragFormat))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但数据中没有 {Format}", OperatorToolboxView.DragFormat);
|
||||
return;
|
||||
OnOperatorDropped(vm, e);
|
||||
}
|
||||
else if (e.Data.GetDataPresent(PipelineNodeDragFormat))
|
||||
{
|
||||
OnInternalNodeDropped(vm, e);
|
||||
}
|
||||
|
||||
ResetDragState();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnOperatorDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
|
||||
{
|
||||
var operatorKey = e.Data.GetData(OperatorToolboxView.DragFormat) as string;
|
||||
if (string.IsNullOrWhiteSpace(operatorKey))
|
||||
{
|
||||
_logger?.Warn("Drop 事件触发,但 OperatorKey 为空");
|
||||
_logger?.Warn("Drop 触发,但 OperatorKey 为空");
|
||||
return;
|
||||
}
|
||||
|
||||
_logger?.Info("算子已放入流水线:{OperatorKey},当前节点数(执行前)={Count}",
|
||||
operatorKey, vm.PipelineNodes.Count);
|
||||
vm.AddOperatorCommand.Execute(operatorKey);
|
||||
_logger?.Info("AddOperator 执行后节点数={Count},PipelineListBox.Items.Count={ItemsCount}",
|
||||
vm.PipelineNodes.Count, PipelineListBox.Items.Count);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnInternalNodeDropped(IPipelineEditorHostViewModel vm, DragEventArgs e)
|
||||
{
|
||||
if (e.Data.GetData(PipelineNodeDragFormat) is not PipelineNodeViewModel draggedNode
|
||||
|| !vm.PipelineNodes.Contains(draggedNode))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldIndex = vm.PipelineNodes.IndexOf(draggedNode);
|
||||
var insertionIndex = GetDropInsertionIndex(e.GetPosition(PipelineListBox), vm.PipelineNodes.Count);
|
||||
var newIndex = insertionIndex > oldIndex ? insertionIndex - 1 : insertionIndex;
|
||||
newIndex = Math.Max(0, Math.Min(newIndex, vm.PipelineNodes.Count - 1));
|
||||
|
||||
if (oldIndex == newIndex)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
vm.ReorderOperatorCommand.Execute(new PipelineReorderArgs
|
||||
{
|
||||
OldIndex = oldIndex,
|
||||
NewIndex = newIndex
|
||||
});
|
||||
}
|
||||
|
||||
private int GetDropInsertionIndex(Point position, int itemCount)
|
||||
{
|
||||
var item = GetItemAtPosition(position);
|
||||
if (item == null)
|
||||
{
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
var targetIndex = PipelineListBox.ItemContainerGenerator.IndexFromContainer(item);
|
||||
if (targetIndex < 0)
|
||||
{
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
var itemTop = item.TranslatePoint(new Point(0, 0), PipelineListBox).Y;
|
||||
var itemMid = itemTop + (item.ActualHeight / 2);
|
||||
return position.Y > itemMid ? targetIndex + 1 : targetIndex;
|
||||
}
|
||||
|
||||
private ListBoxItem GetItemAtPosition(Point position)
|
||||
{
|
||||
var element = PipelineListBox.InputHitTest(position) as DependencyObject;
|
||||
while (element != null)
|
||||
{
|
||||
if (element is ListBoxItem item)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
element = VisualTreeHelper.GetParent(element);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private PipelineNodeViewModel FindNodeFromOriginalSource(object originalSource)
|
||||
{
|
||||
var current = originalSource as DependencyObject;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is FrameworkElement element && element.DataContext is PipelineNodeViewModel node)
|
||||
{
|
||||
return node;
|
||||
}
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsInteractiveChild(object originalSource)
|
||||
{
|
||||
var current = originalSource as DependencyObject;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is ListBoxItem)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current is ButtonBase || current is TextBoxBase || current is ScrollBar)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
current = VisualTreeHelper.GetParent(current);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void ResetDragState()
|
||||
{
|
||||
_isInternalDragging = false;
|
||||
_draggedNode = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -469,21 +469,14 @@
|
||||
Grid.ColumnSpan="3"
|
||||
Margin="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="NavColumn" Width="0" />
|
||||
<ColumnDefinition Width="{Binding ViewportPanelWidth}" />
|
||||
<ColumnDefinition Width="{Binding ImagePanelWidth}" />
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition Width="350" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧: 计划导航 (默认隐藏,点击CNC AccountingNumberFormatButton显示) -->
|
||||
<views:NavigationPanelView
|
||||
x:Name="NavigationPanel"
|
||||
Grid.Column="0"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<!-- 中间: 2D Viewport -->
|
||||
<Border
|
||||
Grid.Column="1"
|
||||
Grid.Column="0"
|
||||
BorderBrush="#DDDDDD"
|
||||
BorderThickness="1,0,1,0">
|
||||
<views:ViewportPanelView />
|
||||
@@ -491,16 +484,16 @@
|
||||
|
||||
<!-- 中间: 图像 -->
|
||||
<Border
|
||||
Grid.Column="2"
|
||||
Grid.Column="1"
|
||||
BorderBrush="#DDDDDD"
|
||||
BorderThickness="0,0,1,0">
|
||||
<views:ImagePanelView />
|
||||
</Border>
|
||||
|
||||
<!-- 右侧: 属性面板 -->
|
||||
<Grid Grid.Column="3">
|
||||
<Grid Grid.Column="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="250*" />
|
||||
<ColumnDefinition Width="350*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
|
||||
@@ -13,12 +13,5 @@ namespace XplorePlane.Views
|
||||
InitializeComponent();
|
||||
DataContext = viewModel;
|
||||
}
|
||||
|
||||
private void AccountingNumberFormatButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
bool show = NavigationPanel.Visibility != Visibility.Visible;
|
||||
NavigationPanel.Visibility = show ? Visibility.Visible : Visibility.Collapsed;
|
||||
NavColumn.Width = show ? new GridLength(180) : new GridLength(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
d:DesignHeight="600"
|
||||
d:DesignHeight="424"
|
||||
d:DesignWidth="400"
|
||||
mc:Ignorable="d">
|
||||
<Grid Background="#FFFFFF">
|
||||
@@ -48,12 +48,15 @@
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Background="#000000"
|
||||
Padding="8,4">
|
||||
<TextBlock FontSize="12">
|
||||
Height="24"
|
||||
Padding="8,0">
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
FontSize="12">
|
||||
<Run Foreground="#FFFFFF" Text="{Binding CameraStatusText, Mode=OneWay}" />
|
||||
<Run Text=" " />
|
||||
<Run Foreground="#0078D4" Text="{Binding CameraPixelCoord, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
</UserControl>
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
ImageSource="{Binding ImageSource}"
|
||||
Background="White" />
|
||||
|
||||
<!-- 图像信息栏 -->
|
||||
<Border Grid.Row="2" Background="#F0F0F0" BorderBrush="#DDDDDD" BorderThickness="0,1,0,0">
|
||||
<TextBlock Margin="4,2" FontSize="11" Foreground="#666666"
|
||||
Text="{Binding ImageInfo}" />
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user