示波器加入源适应性界面
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using DeviceCommand.Devices;
|
||||
using Microsoft.Win32;
|
||||
using Newtonsoft.Json;
|
||||
using Prism.Commands;
|
||||
using Prism.Ioc;
|
||||
@@ -11,6 +12,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
@@ -26,6 +28,7 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
private readonly DeviceManager _dm;
|
||||
private readonly GlobalInfo _globalInfo;
|
||||
private Chroma61800? _dev;
|
||||
private TektronixMSO? _scope;
|
||||
private CancellationTokenSource? _cmdCts;
|
||||
#endregion
|
||||
|
||||
@@ -40,6 +43,92 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
|
||||
#endregion
|
||||
|
||||
#region 示波器(TektronixMSO)屏幕抓图
|
||||
private string _scopeName = "TektronixMSO";
|
||||
/// <summary>示波器设备名称(DeviceMap 键名)</summary>
|
||||
public string ScopeName { get => _scopeName; set => SetProperty(ref _scopeName, value); }
|
||||
|
||||
private bool _isScopeConnected;
|
||||
/// <summary>示波器连接状态</summary>
|
||||
public bool IsScopeConnected { get => _isScopeConnected; set => SetProperty(ref _isScopeConnected, value); }
|
||||
|
||||
private string _scopeImageDir = @"D:\acp\源测试\波形截图";
|
||||
/// <summary>波形图片存储位置(抓取的屏幕 PNG 保存目录),抓图/浏览选择时自动持久化</summary>
|
||||
public string ScopeImageDir { get => _scopeImageDir; set => SetProperty(ref _scopeImageDir, value); }
|
||||
|
||||
private BitmapImage? _scopeImage;
|
||||
/// <summary>最近一次抓取的示波器屏幕图片(供界面 Image 控件显示)</summary>
|
||||
public BitmapImage? ScopeImage { get => _scopeImage; set => SetProperty(ref _scopeImage, value); }
|
||||
|
||||
private bool _hasScopeImage;
|
||||
/// <summary>是否已有截图(控制图片区占位提示的显隐)</summary>
|
||||
public bool HasScopeImage { get => _hasScopeImage; set => SetProperty(ref _hasScopeImage, value); }
|
||||
|
||||
private string _scopeImageHint = "";
|
||||
/// <summary>最近抓取的图片文件提示</summary>
|
||||
public string ScopeImageHint { get => _scopeImageHint; set => SetProperty(ref _scopeImageHint, value); }
|
||||
#endregion
|
||||
|
||||
#region 示波器设置(采集 / 水平 / 通道 / 测量,对应 TektronixMSO 已有 SCPI 方法)
|
||||
/// <summary>采集模式选项 (ACQuire:MODe,显示名与 TekAcquisitionMode 枚举一致)</summary>
|
||||
public IReadOnlyList<string> ScopeAcquireModeOptions { get; } =
|
||||
new[] { "SAMple", "PEAKdetect", "HIRes", "AVErage", "ENVelope" };
|
||||
|
||||
/// <summary>测量类型选项 (MEASUrement:TYPe,显示名与 TekMeasurementType 枚举一致)</summary>
|
||||
public IReadOnlyList<string> ScopeMeasTypeOptions { get; } =
|
||||
new[] { "AMPLitude", "FREQuency", "MEAN", "PK2PK", "MAXimum", "MINimum" };
|
||||
|
||||
/// <summary>通道选择列表 (对应通道号 1~4)</summary>
|
||||
public IReadOnlyList<string> ScopeChannelOptions { get; } = new[] { "CH1", "CH2", "CH3", "CH4" };
|
||||
|
||||
private string _scopeAcquireMode = "SAMple";
|
||||
/// <summary>采集模式 (ACQuire:MODe)</summary>
|
||||
public string ScopeAcquireMode { get => _scopeAcquireMode; set => SetProperty(ref _scopeAcquireMode, value); }
|
||||
|
||||
private double _scopeTimeScale = 0.001;
|
||||
/// <summary>水平刻度待设值 (HORizontal:SCAle, s/Div)</summary>
|
||||
public double ScopeTimeScale { get => _scopeTimeScale; set => SetProperty(ref _scopeTimeScale, value); }
|
||||
|
||||
private long _scopeRecordLength = 6250000;
|
||||
/// <summary>记录长度待设值 (HORizontal:RECOrdlength, 采样点数)</summary>
|
||||
public long ScopeRecordLength { get => _scopeRecordLength; set => SetProperty(ref _scopeRecordLength, value); }
|
||||
|
||||
private string _sampleRateText = "";
|
||||
/// <summary>采样率查询结果显示 (HORizontal:SAMPLERate?, Sa/s)</summary>
|
||||
public string SampleRateText { get => _sampleRateText; set => SetProperty(ref _sampleRateText, value); }
|
||||
|
||||
private int _selectedScopeChannelIndex;
|
||||
/// <summary>当前编辑通道索引 (0~3 → CH1~CH4)</summary>
|
||||
public int SelectedScopeChannelIndex { get => _selectedScopeChannelIndex; set => SetProperty(ref _selectedScopeChannelIndex, value); }
|
||||
|
||||
private double _channelScale = 100;
|
||||
/// <summary>通道垂直刻度待设值 (CH:SCAle, V/Div)</summary>
|
||||
public double ChannelScale { get => _channelScale; set => SetProperty(ref _channelScale, value); }
|
||||
|
||||
private double _channelPosition;
|
||||
/// <summary>通道垂直位置待设值 (CH:POSition, Div)</summary>
|
||||
public double ChannelPosition { get => _channelPosition; set => SetProperty(ref _channelPosition, value); }
|
||||
|
||||
private int _measurementNumber = 1;
|
||||
/// <summary>测量项编号 (MEASUrement:MEASn)</summary>
|
||||
public int MeasurementNumber { get => _measurementNumber; set => SetProperty(ref _measurementNumber, value); }
|
||||
|
||||
private string _measurementType = "FREQuency";
|
||||
/// <summary>测量类型 (MEASUrement:TYPe)</summary>
|
||||
public string MeasurementType { get => _measurementType; set => SetProperty(ref _measurementType, value); }
|
||||
|
||||
private string _measurementSource = "CH1";
|
||||
/// <summary>测量源 (MEASUrement:SOUrce1, 如 CH1/CH2)</summary>
|
||||
public string MeasurementSource { get => _measurementSource; set => SetProperty(ref _measurementSource, value); }
|
||||
|
||||
private string _measResultText = "";
|
||||
/// <summary>测量查询结果显示</summary>
|
||||
public string MeasResultText { get => _measResultText; set => SetProperty(ref _measResultText, value); }
|
||||
|
||||
/// <summary>当前编辑通道号 (1~4)</summary>
|
||||
private int ScopeChannelNo => SelectedScopeChannelIndex + 1;
|
||||
#endregion
|
||||
|
||||
#region 输入参数(三相输出设置)
|
||||
private double _voltage = 220;
|
||||
/// <summary>待设置的交流电压 (V)</summary>
|
||||
@@ -212,6 +301,9 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
/// <summary>当前波形说明(随文件保存,载入后自动填充)</summary>
|
||||
public string SynthPresetNote { get => _synthPresetNote; set => SetProperty(ref _synthPresetNote, value); }
|
||||
|
||||
/// <summary>当前已打开的波形文件完整路径(载入或另存后设置);null 表示尚未与磁盘文件绑定</summary>
|
||||
private string? _currentSynthPath;
|
||||
|
||||
private int _selectedSynthPresetIndex = -1;
|
||||
/// <summary>当前选中的合成波形预设索引(-1 表示未选择)</summary>
|
||||
public int SelectedSynthPresetIndex
|
||||
@@ -260,6 +352,9 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
/// <summary>当前测试项说明(随文件保存,载入后自动填充)</summary>
|
||||
public string PresetNote { get => _presetNote; set => SetProperty(ref _presetNote, value); }
|
||||
|
||||
/// <summary>当前已打开的测试项文件完整路径(载入或另存后设置);null 表示尚未与磁盘文件绑定</summary>
|
||||
private string? _currentPresetPath;
|
||||
|
||||
private int _selectedPresetIndex = -1;
|
||||
/// <summary>当前选中的测试项索引(-1 表示未选择)</summary>
|
||||
public int SelectedPresetIndex
|
||||
@@ -305,6 +400,7 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
public ICommand EnterListModeCommand { get; }
|
||||
public ICommand LoadPresetCommand { get; }
|
||||
public ICommand SavePresetCommand { get; }
|
||||
public ICommand SavePresetAsCommand { get; }
|
||||
public ICommand DeletePresetCommand { get; }
|
||||
public ICommand CopyPhaseToThreeCommand { get; }
|
||||
public ICommand DownloadThreePhaseListCommand { get; }
|
||||
@@ -314,7 +410,28 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
public ICommand ResetSynthHarmonicsCommand { get; }
|
||||
public ICommand LoadSynthPresetCommand { get; }
|
||||
public ICommand SaveSynthPresetCommand { get; }
|
||||
public ICommand SaveSynthPresetAsCommand { get; }
|
||||
public ICommand DeleteSynthPresetCommand { get; }
|
||||
public ICommand CaptureScopeImageCommand { get; }
|
||||
public ICommand BrowseScopeImageDirCommand { get; }
|
||||
public ICommand ScopeAcquireRunCommand { get; }
|
||||
public ICommand ScopeAcquireStopCommand { get; }
|
||||
public ICommand QueryAcquireStateCommand { get; }
|
||||
public ICommand ApplyAcquireModeCommand { get; }
|
||||
public ICommand SetScopeTimeScaleCommand { get; }
|
||||
public ICommand SetRecordLengthCommand { get; }
|
||||
public ICommand QuerySampleRateCommand { get; }
|
||||
public ICommand ChannelDisplayOnCommand { get; }
|
||||
public ICommand ChannelDisplayOffCommand { get; }
|
||||
public ICommand SetChannelScaleCommand { get; }
|
||||
public ICommand SetChannelPositionCommand { get; }
|
||||
public ICommand AddMeasurementCommand { get; }
|
||||
public ICommand QueryMeasCurrentCommand { get; }
|
||||
public ICommand QueryMeasMeanCommand { get; }
|
||||
public ICommand QueryMeasMaxCommand { get; }
|
||||
public ICommand QueryScopeIdnCommand { get; }
|
||||
public ICommand ClearScopeErrorsCommand { get; }
|
||||
public ICommand ReadScopeErrorsCommand { get; }
|
||||
#endregion
|
||||
|
||||
public PowerAdaptabilityViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
@@ -330,6 +447,9 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
EnsureSynthStorage();
|
||||
RefreshSynthPresets();
|
||||
|
||||
// 恢复上次设置的波形图片存储位置
|
||||
LoadScopeImageDir();
|
||||
|
||||
#region 命令初始化
|
||||
QueryIdnCommand = new DelegateCommand(async () =>
|
||||
await Exec(async () => Log("IDN: " + await _dev!.查询设备信息Async(Ct()))));
|
||||
@@ -411,14 +531,18 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
|
||||
ListEditPhaseIndex = 0; // 切到 A 相显示(若原本就是 0,setter 不触发刷新,故下方手动刷新)
|
||||
RaiseListSequenceChanged();
|
||||
PresetName = p.Name; // 填充名称/说明,便于修改后覆盖保存
|
||||
PresetName = p.Name; // 名称框回显当前文件名
|
||||
PresetNote = p.Note;
|
||||
_currentPresetPath = PresetPathOf(p.Name); // 记住当前打开的文件,此后“保存”只覆盖它
|
||||
Log($"已载入测试项:{p.Name}(次数={(p.Count == 0 ? "无限" : p.Count.ToString())} 触发={p.Trigger} 时基={p.TimeBase})");
|
||||
});
|
||||
|
||||
// 保存当前界面的 LIST 配置为测试项文件(写入 D:\acp\源测试\{名称}.json)
|
||||
// 保存:覆盖当前打开的测试项文件(未打开文件时按“另存为”新建)
|
||||
SavePresetCommand = new DelegateCommand(SaveCurrentPreset);
|
||||
|
||||
// 另存为:以“名称”框内容为新文件名新建文件,不修改当前打开的文件
|
||||
SavePresetAsCommand = new DelegateCommand(SaveCurrentPresetAs);
|
||||
|
||||
// 删除当前选中的测试项文件(带二次确认)
|
||||
DeletePresetCommand = new DelegateCommand(DeleteCurrentPreset);
|
||||
|
||||
@@ -517,10 +641,145 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
Log("SYNTHESIS 谐波已重置:振幅/相位两框均填入 50 个 0;点「下发并启动 SYNTH」写入仪器");
|
||||
});
|
||||
|
||||
// 合成波形的载入 / 保存 / 删除(与 LIST 测试项同机制,文件存于 D:\acp\源测试\合成波形)
|
||||
// 合成波形的载入 / 保存 / 另存为 / 删除(与 LIST 测试项同机制,文件存于 D:\acp\源测试\合成波形)
|
||||
LoadSynthPresetCommand = new DelegateCommand(LoadCurrentSynthPreset);
|
||||
SaveSynthPresetCommand = new DelegateCommand(SaveCurrentSynthPreset);
|
||||
SaveSynthPresetAsCommand = new DelegateCommand(SaveCurrentSynthPresetAs);
|
||||
DeleteSynthPresetCommand = new DelegateCommand(DeleteCurrentSynthPreset);
|
||||
|
||||
// 示波器抓图:抓取当前屏幕保存为 PNG 到存储位置,成功后自动加载显示
|
||||
CaptureScopeImageCommand = new DelegateCommand(async () => await ExecScope(async () =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ScopeImageDir))
|
||||
{
|
||||
Log("请先设置波形图片存储位置");
|
||||
return;
|
||||
}
|
||||
|
||||
SaveScopeImageDir(); // 记住本次使用的存储位置
|
||||
Directory.CreateDirectory(ScopeImageDir);
|
||||
string file = Path.Combine(ScopeImageDir, $"屏幕_{DateTime.Now:yyyyMMdd_HHmmss_fff}.png");
|
||||
|
||||
await _scope!.保存屏幕截图(file, Ct()); // SAVe:IMAGe → *OPC? → READFile → 写盘
|
||||
|
||||
LoadScopeImage(file);
|
||||
Log($"示波器屏幕图片已抓取:{file}");
|
||||
}));
|
||||
|
||||
// 浏览选择波形图片存储位置(选择后立即记住)
|
||||
BrowseScopeImageDirCommand = new DelegateCommand(() =>
|
||||
{
|
||||
var dlg = new OpenFolderDialog
|
||||
{
|
||||
Title = "选择波形图片存储位置",
|
||||
InitialDirectory = Directory.Exists(ScopeImageDir) ? ScopeImageDir : PresetStorageDir,
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return; // 用户取消
|
||||
|
||||
ScopeImageDir = dlg.FolderName;
|
||||
SaveScopeImageDir();
|
||||
Log($"波形图片存储位置已设置:{ScopeImageDir}");
|
||||
});
|
||||
|
||||
// —— 采集控制 ——
|
||||
ScopeAcquireRunCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置采集状态(true, Ct()); Log("示波器采集已启动 (RUN)"); }));
|
||||
|
||||
// 停止采集定格画面后抓图,是最稳定的截图方式(截图抓的是当前屏幕画面)
|
||||
ScopeAcquireStopCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置采集状态(false, Ct()); Log("示波器采集已停止 (STOP),画面已定格;可点「抓取示波器屏幕图片」截取稳定波形"); }));
|
||||
|
||||
QueryAcquireStateCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => Log("采集状态: " + await _scope!.查询采集状态(Ct()))));
|
||||
|
||||
ApplyAcquireModeCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
if (!Enum.TryParse<TekAcquisitionMode>(ScopeAcquireMode, out var mode))
|
||||
{
|
||||
Log($"未知采集模式:{ScopeAcquireMode}");
|
||||
return;
|
||||
}
|
||||
await _scope!.设置采集模式(mode, Ct());
|
||||
Log($"采集模式已设置:{mode}");
|
||||
}));
|
||||
|
||||
// —— 水平系统 ——
|
||||
SetScopeTimeScaleCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置水平刻度(ScopeTimeScale, Ct()); Log($"水平刻度={ScopeTimeScale} s/Div"); }));
|
||||
|
||||
SetRecordLengthCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置记录长度(ScopeRecordLength, Ct()); Log($"记录长度={ScopeRecordLength} 点"); }));
|
||||
|
||||
QuerySampleRateCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
double sr = await _scope!.查询采样率(Ct());
|
||||
SampleRateText = $"{sr:0.###} Sa/s";
|
||||
Log($"采样率={sr} Sa/s");
|
||||
}));
|
||||
|
||||
// —— 通道设置 ——
|
||||
ChannelDisplayOnCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置通道显示开关(ScopeChannelNo, true, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 显示已开启"); }));
|
||||
|
||||
ChannelDisplayOffCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置通道显示开关(ScopeChannelNo, false, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 显示已关闭"); }));
|
||||
|
||||
SetChannelScaleCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置通道垂直刻度(ScopeChannelNo, ChannelScale, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 垂直刻度={ChannelScale} V/Div"); }));
|
||||
|
||||
SetChannelPositionCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.设置通道垂直位置(ScopeChannelNo, ChannelPosition, Ct()); Log($"{ScopeChannelOptions[SelectedScopeChannelIndex]} 垂直位置={ChannelPosition} Div"); }));
|
||||
|
||||
// —— 自动测量:开启测量项 + 类型/源一次完成(4/5/6 系必须先 ADDNew 动态创建) ——
|
||||
AddMeasurementCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
if (!Enum.TryParse<TekMeasurementType>(MeasurementType, out var type))
|
||||
{
|
||||
Log($"未知测量类型:{MeasurementType}");
|
||||
return;
|
||||
}
|
||||
await _scope!.开启测量项(MeasurementNumber, Ct());
|
||||
await _scope.设置测量类型(MeasurementNumber, type, Ct());
|
||||
await _scope.设置测量源(MeasurementNumber, MeasurementSource, Ct());
|
||||
Log($"测量项 MEAS{MeasurementNumber} 已开启:{type} @ {MeasurementSource}");
|
||||
}));
|
||||
|
||||
QueryMeasCurrentCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
double v = await _scope!.查询测量项当前值(MeasurementNumber, Ct());
|
||||
MeasResultText = $"MEAS{MeasurementNumber} 当前值: {v}";
|
||||
Log(MeasResultText);
|
||||
}));
|
||||
|
||||
QueryMeasMeanCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
double v = await _scope!.查询测量项全局平均值(MeasurementNumber, Ct());
|
||||
MeasResultText = $"MEAS{MeasurementNumber} 全局均值: {v}";
|
||||
Log(MeasResultText);
|
||||
}));
|
||||
|
||||
QueryMeasMaxCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () =>
|
||||
{
|
||||
double v = await _scope!.查询测量项全局最大值(MeasurementNumber, Ct());
|
||||
MeasResultText = $"MEAS{MeasurementNumber} 全局最大: {v}";
|
||||
Log(MeasResultText);
|
||||
}));
|
||||
|
||||
// —— 设备辅助 ——
|
||||
QueryScopeIdnCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => Log("示波器 IDN: " + await _scope!.查询设备标识(Ct()))));
|
||||
|
||||
ClearScopeErrorsCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => { await _scope!.清除错误队列Async(Ct()); Log("示波器错误队列已清除 (*CLS)"); }));
|
||||
|
||||
ReadScopeErrorsCommand = new DelegateCommand(async () =>
|
||||
await ExecScope(async () => Log("示波器错误队列: " + await _scope!.读取所有错误消息Async(Ct()))));
|
||||
#endregion
|
||||
|
||||
Initialize();
|
||||
@@ -555,6 +814,30 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
Log(found != null
|
||||
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
|
||||
: "未找到 Chroma61800 设备,请先在设置界面添加并启用该设备");
|
||||
|
||||
// 示波器实例:查找方式与交流源一致(DeviceMap 优先,回退全局 HardwarePool)
|
||||
TektronixMSO? scopeFound = null;
|
||||
string? sfn = null;
|
||||
|
||||
foreach (var kv in _dm.DeviceMap)
|
||||
{
|
||||
if (kv.Value is TektronixMSO s) { scopeFound = s; sfn = kv.Key; break; }
|
||||
}
|
||||
|
||||
if (scopeFound == null)
|
||||
{
|
||||
foreach (var kv in _globalInfo.HardwarePool)
|
||||
{
|
||||
if (kv.Value.IsValueCreated && kv.Value.Value is TektronixMSO s) { scopeFound = s; sfn = kv.Key; break; }
|
||||
}
|
||||
}
|
||||
|
||||
_scope = scopeFound;
|
||||
ScopeName = sfn ?? "TektronixMSO (未找到)";
|
||||
IsScopeConnected = _scope?.IsConnected ?? false;
|
||||
|
||||
if (scopeFound != null)
|
||||
Log($"已关联示波器 [{ScopeName}],连接:{(IsScopeConnected ? "已连接" : "未连接")}");
|
||||
}
|
||||
|
||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||
@@ -594,9 +877,14 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从磁盘重新加载测试项列表,刷新下拉框并保持选中项有效。</summary>
|
||||
/// <summary>从磁盘重新加载测试项列表,刷新下拉框并按名称恢复选中项。</summary>
|
||||
private void RefreshPresets()
|
||||
{
|
||||
// 刷新前记住选中项名称:列表重建后索引会漂移,按名称恢复最可靠
|
||||
string? selectedName = (_selectedPresetIndex >= 0 && _selectedPresetIndex < _presets.Count)
|
||||
? _presets[_selectedPresetIndex].Name
|
||||
: null;
|
||||
|
||||
var list = new List<ListPreset>();
|
||||
try
|
||||
{
|
||||
@@ -608,7 +896,7 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var item = JsonConvert.DeserializeObject<ListPreset>(json);
|
||||
if (item != null) list.Add(item);
|
||||
if (item != null) list.Add(NormalizePresetName(item, file));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -627,51 +915,72 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
RaisePropertyChanged(nameof(PresetOptions));
|
||||
RaisePropertyChanged(nameof(PresetStorageHint));
|
||||
|
||||
if (_selectedPresetIndex >= _presets.Count)
|
||||
SelectedPresetIndex = -1;
|
||||
SelectedPresetIndex = selectedName != null
|
||||
? _presets.FindIndex(p => p.Name == selectedName)
|
||||
: -1;
|
||||
}
|
||||
|
||||
/// <summary>把一个测试项写入 D:\acp\源测试\{清洗后名称}.json(缩进格式化,便于人工查看)。</summary>
|
||||
private void WritePresetFile(ListPreset preset)
|
||||
/// <summary>
|
||||
/// 以磁盘文件名校正测试项名称:文件被外部改名后内容中的 Name 仍是旧名,
|
||||
/// 统一改为文件名(不含扩展名),保证“名称=文件名”不变式,载入/保存/删除都按同一名称寻址。
|
||||
/// </summary>
|
||||
private static ListPreset NormalizePresetName(ListPreset item, string file)
|
||||
{
|
||||
string stem = Path.GetFileNameWithoutExtension(file);
|
||||
return string.Equals(item.Name, stem, StringComparison.Ordinal)
|
||||
? item
|
||||
: new ListPreset(stem, item.Note, item.Count, item.Phases, item.Trigger, item.TimeBase);
|
||||
}
|
||||
|
||||
/// <summary>按名称拼出测试项文件完整路径(写入、另存、删除共用同一规则)。</summary>
|
||||
private static string PresetPathOf(string name) => Path.Combine(PresetStorageDir, SanitizeFileName(name) + PresetFileExt);
|
||||
|
||||
/// <summary>把一个测试项写入 D:\acp\源测试\{清洗后名称}.json(缩进格式化,便于人工查看)。</summary>
|
||||
private static void WritePresetFile(ListPreset preset) => WritePresetFile(preset, PresetPathOf(preset.Name));
|
||||
|
||||
/// <summary>把测试项写入指定文件(覆盖写入)。</summary>
|
||||
private static void WritePresetFile(ListPreset preset, string path)
|
||||
{
|
||||
string path = Path.Combine(PresetStorageDir, SanitizeFileName(preset.Name) + PresetFileExt);
|
||||
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
/// <summary>用当前界面参数 + 三相配置快照构建测试项对象(内容名称始终与文件名一致)。</summary>
|
||||
private ListPreset SnapshotPreset(string name) => new(
|
||||
name,
|
||||
PresetNote ?? "",
|
||||
ListCount,
|
||||
new[] { _phaseConfigs[0].Clone(), _phaseConfigs[1].Clone(), _phaseConfigs[2].Clone() },
|
||||
ListTrigger,
|
||||
ListTimeBase);
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前界面的 LIST 配置为测试项文件(名称取自“测试项名称”输入框)。
|
||||
/// <para>快照当前三相序列配置 + 执行次数/触发/时基 + 说明,写入后刷新列表并选中该项。</para>
|
||||
/// 保存:覆盖写入“当前打开的文件”,不改变文件名(名称框内容不参与)。
|
||||
/// <para>尚未打开任何文件时,退化为“另存为”,用名称框内容新建文件。</para>
|
||||
/// </summary>
|
||||
private void SaveCurrentPreset()
|
||||
{
|
||||
string name = (PresetName ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
if (string.IsNullOrEmpty(_currentPresetPath))
|
||||
{
|
||||
Log("请先在“测试项名称”填写要保存的名称");
|
||||
SaveCurrentPresetAs();
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容名称以当前文件名为准,保证“名称=文件名”不变式
|
||||
string name = Path.GetFileNameWithoutExtension(_currentPresetPath);
|
||||
string typed = (PresetName ?? "").Trim();
|
||||
if (!string.IsNullOrEmpty(typed) && !string.Equals(typed, name, StringComparison.Ordinal))
|
||||
Log($"名称框内容 [{typed}] 未用于保存:保存只覆盖当前文件,如需换名请用“另存为”");
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(PresetStorageDir))
|
||||
Directory.CreateDirectory(PresetStorageDir);
|
||||
|
||||
// 用当前界面参数 + 三相配置快照构建测试项
|
||||
var phases = new[]
|
||||
{
|
||||
_phaseConfigs[0].Clone(),
|
||||
_phaseConfigs[1].Clone(),
|
||||
_phaseConfigs[2].Clone(),
|
||||
};
|
||||
var preset = new ListPreset(name, PresetNote ?? "", ListCount, phases, ListTrigger, ListTimeBase);
|
||||
WritePresetFile(preset);
|
||||
WritePresetFile(SnapshotPreset(name), _currentPresetPath);
|
||||
|
||||
RefreshPresets();
|
||||
int idx = _presets.FindIndex(p => p.Name == name);
|
||||
if (idx >= 0) SelectedPresetIndex = idx;
|
||||
|
||||
Log($"测试项已保存:{Path.Combine(PresetStorageDir, SanitizeFileName(name) + PresetFileExt)}");
|
||||
SelectedPresetIndex = _presets.FindIndex(p => p.Name == name);
|
||||
PresetName = name; // 名称框回显当前文件名
|
||||
Log($"测试项已保存(覆盖):{_currentPresetPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -679,6 +988,45 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 另存为:弹出文件对话框选择 .json 保存位置,成功后“当前打开文件”切换到新文件。
|
||||
/// <para>存入预设目录(D:\acp\源测试)内会自动出现在下拉列表;存到目录外则仅本次会话可用“保存”覆盖,
|
||||
/// 对话框自身的“是否替换”提示已覆盖同名覆盖确认。</para>
|
||||
/// </summary>
|
||||
private void SaveCurrentPresetAs()
|
||||
{
|
||||
var dlg = new SaveFileDialog
|
||||
{
|
||||
Title = "测试项另存为",
|
||||
Filter = "JSON 文件 (*.json)|*.json",
|
||||
DefaultExt = ".json",
|
||||
AddExtension = true,
|
||||
InitialDirectory = PresetStorageDir,
|
||||
FileName = SanitizeFileName((PresetName ?? "").Trim()),
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return; // 用户取消
|
||||
|
||||
try
|
||||
{
|
||||
string path = dlg.FileName;
|
||||
string name = Path.GetFileNameWithoutExtension(path); // 内容名称始终与文件名一致
|
||||
WritePresetFile(SnapshotPreset(name), path);
|
||||
|
||||
_currentPresetPath = path; // 另存为成功后,后续“保存”直接修改这个新文件
|
||||
RefreshPresets();
|
||||
SelectedPresetIndex = _presets.FindIndex(p => p.Name == name);
|
||||
PresetName = name;
|
||||
|
||||
Log(Path.GetDirectoryName(path) is string dir && string.Equals(dir, PresetStorageDir, StringComparison.OrdinalIgnoreCase)
|
||||
? $"测试项已另存为:{path}"
|
||||
: $"测试项已另存为:{path}(在预设目录之外,不会出现在下拉列表,“保存”仍可覆盖此文件)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"另存测试项失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除当前选中的测试项文件(带二次确认),删除后刷新列表。
|
||||
/// </summary>
|
||||
@@ -703,6 +1051,9 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
|
||||
if (string.Equals(path, _currentPresetPath, StringComparison.OrdinalIgnoreCase))
|
||||
_currentPresetPath = null; // 当前打开的文件已被删除,解除绑定
|
||||
|
||||
SelectedPresetIndex = -1;
|
||||
RefreshPresets();
|
||||
Log($"已删除测试项:{path}");
|
||||
@@ -740,9 +1091,14 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从磁盘重新加载合成波形预设列表,刷新下拉框并保持选中项有效。</summary>
|
||||
/// <summary>从磁盘重新加载合成波形预设列表,刷新下拉框并按名称恢复选中项。</summary>
|
||||
private void RefreshSynthPresets()
|
||||
{
|
||||
// 刷新前记住选中项名称:列表重建后索引会漂移,按名称恢复最可靠
|
||||
string? selectedName = (_selectedSynthPresetIndex >= 0 && _selectedSynthPresetIndex < _synthPresets.Count)
|
||||
? _synthPresets[_selectedSynthPresetIndex].Name
|
||||
: null;
|
||||
|
||||
var list = new List<SynthPreset>();
|
||||
try
|
||||
{
|
||||
@@ -754,7 +1110,7 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
{
|
||||
var json = File.ReadAllText(file);
|
||||
var item = JsonConvert.DeserializeObject<SynthPreset>(json);
|
||||
if (item != null) list.Add(item);
|
||||
if (item != null) list.Add(NormalizeSynthPresetName(item, file));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -773,18 +1129,39 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
RaisePropertyChanged(nameof(SynthPresetOptions));
|
||||
RaisePropertyChanged(nameof(SynthStorageHint));
|
||||
|
||||
if (_selectedSynthPresetIndex >= _synthPresets.Count)
|
||||
SelectedSynthPresetIndex = -1;
|
||||
SelectedSynthPresetIndex = selectedName != null
|
||||
? _synthPresets.FindIndex(p => p.Name == selectedName)
|
||||
: -1;
|
||||
}
|
||||
|
||||
/// <summary>把一个合成波形预设写入 {SynthStorageDir}\{清洗后名称}.json(缩进格式化)。</summary>
|
||||
private void WriteSynthPresetFile(SynthPreset preset)
|
||||
/// <summary>以磁盘文件名校正波形名称(同 <see cref="NormalizePresetName"/> 的“名称=文件名”不变式)。</summary>
|
||||
private static SynthPreset NormalizeSynthPresetName(SynthPreset item, string file)
|
||||
{
|
||||
string stem = Path.GetFileNameWithoutExtension(file);
|
||||
return string.Equals(item.Name, stem, StringComparison.Ordinal)
|
||||
? item
|
||||
: new SynthPreset(stem, item.Note, item.Compose, item.Frequency, item.Fundamental,
|
||||
item.Dc, item.StartPhase, item.AmplitudeText, item.PhaseText);
|
||||
}
|
||||
|
||||
/// <summary>按名称拼出合成波形文件完整路径(写入、另存、删除共用同一规则)。</summary>
|
||||
private static string SynthPathOf(string name) => Path.Combine(SynthStorageDir, SanitizeFileName(name) + PresetFileExt);
|
||||
|
||||
/// <summary>把一个合成波形预设写入 {SynthStorageDir}\{清洗后名称}.json(缩进格式化)。</summary>
|
||||
private static void WriteSynthPresetFile(SynthPreset preset) => WriteSynthPresetFile(preset, SynthPathOf(preset.Name));
|
||||
|
||||
/// <summary>把合成波形预设写入指定文件(覆盖写入)。</summary>
|
||||
private static void WriteSynthPresetFile(SynthPreset preset, string path)
|
||||
{
|
||||
string path = Path.Combine(SynthStorageDir, SanitizeFileName(preset.Name) + PresetFileExt);
|
||||
string json = JsonConvert.SerializeObject(preset, Formatting.Indented);
|
||||
File.WriteAllText(path, json);
|
||||
}
|
||||
|
||||
/// <summary>用当前界面参数快照构建波形预设对象(内容名称始终与文件名一致)。</summary>
|
||||
private SynthPreset SnapshotSynthPreset(string name) => new(
|
||||
name, SynthPresetNote ?? "", SynthCompose, SynthFrequency,
|
||||
SynthFundamental, SynthDc, SynthStartPhase, SynthAmplitudeText, SynthPhaseText);
|
||||
|
||||
/// <summary>载入选中波形:把文件参数一次性回填到界面各 SYNTHESIS 输入框。</summary>
|
||||
private void LoadCurrentSynthPreset()
|
||||
{
|
||||
@@ -803,35 +1180,37 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
SynthPhaseText = p.PhaseText;
|
||||
SynthPresetName = p.Name;
|
||||
SynthPresetNote = p.Note;
|
||||
_currentSynthPath = SynthPathOf(p.Name); // 记住当前打开的文件,此后“保存”只覆盖它
|
||||
Log($"已载入波形:{p.Name}(格式={p.Compose} 基频={p.Frequency}Hz 基波={p.Fundamental}V)");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存当前界面的 SYNTHESIS 参数快照为波形文件(名称取自“波形名称”输入框)。
|
||||
/// 保存:覆盖写入“当前打开的文件”,不改变文件名(名称框内容不参与)。
|
||||
/// <para>尚未打开任何文件时,退化为“另存为”,用名称框内容新建文件。</para>
|
||||
/// </summary>
|
||||
private void SaveCurrentSynthPreset()
|
||||
{
|
||||
string name = (SynthPresetName ?? "").Trim();
|
||||
if (string.IsNullOrEmpty(name))
|
||||
if (string.IsNullOrEmpty(_currentSynthPath))
|
||||
{
|
||||
Log("请先在“波形名称”填写要保存的名称");
|
||||
SaveCurrentSynthPresetAs();
|
||||
return;
|
||||
}
|
||||
|
||||
// 内容名称以当前文件名为准,保证“名称=文件名”不变式
|
||||
string name = Path.GetFileNameWithoutExtension(_currentSynthPath);
|
||||
string typed = (SynthPresetName ?? "").Trim();
|
||||
if (!string.IsNullOrEmpty(typed) && !string.Equals(typed, name, StringComparison.Ordinal))
|
||||
Log($"名称框内容 [{typed}] 未用于保存:保存只覆盖当前文件,如需换名请用“另存为”");
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(SynthStorageDir))
|
||||
Directory.CreateDirectory(SynthStorageDir);
|
||||
|
||||
var preset = new SynthPreset(name, SynthPresetNote ?? "", SynthCompose, SynthFrequency,
|
||||
SynthFundamental, SynthDc, SynthStartPhase, SynthAmplitudeText, SynthPhaseText);
|
||||
WriteSynthPresetFile(preset);
|
||||
WriteSynthPresetFile(SnapshotSynthPreset(name), _currentSynthPath);
|
||||
|
||||
RefreshSynthPresets();
|
||||
int idx = _synthPresets.FindIndex(p => p.Name == name);
|
||||
if (idx >= 0) SelectedSynthPresetIndex = idx;
|
||||
|
||||
Log($"波形已保存:{Path.Combine(SynthStorageDir, SanitizeFileName(name) + PresetFileExt)}");
|
||||
SelectedSynthPresetIndex = _synthPresets.FindIndex(p => p.Name == name);
|
||||
SynthPresetName = name; // 名称框回显当前文件名
|
||||
Log($"波形已保存(覆盖):{_currentSynthPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -839,6 +1218,45 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 另存为:弹出文件对话框选择 .json 保存位置,成功后“当前打开文件”切换到新文件。
|
||||
/// <para>存入预设目录(D:\acp\源测试\合成波形)内会自动出现在下拉列表;存到目录外则仅本次会话可用“保存”覆盖,
|
||||
/// 对话框自身的“是否替换”提示已覆盖同名覆盖确认。</para>
|
||||
/// </summary>
|
||||
private void SaveCurrentSynthPresetAs()
|
||||
{
|
||||
var dlg = new SaveFileDialog
|
||||
{
|
||||
Title = "波形另存为",
|
||||
Filter = "JSON 文件 (*.json)|*.json",
|
||||
DefaultExt = ".json",
|
||||
AddExtension = true,
|
||||
InitialDirectory = SynthStorageDir,
|
||||
FileName = SanitizeFileName((SynthPresetName ?? "").Trim()),
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return; // 用户取消
|
||||
|
||||
try
|
||||
{
|
||||
string path = dlg.FileName;
|
||||
string name = Path.GetFileNameWithoutExtension(path); // 内容名称始终与文件名一致
|
||||
WriteSynthPresetFile(SnapshotSynthPreset(name), path);
|
||||
|
||||
_currentSynthPath = path; // 另存为成功后,后续“保存”直接修改这个新文件
|
||||
RefreshSynthPresets();
|
||||
SelectedSynthPresetIndex = _synthPresets.FindIndex(p => p.Name == name);
|
||||
SynthPresetName = name;
|
||||
|
||||
Log(Path.GetDirectoryName(path) is string dir && string.Equals(dir, SynthStorageDir, StringComparison.OrdinalIgnoreCase)
|
||||
? $"波形已另存为:{path}"
|
||||
: $"波形已另存为:{path}(在预设目录之外,不会出现在下拉列表,“保存”仍可覆盖此文件)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"另存波形失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>删除当前选中的波形文件(带二次确认),删除后刷新列表。</summary>
|
||||
private void DeleteCurrentSynthPreset()
|
||||
{
|
||||
@@ -861,6 +1279,9 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
|
||||
if (string.Equals(path, _currentSynthPath, StringComparison.OrdinalIgnoreCase))
|
||||
_currentSynthPath = null; // 当前打开的文件已被删除,解除绑定
|
||||
|
||||
SelectedSynthPresetIndex = -1;
|
||||
RefreshSynthPresets();
|
||||
Log($"已删除波形:{path}");
|
||||
@@ -873,6 +1294,84 @@ namespace PowerAdaptabilityModule.ViewModels
|
||||
|
||||
#endregion
|
||||
|
||||
#region 示波器抓图(TektronixMSO)与存储位置持久化
|
||||
/// <summary>
|
||||
/// 示波器命令执行包装:与 Exec 同一防重入/异常日志机制,仅把设备校验对象换成示波器实例。
|
||||
/// </summary>
|
||||
private async Task ExecScope(Func<Task> action)
|
||||
{
|
||||
if (_scope == null) { Log("错误:未找到 TektronixMSO 示波器实例,请先在设置界面添加并启用该设备"); return; }
|
||||
if (IsBusy) return;
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
IsScopeConnected = _scope.IsConnected;
|
||||
}
|
||||
catch (OperationCanceledException) { Log("示波器命令超时或已取消"); }
|
||||
catch (Exception ex) { Log($"示波器错误:{ex.Message}"); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把抓取到的 PNG 加载为 BitmapImage 并刷新界面显示。
|
||||
/// <para>BitmapCacheOption.OnLoad 读完即释放文件句柄,不占用图片文件;Freeze 后跨线程安全。</para>
|
||||
/// </summary>
|
||||
private void LoadScopeImage(string file)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bmp = new BitmapImage();
|
||||
bmp.BeginInit();
|
||||
bmp.CacheOption = BitmapCacheOption.OnLoad;
|
||||
bmp.UriSource = new Uri(file, UriKind.Absolute);
|
||||
bmp.EndInit();
|
||||
bmp.Freeze();
|
||||
|
||||
ScopeImage = bmp;
|
||||
HasScopeImage = true;
|
||||
ScopeImageHint = $"最近抓取:{Path.GetFileName(file)}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"加载截图失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>示波器截图目录配置文件路径(%AppData%\ACP\ScopeCaptureDir.json)。</summary>
|
||||
private static string ScopeDirConfigPath => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ACP", "ScopeCaptureDir.json");
|
||||
|
||||
/// <summary>启动时恢复上次设置的波形图片存储位置(配置缺失/损坏时保持默认目录)。</summary>
|
||||
private void LoadScopeImageDir()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(ScopeDirConfigPath))
|
||||
{
|
||||
var dir = JsonConvert.DeserializeObject<string>(File.ReadAllText(ScopeDirConfigPath));
|
||||
if (!string.IsNullOrWhiteSpace(dir)) ScopeImageDir = dir;
|
||||
}
|
||||
}
|
||||
catch { /* 配置缺失或损坏时静默使用默认目录 */ }
|
||||
}
|
||||
|
||||
/// <summary>把当前波形图片存储位置写入配置(抓图或浏览选择后调用)。</summary>
|
||||
private void SaveScopeImageDir()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(ScopeDirConfigPath)!);
|
||||
File.WriteAllText(ScopeDirConfigPath, JsonConvert.SerializeObject(ScopeImageDir));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"保存截图目录设置失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 辅助
|
||||
/// <summary>创建 10 秒超时的取消令牌(命令用)。</summary>
|
||||
private CancellationToken Ct() => (_cmdCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="360" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ============ 左侧:Chroma 命令面板 ============ -->
|
||||
@@ -184,11 +185,15 @@
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="名称" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="150"
|
||||
ToolTip="保存时作为文件名(写入 D:\acp\源测试);选中已有测试项会自动填充,可改名另存"
|
||||
ToolTip="另存为对话框的默认文件名(写入 D:\acp\源测试);载入文件后自动回显当前文件名,保存不改动文件名"
|
||||
Text="{Binding PresetName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="保存" Command="{Binding SavePresetCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="覆盖保存当前打开的测试项文件(不改变文件名);未打开文件时按“另存为”新建"
|
||||
Background="#388E3C" Foreground="White" />
|
||||
<Button Content="另存为" Command="{Binding SavePresetAsCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="弹出文件对话框选择 .json 位置另存;存入 D:\acp\源测试 才会出现在下拉列表" />
|
||||
<Button Content="删除" Command="{Binding DeletePresetCommand}"
|
||||
Style="{StaticResource WarnBtn}"
|
||||
ToolTip="删除下拉框当前选中的测试项文件(带二次确认)" />
|
||||
@@ -340,11 +345,15 @@
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="名称" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="150"
|
||||
ToolTip="保存时作为文件名(写入 D:\acp\源测试\合成波形);选中已有波形会自动填充,可改名另存"
|
||||
ToolTip="另存为对话框的默认文件名(写入 D:\acp\源测试\合成波形);载入文件后自动回显当前文件名,保存不改动文件名"
|
||||
Text="{Binding SynthPresetName, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="保存" Command="{Binding SaveSynthPresetCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="覆盖保存当前打开的波形文件(不改变文件名);未打开文件时按“另存为”新建"
|
||||
Background="#388E3C" Foreground="White" />
|
||||
<Button Content="另存为" Command="{Binding SaveSynthPresetAsCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="弹出文件对话框选择 .json 位置另存;存入 D:\acp\源测试\合成波形 才会出现在下拉列表" />
|
||||
<Button Content="删除" Command="{Binding DeleteSynthPresetCommand}"
|
||||
Style="{StaticResource WarnBtn}"
|
||||
ToolTip="删除下拉框当前选中的波形文件(带二次确认)" />
|
||||
@@ -426,6 +435,224 @@
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- ============ 右侧:示波器抓波面板 ============ -->
|
||||
<ScrollViewer Grid.Column="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel>
|
||||
|
||||
<GroupBox Header="示波器屏幕抓图(TektronixMSO)"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<!-- 示波器连接状态 -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,2,0,4">
|
||||
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#F44336" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsScopeConnected}" Value="True">
|
||||
<Setter Property="Background" Value="#4CAF50" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding ScopeName}"
|
||||
FontSize="12" Foreground="#616161"
|
||||
VerticalAlignment="Center" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 波形图片存储位置 -->
|
||||
<TextBlock Text="波形图片存储位置"
|
||||
FontSize="11" Foreground="#616161" Margin="4,0,4,2" />
|
||||
<Grid Margin="0,0,0,2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox Style="{StaticResource NumInput}" Width="Auto"
|
||||
ToolTip="抓取的屏幕图片 PNG 保存目录;也可直接在框内修改,抓图/浏览时自动记住"
|
||||
Text="{Binding ScopeImageDir, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Grid.Column="1" Content="浏览…" Command="{Binding BrowseScopeImageDirCommand}"
|
||||
Style="{StaticResource CmdBtn}" Margin="4,0,0,0"
|
||||
ToolTip="弹出文件夹选择对话框选择存储位置" />
|
||||
</Grid>
|
||||
<Button Content="抓取示波器屏幕图片" Command="{Binding CaptureScopeImageCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
Background="#388E3C" Foreground="White"
|
||||
HorizontalAlignment="Left" Margin="0,4"
|
||||
ToolTip="抓取示波器当前屏幕保存为 PNG(SAVe:IMAGe),成功后自动加载显示" />
|
||||
|
||||
<!-- 波形图片显示 -->
|
||||
<Border BorderBrush="#DDDDDD" BorderThickness="1"
|
||||
Background="#FFFFFF" CornerRadius="2" Margin="0,4,0,0">
|
||||
<Grid Height="230">
|
||||
<Image Source="{Binding ScopeImage}" Stretch="Uniform" Margin="4"
|
||||
Visibility="{Binding HasScopeImage, Converter={StaticResource BoolToVis}}" />
|
||||
<TextBlock Text="暂无波形图片
抓取成功后自动显示"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Foreground="#BDBDBD" FontSize="13" TextAlignment="Center">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding HasScopeImage}" Value="True">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBlock.Style>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding ScopeImageHint}"
|
||||
FontSize="11" Foreground="#757575"
|
||||
Margin="4,4,4,2" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 采集控制:运行/停止采集,定格画面后抓图最稳定 -->
|
||||
<GroupBox Header="采集控制" Margin="0,8,0,0"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="运行 (RUN)" Command="{Binding ScopeAcquireRunCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
Background="#388E3C" Foreground="White" />
|
||||
<Button Content="停止 (STOP)" Command="{Binding ScopeAcquireStopCommand}"
|
||||
Style="{StaticResource WarnBtn}"
|
||||
ToolTip="停止采集定格当前画面,再点「抓取示波器屏幕图片」即可截到稳定波形" />
|
||||
<Button Content="查询状态" Command="{Binding QueryAcquireStateCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="采集模式" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox Width="120" Height="32" FontSize="12" Margin="4,0"
|
||||
ToolTip="ACQuire:MODe,采样/峰值检测/高分辨率/平均/包络"
|
||||
ItemsSource="{Binding ScopeAcquireModeOptions}"
|
||||
SelectedItem="{Binding ScopeAcquireMode}" />
|
||||
<Button Content="应用" Command="{Binding ApplyAcquireModeCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 水平系统 -->
|
||||
<GroupBox Header="水平系统" Margin="0,8,0,0"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="刻度(s/Div)" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="90"
|
||||
ToolTip="HORizontal:SCAle,每格时间,单位 s,如 0.001"
|
||||
Text="{Binding ScopeTimeScale, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="设置" Command="{Binding SetScopeTimeScaleCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="记录长度" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="90"
|
||||
ToolTip="HORizontal:RECOrdlength,采样点数,如 6250000"
|
||||
Text="{Binding ScopeRecordLength, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="设置" Command="{Binding SetRecordLengthCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="查询采样率" Command="{Binding QuerySampleRateCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
<TextBox Style="{StaticResource MeasureBox}" Width="150"
|
||||
Text="{Binding SampleRateText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 通道设置 -->
|
||||
<GroupBox Header="通道设置" Margin="0,8,0,0"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="通道" Style="{StaticResource ParamLabel}" />
|
||||
<ComboBox Width="80" Height="32" FontSize="12" Margin="4,0"
|
||||
ItemsSource="{Binding ScopeChannelOptions}"
|
||||
SelectedIndex="{Binding SelectedScopeChannelIndex}" />
|
||||
<Button Content="显示开" Command="{Binding ChannelDisplayOnCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
<Button Content="显示关" Command="{Binding ChannelDisplayOffCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="刻度(V/Div)" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="90"
|
||||
ToolTip="CH:SCAle,每格电压,单位 V,如 100"
|
||||
Text="{Binding ChannelScale, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="设置" Command="{Binding SetChannelScaleCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="位置(Div)" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="90"
|
||||
ToolTip="CH:POSition,垂直位置偏移,单位格,如 0"
|
||||
Text="{Binding ChannelPosition, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="设置" Command="{Binding SetChannelPositionCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 自动测量 -->
|
||||
<GroupBox Header="自动测量" Margin="0,8,0,0"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Margin="4">
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="测量项" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="52"
|
||||
ToolTip="MEASUrement:MEASn 的编号 n"
|
||||
Text="{Binding MeasurementNumber, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<ComboBox Width="110" Height="32" FontSize="12" Margin="4,0"
|
||||
ToolTip="MEASUrement:TYPe,幅度/频率/平均/峰峰值/最大/最小"
|
||||
ItemsSource="{Binding ScopeMeasTypeOptions}"
|
||||
SelectedItem="{Binding MeasurementType}" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<TextBlock Text="测量源" Style="{StaticResource ParamLabel}" />
|
||||
<TextBox Style="{StaticResource NumInput}" Width="90"
|
||||
ToolTip="MEASUrement:SOUrce1,如 CH1 / CH2 / MATH1"
|
||||
Text="{Binding MeasurementSource, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<Button Content="开启测量" Command="{Binding AddMeasurementCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="ADDNew 创建测量项并设置类型与源(4/5/6 系必须先动态创建)" />
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4">
|
||||
<Button Content="当前值" Command="{Binding QueryMeasCurrentCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="本次采集的测量平均值" />
|
||||
<Button Content="全局均值" Command="{Binding QueryMeasMeanCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
<Button Content="全局最大" Command="{Binding QueryMeasMaxCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
</StackPanel>
|
||||
<TextBox Style="{StaticResource MeasureBox}" Width="300" HorizontalAlignment="Left"
|
||||
Margin="0,2" Text="{Binding MeasResultText, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 设备辅助 -->
|
||||
<GroupBox Header="设备辅助" Margin="0,8,0,0"
|
||||
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
|
||||
<StackPanel Orientation="Horizontal" Margin="4,4">
|
||||
<Button Content="查询 IDN" Command="{Binding QueryScopeIdnCommand}"
|
||||
Style="{StaticResource CmdBtn}" />
|
||||
<Button Content="清除错误" Command="{Binding ClearScopeErrorsCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="*CLS 清除所有状态寄存器与错误/事件队列" />
|
||||
<Button Content="读取错误" Command="{Binding ReadScopeErrorsCommand}"
|
||||
Style="{StaticResource CmdBtn}"
|
||||
ToolTip="ALLEv? 读取并清空示波器错误/事件队列,命令不生效时先看这里" />
|
||||
</StackPanel>
|
||||
</GroupBox>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
||||
Reference in New Issue
Block a user