This commit is contained in:
jimmy committed 2025-12-24 22:55:59 +08:00
1 parent 99c2c59552
commit 012c9522f8
5 files changed
+1717 -106

No files matched your search

@@ -1,34 +1,46 @@
using System; using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Sharp.Wpf.Mvvm; using Sharp.Wpf.Mvvm;
using Sharp.Wpf.Commands; using Sharp.Wpf.Commands;
using Sharp.Wpf.Navigation; using Sharp.Wpf.Navigation;
using Sharp.Wpf.Abstractions;
using Plc.Abstractions; using Plc.Abstractions;
using Plc.Models;
namespace Sharp.Demo.ViewModels.Pages namespace Sharp.Demo.ViewModels.Pages
{ {
/// <summary> /// <summary>
/// PLC 调试页面 ViewModel - 简化版 /// PLC 调试页面 ViewModel - 完整功能版
/// </summary> /// </summary>
public class PlcDebugViewModel : ViewModelBase, INavigationAware public class PlcDebugViewModel : ViewModelBase, INavigationAware
{ {
private readonly IPlcService _plcService; private readonly IPlcService _plcService;
private readonly ILogger<PlcDebugViewModel> _logger; private readonly ILogger<PlcDebugViewModel> _logger;
private readonly IDispatcher _dispatcher;
private string _ipAddress = "192.168.1.100"; private string _ipAddress = "192.168.1.100";
private string _status = "未连接"; private string _status = "未连接";
private string _readAddress = "DB1.DBW0"; private string _readAddress = "DB1.DBW0";
private string _readValue = "-"; private string _readValue = "-";
private Type _readDataType = typeof(short);
private string _writeAddress = "DB1.DBW0"; private string _writeAddress = "DB1.DBW0";
private string _writeValue = "0"; private string _writeValue = "0";
private Type _writeDataType = typeof(short);
private string _logText = string.Empty; private string _logText = string.Empty;
private string _monitorAddress = "DB1.DBW0";
private Type _monitorDataType = typeof(short);
public PlcDebugViewModel(IPlcService plcService, ILogger<PlcDebugViewModel> logger) public PlcDebugViewModel(
IPlcService plcService,
ILogger<PlcDebugViewModel> logger,
IDispatcher dispatcher)
{ {
_plcService = plcService ?? throw new ArgumentNullException(nameof(plcService)); _plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
// 初始化命令 // 初始化命令
ConnectCommand = new AsyncCommand(ConnectAsync); ConnectCommand = new AsyncCommand(ConnectAsync);
@@ -36,6 +48,14 @@ namespace Sharp.Demo.ViewModels.Pages
ReadCommand = new AsyncCommand(ReadAsync); ReadCommand = new AsyncCommand(ReadAsync);
WriteCommand = new AsyncCommand(WriteAsync); WriteCommand = new AsyncCommand(WriteAsync);
ClearLogCommand = new RelayCommand(_ => ClearLog()); ClearLogCommand = new RelayCommand(_ => ClearLog());
AddMonitorCommand = new RelayCommand(_ => AddMonitor());
RemoveMonitorCommand = new RelayCommand<PlcTagViewModel>(RemoveMonitor, vm => vm != null);
AddToQueueCommand = new RelayCommand(_ => AddToQueue());
RemoveFromQueueCommand = new RelayCommand<WriteQueueItem>(RemoveFromQueue, item => item != null);
ExecuteQueueCommand = new AsyncCommand(ExecuteQueueAsync);
ClearQueueCommand = new RelayCommand(_ => WriteQueue.Clear());
MoveUpQueueCommand = new RelayCommand<WriteQueueItem>(MoveUpQueue, item => item != null);
MoveDownQueueCommand = new RelayCommand<WriteQueueItem>(MoveDownQueue, item => item != null);
AppendLog("PLC 调试工具已加载"); AppendLog("PLC 调试工具已加载");
AppendLog("提示:需要配置真实的 PLC 连接信息才能使用"); AppendLog("提示:需要配置真实的 PLC 连接信息才能使用");
@@ -67,6 +87,12 @@ namespace Sharp.Demo.ViewModels.Pages
set => SetProperty(ref _readValue, value); set => SetProperty(ref _readValue, value);
} }
public Type ReadDataType
{
get => _readDataType;
set => SetProperty(ref _readDataType, value);
}
public string WriteAddress public string WriteAddress
{ {
get => _writeAddress; get => _writeAddress;
@@ -79,12 +105,58 @@ namespace Sharp.Demo.ViewModels.Pages
set => SetProperty(ref _writeValue, value); set => SetProperty(ref _writeValue, value);
} }
public Type WriteDataType
{
get => _writeDataType;
set => SetProperty(ref _writeDataType, value);
}
public string LogText public string LogText
{ {
get => _logText; get => _logText;
set => SetProperty(ref _logText, value); set => SetProperty(ref _logText, value);
} }
public string MonitorAddress
{
get => _monitorAddress;
set => SetProperty(ref _monitorAddress, value);
}
public Type MonitorDataType
{
get => _monitorDataType;
set => SetProperty(ref _monitorDataType, value);
}
/// <summary>
/// 支持的数据类型
/// </summary>
public ObservableCollection<DataTypeOption> SupportedDataTypes { get; } = new()
{
new("Bool", typeof(bool)),
new("Byte", typeof(byte)),
new("Short (Int16)", typeof(short)),
new("UShort (UInt16)", typeof(ushort)),
new("Int (Int32)", typeof(int)),
new("UInt (UInt32)", typeof(uint)),
new("Long (Int64)", typeof(long)),
new("ULong (UInt64)", typeof(ulong)),
new("Float (Real)", typeof(float)),
new("Double (LReal)", typeof(double)),
new("String", typeof(string))
};
/// <summary>
/// 监控列表
/// </summary>
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
/// <summary>
/// 写入队列
/// </summary>
public ObservableCollection<WriteQueueItem> WriteQueue { get; } = new();
#endregion #endregion
#region 命令 #region 命令
@@ -94,6 +166,14 @@ namespace Sharp.Demo.ViewModels.Pages
public ICommand ReadCommand { get; } public ICommand ReadCommand { get; }
public ICommand WriteCommand { get; } public ICommand WriteCommand { get; }
public ICommand ClearLogCommand { get; } public ICommand ClearLogCommand { get; }
public ICommand AddMonitorCommand { get; }
public ICommand RemoveMonitorCommand { get; }
public ICommand AddToQueueCommand { get; }
public ICommand RemoveFromQueueCommand { get; }
public ICommand ExecuteQueueCommand { get; }
public ICommand ClearQueueCommand { get; }
public ICommand MoveUpQueueCommand { get; }
public ICommand MoveDownQueueCommand { get; }
#endregion #endregion
@@ -153,18 +233,36 @@ namespace Sharp.Demo.ViewModels.Pages
{ {
try try
{ {
AppendLog($"读取地址: {ReadAddress}"); AppendLog($"读取地址: {ReadAddress}, 类型: {GetTypeName(ReadDataType)}");
var result = await _plcService.ReadAsync<short>(ReadAddress);
if (result.IsSuccess) // 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
.MakeGenericMethod(ReadDataType);
var task = (Task)method.Invoke(_plcService, new object[] { ReadAddress })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{ {
ReadValue = result.Value.ToString(); var valueProperty = result.GetType().GetProperty("Value")!;
var value = valueProperty.GetValue(result);
ReadValue = value?.ToString() ?? "null";
AppendLog($"✅ 读取成功: {ReadValue}"); AppendLog($"✅ 读取成功: {ReadValue}");
} }
else else
{ {
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
ReadValue = "读取失败"; ReadValue = "读取失败";
AppendLog($"❌ 读取失败: {result.ErrorMessage}"); AppendLog($"❌ 读取失败: {errorMessage}");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -179,22 +277,38 @@ namespace Sharp.Demo.ViewModels.Pages
{ {
try try
{ {
if (!short.TryParse(WriteValue, out short value)) // 解析值
var value = ParseValue(WriteValue, WriteDataType);
if (value == null)
{ {
AppendLog($"❌ 写入失败: 值格式不正确"); AppendLog($"❌ 写入失败: 值格式不正确");
return; return;
} }
AppendLog($"写入地址: {WriteAddress} = {value}"); AppendLog($"写入地址: {WriteAddress} = {value}, 类型: {GetTypeName(WriteDataType)}");
var result = await _plcService.WriteAsync(WriteAddress, value);
if (result.IsSuccess) // 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
var task = (Task)method.Invoke(_plcService, new object[] { WriteAddress, value })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{ {
AppendLog($"✅ 写入成功"); AppendLog($"✅ 写入成功");
} }
else else
{ {
AppendLog($"❌ 写入失败: {result.ErrorMessage}"); var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
AppendLog($"❌ 写入失败: {errorMessage}");
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -216,6 +330,326 @@ namespace Sharp.Demo.ViewModels.Pages
LogText += $"[{timestamp}] {message}\n"; LogText += $"[{timestamp}] {message}\n";
} }
/// <summary>
/// 添加监控
/// </summary>
private void AddMonitor()
{
try
{
var tag = new PlcTag
{
Address = MonitorAddress,
DataType = MonitorDataType
};
var vm = new PlcTagViewModel(tag);
tag.OnChanged = (t, v) =>
{
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
_dispatcher.InvokeAsync(() =>
{
vm.UpdateValue();
});
};
_plcService.Subscribe(tag);
// 在UI线程添加到集合
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Add(vm);
});
AppendLog($"✅ 添加监控: {MonitorAddress} ({GetTypeName(MonitorDataType)})");
}
catch (Exception ex)
{
AppendLog($"❌ 添加监控失败: {ex.Message}");
_logger.LogError(ex, "添加监控失败");
}
}
/// <summary>
/// 移除监控
/// </summary>
private void RemoveMonitor(PlcTagViewModel? tagVm)
{
if (tagVm == null) return;
try
{
// 从PLC服务取消订阅
_plcService.Unsubscribe(tagVm.Id);
// 从UI集合移除
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Remove(tagVm);
});
AppendLog($"✅ 移除监控: {tagVm.Address}");
}
catch (Exception ex)
{
AppendLog($"❌ 移除监控失败: {ex.Message}");
_logger.LogError(ex, "移除监控失败");
}
}
/// <summary>
/// 添加到写入队列
/// </summary>
private void AddToQueue()
{
try
{
var value = ParseValue(WriteValue, WriteDataType);
if (value == null)
{
AppendLog($"❌ 添加到队列失败: 值格式不正确");
return;
}
var item = new WriteQueueItem
{
Order = WriteQueue.Count + 1,
Address = WriteAddress,
Value = value,
DataType = WriteDataType,
DataTypeName = GetTypeName(WriteDataType)
};
WriteQueue.Add(item);
AppendLog($"✅ 添加到队列: {WriteAddress} = {value} ({GetTypeName(WriteDataType)})");
// 更新序号
UpdateQueueOrder();
}
catch (Exception ex)
{
AppendLog($"❌ 添加到队列失败: {ex.Message}");
_logger.LogError(ex, "添加到队列失败");
}
}
/// <summary>
/// 从队列移除
/// </summary>
private void RemoveFromQueue(WriteQueueItem? item)
{
if (item == null) return;
WriteQueue.Remove(item);
UpdateQueueOrder();
AppendLog($"移除队列项: {item.Address}");
}
/// <summary>
/// 执行写入队列
/// </summary>
private async Task ExecuteQueueAsync()
{
if (WriteQueue.Count == 0)
{
AppendLog("⚠️ 队列为空,无需执行");
return;
}
AppendLog($"开始执行写入队列 ({WriteQueue.Count} 项)...");
int successCount = 0;
int failCount = 0;
foreach (var item in WriteQueue)
{
try
{
AppendLog($"[{item.Order}] 写入: {item.Address} = {item.Value}");
// 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
var task = (Task)method.Invoke(_plcService, new object[] { item.Address, item.Value })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{
successCount++;
AppendLog($" ✅ 成功");
}
else
{
failCount++;
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
AppendLog($" ❌ 失败: {errorMessage}");
}
}
catch (Exception ex)
{
failCount++;
AppendLog($" ❌ 异常: {ex.Message}");
_logger.LogError(ex, "写入队列项失败");
}
}
AppendLog($"队列执行完成: 成功 {successCount}, 失败 {failCount}");
}
/// <summary>
/// 队列项上移
/// </summary>
private void MoveUpQueue(WriteQueueItem? item)
{
if (item == null) return;
var index = WriteQueue.IndexOf(item);
if (index > 0)
{
WriteQueue.Move(index, index - 1);
UpdateQueueOrder();
}
}
/// <summary>
/// 队列项下移
/// </summary>
private void MoveDownQueue(WriteQueueItem? item)
{
if (item == null) return;
var index = WriteQueue.IndexOf(item);
if (index < WriteQueue.Count - 1)
{
WriteQueue.Move(index, index + 1);
UpdateQueueOrder();
}
}
/// <summary>
/// 更新队列序号
/// </summary>
private void UpdateQueueOrder()
{
for (int i = 0; i < WriteQueue.Count; i++)
{
WriteQueue[i].Order = i + 1;
}
}
/// <summary>
/// 解析值
/// </summary>
private object? ParseValue(string valueStr, Type type)
{
try
{
if (type == typeof(bool))
return bool.Parse(valueStr);
if (type == typeof(byte))
return byte.Parse(valueStr);
if (type == typeof(short))
return short.Parse(valueStr);
if (type == typeof(ushort))
return ushort.Parse(valueStr);
if (type == typeof(int))
return int.Parse(valueStr);
if (type == typeof(uint))
return uint.Parse(valueStr);
if (type == typeof(long))
return long.Parse(valueStr);
if (type == typeof(ulong))
return ulong.Parse(valueStr);
if (type == typeof(float))
return float.Parse(valueStr);
if (type == typeof(double))
return double.Parse(valueStr);
if (type == typeof(string))
return valueStr;
return null;
}
catch
{
return null;
}
}
/// <summary>
/// 获取类型名称
/// </summary>
private string GetTypeName(Type type)
{
if (type == typeof(bool)) return "Bool";
if (type == typeof(byte)) return "Byte";
if (type == typeof(short)) return "Short";
if (type == typeof(ushort)) return "UShort";
if (type == typeof(int)) return "Int";
if (type == typeof(uint)) return "UInt";
if (type == typeof(long)) return "Long";
if (type == typeof(ulong)) return "ULong";
if (type == typeof(float)) return "Float";
if (type == typeof(double)) return "Double";
if (type == typeof(string)) return "String";
return type.Name;
}
#endregion #endregion
} }
/// <summary>
/// 数据类型选项
/// </summary>
public record DataTypeOption(string Name, Type Type);
/// <summary>
/// 写入队列项
/// </summary>
public class WriteQueueItem : ViewModelBase
{
private int _order;
private string _address = string.Empty;
private object _value = 0;
private Type _dataType = typeof(short);
private string _dataTypeName = "Short";
public int Order
{
get => _order;
set => SetProperty(ref _order, value);
}
public string Address
{
get => _address;
set => SetProperty(ref _address, value);
}
public object Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public Type DataType
{
get => _dataType;
set => SetProperty(ref _dataType, value);
}
public string DataTypeName
{
get => _dataTypeName;
set => SetProperty(ref _dataTypeName, value);
}
}
} }
@@ -0,0 +1,74 @@
using System;
using Plc.Models;
using Sharp.Wpf.Mvvm;
namespace Sharp.Demo.ViewModels.Pages
{
/// <summary>
/// PLC 标签 ViewModel - 用于监控列表显示
/// </summary>
public class PlcTagViewModel : ViewModelBase
{
private readonly PlcTag _tag;
public PlcTagViewModel(PlcTag tag)
{
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
}
/// <summary>
/// 标签 ID
/// </summary>
public string Id => _tag.Id;
/// <summary>
/// PLC 地址
/// </summary>
public string Address => _tag.Address;
/// <summary>
/// 数据类型
/// </summary>
public Type DataType => _tag.DataType;
/// <summary>
/// 数据类型名称
/// </summary>
public string DataTypeName
{
get
{
if (_tag.DataType == typeof(bool)) return "Bool";
if (_tag.DataType == typeof(byte)) return "Byte";
if (_tag.DataType == typeof(short)) return "Short";
if (_tag.DataType == typeof(ushort)) return "UShort";
if (_tag.DataType == typeof(int)) return "Int";
if (_tag.DataType == typeof(uint)) return "UInt";
if (_tag.DataType == typeof(long)) return "Long";
if (_tag.DataType == typeof(ulong)) return "ULong";
if (_tag.DataType == typeof(float)) return "Float";
if (_tag.DataType == typeof(double)) return "Double";
if (_tag.DataType == typeof(string)) return "String";
return _tag.DataType.Name;
}
}
/// <summary>
/// 当前值
/// </summary>
public object? Value => _tag.Value;
/// <summary>
/// 底层标签对象
/// </summary>
public PlcTag Tag => _tag;
/// <summary>
/// 更新值(通知UI刷新)
/// </summary>
public void UpdateValue()
{
OnPropertyChanged(nameof(Value));
}
}
}
@@ -1,26 +1,67 @@
using System; using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Extensions.Logging;
using Sharp.Wpf.Mvvm; using Sharp.Wpf.Mvvm;
using Sharp.Wpf.Commands; using Sharp.Wpf.Commands;
using Sharp.Wpf.Abstractions;
using Plc.Abstractions;
using Plc.Models;
namespace Sharp.Demo.ViewModels.Windows namespace Sharp.Demo.ViewModels.Windows
{ {
/// <summary>
/// PLC 调试窗口 ViewModel - 完整功能版
/// </summary>
public class PlcDebugViewModel : ViewModelBase public class PlcDebugViewModel : ViewModelBase
{ {
private string _ipAddress = "127.0.0.1"; private readonly IPlcService _plcService;
private readonly ILogger<PlcDebugViewModel> _logger;
private readonly IDispatcher _dispatcher;
private string _ipAddress = "192.168.1.100";
private string _status = "未连接"; private string _status = "未连接";
private string _readAddress = "DB1.DBW0"; private string _readAddress = "DB1.DBW0";
private string _readValue = ""; private string _readValue = "-";
private Type _readDataType = typeof(short);
private string _writeAddress = "DB1.DBW0"; private string _writeAddress = "DB1.DBW0";
private string _writeValue = ""; private string _writeValue = "0";
private string _logText = "等待连接PLC...\n"; private Type _writeDataType = typeof(short);
private string _lastLogMessage = "等待操作...";
private string _monitorAddress = "DB1.DBW0";
private Type _monitorDataType = typeof(short);
public PlcDebugViewModel() public PlcDebugViewModel(
IPlcService plcService,
ILogger<PlcDebugViewModel> logger,
IDispatcher dispatcher)
{ {
ConnectCommand = new RelayCommand(Connect); _plcService = plcService ?? throw new ArgumentNullException(nameof(plcService));
DisconnectCommand = new RelayCommand(Disconnect); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
ReadCommand = new RelayCommand(Read); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
WriteCommand = new RelayCommand(Write);
ClearLogCommand = new RelayCommand(() => LogText = ""); // 初始化命令
ConnectCommand = new AsyncCommand(ConnectAsync);
DisconnectCommand = new AsyncCommand(DisconnectAsync);
ReadCommand = new AsyncCommand(ReadAsync);
WriteCommand = new AsyncCommand(WriteAsync);
AddMonitorCommand = new RelayCommand(_ => AddMonitor());
RemoveMonitorCommand = new RelayCommand<PlcTagViewModel>(RemoveMonitor, vm => vm != null);
AddToQueueCommand = new RelayCommand(_ => AddToQueue());
RemoveFromQueueCommand = new RelayCommand<WriteQueueItem>(RemoveFromQueue, item => item != null);
ExecuteQueueCommand = new AsyncCommand(ExecuteQueueAsync);
ClearQueueCommand = new RelayCommand(_ => WriteQueue.Clear());
MoveUpQueueCommand = new RelayCommand<WriteQueueItem>(MoveUpQueue, item => item != null);
MoveDownQueueCommand = new RelayCommand<WriteQueueItem>(MoveDownQueue, item => item != null);
SaveQueueCommand = new AsyncCommand(SaveQueueAsync);
LoadQueueCommand = new AsyncCommand(LoadQueueAsync);
AppendLog("PLC 调试工具已加载");
AppendLog("提示:需要配置真实的 PLC 连接信息才能使用");
} }
#region 属性 #region 属性
@@ -49,6 +90,12 @@ namespace Sharp.Demo.ViewModels.Windows
set => SetProperty(ref _readValue, value); set => SetProperty(ref _readValue, value);
} }
public Type ReadDataType
{
get => _readDataType;
set => SetProperty(ref _readDataType, value);
}
public string WriteAddress public string WriteAddress
{ {
get => _writeAddress; get => _writeAddress;
@@ -61,57 +108,773 @@ namespace Sharp.Demo.ViewModels.Windows
set => SetProperty(ref _writeValue, value); set => SetProperty(ref _writeValue, value);
} }
public string LogText public Type WriteDataType
{ {
get => _logText; get => _writeDataType;
set => SetProperty(ref _logText, value); set => SetProperty(ref _writeDataType, value);
} }
public string LastLogMessage
{
get => _lastLogMessage;
set => SetProperty(ref _lastLogMessage, value);
}
public string MonitorAddress
{
get => _monitorAddress;
set => SetProperty(ref _monitorAddress, value);
}
public Type MonitorDataType
{
get => _monitorDataType;
set => SetProperty(ref _monitorDataType, value);
}
/// <summary>
/// 支持的数据类型
/// </summary>
public ObservableCollection<DataTypeOption> SupportedDataTypes { get; } = new()
{
new("Bool", typeof(bool)),
new("Byte", typeof(byte)),
new("Short (Int16)", typeof(short)),
new("UShort (UInt16)", typeof(ushort)),
new("Int (Int32)", typeof(int)),
new("UInt (UInt32)", typeof(uint)),
new("Long (Int64)", typeof(long)),
new("ULong (UInt64)", typeof(ulong)),
new("Float (Real)", typeof(float)),
new("Double (LReal)", typeof(double)),
new("String", typeof(string))
};
/// <summary>
/// 监控列表
/// </summary>
public ObservableCollection<PlcTagViewModel> MonitoredTags { get; } = new();
/// <summary>
/// 写入队列
/// </summary>
public ObservableCollection<WriteQueueItem> WriteQueue { get; } = new();
#endregion #endregion
#region 命令 #region 命令
public RelayCommand ConnectCommand { get; } public ICommand ConnectCommand { get; }
public RelayCommand DisconnectCommand { get; } public ICommand DisconnectCommand { get; }
public RelayCommand ReadCommand { get; } public ICommand ReadCommand { get; }
public RelayCommand WriteCommand { get; } public ICommand WriteCommand { get; }
public RelayCommand ClearLogCommand { get; } public ICommand AddMonitorCommand { get; }
public ICommand RemoveMonitorCommand { get; }
public ICommand AddToQueueCommand { get; }
public ICommand RemoveFromQueueCommand { get; }
public ICommand ExecuteQueueCommand { get; }
public ICommand ClearQueueCommand { get; }
public ICommand MoveUpQueueCommand { get; }
public ICommand MoveDownQueueCommand { get; }
public ICommand SaveQueueCommand { get; }
public ICommand LoadQueueCommand { get; }
#endregion #endregion
#region 命令实现 #region 私有方法
private void Connect() private async Task ConnectAsync()
{ {
AppendLog($"[{DateTime.Now:HH:mm:ss}] 正在连接 PLC: {IpAddress}"); try
Status = "已连接"; {
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 连接成功"); Status = "正在连接...";
AppendLog($"正在连接 PLC: {IpAddress}");
await _plcService.StartAsync();
Status = "已连接";
AppendLog("✅ PLC 连接成功");
}
catch (Exception ex)
{
Status = $"连接失败: {ex.Message}";
AppendLog($"❌ 连接失败: {ex.Message}");
_logger.LogError(ex, "PLC 连接失败");
}
} }
private void Disconnect() private async Task DisconnectAsync()
{ {
AppendLog($"[{DateTime.Now:HH:mm:ss}] 断开连接"); try
Status = "未连接"; {
await _plcService.StopAsync();
Status = "已断开";
AppendLog("PLC 已断开连接");
}
catch (Exception ex)
{
AppendLog($"❌ 断开失败: {ex.Message}");
_logger.LogError(ex, "PLC 断开失败");
}
} }
private void Read() private async Task ReadAsync()
{ {
AppendLog($"[{DateTime.Now:HH:mm:ss}] 读取 {ReadAddress}"); try
ReadValue = "42"; // 示例值 {
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 读取成功: {ReadValue}"); AppendLog($"读取地址: {ReadAddress}, 类型: {GetTypeName(ReadDataType)}");
// 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
.MakeGenericMethod(ReadDataType);
var task = (Task)method.Invoke(_plcService, new object[] { ReadAddress })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{
var valueProperty = result.GetType().GetProperty("Value")!;
var value = valueProperty.GetValue(result);
ReadValue = value?.ToString() ?? "null";
AppendLog($"✅ 读取成功: {ReadValue}");
}
else
{
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
ReadValue = "读取失败";
AppendLog($"❌ 读取失败: {errorMessage}");
}
}
catch (Exception ex)
{
ReadValue = "读取失败";
AppendLog($"❌ 读取异常: {ex.Message}");
_logger.LogError(ex, "PLC 读取失败");
}
} }
private void Write() private async Task WriteAsync()
{ {
AppendLog($"[{DateTime.Now:HH:mm:ss}] 写入 {WriteAddress} = {WriteValue}"); try
AppendLog($"[{DateTime.Now:HH:mm:ss}] ✅ 写入成功"); {
} // 解析值
var value = ParseValue(WriteValue, WriteDataType);
if (value == null)
{
AppendLog($"❌ 写入失败: 值格式不正确");
return;
}
#endregion AppendLog($"写入地址: {WriteAddress} = {value}, 类型: {GetTypeName(WriteDataType)}");
// 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
var task = (Task)method.Invoke(_plcService, new object[] { WriteAddress, value })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{
AppendLog($"✅ 写入成功");
}
else
{
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
AppendLog($"❌ 写入失败: {errorMessage}");
}
}
catch (Exception ex)
{
AppendLog($"❌ 写入异常: {ex.Message}");
_logger.LogError(ex, "PLC 写入失败");
}
}
private void AppendLog(string message) private void AppendLog(string message)
{ {
LogText += message + "\n"; var timestamp = DateTime.Now.ToString("HH:mm:ss");
LastLogMessage = $"[{timestamp}] {message}";
}
/// <summary>
/// 添加监控
/// </summary>
private void AddMonitor()
{
try
{
var tag = new PlcTag
{
Address = MonitorAddress,
DataType = MonitorDataType
};
var vm = new PlcTagViewModel(tag);
tag.OnChanged = (t, v) =>
{
// 使用Dispatcher在UI线程更新值,避免跨线程访问异常
_dispatcher.InvokeAsync(() =>
{
vm.UpdateValue();
});
};
_plcService.Subscribe(tag);
// 在UI线程添加到集合
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Add(vm);
});
AppendLog($"✅ 添加监控: {MonitorAddress} ({GetTypeName(MonitorDataType)})");
}
catch (Exception ex)
{
AppendLog($"❌ 添加监控失败: {ex.Message}");
_logger.LogError(ex, "添加监控失败");
}
}
/// <summary>
/// 移除监控
/// </summary>
private void RemoveMonitor(PlcTagViewModel? tagVm)
{
if (tagVm == null) return;
try
{
// 从PLC服务取消订阅
_plcService.Unsubscribe(tagVm.Id);
// 从UI集合移除
_dispatcher.InvokeAsync(() =>
{
MonitoredTags.Remove(tagVm);
});
AppendLog($"✅ 移除监控: {tagVm.Address}");
}
catch (Exception ex)
{
AppendLog($"❌ 移除监控失败: {ex.Message}");
_logger.LogError(ex, "移除监控失败");
}
}
/// <summary>
/// 添加到写入队列
/// </summary>
private void AddToQueue()
{
try
{
var value = ParseValue(WriteValue, WriteDataType);
if (value == null)
{
AppendLog($"❌ 添加到队列失败: 值格式不正确");
return;
}
var item = new WriteQueueItem
{
Order = WriteQueue.Count + 1,
Address = WriteAddress,
Value = value,
DataType = WriteDataType,
DataTypeName = GetTypeName(WriteDataType)
};
WriteQueue.Add(item);
AppendLog($"✅ 添加到队列: {WriteAddress} = {value} ({GetTypeName(WriteDataType)})");
// 更新序号
UpdateQueueOrder();
}
catch (Exception ex)
{
AppendLog($"❌ 添加到队列失败: {ex.Message}");
_logger.LogError(ex, "添加到队列失败");
}
}
/// <summary>
/// 从队列移除
/// </summary>
private void RemoveFromQueue(WriteQueueItem? item)
{
if (item == null) return;
WriteQueue.Remove(item);
UpdateQueueOrder();
AppendLog($"移除队列项: {item.Address}");
}
/// <summary>
/// 执行写入队列
/// </summary>
private async Task ExecuteQueueAsync()
{
var enabledItems = WriteQueue.Where(x => x.Enabled).ToList();
if (enabledItems.Count == 0)
{
AppendLog("⚠️ 队列中没有启用的项,无需执行");
return;
}
AppendLog($"开始执行写入队列 ({enabledItems.Count}/{WriteQueue.Count} 项启用)...");
int successCount = 0;
int failCount = 0;
int verifyFailCount = 0;
foreach (var item in enabledItems)
{
try
{
AppendLog($"[{item.Order}] 写入: {item.Address} = {item.Value} (类型: {item.DataTypeName})");
// 使用反射调用泛型方法
var method = _plcService.GetType().GetMethod(nameof(IPlcService.WriteAsync))!;
var task = (Task)method.Invoke(_plcService, new object[] { item.Address, item.Value })!;
await task.ConfigureAwait(false);
// 获取 Result 属性
var resultProperty = task.GetType().GetProperty("Result")!;
var result = resultProperty.GetValue(task);
// 检查 IsSuccess
var isSuccessProperty = result!.GetType().GetProperty("IsSuccess")!;
var isSuccess = (bool)isSuccessProperty.GetValue(result)!;
if (isSuccess)
{
AppendLog($" ✅ 写入成功");
// 校验
if (item.Verify)
{
AppendLog($" 🔍 开始校验...");
await Task.Delay(50); // 给PLC一点时间处理
var readMethod = _plcService.GetType().GetMethod(nameof(IPlcService.ReadAsync))!
.MakeGenericMethod(item.DataType);
var readTask = (Task)readMethod.Invoke(_plcService, new object[] { item.Address })!;
await readTask.ConfigureAwait(false);
var readResultProperty = readTask.GetType().GetProperty("Result")!;
var readResult = readResultProperty.GetValue(readTask);
var readIsSuccess = (bool)readResult!.GetType().GetProperty("IsSuccess")!.GetValue(readResult)!;
if (readIsSuccess)
{
var readValue = readResult.GetType().GetProperty("Value")!.GetValue(readResult);
if (Equals(readValue, item.Value))
{
AppendLog($" ✅ 校验通过: {readValue}");
}
else
{
verifyFailCount++;
AppendLog($" ❌ 校验失败: 期望 {item.Value}, 实际 {readValue}");
}
}
else
{
verifyFailCount++;
var errorMsg = readResult.GetType().GetProperty("ErrorMessage")!.GetValue(readResult);
AppendLog($" ❌ 校验失败: 读取错误 - {errorMsg}");
}
}
successCount++;
// 延迟
if (item.DelayMs > 0)
{
AppendLog($" ⏱️ 等待 {item.DelayMs} ms...");
await Task.Delay(item.DelayMs);
}
}
else
{
failCount++;
var errorProperty = result.GetType().GetProperty("ErrorMessage")!;
var errorMessage = errorProperty.GetValue(result)?.ToString() ?? "未知错误";
AppendLog($" ❌ 写入失败: {errorMessage}");
}
}
catch (Exception ex)
{
failCount++;
AppendLog($" ❌ 异常: {ex.Message}");
_logger.LogError(ex, "写入队列项失败");
}
}
AppendLog($"队列执行完成: 成功 {successCount}, 失败 {failCount}, 校验失败 {verifyFailCount}");
}
/// <summary>
/// 队列项上移
/// </summary>
private void MoveUpQueue(WriteQueueItem? item)
{
if (item == null) return;
var index = WriteQueue.IndexOf(item);
if (index > 0)
{
WriteQueue.Move(index, index - 1);
UpdateQueueOrder();
}
}
/// <summary>
/// 队列项下移
/// </summary>
private void MoveDownQueue(WriteQueueItem? item)
{
if (item == null) return;
var index = WriteQueue.IndexOf(item);
if (index < WriteQueue.Count - 1)
{
WriteQueue.Move(index, index + 1);
UpdateQueueOrder();
}
}
/// <summary>
/// 更新队列序号
/// </summary>
private void UpdateQueueOrder()
{
for (int i = 0; i < WriteQueue.Count; i++)
{
WriteQueue[i].Order = i + 1;
}
}
/// <summary>
/// 解析值
/// </summary>
private object? ParseValue(string valueStr, Type type)
{
try
{
if (type == typeof(bool))
return bool.Parse(valueStr);
if (type == typeof(byte))
return byte.Parse(valueStr);
if (type == typeof(short))
return short.Parse(valueStr);
if (type == typeof(ushort))
return ushort.Parse(valueStr);
if (type == typeof(int))
return int.Parse(valueStr);
if (type == typeof(uint))
return uint.Parse(valueStr);
if (type == typeof(long))
return long.Parse(valueStr);
if (type == typeof(ulong))
return ulong.Parse(valueStr);
if (type == typeof(float))
return float.Parse(valueStr);
if (type == typeof(double))
return double.Parse(valueStr);
if (type == typeof(string))
return valueStr;
return null;
}
catch
{
return null;
}
}
/// <summary>
/// 获取类型名称
/// </summary>
private string GetTypeName(Type type)
{
if (type == typeof(bool)) return "Bool";
if (type == typeof(byte)) return "Byte";
if (type == typeof(short)) return "Short";
if (type == typeof(ushort)) return "UShort";
if (type == typeof(int)) return "Int";
if (type == typeof(uint)) return "UInt";
if (type == typeof(long)) return "Long";
if (type == typeof(ulong)) return "ULong";
if (type == typeof(float)) return "Float";
if (type == typeof(double)) return "Double";
if (type == typeof(string)) return "String";
return type.Name;
}
/// <summary>
/// 保存队列到JSON文件
/// </summary>
private async Task SaveQueueAsync()
{
try
{
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcQueue.json");
var queueData = WriteQueue.Select(item => new
{
item.Order,
item.Address,
Value = item.Value.ToString(),
DataTypeName = item.DataTypeName,
item.Enabled,
item.DelayMs,
item.Verify
}).ToList();
var json = JsonSerializer.Serialize(queueData, new JsonSerializerOptions
{
WriteIndented = true
});
await File.WriteAllTextAsync(filePath, json);
AppendLog($"✅ 队列已保存到: {filePath}");
}
catch (Exception ex)
{
AppendLog($"❌ 保存失败: {ex.Message}");
_logger.LogError(ex, "保存队列失败");
}
}
/// <summary>
/// 从JSON文件加载队列
/// </summary>
private async Task LoadQueueAsync()
{
try
{
var filePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "PlcQueue.json");
if (!File.Exists(filePath))
{
AppendLog("⚠️ 队列文件不存在");
return;
}
var json = await File.ReadAllTextAsync(filePath);
var queueData = JsonSerializer.Deserialize<List<QueueItemData>>(json);
if (queueData == null || queueData.Count == 0)
{
AppendLog("⚠️ 队列文件为空");
return;
}
WriteQueue.Clear();
foreach (var data in queueData)
{
// 根据类型名称获取 Type
Type? dataType = data.DataTypeName switch
{
"Bool" => typeof(bool),
"Byte" => typeof(byte),
"Short" => typeof(short),
"UShort" => typeof(ushort),
"Int" => typeof(int),
"UInt" => typeof(uint),
"Long" => typeof(long),
"ULong" => typeof(ulong),
"Float" => typeof(float),
"Double" => typeof(double),
"String" => typeof(string),
_ => typeof(short)
};
// 解析值
var value = ParseValue(data.Value, dataType);
if (value == null) continue;
var item = new WriteQueueItem
{
Order = data.Order,
Address = data.Address,
Value = value,
DataType = dataType,
DataTypeName = data.DataTypeName,
Enabled = data.Enabled,
DelayMs = data.DelayMs,
Verify = data.Verify
};
WriteQueue.Add(item);
}
UpdateQueueOrder();
AppendLog($"✅ 已加载 {WriteQueue.Count} 个队列项");
}
catch (Exception ex)
{
AppendLog($"❌ 加载失败: {ex.Message}");
_logger.LogError(ex, "加载队列失败");
}
}
#endregion
}
/// <summary>
/// 队列项数据(用于JSON序列化)
/// </summary>
public class QueueItemData
{
public int Order { get; set; }
public string Address { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string DataTypeName { get; set; } = "Short";
public bool Enabled { get; set; } = true;
public int DelayMs { get; set; }
public bool Verify { get; set; }
}
/// <summary>
/// PLC 标签 ViewModel - 用于监控列表显示
/// </summary>
public class PlcTagViewModel : ViewModelBase
{
private readonly PlcTag _tag;
public PlcTagViewModel(PlcTag tag)
{
_tag = tag ?? throw new ArgumentNullException(nameof(tag));
}
public string Id => _tag.Id;
public string Address => _tag.Address;
public Type DataType => _tag.DataType;
public string DataTypeName
{
get
{
if (_tag.DataType == typeof(bool)) return "Bool";
if (_tag.DataType == typeof(byte)) return "Byte";
if (_tag.DataType == typeof(short)) return "Short";
if (_tag.DataType == typeof(ushort)) return "UShort";
if (_tag.DataType == typeof(int)) return "Int";
if (_tag.DataType == typeof(uint)) return "UInt";
if (_tag.DataType == typeof(long)) return "Long";
if (_tag.DataType == typeof(ulong)) return "ULong";
if (_tag.DataType == typeof(float)) return "Float";
if (_tag.DataType == typeof(double)) return "Double";
if (_tag.DataType == typeof(string)) return "String";
return _tag.DataType.Name;
}
}
public object? Value => _tag.Value;
public PlcTag Tag => _tag;
public void UpdateValue()
{
OnPropertyChanged(nameof(Value));
}
}
/// <summary>
/// 数据类型选项
/// </summary>
public record DataTypeOption(string Name, Type Type);
/// <summary>
/// 写入队列项
/// </summary>
public class WriteQueueItem : ViewModelBase
{
private int _order;
private string _address = string.Empty;
private object _value = 0;
private Type _dataType = typeof(short);
private string _dataTypeName = "Short";
private bool _enabled = true;
private int _delayMs = 0;
private bool _verify = false;
public int Order
{
get => _order;
set => SetProperty(ref _order, value);
}
public string Address
{
get => _address;
set => SetProperty(ref _address, value);
}
public object Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public Type DataType
{
get => _dataType;
set => SetProperty(ref _dataType, value);
}
public string DataTypeName
{
get => _dataTypeName;
set => SetProperty(ref _dataTypeName, value);
}
/// <summary>
/// 是否启用此项
/// </summary>
public bool Enabled
{
get => _enabled;
set => SetProperty(ref _enabled, value);
}
/// <summary>
/// 执行后延迟(毫秒)
/// </summary>
public int DelayMs
{
get => _delayMs;
set => SetProperty(ref _delayMs, value);
}
/// <summary>
/// 是否写入后校验
/// </summary>
public bool Verify
{
get => _verify;
set => SetProperty(ref _verify, value);
} }
} }
} }
@@ -5,8 +5,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol" xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="600" d:DesignHeight="1000"
d:DesignWidth="800"> d:DesignWidth="1200">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="20"> <StackPanel Margin="20">
<!-- 标题区域 --> <!-- 标题区域 -->
@@ -62,13 +62,14 @@
Padding="20" Padding="20"
Margin="0,0,0,15"> Margin="0,0,0,15">
<StackPanel> <StackPanel>
<TextBlock Text="单次读写测试 (数据类型: Short)" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/> <TextBlock Text="单次读写测试" FontSize="16" FontWeight="Bold" Margin="0,0,0,15"/>
<!-- 读取 --> <!-- 读取 -->
<Grid Margin="0,0,0,15"> <Grid Margin="0,0,0,15">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="80"/>
<ColumnDefinition Width="200"/> <ColumnDefinition Width="200"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="100"/> <ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -76,10 +77,15 @@
<TextBlock Text="读取地址:" VerticalAlignment="Center"/> <TextBlock Text="读取地址:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Padding="8,0" <TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Padding="8,0"
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/> Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
<Button Grid.Column="2" Content="读取" Height="32" Margin="0,0,10,0" <ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding ReadDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<Button Grid.Column="3" Content="读取" Height="32" Margin="0,0,10,0"
Command="{Binding ReadCommand}" Command="{Binding ReadCommand}"
Style="{StaticResource ButtonPrimary}"/> Style="{StaticResource ButtonPrimary}"/>
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" VerticalAlignment="Center" <TextBlock Grid.Column="4" Text="{Binding ReadValue}" VerticalAlignment="Center"
FontWeight="Bold" FontSize="14"/> FontWeight="Bold" FontSize="14"/>
</Grid> </Grid>
@@ -88,6 +94,8 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="80"/>
<ColumnDefinition Width="200"/> <ColumnDefinition Width="200"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/> <ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
@@ -95,15 +103,165 @@
<TextBlock Text="写入地址:" VerticalAlignment="Center"/> <TextBlock Text="写入地址:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Padding="8,0" <TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Padding="8,0"
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/> Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
<TextBox Grid.Column="2" Text="{Binding WriteValue}" Height="32" Padding="8,0" <ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding WriteDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<TextBox Grid.Column="3" Text="{Binding WriteValue}" Height="32" Padding="8,0"
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/> Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"/>
<Button Grid.Column="3" Content="写入" Width="100" Height="32" HorizontalAlignment="Left" <Button Grid.Column="4" Content="写入" Height="32" Margin="0,0,10,0"
Command="{Binding WriteCommand}" Command="{Binding WriteCommand}"
Style="{StaticResource ButtonWarning}"/> Style="{StaticResource ButtonWarning}"/>
<Button Grid.Column="5" Content="添加到队列" Width="100" Height="32" HorizontalAlignment="Left"
Command="{Binding AddToQueueCommand}"
Style="{StaticResource ButtonInfo}"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- 两列布局:监控列表 和 写入队列 -->
<Grid Margin="0,0,0,15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="15"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 监控列表 -->
<Border
Grid.Column="0"
Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="300"/>
</Grid.RowDefinitions>
<TextBlock Text="实时监控列表" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
<!-- 添加监控 -->
<Grid Grid.Row="1" Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding MonitorAddress}" Height="32" Padding="8,0"
Margin="0,0,10,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
<ComboBox Grid.Column="1" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding MonitorDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<Button Grid.Column="2" Content="添加监控" Height="32"
Command="{Binding AddMonitorCommand}"
Style="{StaticResource ButtonSuccess}"/>
</Grid>
<!-- 监控列表 -->
<DataGrid Grid.Row="2" ItemsSource="{Binding MonitoredTags}"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
CanUserResizeRows="False"
CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="150"/>
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="80"/>
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
<DataGridTemplateColumn Header="操作" Width="80">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="移除" Height="28" Padding="10,0"
Command="{Binding DataContext.RemoveMonitorCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDanger}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<!-- 写入队列 -->
<Border
Grid.Column="2"
Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="300"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="写入队列" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding WriteQueue.Count, StringFormat=({0} 项)}"
VerticalAlignment="Center" Margin="10,0,0,0"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
<!-- 队列列表 -->
<DataGrid Grid.Row="1" ItemsSource="{Binding WriteQueue}"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
CanUserResizeRows="False"
CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Binding="{Binding Order}" Width="40"/>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="120"/>
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="70"/>
<DataGridTextColumn Header="值" Binding="{Binding Value}" Width="80"/>
<DataGridTemplateColumn Header="操作" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="↑" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.MoveUpQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDefault}"/>
<Button Content="↓" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.MoveDownQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDefault}"/>
<Button Content="×" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.RemoveFromQueueCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDanger}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- 队列操作按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,10,0,0" HorizontalAlignment="Right">
<Button Content="执行队列" Width="100" Height="35" Margin="0,0,10,0"
Command="{Binding ExecuteQueueCommand}"
Style="{StaticResource ButtonSuccess}"/>
<Button Content="清空队列" Width="100" Height="35"
Command="{Binding ClearQueueCommand}"
Style="{StaticResource ButtonWarning}"/>
</StackPanel>
</Grid>
</Border>
</Grid>
<!-- 日志区域 --> <!-- 日志区域 -->
<Border <Border
Background="{DynamicResource RegionBrush}" Background="{DynamicResource RegionBrush}"
@@ -3,10 +3,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:hc="https://handyorg.github.io/handycontrol"
mc:Ignorable="d" mc:Ignorable="d"
Title="PLC 调试工具" Title="PLC 调试工具"
Height="650" Height="800"
Width="900" Width="1200"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<Grid> <Grid>
@@ -18,29 +19,42 @@
<!-- 工具栏 --> <!-- 工具栏 -->
<Border Grid.Row="0" Background="{DynamicResource RegionBrush}" <Border Grid.Row="0" Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1" Padding="15,10"> BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,0,1" Padding="15,10">
<StackPanel Orientation="Horizontal"> <Grid>
<TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"/> <Grid.RowDefinitions>
<TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0" <StackPanel Orientation="Horizontal">
Command="{Binding ConnectCommand}"/> <TextBlock Text="IP 地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0" <TextBox Text="{Binding IpAddress}" Width="150" Height="28" Margin="0,0,10,0"/>
Command="{Binding DisconnectCommand}"/>
<Separator Margin="10,0"/> <Button Content="🔗 连接" Width="90" Height="32" Margin="0,0,10,0"
Command="{Binding ConnectCommand}" Style="{StaticResource ButtonSuccess}"/>
<Button Content="🔌 断开" Width="90" Height="32" Margin="0,0,10,0"
Command="{Binding DisconnectCommand}" Style="{StaticResource ButtonDanger}"/>
<TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0"/> <Separator Margin="10,0"/>
<TextBlock Text="{Binding Status}" FontWeight="Bold" VerticalAlignment="Center"
Foreground="{DynamicResource SuccessBrush}"/> <TextBlock Text="状态:" VerticalAlignment="Center" Margin="0,0,10,0"/>
</StackPanel> <TextBlock Text="{Binding Status}" FontWeight="Bold" VerticalAlignment="Center"
Foreground="{DynamicResource SuccessBrush}"/>
</StackPanel>
<!-- 最新日志 -->
<TextBlock Grid.Row="1" Text="{Binding LastLogMessage}"
Foreground="{DynamicResource SecondaryTextBrush}"
FontSize="11" Margin="0,5,0,0" TextTrimming="CharacterEllipsis"/>
</Grid>
</Border> </Border>
<!-- 主内容区 --> <!-- 主内容区 -->
<TabControl Grid.Row="1" Margin="15"> <TabControl Grid.Row="1" Margin="10">
<!-- 单次读写 --> <!-- 单次读写 -->
<TabItem Header="📝 单次读写"> <TabItem Header="📝 单次读写">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="15"> <StackPanel Margin="15">
<!-- 读取 -->
<Border Background="{DynamicResource RegionBrush}" <Border Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1" CornerRadius="8" Padding="20" Margin="0,0,0,15"> BorderThickness="1" CornerRadius="8" Padding="20" Margin="0,0,0,15">
@@ -50,20 +64,27 @@
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="250"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/> <TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Margin="0,0,15,0"/> <TextBox Grid.Column="1" Text="{Binding ReadAddress}" Height="32" Margin="0,0,10,0"/>
<Button Grid.Column="2" Content="读取" Width="100" Height="32" Margin="0,0,15,0" <ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding ReadDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<Button Grid.Column="3" Content="读取" Width="100" Height="32" Margin="0,0,10,0"
Command="{Binding ReadCommand}" Style="{StaticResource ButtonPrimary}"/> Command="{Binding ReadCommand}" Style="{StaticResource ButtonPrimary}"/>
<TextBlock Grid.Column="3" Text="{Binding ReadValue}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/> <TextBlock Grid.Column="4" Text="{Binding ReadValue}" FontWeight="Bold" FontSize="14" VerticalAlignment="Center"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
<!-- 写入 -->
<Border Background="{DynamicResource RegionBrush}" <Border Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1" CornerRadius="8" Padding="20"> BorderThickness="1" CornerRadius="8" Padding="20">
@@ -77,17 +98,26 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="250"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/> <TextBlock Text="地址:" VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,0,10"/> <TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,10,0"/>
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding WriteDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<TextBlock Grid.Row="1" Text="值:" VerticalAlignment="Center" Margin="0,0,10,0"/> <TextBlock Grid.Row="1" Text="值:" VerticalAlignment="Center" Margin="0,10,10,0"/>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal"> <TextBox Grid.Row="1" Grid.Column="1" Text="{Binding WriteValue}" Height="32" Margin="0,10,10,0"/>
<TextBox Text="{Binding WriteValue}" Width="150" Height="32" Margin="0,0,10,0"/> <StackPanel Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2" Orientation="Horizontal" Margin="0,10,0,0">
<Button Content="写入" Width="100" Height="32" <Button Content="写入" Width="100" Height="32" Margin="0,0,10,0"
Command="{Binding WriteCommand}"/> Command="{Binding WriteCommand}" Style="{StaticResource ButtonWarning}"/>
<Button Content="添加到队列" Width="120" Height="32"
Command="{Binding AddToQueueCommand}" Style="{StaticResource ButtonInfo}"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</StackPanel> </StackPanel>
@@ -96,34 +126,186 @@
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<!-- 日志 --> <!-- 实时监控 -->
<TabItem Header="📋 操作日志"> <TabItem Header="📊 实时监控">
<Border Margin="15" Background="{DynamicResource RegionBrush}" <Grid Margin="15">
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1" CornerRadius="8"> <Grid.RowDefinitions>
<Grid Margin="15"> <RowDefinition Height="Auto"/>
<Grid.RowDefinitions> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> </Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10"> <!-- 添加监控 -->
<TextBlock Text="操作日志" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/> <Border Background="{DynamicResource RegionBrush}"
<Button Content="清空" Width="80" Height="30" Margin="15,0,0,0" BorderBrush="{DynamicResource BorderBrush}"
Command="{Binding ClearLogCommand}"/> BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
</StackPanel> <Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"> <TextBlock Text="添加监控:" VerticalAlignment="Center" Margin="0,0,10,0" FontWeight="Bold"/>
<TextBox Text="{Binding LogText, Mode=OneWay}" <TextBox Grid.Column="1" Text="{Binding MonitorAddress}" Height="32" Margin="0,0,10,0"
IsReadOnly="True" hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
TextWrapping="Wrap" <ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
Background="{DynamicResource SecondaryRegionBrush}" ItemsSource="{Binding SupportedDataTypes}"
BorderThickness="0" SelectedValue="{Binding MonitorDataType}"
FontFamily="Consolas" SelectedValuePath="Type"
FontSize="11" DisplayMemberPath="Name"/>
Padding="10"/> <Button Grid.Column="3" Content="添加监控" Width="100" Height="32"
</ScrollViewer> Command="{Binding AddMonitorCommand}" Style="{StaticResource ButtonSuccess}"/>
</Grid> </Grid>
</Border> </Border>
<!-- 监控列表 -->
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1" CornerRadius="8" Padding="15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="监控列表" FontSize="14" FontWeight="Bold" Margin="0,0,0,10"/>
<DataGrid Grid.Row="1" ItemsSource="{Binding MonitoredTags}"
AutoGenerateColumns="False"
IsReadOnly="True"
HeadersVisibility="Column"
CanUserResizeRows="False"
CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="200"/>
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="100"/>
<DataGridTextColumn Header="当前值" Binding="{Binding Value}" Width="*"/>
<DataGridTemplateColumn Header="操作" Width="100">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="移除" Height="28" Padding="10,0"
Command="{Binding DataContext.RemoveMonitorCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDanger}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
</Grid>
</TabItem>
<!-- 写入队列 -->
<TabItem Header="📋 写入队列">
<Grid Margin="15">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 添加队列项 -->
<Border Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="添加项目:" VerticalAlignment="Center" Margin="0,0,10,0" FontWeight="Bold"/>
<TextBox Grid.Column="1" Text="{Binding WriteAddress}" Height="32" Margin="0,0,10,0"
hc:InfoElement.Placeholder="地址 (如 DB1.DBW0)"/>
<ComboBox Grid.Column="2" Height="32" Margin="0,0,10,0"
ItemsSource="{Binding SupportedDataTypes}"
SelectedValue="{Binding WriteDataType}"
SelectedValuePath="Type"
DisplayMemberPath="Name"/>
<TextBox Grid.Column="3" Text="{Binding WriteValue}" Height="32" Margin="0,0,10,0"
hc:InfoElement.Placeholder="值"/>
<Button Grid.Column="4" Content="添加到队列" Width="120" Height="32"
Command="{Binding AddToQueueCommand}" Style="{StaticResource ButtonSuccess}"/>
</Grid>
</Border>
<!-- 队列列表 -->
<Border Grid.Row="1" Background="{DynamicResource RegionBrush}"
BorderBrush="{DynamicResource BorderBrush}"
BorderThickness="1" CornerRadius="8" Padding="15" Margin="0,0,0,15">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="队列列表" FontSize="14" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Text="{Binding WriteQueue.Count, StringFormat=({0} 项)}"
VerticalAlignment="Center" Margin="10,0,0,0"
Foreground="{DynamicResource SecondaryTextBrush}"/>
</StackPanel>
<DataGrid Grid.Row="1" ItemsSource="{Binding WriteQueue}"
AutoGenerateColumns="False"
HeadersVisibility="Column"
CanUserResizeRows="False"
CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="#" Binding="{Binding Order}" Width="40" IsReadOnly="True"/>
<DataGridCheckBoxColumn Header="启用" Binding="{Binding Enabled}" Width="50"/>
<DataGridTextColumn Header="地址" Binding="{Binding Address}" Width="140" IsReadOnly="True"/>
<DataGridTextColumn Header="类型" Binding="{Binding DataTypeName}" Width="80" IsReadOnly="True"/>
<DataGridTextColumn Header="值" Binding="{Binding Value}" Width="80" IsReadOnly="True"/>
<DataGridTextColumn Header="延迟(ms)" Binding="{Binding DelayMs}" Width="80"/>
<DataGridCheckBoxColumn Header="校验" Binding="{Binding Verify}" Width="50"/>
<DataGridTemplateColumn Header="操作" Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="↑" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.MoveUpQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDefault}"/>
<Button Content="↓" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.MoveDownQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDefault}"/>
<Button Content="×" Width="30" Height="28" Margin="2"
Command="{Binding DataContext.RemoveFromQueueCommand, RelativeSource={RelativeSource AncestorType=Window}}"
CommandParameter="{Binding}"
Style="{StaticResource ButtonDanger}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Border>
<!-- 队列操作按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="保存队列" Width="100" Height="38" Margin="0,0,10,0"
Command="{Binding SaveQueueCommand}"
Style="{StaticResource ButtonInfo}"/>
<Button Content="加载队列" Width="100" Height="38" Margin="0,0,10,0"
Command="{Binding LoadQueueCommand}"
Style="{StaticResource ButtonInfo}"/>
<Button Content="执行队列" Width="100" Height="38" Margin="0,0,10,0"
Command="{Binding ExecuteQueueCommand}"
Style="{StaticResource ButtonSuccess}"/>
<Button Content="清空队列" Width="100" Height="38"
Command="{Binding ClearQueueCommand}"
Style="{StaticResource ButtonWarning}"/>
</StackPanel>
</Grid>
</TabItem> </TabItem>
</TabControl> </TabControl>
</Grid> </Grid>