矩阵编排允许用户通过界面设定矩阵参数(行数、列数、行间距、列间距),将一个已编写好的 CNC 模板程序(.xp 文件)自动扩展为覆盖所有矩阵位置的完整检测序列,并按行优先顺序依次完成移轴→采图→检测的闭环执行

This commit is contained in:
zhengxuan.zhang
2026-06-02 11:25:11 +08:00
parent df50000e6a
commit dee9359c5c
11 changed files with 1241 additions and 238 deletions
+2
View File
@@ -632,6 +632,8 @@ namespace XplorePlane
containerRegistry.RegisterSingleton<IInspectionResultStore, InspectionResultStore>();
containerRegistry.RegisterSingleton<IImagePersistenceService, ImagePersistenceService>();
containerRegistry.RegisterSingleton<ICncExecutionService, CncExecutionService>();
containerRegistry.RegisterSingleton<IMatrixOrchestrationService, MatrixOrchestrationService>();
containerRegistry.RegisterSingleton<MatrixSummaryWriter>();
// ── 主界面实时图像 / 探测器双队列服务(单例)──
containerRegistry.RegisterSingleton<IMainViewportService, MainViewportService>();
@@ -0,0 +1,24 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace XplorePlane.Converters
{
/// <summary>
/// 布尔值取反转换器,用于将 true 转为 false、false 转为 true
/// Inverse boolean converter: converts true to false and false to true
/// </summary>
[ValueConversion(typeof(bool), typeof(bool))]
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b ? !b : true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b ? !b : false;
}
}
}
+35
View File
@@ -42,4 +42,39 @@ namespace XplorePlane.Models
string CncProgramPath,
IReadOnlyList<MatrixCell> Cells
);
// ── 矩阵执行摘要模型(用于 matrix_summary.json 序列化)────────────
/// <summary>矩阵执行摘要文件根对象 | Matrix execution summary file root object</summary>
public class MatrixSummaryFile
{
public MatrixSummaryConfig Config { get; set; }
public string ProgramName { get; set; }
public string StartTime { get; set; } // ISO 8601
public double DurationSeconds { get; set; }
public int TotalCells { get; set; }
public int EnabledCells { get; set; }
public int CompletedCells { get; set; }
public int FailedCells { get; set; }
public List<MatrixCellSummaryEntry> Cells { get; set; }
}
/// <summary>矩阵配置信息 | Matrix configuration</summary>
public class MatrixSummaryConfig
{
public int Rows { get; set; }
public int Columns { get; set; }
public double RowSpacing { get; set; }
public double ColumnSpacing { get; set; }
}
/// <summary>矩阵单元格摘要条目 | Matrix cell summary entry</summary>
public class MatrixCellSummaryEntry
{
public string RunId { get; set; }
public int Row { get; set; }
public int Column { get; set; }
public string Status { get; set; }
public bool? Pass { get; set; }
}
}
@@ -0,0 +1,25 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using XplorePlane.Models;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵编排执行服务接口。
/// 按行优先顺序展开矩阵并执行每个启用单元格。
/// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
/// </summary>
public interface IMatrixOrchestrationService
{
/// <summary>
/// 按行优先顺序展开矩阵并执行每个启用单元格。
/// 对每个单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
/// </summary>
Task ExecuteAsync(
MatrixLayout layout,
CncProgram templateProgram,
IProgress<MatrixCellExecutionProgress> progress,
CancellationToken cancellationToken);
}
}
@@ -0,0 +1,16 @@
using XplorePlane.Models;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵单元格执行进度报告。
/// </summary>
public record MatrixCellExecutionProgress(
int Row,
int Column,
MatrixCellStatus Status,
int CurrentIndex, // 当前启用单元格序号(从 1 开始)
int TotalEnabled, // 总启用单元格数
string ErrorMessage = null
);
}
@@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.Cnc;
using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.Storage;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵编排执行服务实现。
/// 按行优先顺序展开矩阵并执行每个启用单元格:深度克隆模板程序 → 叠加偏移量 → 调用 ICncExecutionService。
/// Matrix orchestration execution service implementation.
/// Expands matrix in row-major order and executes each enabled cell: deep clone template → apply offset → call ICncExecutionService.
/// </summary>
public class MatrixOrchestrationService : IMatrixOrchestrationService
{
private readonly ICncExecutionService _cncExecutionService;
private readonly IInspectionResultStore _inspectionResultStore;
private readonly MatrixSummaryWriter _matrixSummaryWriter;
private readonly IXpDataPathService _dataPathService;
private readonly ILoggerService _logger;
public MatrixOrchestrationService(
ICncExecutionService cncExecutionService,
IInspectionResultStore inspectionResultStore,
MatrixSummaryWriter matrixSummaryWriter,
IXpDataPathService dataPathService,
ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(cncExecutionService);
ArgumentNullException.ThrowIfNull(inspectionResultStore);
ArgumentNullException.ThrowIfNull(matrixSummaryWriter);
ArgumentNullException.ThrowIfNull(dataPathService);
ArgumentNullException.ThrowIfNull(logger);
_cncExecutionService = cncExecutionService;
_inspectionResultStore = inspectionResultStore;
_matrixSummaryWriter = matrixSummaryWriter;
_dataPathService = dataPathService;
_logger = logger.ForModule<MatrixOrchestrationService>();
}
/// <inheritdoc />
public async Task ExecuteAsync(
MatrixLayout layout,
CncProgram templateProgram,
IProgress<MatrixCellExecutionProgress> progress,
CancellationToken cancellationToken)
{
// 筛选启用的单元格,按行优先(Row 升序,Column 升序)排列
var enabledCells = layout.Cells
.Where(c => c.IsEnabled)
.OrderBy(c => c.Row)
.ThenBy(c => c.Column)
.ToList();
if (enabledCells.Count == 0)
{
throw new InvalidOperationException("没有已启用的位置,请至少启用一个单元格");
}
var totalEnabled = enabledCells.Count;
var startTime = DateTime.UtcNow;
var stopwatch = Stopwatch.StartNew();
int completedCount = 0;
int failedCount = 0;
var cellEntries = new List<MatrixCellSummaryEntry>();
_logger.Info(
"矩阵执行开始 | Matrix execution started: Program={ProgramName}, EnabledCells={EnabledCount}, TotalCells={TotalCells}",
templateProgram.Name, totalEnabled, layout.Rows * layout.Columns);
for (int i = 0; i < enabledCells.Count; i++)
{
var cell = enabledCells[i];
var currentIndex = i + 1;
// 检查取消请求
cancellationToken.ThrowIfCancellationRequested();
// 报告 Executing 状态
progress?.Report(new MatrixCellExecutionProgress(
cell.Row,
cell.Column,
MatrixCellStatus.Executing,
currentIndex,
totalEnabled));
// 生成单元格专用程序
var cellProgram = ApplyOffset(templateProgram, cell);
try
{
// 调用 CNC 执行服务(内部已处理 BeginRunAsync 归档)
await _cncExecutionService.ExecuteAsync(
cellProgram,
new Progress<CncNodeExecutionProgress>(),
cancellationToken).ConfigureAwait(false);
// 成功完成
completedCount++;
progress?.Report(new MatrixCellExecutionProgress(
cell.Row,
cell.Column,
MatrixCellStatus.Completed,
currentIndex,
totalEnabled));
cellEntries.Add(new MatrixCellSummaryEntry
{
RunId = cellProgram.Id.ToString(),
Row = cell.Row,
Column = cell.Column,
Status = nameof(MatrixCellStatus.Completed),
Pass = true
});
_logger.Info(
"单元格执行完成 | Cell completed: R{Row}C{Col} ({Current}/{Total})",
cell.Row, cell.Column, currentIndex, totalEnabled);
}
catch (OperationCanceledException)
{
// 探测器断连或用户取消 — 标记当前单元格为 Error,传播异常退出循环
failedCount++;
progress?.Report(new MatrixCellExecutionProgress(
cell.Row,
cell.Column,
MatrixCellStatus.Error,
currentIndex,
totalEnabled,
"操作已取消(探测器断连或用户停止)"));
cellEntries.Add(new MatrixCellSummaryEntry
{
RunId = cellProgram.Id.ToString(),
Row = cell.Row,
Column = cell.Column,
Status = nameof(MatrixCellStatus.Error),
Pass = false
});
_logger.Info(
"矩阵执行被取消 | Matrix execution cancelled at R{Row}C{Col} ({Current}/{Total})",
cell.Row, cell.Column, currentIndex, totalEnabled);
// 写入摘要(即使被取消也尝试写入已有结果)
stopwatch.Stop();
await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false);
LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed);
throw; // 传播 OperationCanceledException
}
catch (Exception ex)
{
// 普通异常 — 标记 Error,继续下一个单元格
failedCount++;
var errorMessage = ex.Message;
progress?.Report(new MatrixCellExecutionProgress(
cell.Row,
cell.Column,
MatrixCellStatus.Error,
currentIndex,
totalEnabled,
errorMessage));
cellEntries.Add(new MatrixCellSummaryEntry
{
RunId = cellProgram.Id.ToString(),
Row = cell.Row,
Column = cell.Column,
Status = nameof(MatrixCellStatus.Error),
Pass = false
});
_logger.Error(ex,
"单元格执行失败 | Cell execution failed: R{Row}C{Col} ({Current}/{Total})",
cell.Row, cell.Column, currentIndex, totalEnabled);
}
}
// 全部完成后写入摘要
stopwatch.Stop();
await WriteSummaryAsync(layout, templateProgram.Name, startTime, cellEntries, cancellationToken: default).ConfigureAwait(false);
LogSummary(templateProgram.Name, totalEnabled, completedCount, failedCount, stopwatch.Elapsed);
}
/// <summary>
/// 写入矩阵执行摘要文件。
/// </summary>
private async Task WriteSummaryAsync(
MatrixLayout layout,
string templateProgramName,
DateTime startTime,
IReadOnlyList<MatrixCellSummaryEntry> cellEntries,
CancellationToken cancellationToken)
{
var outputDirectory = System.IO.Path.Combine(_dataPathService.DataPath, "MatrixResults");
await _matrixSummaryWriter.WriteAsync(
layout,
templateProgramName,
startTime,
cellEntries,
outputDirectory,
cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 记录汇总日志。
/// </summary>
private void LogSummary(string programName, int totalCells, int completed, int failed, TimeSpan duration)
{
_logger.Info(
"矩阵执行汇总 | Matrix execution summary: Program={ProgramName}, TotalCells={TotalCells}, Completed={Completed}, Failed={Failed}, Duration={Duration:F1}s",
programName, totalCells, completed, failed, duration.TotalSeconds);
}
/// <summary>
/// 将模板程序按单元格偏移量生成该单元格专用的 CNC 程序副本。
/// 使用 record with 表达式深度克隆,对每个 SavePositionNode 叠加偏移量(μm),
/// 生成新 Id 和新程序名,原模板对象不被修改。
///
/// Generates a cell-specific CNC program copy by applying cell offset to the template.
/// Uses record with expression for deep clone, adds offset (μm) to each SavePositionNode,
/// generates new Id and program name. Original template is NOT modified.
/// </summary>
internal static CncProgram ApplyOffset(CncProgram template, MatrixCell cell)
{
var offsetXum = cell.OffsetX * 1000.0;
var offsetYum = cell.OffsetY * 1000.0;
var newNodes = template.Nodes
.Select(node => node is SavePositionNode sp
? sp with
{
MotionState = sp.MotionState with
{
StageX = sp.MotionState.StageX + offsetXum,
StageY = sp.MotionState.StageY + offsetYum
}
}
: node)
.ToList()
.AsReadOnly();
return template with
{
Id = Guid.NewGuid(),
Name = $"{template.Name}_R{cell.Row}C{cell.Column}",
Nodes = newNodes
};
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
@@ -24,6 +25,7 @@ namespace XplorePlane.Services.Matrix
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters = { new JsonStringEnumConverter() }
};
@@ -40,6 +42,8 @@ namespace XplorePlane.Services.Matrix
public MatrixLayout CreateLayout(int rows, int columns, double rowSpacing, double columnSpacing)
{
ValidateDimensions(rows, columns);
ValidateSpacing(rowSpacing);
ValidateSpacing(columnSpacing);
var layout = new MatrixLayout(
Id: Guid.NewGuid(),
@@ -62,6 +66,8 @@ namespace XplorePlane.Services.Matrix
{
ArgumentNullException.ThrowIfNull(layout);
ValidateDimensions(rows, columns);
ValidateSpacing(rowSpacing);
ValidateSpacing(columnSpacing);
var updated = layout with
{
@@ -249,8 +255,24 @@ namespace XplorePlane.Services.Matrix
return cells.AsReadOnly();
}
/// <summary>
/// 验证间距参数 | Validate spacing parameter
/// Spacing must be in [0.0, 1000.0].
/// </summary>
private static void ValidateSpacing(double spacing)
{
if (spacing < 0)
throw new ArgumentOutOfRangeException(nameof(spacing),
$"间距不能为负数 | Spacing must not be negative: {spacing}");
if (spacing > 1000.0)
throw new ArgumentOutOfRangeException(nameof(spacing),
$"间距不能超过 1000mm | Spacing must not exceed 1000mm: {spacing}");
}
/// <summary>
/// 验证行列数参数 | Validate row and column dimensions
/// Rows and Columns must be in [1, 50].
/// </summary>
private static void ValidateDimensions(int rows, int columns)
{
@@ -258,9 +280,17 @@ namespace XplorePlane.Services.Matrix
throw new ArgumentOutOfRangeException(nameof(rows),
$"行数必须大于 0 | Rows must be greater than 0: {rows}");
if (rows > 50)
throw new ArgumentOutOfRangeException(nameof(rows),
$"行数不能超过 50 | Rows must not exceed 50: {rows}");
if (columns <= 0)
throw new ArgumentOutOfRangeException(nameof(columns),
$"列数必须大于 0 | Columns must be greater than 0: {columns}");
if (columns > 50)
throw new ArgumentOutOfRangeException(nameof(columns),
$"列数不能超过 50 | Columns must not exceed 50: {columns}");
}
/// <summary>
@@ -0,0 +1,93 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
namespace XplorePlane.Services.Matrix
{
/// <summary>
/// 矩阵执行摘要写入器,负责将矩阵执行结果序列化为 matrix_summary.json 文件。
/// Matrix execution summary writer, responsible for serializing matrix execution results to matrix_summary.json.
/// </summary>
public class MatrixSummaryWriter
{
private readonly ILoggerService _logger;
private static readonly JsonSerializerOptions SummaryJsonOptions = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() }
};
public MatrixSummaryWriter(ILoggerService logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger.ForModule<MatrixSummaryWriter>();
}
/// <summary>
/// 将矩阵执行摘要写入 JSON 文件。写入失败时记录错误日志,不重新抛出异常。
/// Write matrix execution summary to JSON file. Logs error on failure without rethrowing.
/// </summary>
/// <param name="layout">矩阵布局 | Matrix layout</param>
/// <param name="templateProgramName">模板程序名称 | Template program name</param>
/// <param name="startTime">执行开始时间 | Execution start time</param>
/// <param name="cellEntries">各单元格摘要条目 | Cell summary entries</param>
/// <param name="outputDirectory">输出目录 | Output directory</param>
/// <param name="ct">取消令牌 | Cancellation token</param>
public async Task WriteAsync(
MatrixLayout layout,
string templateProgramName,
DateTime startTime,
IReadOnlyList<MatrixCellSummaryEntry> cellEntries,
string outputDirectory,
CancellationToken ct = default)
{
try
{
var duration = DateTime.UtcNow - startTime;
var summaryFile = new MatrixSummaryFile
{
Config = new MatrixSummaryConfig
{
Rows = layout.Rows,
Columns = layout.Columns,
RowSpacing = layout.RowSpacing,
ColumnSpacing = layout.ColumnSpacing
},
ProgramName = templateProgramName,
StartTime = startTime.ToString("o"),
DurationSeconds = Math.Round(duration.TotalSeconds, 2),
TotalCells = layout.Rows * layout.Columns,
EnabledCells = layout.Cells.Count(c => c.IsEnabled),
CompletedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Completed)),
FailedCells = cellEntries.Count(e => e.Status == nameof(MatrixCellStatus.Error)),
Cells = cellEntries.ToList()
};
var json = JsonSerializer.Serialize(summaryFile, SummaryJsonOptions);
var filePath = Path.Combine(outputDirectory, "matrix_summary.json");
Directory.CreateDirectory(outputDirectory);
await File.WriteAllTextAsync(filePath, json, ct).ConfigureAwait(false);
_logger.Info("已写入矩阵执行摘要 | Matrix summary written: {FilePath}", filePath);
}
catch (Exception ex)
{
_logger.Error(ex, "写入矩阵执行摘要失败 | Failed to write matrix summary to {OutputDirectory}", outputDirectory);
// 不重新抛出,确保不影响已完成的单元格检测结果归档
}
}
}
}
@@ -1,10 +1,14 @@
using Microsoft.Win32;
using Prism.Commands;
using Prism.Events;
using Prism.Mvvm;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using XP.Common.Logging.Interfaces;
using XplorePlane.Models;
using XplorePlane.Services.Cnc;
@@ -20,12 +24,16 @@ namespace XplorePlane.ViewModels.Cnc
{
private readonly IMatrixService _matrixService;
private readonly ICncProgramService _cncProgramService;
private readonly IMatrixOrchestrationService _orchestrationService;
private readonly IEventAggregator _eventAggregator;
private readonly ILoggerService _logger;
// 当前矩阵布局 | Current matrix layout
private MatrixLayout _currentLayout;
// 执行取消令牌源 | Execution cancellation token source
private CancellationTokenSource _executionCts;
private int _rows = 1;
private int _columns = 1;
private double _rowSpacing;
@@ -34,29 +42,51 @@ namespace XplorePlane.ViewModels.Cnc
private MatrixCellViewModel _selectedCell;
private string _associatedProgramPath;
// ── 验证错误属性 | Validation error properties ──
private string _rowsError;
private string _columnsError;
private string _rowSpacingError;
private string _colSpacingError;
private bool _canUpdateLayout = true;
// ── 显示与状态属性 | Display and state properties ──
private string _associatedProgramName;
private int _enabledCount;
private int _totalCount;
private bool _isExecuting;
private string _statusText;
/// <summary>
/// 构造函数 | Constructor
/// </summary>
public MatrixEditorViewModel(
IMatrixService matrixService,
ICncProgramService cncProgramService,
IMatrixOrchestrationService orchestrationService,
IEventAggregator eventAggregator,
ILoggerService logger)
{
_matrixService = matrixService ?? throw new ArgumentNullException(nameof(matrixService));
_cncProgramService = cncProgramService ?? throw new ArgumentNullException(nameof(cncProgramService));
_orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<MatrixEditorViewModel>();
_cells = new ObservableCollection<MatrixCellViewModel>();
// ── 命令初始化 | Command initialization ──
UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout);
UpdateLayoutCommand = new DelegateCommand(ExecuteUpdateLayout, () => CanUpdateLayout)
.ObservesProperty(() => CanUpdateLayout);
ToggleCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteToggleCell);
SelectCellCommand = new DelegateCommand<MatrixCellViewModel>(ExecuteSelectCell);
AssociateProgramCommand = new DelegateCommand(async () => await ExecuteAssociateProgramAsync());
SaveLayoutCommand = new DelegateCommand(async () => await ExecuteSaveLayoutAsync());
LoadLayoutCommand = new DelegateCommand(async () => await ExecuteLoadLayoutAsync());
RunMatrixCommand = new DelegateCommand(async () => await ExecuteRunMatrixAsync(), CanExecuteRunMatrix)
.ObservesProperty(() => IsExecuting)
.ObservesProperty(() => AssociatedProgramPath);
StopCommand = new DelegateCommand(ExecuteStop, () => IsExecuting)
.ObservesProperty(() => IsExecuting);
_logger.Info("MatrixEditorViewModel 已初始化 | MatrixEditorViewModel initialized");
}
@@ -109,7 +139,89 @@ namespace XplorePlane.ViewModels.Cnc
public string AssociatedProgramPath
{
get => _associatedProgramPath;
set => SetProperty(ref _associatedProgramPath, value);
set
{
if (SetProperty(ref _associatedProgramPath, value))
{
AssociatedProgramName = string.IsNullOrWhiteSpace(value)
? null
: Path.GetFileName(value);
}
}
}
// ── 验证错误属性 | Validation error properties ──────────────────
/// <summary>行数输入错误提示 | Rows input error message</summary>
public string RowsError
{
get => _rowsError;
set => SetProperty(ref _rowsError, value);
}
/// <summary>列数输入错误提示 | Columns input error message</summary>
public string ColumnsError
{
get => _columnsError;
set => SetProperty(ref _columnsError, value);
}
/// <summary>行间距输入错误提示 | Row spacing input error message</summary>
public string RowSpacingError
{
get => _rowSpacingError;
set => SetProperty(ref _rowSpacingError, value);
}
/// <summary>列间距输入错误提示 | Column spacing input error message</summary>
public string ColSpacingError
{
get => _colSpacingError;
set => SetProperty(ref _colSpacingError, value);
}
/// <summary>是否可以更新布局(输入合法时为 true| Whether layout can be updated (true when inputs are valid)</summary>
public bool CanUpdateLayout
{
get => _canUpdateLayout;
set => SetProperty(ref _canUpdateLayout, value);
}
// ── 显示与状态属性 | Display and state properties ────────────────
/// <summary>关联程序的文件名(不含路径)| Associated program filename (without path)</summary>
public string AssociatedProgramName
{
get => _associatedProgramName;
set => SetProperty(ref _associatedProgramName, value);
}
/// <summary>已启用的单元格数量 | Number of enabled cells</summary>
public int EnabledCount
{
get => _enabledCount;
set => SetProperty(ref _enabledCount, value);
}
/// <summary>单元格总数 | Total number of cells</summary>
public int TotalCount
{
get => _totalCount;
set => SetProperty(ref _totalCount, value);
}
/// <summary>是否正在执行矩阵 | Whether matrix execution is in progress</summary>
public bool IsExecuting
{
get => _isExecuting;
set => SetProperty(ref _isExecuting, value);
}
/// <summary>底部状态栏文本 | Bottom status bar text</summary>
public string StatusText
{
get => _statusText;
set => SetProperty(ref _statusText, value);
}
// ── 命令 | Commands ────────────────────────────────────────────
@@ -132,6 +244,12 @@ namespace XplorePlane.ViewModels.Cnc
/// <summary>加载矩阵布局命令 | Load matrix layout command</summary>
public DelegateCommand LoadLayoutCommand { get; }
/// <summary>运行矩阵执行命令 | Run matrix execution command</summary>
public DelegateCommand RunMatrixCommand { get; }
/// <summary>停止执行命令 | Stop execution command</summary>
public DelegateCommand StopCommand { get; }
// ── 命令执行方法 | Command execution methods ────────────────────
/// <summary>
@@ -140,6 +258,13 @@ namespace XplorePlane.ViewModels.Cnc
/// </summary>
private void ExecuteUpdateLayout()
{
// 清除之前的错误 | Clear previous errors
RowsError = null;
ColumnsError = null;
RowSpacingError = null;
ColSpacingError = null;
CanUpdateLayout = true;
try
{
if (_currentLayout == null)
@@ -154,12 +279,56 @@ namespace XplorePlane.ViewModels.Cnc
RefreshCells();
_logger.Info("已更新矩阵布局 | Updated matrix layout: Rows={Rows}, Columns={Columns}", Rows, Columns);
}
catch (ArgumentOutOfRangeException ex)
{
HandleValidationError(ex);
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
}
catch (Exception ex)
{
_logger.Error(ex, "更新矩阵布局失败 | Failed to update matrix layout");
}
}
/// <summary>
/// 处理参数验证异常,设置对应的错误属性
/// Handle parameter validation exception and set corresponding error properties
/// </summary>
private void HandleValidationError(ArgumentOutOfRangeException ex)
{
CanUpdateLayout = false;
var paramName = ex.ParamName?.ToLowerInvariant() ?? string.Empty;
if (paramName.Contains("row") && paramName.Contains("spacing"))
{
RowSpacingError = "间距不能为负数";
}
else if (paramName.Contains("col") && paramName.Contains("spacing"))
{
ColSpacingError = "间距不能为负数";
}
else if (paramName.Contains("spacing"))
{
// Generic spacing error — set both
RowSpacingError = "间距不能为负数";
ColSpacingError = "间距不能为负数";
}
else if (paramName.Contains("row"))
{
RowsError = "行数/列数必须在 1 到 50 之间";
}
else if (paramName.Contains("col"))
{
ColumnsError = "行数/列数必须在 1 到 50 之间";
}
else
{
// Fallback: try to determine from message or set both
RowsError = "行数/列数必须在 1 到 50 之间";
ColumnsError = "行数/列数必须在 1 到 50 之间";
}
}
/// <summary>
/// 选中指定单元格,更新高亮状态
/// Select the specified cell and update highlight state
@@ -199,8 +368,8 @@ namespace XplorePlane.ViewModels.Cnc
}
/// <summary>
/// 关联 CNC 程序到当前矩阵布局(占位:从 AssociatedProgramPath 加载)
/// Associate a CNC program with the current layout (placeholder: loads from AssociatedProgramPath)
/// 关联 CNC 程序:使用 OpenFileDialog 选择 .xp 文件,验证文件有效性
/// Associate CNC program: use OpenFileDialog to select .xp file, validate file
/// </summary>
private async Task ExecuteAssociateProgramAsync()
{
@@ -210,26 +379,50 @@ namespace XplorePlane.ViewModels.Cnc
return;
}
if (string.IsNullOrWhiteSpace(AssociatedProgramPath))
var dialog = new OpenFileDialog
{
_logger.Warn("无法关联程序:程序路径为空 | Cannot associate program: program path is empty");
Title = "选择 CNC 模板程序",
Filter = "CNC 程序文件 (*.xp)|*.xp",
CheckFileExists = true
};
if (dialog.ShowDialog() != true)
return;
}
var filePath = dialog.FileName;
var fileName = Path.GetFileName(filePath);
try
{
var program = await _cncProgramService.LoadAsync(AssociatedProgramPath);
if (!File.Exists(filePath))
{
MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
var program = await _cncProgramService.LoadAsync(filePath);
// 验证程序包含至少一个 SavePositionNode
if (!program.Nodes.OfType<SavePositionNode>().Any())
{
MessageBox.Show("所选程序不包含位置节点,无法用作矩阵模板", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
_currentLayout = _matrixService.AssociateCncProgram(_currentLayout, program);
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", AssociatedProgramPath);
AssociatedProgramPath = filePath;
_logger.Info("已关联 CNC 程序 | Associated CNC program: {ProgramPath}", filePath);
}
catch (Exception ex)
{
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program");
MessageBox.Show($"无法加载程序文件:{fileName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
_logger.Error(ex, "关联 CNC 程序失败 | Failed to associate CNC program: {FileName}", fileName);
}
}
/// <summary>
/// 保存当前矩阵布局到文件 | Save current matrix layout to file
/// 保存当前矩阵布局:使用 SaveFileDialog 选择保存路径
/// Save current matrix layout: use SaveFileDialog to select save path
/// </summary>
private async Task ExecuteSaveLayoutAsync()
{
@@ -239,12 +432,21 @@ namespace XplorePlane.ViewModels.Cnc
return;
}
var dialog = new SaveFileDialog
{
Title = "保存矩阵方案",
Filter = "JSON 文件 (*.json)|*.json",
DefaultExt = ".json",
FileName = $"matrix_{_currentLayout.Id}"
};
if (dialog.ShowDialog() != true)
return;
try
{
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
var filePath = $"matrix_{_currentLayout.Id}.json";
await _matrixService.SaveAsync(_currentLayout, filePath);
_logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", filePath);
await _matrixService.SaveAsync(_currentLayout, dialog.FileName);
_logger.Info("矩阵布局已保存 | Matrix layout saved: {FilePath}", dialog.FileName);
}
catch (Exception ex)
{
@@ -253,15 +455,24 @@ namespace XplorePlane.ViewModels.Cnc
}
/// <summary>
/// 从文件加载矩阵布局 | Load matrix layout from file
/// 从文件加载矩阵布局:使用 OpenFileDialog 选择文件
/// Load matrix layout from file: use OpenFileDialog to select file
/// </summary>
private async Task ExecuteLoadLayoutAsync()
{
var dialog = new OpenFileDialog
{
Title = "加载矩阵方案",
Filter = "JSON 文件 (*.json)|*.json",
CheckFileExists = true
};
if (dialog.ShowDialog() != true)
return;
try
{
// 占位:实际应通过文件对话框选择路径 | Placeholder: should use file dialog to select path
var filePath = $"matrix_layout.json";
_currentLayout = await _matrixService.LoadAsync(filePath);
_currentLayout = await _matrixService.LoadAsync(dialog.FileName);
// 同步属性到 ViewModel | Sync properties to ViewModel
Rows = _currentLayout.Rows;
@@ -271,14 +482,132 @@ namespace XplorePlane.ViewModels.Cnc
AssociatedProgramPath = _currentLayout.CncProgramPath;
RefreshCells();
// 检查关联的 CNC 程序文件是否存在 | Check if associated CNC program file exists
if (!string.IsNullOrWhiteSpace(_currentLayout.CncProgramPath)
&& !File.Exists(_currentLayout.CncProgramPath))
{
MessageBox.Show("关联的 CNC 程序文件已移动或删除,请重新关联", "警告", MessageBoxButton.OK, MessageBoxImage.Warning);
}
_logger.Info("矩阵布局已加载 | Matrix layout loaded: Rows={Rows}, Columns={Columns}", Rows, Columns);
}
catch (InvalidDataException ex)
{
MessageBox.Show($"方案文件格式无效:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
_logger.Error(ex, "加载矩阵布局失败:格式无效 | Failed to load matrix layout: invalid format");
}
catch (Exception ex)
{
_logger.Error(ex, "加载矩阵布局失败 | Failed to load matrix layout");
}
}
/// <summary>
/// 判断 RunMatrixCommand 是否可执行
/// Determine if RunMatrixCommand can execute
/// </summary>
private bool CanExecuteRunMatrix()
{
return !IsExecuting && !string.IsNullOrWhiteSpace(AssociatedProgramPath);
}
/// <summary>
/// 执行矩阵运行:加载模板程序 → 重置状态 → 执行 → 更新 UI
/// Execute matrix run: load template → reset states → execute → update UI
/// </summary>
private async Task ExecuteRunMatrixAsync()
{
if (_currentLayout == null || string.IsNullOrWhiteSpace(AssociatedProgramPath))
return;
CncProgram templateProgram;
try
{
templateProgram = await _cncProgramService.LoadAsync(AssociatedProgramPath);
}
catch (Exception ex)
{
MessageBox.Show($"无法加载程序文件:{AssociatedProgramName}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
_logger.Error(ex, "运行矩阵失败:无法加载模板程序 | Run matrix failed: cannot load template program");
return;
}
IsExecuting = true;
// 重置所有单元格状态为 NotExecuted | Reset all cell statuses to NotExecuted
foreach (var cell in Cells)
{
cell.Status = MatrixCellStatus.NotExecuted;
cell.ErrorMessage = null;
}
StatusText = null;
_executionCts = new CancellationTokenSource();
var progress = new Progress<MatrixCellExecutionProgress>(p =>
{
Application.Current.Dispatcher.Invoke(() =>
{
// 更新对应单元格的状态 | Update the corresponding cell's status
var cellVm = Cells.FirstOrDefault(c => c.Row == p.Row && c.Column == p.Column);
if (cellVm != null)
{
cellVm.Status = p.Status;
if (!string.IsNullOrEmpty(p.ErrorMessage))
cellVm.ErrorMessage = p.ErrorMessage;
}
// 更新状态文本 | Update status text
if (p.Status == MatrixCellStatus.Executing)
{
StatusText = $"正在执行:{AssociatedProgramName} | 当前位置:R{p.Row}C{p.Column}{p.CurrentIndex}/{p.TotalEnabled}";
}
});
});
try
{
await _orchestrationService.ExecuteAsync(_currentLayout, templateProgram, progress, _executionCts.Token);
// 执行完成 | Execution completed
var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed);
var failedCount = Cells.Count(c => c.Status == MatrixCellStatus.Error);
var totalEnabled = Cells.Count(c => c.IsEnabled);
StatusText = $"执行完成:共 {totalEnabled} 个位置,{completedCount} 成功,{failedCount} 失败";
}
catch (OperationCanceledException)
{
// 用户停止或外部取消 | User stopped or external cancellation
var completedCount = Cells.Count(c => c.Status == MatrixCellStatus.Completed || c.Status == MatrixCellStatus.Error);
var notExecutedCount = Cells.Count(c => c.IsEnabled && c.Status == MatrixCellStatus.NotExecuted);
StatusText = $"执行已停止:已完成 {completedCount} 个位置,{notExecutedCount} 个位置未执行";
}
catch (InvalidOperationException ex)
{
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
_logger.Error(ex, "运行矩阵失败 | Run matrix failed");
}
catch (Exception ex)
{
_logger.Error(ex, "矩阵执行异常 | Matrix execution exception");
}
finally
{
IsExecuting = false;
_executionCts?.Dispose();
_executionCts = null;
}
}
/// <summary>
/// 停止矩阵执行 | Stop matrix execution
/// </summary>
private void ExecuteStop()
{
_executionCts?.Cancel();
}
// ── 辅助方法 | Helper methods ───────────────────────────────────
/// <summary>
@@ -290,13 +619,21 @@ namespace XplorePlane.ViewModels.Cnc
Cells.Clear();
if (_currentLayout?.Cells == null)
{
EnabledCount = 0;
TotalCount = 0;
return;
}
foreach (var cell in _currentLayout.Cells)
{
Cells.Add(new MatrixCellViewModel(cell));
}
// 更新统计 | Update statistics
TotalCount = Cells.Count;
EnabledCount = Cells.Count(c => c.IsEnabled);
// 尝试保持选中状态 | Try to preserve selection
if (SelectedCell != null)
{
+189 -9
View File
@@ -6,6 +6,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:models="clr-namespace:XplorePlane.Models"
xmlns:converters="clr-namespace:XplorePlane.Converters"
d:DesignHeight="700"
d:DesignWidth="900"
prism:ViewModelLocator.AutoWireViewModel="True"
@@ -19,6 +21,10 @@
<SolidColorBrush x:Key="AccentBlue" Color="#E3F0FF" />
<FontFamily x:Key="CsdFont">Microsoft YaHei UI</FontFamily>
<!-- 转换器 | Converters -->
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<converters:InverseBoolConverter x:Key="InverseBoolConverter" />
<!-- 配置面板标签样式 | Configuration panel label style -->
<Style x:Key="ConfigLabel" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
@@ -38,6 +44,15 @@
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- 错误提示文本样式 | Error message text style -->
<Style x:Key="ErrorText" TargetType="TextBlock">
<Setter Property="FontFamily" Value="Microsoft YaHei UI" />
<Setter Property="FontSize" Value="10" />
<Setter Property="Foreground" Value="#E53935" />
<Setter Property="Margin" Value="0,0,0,4" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<!-- 工具栏按钮样式(与 CncPageView 一致)| Toolbar button style (consistent with CncPageView) -->
<Style x:Key="ToolbarBtn" TargetType="Button">
<Setter Property="Height" Value="28" />
@@ -58,6 +73,14 @@
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.RowDefinitions>
<!-- 主内容区域 | Main content area -->
<RowDefinition Height="*" />
<!-- 底部状态栏 | Bottom status bar -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<!-- 左侧:配置面板 | Left: configuration panel -->
<ColumnDefinition Width="220" />
@@ -85,34 +108,50 @@
<!-- 行数 | Rows -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="行数 | Rows" />
<TextBox
Margin="0,2,0,8"
Margin="0,2,0,2"
Style="{StaticResource ConfigInput}"
Text="{Binding Rows, UpdateSourceTrigger=PropertyChanged}"
ToolTip="矩阵行数 | Number of rows" />
<!-- 行数错误提示 | Rows error message -->
<TextBlock
Style="{StaticResource ErrorText}"
Text="{Binding RowsError}" />
<!-- 列数 | Columns -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="列数 | Columns" />
<TextBox
Margin="0,2,0,8"
Margin="0,2,0,2"
Style="{StaticResource ConfigInput}"
Text="{Binding Columns, UpdateSourceTrigger=PropertyChanged}"
ToolTip="矩阵列数 | Number of columns" />
<!-- 列数错误提示 | Columns error message -->
<TextBlock
Style="{StaticResource ErrorText}"
Text="{Binding ColumnsError}" />
<!-- 行间距 | Row Spacing -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="行间距 (mm) | Row Spacing" />
<TextBox
Margin="0,2,0,8"
Margin="0,2,0,2"
Style="{StaticResource ConfigInput}"
Text="{Binding RowSpacing, UpdateSourceTrigger=PropertyChanged}"
ToolTip="行间距(毫米)| Row spacing in mm" />
<!-- 行间距错误提示 | Row spacing error message -->
<TextBlock
Style="{StaticResource ErrorText}"
Text="{Binding RowSpacingError}" />
<!-- 列间距 | Column Spacing -->
<TextBlock Style="{StaticResource ConfigLabel}" Text="列间距 (mm) | Col Spacing" />
<TextBox
Margin="0,2,0,8"
Margin="0,2,0,2"
Style="{StaticResource ConfigInput}"
Text="{Binding ColumnSpacing, UpdateSourceTrigger=PropertyChanged}"
ToolTip="列间距(毫米)| Column spacing in mm" />
<!-- 列间距错误提示 | Column spacing error message -->
<TextBlock
Style="{StaticResource ErrorText}"
Text="{Binding ColSpacingError}" />
<!-- 分隔线 | Separator -->
<Rectangle
@@ -124,11 +163,13 @@
<Button
Command="{Binding UpdateLayoutCommand}"
Content="更新布局 | Update"
IsEnabled="{Binding IsExecuting, Converter={StaticResource InverseBoolConverter}}"
Style="{StaticResource ToolbarBtn}"
ToolTip="根据当前参数更新矩阵网格 | Update matrix grid with current parameters" />
<Button
Command="{Binding AssociateProgramCommand}"
Content="关联程序 | Associate"
IsEnabled="{Binding IsExecuting, Converter={StaticResource InverseBoolConverter}}"
Style="{StaticResource ToolbarBtn}"
ToolTip="关联 CNC 程序到矩阵 | Associate CNC program to matrix" />
<Button
@@ -142,12 +183,69 @@
Style="{StaticResource ToolbarBtn}"
ToolTip="加载矩阵方案 | Load matrix layout" />
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,4,0,8"
Fill="{StaticResource SeparatorBrush}" />
<!-- 运行/停止按钮 | Run/Stop buttons -->
<Button
Command="{Binding RunMatrixCommand}"
Content="▶ 运行矩阵 | Run"
Style="{StaticResource ToolbarBtn}"
ToolTip="开始执行矩阵检测 | Start matrix execution" />
<Button
Command="{Binding StopCommand}"
Content="■ 停止执行 | Stop"
Style="{StaticResource ToolbarBtn}"
ToolTip="停止当前矩阵执行 | Stop current matrix execution"
Visibility="{Binding IsExecuting, Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,8,0,8"
Fill="{StaticResource SeparatorBrush}" />
<!-- 关联程序显示区域 | Associated program display area -->
<TextBlock
Margin="0,0,0,4"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
FontWeight="Bold"
Foreground="#555"
Text="关联程序 | Program" />
<TextBlock
FontFamily="{StaticResource CsdFont}"
FontSize="11"
Margin="0,0,0,8">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="{Binding AssociatedProgramName}" />
<Setter Property="Foreground" Value="#333" />
<Style.Triggers>
<DataTrigger Binding="{Binding AssociatedProgramName}" Value="{x:Null}">
<Setter Property="Text" Value="请先关联 CNC 程序" />
<Setter Property="Foreground" Value="#999" />
<Setter Property="FontStyle" Value="Italic" />
</DataTrigger>
<DataTrigger Binding="{Binding AssociatedProgramName}" Value="">
<Setter Property="Text" Value="请先关联 CNC 程序" />
<Setter Property="Foreground" Value="#999" />
<Setter Property="FontStyle" Value="Italic" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- 分隔线 | Separator -->
<Rectangle
Height="1"
Margin="0,0,0,8"
Fill="{StaticResource SeparatorBrush}" />
<!-- 选中单元格详情 | Selected cell details -->
<TextBlock
Margin="0,0,0,6"
@@ -186,8 +284,39 @@
Fill="{StaticResource SeparatorBrush}" />
<!-- ═══ 右侧:矩阵网格视图 | Right: matrix grid view ═══ -->
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<!-- 网格标题与统计 | Grid title and statistics -->
<RowDefinition Height="Auto" />
<!-- 矩阵网格 | Matrix grid -->
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 矩阵网格标题和统计标签 | Matrix grid title and statistics label -->
<StackPanel
Grid.Row="0"
Margin="8,6,8,2"
Orientation="Horizontal">
<TextBlock
FontFamily="{StaticResource CsdFont}"
FontSize="12"
FontWeight="Bold"
Foreground="#333"
VerticalAlignment="Center"
Text="矩阵网格" />
<TextBlock
Margin="12,0,0,0"
FontFamily="{StaticResource CsdFont}"
FontSize="11"
Foreground="#666"
VerticalAlignment="Center">
<Run Text="已启用:" /><Run Text="{Binding EnabledCount, Mode=OneWay}" /><Run Text=" / 共 " /><Run Text="{Binding TotalCount, Mode=OneWay}" /><Run Text=" 个位置" />
</TextBlock>
</StackPanel>
<!-- 矩阵网格内容 | Matrix grid content -->
<ScrollViewer
Grid.Column="2"
Grid.Row="1"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsControl
@@ -208,7 +337,7 @@
MinHeight="64"
Margin="2"
Padding="4"
Background="#F0F8FF"
Background="White"
BorderBrush="#B0C4DE"
BorderThickness="1"
CornerRadius="3"
@@ -231,6 +360,7 @@
VerticalAlignment="Center">
<!-- 行列索引 | Row/Column index -->
<TextBlock
x:Name="CellIndexText"
HorizontalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="11"
@@ -242,6 +372,7 @@
</TextBlock>
<!-- 偏移坐标 | Offset coordinates -->
<TextBlock
x:Name="CellOffsetText"
HorizontalAlignment="Center"
FontFamily="Microsoft YaHei UI"
FontSize="9"
@@ -271,15 +402,47 @@
</Grid>
</Border>
<DataTemplate.Triggers>
<!-- 禁用状态:灰色背景 | Disabled state: gray background -->
<!-- ═══ 执行状态颜色样式 | Execution status color styles ═══ -->
<!-- 未执行状态:默认白色背景 | NotExecuted: default white background -->
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.NotExecuted}">
<Setter TargetName="CellBorder" Property="Background" Value="White" />
</DataTrigger>
<!-- 执行中状态:蓝色高亮 | Executing: blue highlight -->
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Executing}">
<Setter TargetName="CellBorder" Property="Background" Value="#2196F3" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#1565C0" />
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#E3F2FD" />
</DataTrigger>
<!-- 已完成状态:绿色背景 | Completed: green background -->
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Completed}">
<Setter TargetName="CellBorder" Property="Background" Value="#4CAF50" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#2E7D32" />
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#E8F5E9" />
</DataTrigger>
<!-- 错误状态:红色背景 + ToolTip 显示错误信息 | Error: red background + ToolTip with error message -->
<DataTrigger Binding="{Binding Status}" Value="{x:Static models:MatrixCellStatus.Error}">
<Setter TargetName="CellBorder" Property="Background" Value="#F44336" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#C62828" />
<Setter TargetName="CellBorder" Property="ToolTip" Value="{Binding ErrorMessage}" />
<Setter TargetName="CellIndexText" Property="Foreground" Value="White" />
<Setter TargetName="CellOffsetText" Property="Foreground" Value="#FFEBEE" />
</DataTrigger>
<!-- 已禁用状态:灰色半透明 | Disabled: gray semi-transparent -->
<DataTrigger Binding="{Binding IsEnabled}" Value="False">
<Setter TargetName="CellBorder" Property="Background" Value="#E8E8E8" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#CCCCCC" />
<Setter TargetName="CellBorder" Property="Opacity" Value="0.6" />
</DataTrigger>
<!-- 选中状态:蓝色高亮 | Selected state: blue highlight -->
<!-- 选中状态:蓝色高亮边框 | Selected state: blue highlight border -->
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter TargetName="CellBorder" Property="Background" Value="#E3F0FF" />
<Setter TargetName="CellBorder" Property="BorderBrush" Value="#5B9BD5" />
<Setter TargetName="CellBorder" Property="BorderThickness" Value="2" />
</DataTrigger>
@@ -289,5 +452,22 @@
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid>
<!-- ═══ 底部状态栏 | Bottom status bar ═══ -->
<Border
Grid.Row="1"
BorderBrush="{StaticResource SeparatorBrush}"
BorderThickness="0,1,0,0"
Padding="8,4">
<TextBlock
FontFamily="{StaticResource CsdFont}"
FontSize="11"
Foreground="#555"
Text="{Binding StatusText}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" />
</Border>
</Grid>
</Border>
</UserControl>
@@ -2,9 +2,9 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="系统设置"
Width="1000"
Width="800"
Height="700"
MinWidth="900"
MinWidth="800"
MinHeight="600"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"