A
This commit is contained in:
1 parent
012c9522f8
commit
54545aad35
12 files changed
+2332
-161
No files matched your search
@@ -8,8 +8,7 @@ using Microsoft.Extensions.Configuration;
|
|||||||
using Common;
|
using Common;
|
||||||
using Config;
|
using Config;
|
||||||
using Sharp.Log.Extensions;
|
using Sharp.Log.Extensions;
|
||||||
using Sharp.Wpf.Services;
|
using Sharp.Wpf.DependencyInjection;
|
||||||
using Sharp.Wpf.Abstractions;
|
|
||||||
using Sharp.Wpf.HandyControl.DependencyInjection;
|
using Sharp.Wpf.HandyControl.DependencyInjection;
|
||||||
using Sharp.Wpf.DevTools.DependencyInjection;
|
using Sharp.Wpf.DevTools.DependencyInjection;
|
||||||
using Sharp.NetServer.DependencyInjection;
|
using Sharp.NetServer.DependencyInjection;
|
||||||
@@ -44,41 +43,38 @@ namespace Sharp.Demo
|
|||||||
// === 1. 全局异常处理 ===
|
// === 1. 全局异常处理 ===
|
||||||
RegisterExceptionHandlers();
|
RegisterExceptionHandlers();
|
||||||
|
|
||||||
// === 2. 配置管理 ===
|
// === 2. 配置管理(Quick Start API)===
|
||||||
var cm = ConfigBuilder.BuildFromFiles(
|
var configManager = ConfigBuilderExtensions.CreateDefault(
|
||||||
new[] { "appsettings.json" },
|
basePath: AppContext.BaseDirectory,
|
||||||
reloadOnChange: true,
|
environmentPrefix: "APP_"
|
||||||
includeEnvironment: true,
|
|
||||||
basePath: AppContext.BaseDirectory
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// === 3. 依赖注入容器 ===
|
// === 3. 依赖注入容器(Quick Start API)===
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
// 配置
|
// 基础服务:配置 + 日志 + 缓存(一行搞定)
|
||||||
services.AddSingleton(cm);
|
services.AddSingleton(configManager);
|
||||||
services.AddSingleton<IConfiguration>(cm.Configuration);
|
services.AddSingleton<IConfiguration>(configManager.Configuration);
|
||||||
|
|
||||||
// 日志
|
|
||||||
services.AddLogging(b => b.AddSharpLog());
|
services.AddLogging(b => b.AddSharpLog());
|
||||||
|
services.AddMemoryCache();
|
||||||
|
|
||||||
// 数据库
|
// 数据库(Quick Start API)
|
||||||
services.AddSingleton<IDataContext>(sp =>
|
services.AddSingleton<IDataContext>(sp =>
|
||||||
{
|
{
|
||||||
var builder = new DataContextBuilder<AppDbContext>();
|
var builder = new DataContextBuilder<AppDbContext>();
|
||||||
builder.UseSqlite("Data Source=demo.db");
|
builder.UseSqlite("Data Source=demo.db")
|
||||||
builder.UseAudit(true);
|
.UseAudit(true)
|
||||||
builder.UseSoftDelete(true);
|
.UseSoftDelete(true)
|
||||||
builder.UseCurrentUser("system");
|
.UseCurrentUser("system")
|
||||||
builder.EnableDetailedErrors(true);
|
.EnableDetailedErrors(true);
|
||||||
return builder.Build();
|
return builder.Build();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 网络模块
|
// 网络模块(从配置读取)
|
||||||
services.AddNetClient(cm);
|
services.AddNetClient(configManager);
|
||||||
services.AddNetServer(cm);
|
services.AddNetServer(configManager);
|
||||||
|
|
||||||
// 工业控制
|
// 工业控制(从配置读取或代码配置)
|
||||||
services.AddSiemensPlc(options =>
|
services.AddSiemensPlc(options =>
|
||||||
{
|
{
|
||||||
options.IpAddress = "127.0.0.1";
|
options.IpAddress = "127.0.0.1";
|
||||||
@@ -90,61 +86,56 @@ namespace Sharp.Demo
|
|||||||
services.AddSharpProcess();
|
services.AddSharpProcess();
|
||||||
services.AddSharpPython();
|
services.AddSharpPython();
|
||||||
|
|
||||||
// WPF 服务
|
// WPF 服务(Quick Start API)
|
||||||
services.AddSharpHandyControl(); // HandyControl 对话框服务
|
services.AddSharpWpf(); // 标准 WPF 服务(对话框、调度器等)
|
||||||
|
services.AddSharpHandyControl(); // HandyControl UI 库集成
|
||||||
services.AddSharpDevTools(); // 开发者工具
|
services.AddSharpDevTools(); // 开发者工具
|
||||||
services.AddSingleton<IDispatcher>(new DispatcherService(Dispatcher));
|
|
||||||
|
|
||||||
// 业务服务(单例)
|
// 业务服务(单例)
|
||||||
services.AddSingleton<ServerService>();
|
services.AddSingleton<ServerService>();
|
||||||
services.AddSingleton<WindowManager>(); // 窗口管理器
|
services.AddSingleton<WindowManager>();
|
||||||
|
services.AddSingleton<IApplicationState, ApplicationState>();
|
||||||
|
|
||||||
// ViewModels(瞬态 - 每次导航创建新实例)
|
// ViewModels(瞬态)
|
||||||
services.AddTransient<LauncherViewModel>(); // 启动器 ViewModel
|
services.AddViewModel<LauncherViewModel>();
|
||||||
services.AddTransient<MainViewModel>();
|
services.AddViewModel<MainViewModel>();
|
||||||
services.AddTransient<HomeViewModel>();
|
services.AddViewModel<HomeViewModel>();
|
||||||
|
|
||||||
// P0 页面 ViewModels(优先实现)
|
// 页面 ViewModels
|
||||||
services.AddTransient<HttpServerViewModel>();
|
services.AddViewModel<HttpServerViewModel>();
|
||||||
services.AddTransient<DatabaseViewModel>();
|
services.AddViewModel<DatabaseViewModel>();
|
||||||
services.AddTransient<PlcDebugViewModel>();
|
services.AddViewModel<PlcDebugViewModel>();
|
||||||
|
services.AddViewModel<SerialPortViewModel>();
|
||||||
|
services.AddViewModel<ProcessTestViewModel>();
|
||||||
|
services.AddViewModel<HttpClientViewModel>();
|
||||||
|
services.AddViewModel<LogDemoViewModel>();
|
||||||
|
services.AddViewModel<ConfigDemoViewModel>();
|
||||||
|
services.AddViewModel<WpfServicesViewModel>();
|
||||||
|
|
||||||
// P1 页面 ViewModels(已实现)
|
// 窗口 ViewModels
|
||||||
services.AddTransient<SerialPortViewModel>();
|
services.AddViewModel<Sharp.Demo.ViewModels.Windows.HttpServerViewModel>();
|
||||||
services.AddTransient<ProcessTestViewModel>();
|
services.AddViewModel<Sharp.Demo.ViewModels.Windows.HttpClientViewModel>();
|
||||||
|
services.AddViewModel<Sharp.Demo.ViewModels.Windows.PlcDebugViewModel>();
|
||||||
// 新增页面 ViewModels
|
services.AddViewModel<Sharp.Demo.ViewModels.Windows.ProcessTestViewModel>();
|
||||||
services.AddTransient<HttpClientViewModel>();
|
|
||||||
services.AddTransient<LogDemoViewModel>();
|
|
||||||
services.AddTransient<ConfigDemoViewModel>();
|
|
||||||
services.AddTransient<WpfServicesViewModel>();
|
|
||||||
|
|
||||||
// Views(瞬态)
|
// Views(瞬态)
|
||||||
services.AddTransient<LauncherWindow>(); // 启动器窗口
|
services.AddTransient<LauncherWindow>();
|
||||||
services.AddTransient<MainWindow>();
|
services.AddTransient<MainWindow>();
|
||||||
|
|
||||||
// 模块窗口(瞬态 - 每次打开创建新实例)
|
|
||||||
services.AddTransient<Sharp.Demo.Views.Windows.HttpServerWindow>();
|
services.AddTransient<Sharp.Demo.Views.Windows.HttpServerWindow>();
|
||||||
services.AddTransient<Sharp.Demo.Views.Windows.HttpClientWindow>();
|
services.AddTransient<Sharp.Demo.Views.Windows.HttpClientWindow>();
|
||||||
services.AddTransient<Sharp.Demo.Views.Windows.PlcDebugWindow>();
|
services.AddTransient<Sharp.Demo.Views.Windows.PlcDebugWindow>();
|
||||||
services.AddTransient<Sharp.Demo.Views.Windows.DatabaseWindow>();
|
services.AddTransient<Sharp.Demo.Views.Windows.DatabaseWindow>();
|
||||||
// TODO: 添加其他窗口注册
|
services.AddTransient<Sharp.Demo.Views.Windows.SerialPortWindow>();
|
||||||
// services.AddTransient<Sharp.Demo.Views.Windows.SerialPortWindow>();
|
services.AddTransient<Sharp.Demo.Views.Windows.ProcessTestWindow>();
|
||||||
// etc...
|
|
||||||
|
|
||||||
// 模块窗口 ViewModels(瞬态)
|
|
||||||
services.AddTransient<Sharp.Demo.ViewModels.Windows.HttpServerViewModel>();
|
|
||||||
services.AddTransient<Sharp.Demo.ViewModels.Windows.HttpClientViewModel>();
|
|
||||||
services.AddTransient<Sharp.Demo.ViewModels.Windows.PlcDebugViewModel>();
|
|
||||||
// TODO: 添加其他 ViewModel 注册
|
|
||||||
|
|
||||||
// === 4. 构建容器 ===
|
// === 4. 构建容器 ===
|
||||||
_serviceProvider = services.BuildServiceProvider();
|
_serviceProvider = services.BuildServiceProvider();
|
||||||
|
|
||||||
// === 5. 全局容器(兼容旧代码) ===
|
// === 5. Global 容器演示(仅用于演示目的,不推荐在生产代码中使用)===
|
||||||
|
#pragma warning disable CS0618 // 禁用 Obsolete 警告(这里是演示用途)
|
||||||
Global.RegisterSingleton<IServiceProvider>(_serviceProvider);
|
Global.RegisterSingleton<IServiceProvider>(_serviceProvider);
|
||||||
Global.RegisterSingleton(cm);
|
Global.RegisterSingleton<IConfiguration>(configManager.Configuration);
|
||||||
Global.RegisterSingleton(_serviceProvider.GetRequiredService<IDispatcher>());
|
#pragma warning restore CS0618
|
||||||
|
|
||||||
// === 6. 显示启动器窗口 ===
|
// === 6. 显示启动器窗口 ===
|
||||||
var launcherWindow = _serviceProvider.GetRequiredService<LauncherWindow>();
|
var launcherWindow = _serviceProvider.GetRequiredService<LauncherWindow>();
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace Sharp.Demo.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 应用程序状态管理服务实现
|
||||||
|
/// 单例服务,全局共享状态
|
||||||
|
/// </summary>
|
||||||
|
public class ApplicationState : IApplicationState, INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
private string _theme = "Light";
|
||||||
|
private string _language = "zh-CN";
|
||||||
|
private string? _userDisplayName = "演示用户";
|
||||||
|
private bool _isDebugMode = false;
|
||||||
|
|
||||||
|
public ApplicationState()
|
||||||
|
{
|
||||||
|
StartupTime = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 属性
|
||||||
|
|
||||||
|
public string Theme
|
||||||
|
{
|
||||||
|
get => _theme;
|
||||||
|
set => SetProperty(ref _theme, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Language
|
||||||
|
{
|
||||||
|
get => _language;
|
||||||
|
set => SetProperty(ref _language, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UserDisplayName
|
||||||
|
{
|
||||||
|
get => _userDisplayName;
|
||||||
|
set => SetProperty(ref _userDisplayName, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime StartupTime { get; }
|
||||||
|
|
||||||
|
public TimeSpan Uptime => DateTime.Now - StartupTime;
|
||||||
|
|
||||||
|
public bool IsDebugMode
|
||||||
|
{
|
||||||
|
get => _isDebugMode;
|
||||||
|
set => SetProperty(ref _isDebugMode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 事件
|
||||||
|
|
||||||
|
public event EventHandler<StateChangedEventArgs>? StateChanged;
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 方法
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置所有状态为默认值
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Theme = "Light";
|
||||||
|
Language = "zh-CN";
|
||||||
|
UserDisplayName = "演示用户";
|
||||||
|
IsDebugMode = false;
|
||||||
|
|
||||||
|
OnStateChanged(new StateChangedEventArgs("All", null, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取状态摘要信息
|
||||||
|
/// </summary>
|
||||||
|
public string GetStateSummary()
|
||||||
|
{
|
||||||
|
return $"主题: {Theme}, 语言: {Language}, 用户: {UserDisplayName ?? "未设置"}, " +
|
||||||
|
$"调试模式: {(IsDebugMode ? "启用" : "禁用")}, 运行时长: {Uptime:hh\\:mm\\:ss}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 私有方法
|
||||||
|
|
||||||
|
protected virtual void SetProperty<T>(
|
||||||
|
ref T field,
|
||||||
|
T value,
|
||||||
|
[CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
if (Equals(field, value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var oldValue = field;
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
OnPropertyChanged(propertyName);
|
||||||
|
OnStateChanged(new StateChangedEventArgs(propertyName ?? string.Empty, oldValue, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void OnStateChanged(StateChangedEventArgs e)
|
||||||
|
{
|
||||||
|
StateChanged?.Invoke(this, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Sharp.Demo.Services
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 应用程序状态管理服务
|
||||||
|
/// 用于跨页面/组件共享应用级别的状态
|
||||||
|
/// </summary>
|
||||||
|
public interface IApplicationState
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 当前主题(Light/Dark/Auto)
|
||||||
|
/// </summary>
|
||||||
|
string Theme { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前语言(zh-CN/en-US)
|
||||||
|
/// </summary>
|
||||||
|
string Language { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户显示名称(可选)
|
||||||
|
/// </summary>
|
||||||
|
string? UserDisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用启动时间
|
||||||
|
/// </summary>
|
||||||
|
DateTime StartupTime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用运行时长
|
||||||
|
/// </summary>
|
||||||
|
TimeSpan Uptime { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 调试模式是否启用
|
||||||
|
/// </summary>
|
||||||
|
bool IsDebugMode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态变化事件
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<StateChangedEventArgs>? StateChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置所有状态为默认值
|
||||||
|
/// </summary>
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取状态摘要信息
|
||||||
|
/// </summary>
|
||||||
|
string GetStateSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态变化事件参数
|
||||||
|
/// </summary>
|
||||||
|
public class StateChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public string PropertyName { get; }
|
||||||
|
public object? OldValue { get; }
|
||||||
|
public object? NewValue { get; }
|
||||||
|
|
||||||
|
public StateChangedEventArgs(string propertyName, object? oldValue, object? newValue)
|
||||||
|
{
|
||||||
|
PropertyName = propertyName;
|
||||||
|
OldValue = oldValue;
|
||||||
|
NewValue = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ using System.Linq;
|
|||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Common;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
using Sharp.Demo.Services;
|
using Sharp.Demo.Services;
|
||||||
@@ -20,6 +19,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
private readonly WindowManager _windowManager;
|
private readonly WindowManager _windowManager;
|
||||||
private readonly ServerService _serverService;
|
private readonly ServerService _serverService;
|
||||||
private readonly ILogger<LauncherViewModel> _logger;
|
private readonly ILogger<LauncherViewModel> _logger;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly DispatcherTimer _timer;
|
private readonly DispatcherTimer _timer;
|
||||||
private readonly DateTime _startTime;
|
private readonly DateTime _startTime;
|
||||||
|
|
||||||
@@ -28,11 +28,16 @@ namespace Sharp.Demo.ViewModels
|
|||||||
private int _openWindowCount = 0;
|
private int _openWindowCount = 0;
|
||||||
private string _uptime = "00:00:00";
|
private string _uptime = "00:00:00";
|
||||||
|
|
||||||
public LauncherViewModel(WindowManager windowManager, ServerService serverService, ILogger<LauncherViewModel> logger)
|
public LauncherViewModel(
|
||||||
|
WindowManager windowManager,
|
||||||
|
ServerService serverService,
|
||||||
|
ILogger<LauncherViewModel> logger,
|
||||||
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
_windowManager = windowManager ?? throw new ArgumentNullException(nameof(windowManager));
|
_windowManager = windowManager ?? throw new ArgumentNullException(nameof(windowManager));
|
||||||
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
|
_serverService = serverService ?? throw new ArgumentNullException(nameof(serverService));
|
||||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||||
_startTime = DateTime.Now;
|
_startTime = DateTime.Now;
|
||||||
|
|
||||||
_logger.LogInformation("启动器 ViewModel 已初始化");
|
_logger.LogInformation("启动器 ViewModel 已初始化");
|
||||||
@@ -173,9 +178,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
Description = "Python 脚本执行和集成",
|
Description = "Python 脚本执行和集成",
|
||||||
Icon = "🐍",
|
Icon = "🐍",
|
||||||
Category = "进程管理",
|
Category = "进程管理",
|
||||||
Status = "开发中",
|
OpenCommand = new RelayCommand(() => OpenModuleWindow("PythonTest"))
|
||||||
IsEnabled = false,
|
|
||||||
OpenCommand = new RelayCommand(() => { /* 待实现 */ })
|
|
||||||
});
|
});
|
||||||
ModuleCategories.Add(processCategory);
|
ModuleCategories.Add(processCategory);
|
||||||
|
|
||||||
@@ -242,9 +245,18 @@ namespace Sharp.Demo.ViewModels
|
|||||||
_windowManager.ShowWindow<Sharp.Demo.Views.Windows.DatabaseWindow>(moduleKey);
|
_windowManager.ShowWindow<Sharp.Demo.Views.Windows.DatabaseWindow>(moduleKey);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// 其他窗口待实现
|
|
||||||
case "SerialPort":
|
case "SerialPort":
|
||||||
|
_logger.LogInformation("正在打开串口调试窗口...");
|
||||||
|
_windowManager.ShowWindow<Sharp.Demo.Views.Windows.SerialPortWindow>(moduleKey);
|
||||||
|
break;
|
||||||
|
|
||||||
case "ProcessTest":
|
case "ProcessTest":
|
||||||
|
case "PythonTest":
|
||||||
|
_logger.LogInformation("正在打开Process/Python测试窗口...");
|
||||||
|
_windowManager.ShowWindow<Sharp.Demo.Views.Windows.ProcessTestWindow>(moduleKey);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 其他窗口待实现
|
||||||
case "LogDemo":
|
case "LogDemo":
|
||||||
case "ConfigDemo":
|
case "ConfigDemo":
|
||||||
case "WpfServices":
|
case "WpfServices":
|
||||||
@@ -289,8 +301,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("用户点击了 F12 开发者工具");
|
_logger.LogInformation("用户点击了 F12 开发者工具");
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.DevToolsWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.DevToolsWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -305,8 +316,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("用户点击了 F11 日志查看器");
|
_logger.LogInformation("用户点击了 F11 日志查看器");
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.LogViewerWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.LogViewerWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -321,8 +331,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("用户点击了 F10 性能监控");
|
_logger.LogInformation("用户点击了 F10 性能监控");
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.PerformanceMonitorWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.PerformanceMonitorWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Common;
|
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Abstractions;
|
using Sharp.Wpf.Abstractions;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
@@ -192,8 +191,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.DevToolsWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.DevToolsWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -206,8 +204,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.LogViewerWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.LogViewerWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -220,8 +217,7 @@ namespace Sharp.Demo.ViewModels
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var sp = Global.Get<IServiceProvider>();
|
var window = _serviceProvider.GetService<Sharp.Wpf.DevTools.Views.PerformanceMonitorWindow>();
|
||||||
var window = sp?.GetService<Sharp.Wpf.DevTools.Views.PerformanceMonitorWindow>();
|
|
||||||
window?.Show();
|
window?.Show();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Windows;
|
|||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Common.Results;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
using Sharp.Wpf.Navigation;
|
using Sharp.Wpf.Navigation;
|
||||||
@@ -102,48 +103,58 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
|
|
||||||
#region 命令实现
|
#region 命令实现
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动服务器(演示 Result<T> 模式 - Railway-Oriented Programming)
|
||||||
|
/// </summary>
|
||||||
private void Start()
|
private void Start()
|
||||||
{
|
{
|
||||||
try
|
// ✅ 最佳实践:使用 Result.Try() 包装可能抛出异常的操作
|
||||||
{
|
Result.Try(() => _serverService.Start())
|
||||||
_serverService.Start();
|
.OnSuccess(() =>
|
||||||
UpdateButtonStates();
|
{
|
||||||
}
|
UpdateButtonStates();
|
||||||
catch (Exception ex)
|
AppendLog("✅ 服务器启动成功");
|
||||||
{
|
})
|
||||||
_logger.LogError(ex, "启动服务器失败");
|
.OnFailure((ex, msg) =>
|
||||||
AppendLog($"❌ 启动失败: {ex.Message}");
|
{
|
||||||
MessageBox.Show($"启动服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
_logger.LogError(ex, "启动服务器失败");
|
||||||
}
|
AppendLog($"❌ 启动失败: {msg}");
|
||||||
|
MessageBox.Show($"启动服务器失败: {msg}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止服务器(演示 Result<T> 模式)
|
||||||
|
/// </summary>
|
||||||
private void Stop()
|
private void Stop()
|
||||||
{
|
{
|
||||||
try
|
Result.Try(() => _serverService.Stop())
|
||||||
{
|
.OnSuccess(() =>
|
||||||
_serverService.Stop();
|
{
|
||||||
UpdateButtonStates();
|
UpdateButtonStates();
|
||||||
}
|
AppendLog("✅ 服务器停止成功");
|
||||||
catch (Exception ex)
|
})
|
||||||
{
|
.OnFailure((ex, msg) =>
|
||||||
_logger.LogError(ex, "停止服务器失败");
|
{
|
||||||
AppendLog($"❌ 停止失败: {ex.Message}");
|
_logger.LogError(ex, "停止服务器失败");
|
||||||
MessageBox.Show($"停止服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
AppendLog($"❌ 停止失败: {msg}");
|
||||||
}
|
MessageBox.Show($"停止服务器失败: {msg}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重启服务器(演示 Result<T> 模式)
|
||||||
|
/// </summary>
|
||||||
private void Restart()
|
private void Restart()
|
||||||
{
|
{
|
||||||
try
|
Result.Try(() => _serverService.Restart())
|
||||||
{
|
.OnSuccess(() => AppendLog("✅ 服务器重启成功"))
|
||||||
_serverService.Restart();
|
.OnFailure((ex, msg) =>
|
||||||
}
|
{
|
||||||
catch (Exception ex)
|
_logger.LogError(ex, "重启服务器失败");
|
||||||
{
|
AppendLog($"❌ 重启失败: {msg}");
|
||||||
_logger.LogError(ex, "重启服务器失败");
|
MessageBox.Show($"重启服务器失败: {msg}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
AppendLog($"❌ 重启失败: {ex.Message}");
|
});
|
||||||
MessageBox.Show($"重启服务器失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearLog()
|
private void ClearLog()
|
||||||
|
|||||||
@@ -1,15 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using Sharp.Wpf.Mvvm;
|
using Sharp.Wpf.Mvvm;
|
||||||
using Sharp.Wpf.Commands;
|
using Sharp.Wpf.Commands;
|
||||||
|
using Sharp.Wpf.Services;
|
||||||
|
using Sharp.Wpf.Abstractions;
|
||||||
|
using Sharp.Demo.Services;
|
||||||
|
|
||||||
namespace Sharp.Demo.ViewModels.Pages
|
namespace Sharp.Demo.ViewModels.Pages
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// WPF 服务演示 ViewModel
|
/// WPF 服务演示 ViewModel - 交互式功能验证
|
||||||
|
/// 演示:对话框、文件选择、UI 线程调度、应用状态管理、MVVM 基础
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class WpfServicesViewModel : ViewModelBase
|
public class WpfServicesViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
private readonly IDialogService _dialogService;
|
||||||
|
private readonly IFileDialogService _fileDialogService;
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
private readonly IApplicationState _appState;
|
||||||
|
private readonly ILogger<WpfServicesViewModel> _logger;
|
||||||
|
|
||||||
private string _sampleProperty = "示例属性值";
|
private string _sampleProperty = "示例属性值";
|
||||||
private string _logText = "等待操作...\n";
|
private string _logText = "📝 WPF 服务演示已加载,点击按钮测试各项功能...\n\n";
|
||||||
|
private string _selectedTheme = "Light";
|
||||||
|
private string _selectedLanguage = "zh-CN";
|
||||||
|
private string _userName = "演示用户";
|
||||||
|
private bool _isDebugMode = false;
|
||||||
|
|
||||||
|
public WpfServicesViewModel(
|
||||||
|
IDialogService dialogService,
|
||||||
|
IFileDialogService fileDialogService,
|
||||||
|
IDispatcher dispatcher,
|
||||||
|
IApplicationState appState,
|
||||||
|
ILogger<WpfServicesViewModel> logger)
|
||||||
|
{
|
||||||
|
_dialogService = dialogService ?? throw new ArgumentNullException(nameof(dialogService));
|
||||||
|
_fileDialogService = fileDialogService ?? throw new ArgumentNullException(nameof(fileDialogService));
|
||||||
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||||
|
_appState = appState ?? throw new ArgumentNullException(nameof(appState));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
// 从应用状态初始化
|
||||||
|
_selectedTheme = _appState.Theme;
|
||||||
|
_selectedLanguage = _appState.Language;
|
||||||
|
_userName = _appState.UserDisplayName ?? "演示用户";
|
||||||
|
_isDebugMode = _appState.IsDebugMode;
|
||||||
|
|
||||||
|
// 订阅应用状态变化
|
||||||
|
_appState.StateChanged += OnAppStateChanged;
|
||||||
|
|
||||||
|
InitializeCommands();
|
||||||
|
AppendLog("✅ 服务初始化完成");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 属性
|
||||||
|
|
||||||
public string SampleProperty
|
public string SampleProperty
|
||||||
{
|
{
|
||||||
@@ -23,79 +68,339 @@ namespace Sharp.Demo.ViewModels.Pages
|
|||||||
set => SetProperty(ref _logText, value);
|
set => SetProperty(ref _logText, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string SelectedTheme
|
||||||
|
{
|
||||||
|
get => _selectedTheme;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedTheme, value))
|
||||||
|
{
|
||||||
|
_appState.Theme = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SelectedLanguage
|
||||||
|
{
|
||||||
|
get => _selectedLanguage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedLanguage, value))
|
||||||
|
{
|
||||||
|
_appState.Language = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string UserName
|
||||||
|
{
|
||||||
|
get => _userName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _userName, value))
|
||||||
|
{
|
||||||
|
_appState.UserDisplayName = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsDebugMode
|
||||||
|
{
|
||||||
|
get => _isDebugMode;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _isDebugMode, value))
|
||||||
|
{
|
||||||
|
_appState.IsDebugMode = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string AppStateSummary => _appState.GetStateSummary();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 命令
|
||||||
|
|
||||||
// 对话框命令
|
// 对话框命令
|
||||||
public RelayCommand ShowMessageCommand { get; }
|
public AsyncCommand ShowMessageCommand { get; private set; } = null!;
|
||||||
public RelayCommand ShowConfirmCommand { get; }
|
public AsyncCommand ShowConfirmCommand { get; private set; } = null!;
|
||||||
public RelayCommand ShowWarningCommand { get; }
|
public AsyncCommand ShowWarningCommand { get; private set; } = null!;
|
||||||
public RelayCommand ShowErrorCommand { get; }
|
public AsyncCommand ShowErrorCommand { get; private set; } = null!;
|
||||||
public RelayCommand ShowInputCommand { get; }
|
|
||||||
public RelayCommand ShowCustomDialogCommand { get; }
|
|
||||||
|
|
||||||
// 文件对话框命令
|
// 文件对话框命令
|
||||||
public RelayCommand OpenFileCommand { get; }
|
public AsyncCommand OpenFileCommand { get; private set; } = null!;
|
||||||
public RelayCommand SaveFileCommand { get; }
|
public AsyncCommand SaveFileCommand { get; private set; } = null!;
|
||||||
public RelayCommand SelectFolderCommand { get; }
|
public AsyncCommand SelectFolderCommand { get; private set; } = null!;
|
||||||
public RelayCommand OpenMultipleFilesCommand { get; }
|
public AsyncCommand OpenMultipleFilesCommand { get; private set; } = null!;
|
||||||
|
|
||||||
// 线程分发器命令
|
// 线程分发器命令
|
||||||
public RelayCommand InvokeOnUIThreadCommand { get; }
|
public AsyncCommand InvokeOnUIThreadCommand { get; private set; } = null!;
|
||||||
public RelayCommand InvokeAsyncCommand { get; }
|
public AsyncCommand InvokeAsyncCommand { get; private set; } = null!;
|
||||||
public RelayCommand BackgroundTaskCommand { get; }
|
public AsyncCommand BackgroundTaskCommand { get; private set; } = null!;
|
||||||
|
|
||||||
// MVVM 基础命令
|
// MVVM 基础命令
|
||||||
public RelayCommand TriggerPropertyChangeCommand { get; }
|
public RelayCommand TriggerPropertyChangeCommand { get; private set; } = null!;
|
||||||
public RelayCommand SyncCommandDemo { get; }
|
public RelayCommand SyncCommandDemo { get; private set; } = null!;
|
||||||
public AsyncCommand AsyncCommandDemo { get; }
|
public AsyncCommand AsyncCommandDemo { get; private set; } = null!;
|
||||||
public RelayCommand<string> CommandWithParamDemo { get; }
|
public RelayCommand<string> CommandWithParamDemo { get; private set; } = null!;
|
||||||
|
|
||||||
public RelayCommand ClearLogCommand { get; }
|
// 应用状态管理命令
|
||||||
|
public RelayCommand RefreshStateSummaryCommand { get; private set; } = null!;
|
||||||
|
public RelayCommand ResetAppStateCommand { get; private set; } = null!;
|
||||||
|
|
||||||
public WpfServicesViewModel()
|
public RelayCommand ClearLogCommand { get; private set; } = null!;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 初始化
|
||||||
|
|
||||||
|
private void InitializeCommands()
|
||||||
{
|
{
|
||||||
// 对话框
|
// 对话框演示
|
||||||
ShowMessageCommand = new RelayCommand(() => AppendLog("消息框功能待实现..."));
|
ShowMessageCommand = new AsyncCommand(async () =>
|
||||||
ShowConfirmCommand = new RelayCommand(() => AppendLog("确认对话框功能待实现..."));
|
|
||||||
ShowWarningCommand = new RelayCommand(() => AppendLog("警告对话框功能待实现..."));
|
|
||||||
ShowErrorCommand = new RelayCommand(() => AppendLog("错误对话框功能待实现..."));
|
|
||||||
ShowInputCommand = new RelayCommand(() => AppendLog("输入框功能待实现..."));
|
|
||||||
ShowCustomDialogCommand = new RelayCommand(() => AppendLog("自定义对话框功能待实现..."));
|
|
||||||
|
|
||||||
// 文件对话框
|
|
||||||
OpenFileCommand = new RelayCommand(() => AppendLog("打开文件对话框功能待实现..."));
|
|
||||||
SaveFileCommand = new RelayCommand(() => AppendLog("保存文件对话框功能待实现..."));
|
|
||||||
SelectFolderCommand = new RelayCommand(() => AppendLog("选择文件夹功能待实现..."));
|
|
||||||
OpenMultipleFilesCommand = new RelayCommand(() => AppendLog("多选文件功能待实现..."));
|
|
||||||
|
|
||||||
// 线程分发器
|
|
||||||
InvokeOnUIThreadCommand = new RelayCommand(() => AppendLog("UI 线程调用功能待实现..."));
|
|
||||||
InvokeAsyncCommand = new RelayCommand(() => AppendLog("异步 UI 调用功能待实现..."));
|
|
||||||
BackgroundTaskCommand = new RelayCommand(() => AppendLog("后台任务功能待实现..."));
|
|
||||||
|
|
||||||
// MVVM 基础
|
|
||||||
TriggerPropertyChangeCommand = new RelayCommand(() =>
|
|
||||||
{
|
{
|
||||||
SampleProperty = $"已变更 - {DateTime.Now:HH:mm:ss}";
|
AppendLog("📢 显示信息对话框...");
|
||||||
AppendLog($"属性变更: {SampleProperty}");
|
await _dialogService.ShowInfoAsync("这是一个信息提示框", "信息");
|
||||||
|
AppendLog("✅ 用户关闭了信息对话框");
|
||||||
});
|
});
|
||||||
|
|
||||||
SyncCommandDemo = new RelayCommand(() => AppendLog("同步命令执行"));
|
ShowConfirmCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("❓ 显示确认对话框...");
|
||||||
|
var result = await _dialogService.ShowConfirmAsync("确认执行此操作吗?", "确认");
|
||||||
|
AppendLog($"✅ 用户选择: {(result ? "确定" : "取消")}");
|
||||||
|
});
|
||||||
|
|
||||||
|
ShowWarningCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("⚠️ 显示警告对话框...");
|
||||||
|
await _dialogService.ShowWarningAsync("这是一个警告消息!", "警告");
|
||||||
|
AppendLog("✅ 用户关闭了警告对话框");
|
||||||
|
});
|
||||||
|
|
||||||
|
ShowErrorCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("❌ 显示错误对话框...");
|
||||||
|
await _dialogService.ShowErrorAsync("发生了一个演示错误!", "错误");
|
||||||
|
AppendLog("✅ 用户关闭了错误对话框");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 文件对话框演示
|
||||||
|
OpenFileCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("📁 打开文件对话框...");
|
||||||
|
var result = await _fileDialogService.OpenFileAsync("所有文件|*.*|文本文件|*.txt");
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
AppendLog($"✅ 选择文件: {result}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendLog("❌ 用户取消了选择");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SaveFileCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("💾 保存文件对话框...");
|
||||||
|
var result = await _fileDialogService.SaveFileAsync("文本文件|*.txt|所有文件|*.*", "output.txt");
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
AppendLog($"✅ 保存文件: {result}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendLog("❌ 用户取消了保存");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SelectFolderCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("📂 选择文件夹对话框...");
|
||||||
|
var result = await _fileDialogService.SelectFolderAsync();
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
AppendLog($"✅ 选择文件夹: {result}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendLog("❌ 用户取消了选择");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
OpenMultipleFilesCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("📁 多选文件对话框...");
|
||||||
|
var results = await _fileDialogService.OpenFilesAsync("所有文件|*.*");
|
||||||
|
if (results != null && results.Count > 0)
|
||||||
|
{
|
||||||
|
AppendLog($"✅ 选择了 {results.Count} 个文件:");
|
||||||
|
foreach (var file in results)
|
||||||
|
{
|
||||||
|
AppendLog($" - {file}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AppendLog("❌ 用户取消了选择");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI 线程调度演示
|
||||||
|
InvokeOnUIThreadCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("🔄 从后台线程调用 UI 线程...");
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
SampleProperty = $"UI 线程更新 - {DateTime.Now:HH:mm:ss.fff}";
|
||||||
|
AppendLog("✅ UI 线程调用成功");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
InvokeAsyncCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("🔄 异步 UI 调用...");
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
SampleProperty = $"异步 UI 更新 - {DateTime.Now:HH:mm:ss.fff}";
|
||||||
|
AppendLog("✅ 异步 UI 调用成功");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
BackgroundTaskCommand = new AsyncCommand(async () =>
|
||||||
|
{
|
||||||
|
AppendLog("⏳ 启动后台任务(3 秒)...");
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(async () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 3; i++)
|
||||||
|
{
|
||||||
|
await Task.Delay(1000);
|
||||||
|
await _dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
AppendLog($"⏱️ 后台任务进度: {i}/3");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
AppendLog("✅ 后台任务完成");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// MVVM 基础演示
|
||||||
|
TriggerPropertyChangeCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
SampleProperty = $"属性变更 - {DateTime.Now:HH:mm:ss}";
|
||||||
|
AppendLog($"🔔 属性变更通知: {SampleProperty}");
|
||||||
|
});
|
||||||
|
|
||||||
|
SyncCommandDemo = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
AppendLog("⚡ 同步命令执行");
|
||||||
|
_logger.LogInformation("同步命令被执行");
|
||||||
|
});
|
||||||
|
|
||||||
AsyncCommandDemo = new AsyncCommand(async () =>
|
AsyncCommandDemo = new AsyncCommand(async () =>
|
||||||
{
|
{
|
||||||
AppendLog("异步命令开始...");
|
AppendLog("⏳ 异步命令开始...");
|
||||||
await Task.Delay(1000);
|
IsBusy = true;
|
||||||
AppendLog("异步命令完成");
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(2000);
|
||||||
|
AppendLog("✅ 异步命令完成");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsBusy = false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
CommandWithParamDemo = new RelayCommand<string>(param =>
|
CommandWithParamDemo = new RelayCommand<string>(param =>
|
||||||
AppendLog($"带参数命令: {param}"));
|
{
|
||||||
|
AppendLog($"📋 带参数命令: {param ?? "null"}");
|
||||||
|
});
|
||||||
|
|
||||||
ClearLogCommand = new RelayCommand(() => LogText = "");
|
// 应用状态管理演示
|
||||||
|
RefreshStateSummaryCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(AppStateSummary));
|
||||||
|
AppendLog($"📊 应用状态: {AppStateSummary}");
|
||||||
|
});
|
||||||
|
|
||||||
|
ResetAppStateCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
AppendLog("🔄 重置应用状态...");
|
||||||
|
_appState.Reset();
|
||||||
|
AppendLog("✅ 应用状态已重置为默认值");
|
||||||
|
});
|
||||||
|
|
||||||
|
ClearLogCommand = new RelayCommand(() =>
|
||||||
|
{
|
||||||
|
LogText = "📝 日志已清空\n\n";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 事件处理
|
||||||
|
|
||||||
|
private void OnAppStateChanged(object? sender, StateChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == "Theme")
|
||||||
|
{
|
||||||
|
SelectedTheme = _appState.Theme;
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == "Language")
|
||||||
|
{
|
||||||
|
SelectedLanguage = _appState.Language;
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == "UserDisplayName")
|
||||||
|
{
|
||||||
|
UserName = _appState.UserDisplayName ?? "演示用户";
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == "IsDebugMode")
|
||||||
|
{
|
||||||
|
IsDebugMode = _appState.IsDebugMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(AppStateSummary));
|
||||||
|
AppendLog($"🔔 应用状态变更: {e.PropertyName} = {e.NewValue}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助方法
|
||||||
|
|
||||||
private void AppendLog(string message)
|
private void AppendLog(string message)
|
||||||
{
|
{
|
||||||
LogText += $"[{DateTime.Now:HH:mm:ss}] {message}\n";
|
LogText += $"[{DateTime.Now:HH:mm:ss}] {message}\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 资源清理
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
_appState.StateChanged -= OnAppStateChanged;
|
||||||
|
base.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Sharp.Wpf.Mvvm;
|
||||||
|
using Sharp.Wpf.Commands;
|
||||||
|
using Sharp.Wpf.Abstractions;
|
||||||
|
using Sharp.Process;
|
||||||
|
using Sharp.Python;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace Sharp.Demo.ViewModels.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process/Python 测试工具 ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessTestViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IProcessExecutor _processExecutor;
|
||||||
|
private readonly IPythonExecutor _pythonExecutor;
|
||||||
|
private readonly IDispatcher _dispatcher;
|
||||||
|
private readonly ILogger<ProcessTestViewModel> _logger;
|
||||||
|
|
||||||
|
// Process 测试
|
||||||
|
private string _command = "cmd";
|
||||||
|
private string _arguments = "/c echo Hello from Process!";
|
||||||
|
private int _timeoutSeconds = 30;
|
||||||
|
private string _processOutput = "输出将显示在这里...";
|
||||||
|
private string _processStatus = "就绪";
|
||||||
|
|
||||||
|
// Python 测试
|
||||||
|
private string _pythonPath = "(自动检测)";
|
||||||
|
private string _pythonVersion = "未检测";
|
||||||
|
private string _pythonCode = "print('Hello from Python!')";
|
||||||
|
private string _pythonScriptPath = "";
|
||||||
|
private string _pythonArgs = "";
|
||||||
|
private string _pythonOutput = "输出将显示在这里...";
|
||||||
|
private string _pythonStatus = "就绪";
|
||||||
|
|
||||||
|
// 日志
|
||||||
|
public ObservableCollection<string> LogMessages { get; } = new();
|
||||||
|
|
||||||
|
public ProcessTestViewModel(
|
||||||
|
IProcessExecutor processExecutor,
|
||||||
|
IPythonExecutor pythonExecutor,
|
||||||
|
IDispatcher dispatcher,
|
||||||
|
ILogger<ProcessTestViewModel> logger)
|
||||||
|
{
|
||||||
|
_processExecutor = processExecutor;
|
||||||
|
_pythonExecutor = pythonExecutor;
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
// Process 命令
|
||||||
|
RunProcessCommand = new AsyncCommand(RunProcessAsync);
|
||||||
|
RunShellCommand = new AsyncCommand(RunShellAsync);
|
||||||
|
TestDotnetCommand = new AsyncCommand(TestDotnetAsync);
|
||||||
|
TestGitCommand = new AsyncCommand(TestGitAsync);
|
||||||
|
|
||||||
|
// Python 命令
|
||||||
|
DetectPythonCommand = new AsyncCommand(DetectPythonAsync);
|
||||||
|
RunPythonCodeCommand = new AsyncCommand(RunPythonCodeAsync);
|
||||||
|
RunPythonScriptCommand = new AsyncCommand(RunPythonScriptAsync);
|
||||||
|
TestPythonJsonCommand = new AsyncCommand(TestPythonJsonAsync);
|
||||||
|
CreateTestScriptCommand = new RelayCommand(CreateTestScript);
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
ClearLogCommand = new RelayCommand(() => LogMessages.Clear());
|
||||||
|
|
||||||
|
// 自动检测 Python
|
||||||
|
_ = DetectPythonAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Properties
|
||||||
|
|
||||||
|
public string Command
|
||||||
|
{
|
||||||
|
get => _command;
|
||||||
|
set => SetProperty(ref _command, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Arguments
|
||||||
|
{
|
||||||
|
get => _arguments;
|
||||||
|
set => SetProperty(ref _arguments, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TimeoutSeconds
|
||||||
|
{
|
||||||
|
get => _timeoutSeconds;
|
||||||
|
set => SetProperty(ref _timeoutSeconds, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProcessOutput
|
||||||
|
{
|
||||||
|
get => _processOutput;
|
||||||
|
set => SetProperty(ref _processOutput, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string ProcessStatus
|
||||||
|
{
|
||||||
|
get => _processStatus;
|
||||||
|
set => SetProperty(ref _processStatus, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonPath
|
||||||
|
{
|
||||||
|
get => _pythonPath;
|
||||||
|
set => SetProperty(ref _pythonPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonVersion
|
||||||
|
{
|
||||||
|
get => _pythonVersion;
|
||||||
|
set => SetProperty(ref _pythonVersion, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonCode
|
||||||
|
{
|
||||||
|
get => _pythonCode;
|
||||||
|
set => SetProperty(ref _pythonCode, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonScriptPath
|
||||||
|
{
|
||||||
|
get => _pythonScriptPath;
|
||||||
|
set => SetProperty(ref _pythonScriptPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonArgs
|
||||||
|
{
|
||||||
|
get => _pythonArgs;
|
||||||
|
set => SetProperty(ref _pythonArgs, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonOutput
|
||||||
|
{
|
||||||
|
get => _pythonOutput;
|
||||||
|
set => SetProperty(ref _pythonOutput, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string PythonStatus
|
||||||
|
{
|
||||||
|
get => _pythonStatus;
|
||||||
|
set => SetProperty(ref _pythonStatus, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
public IAsyncCommand RunProcessCommand { get; }
|
||||||
|
public IAsyncCommand RunShellCommand { get; }
|
||||||
|
public IAsyncCommand TestDotnetCommand { get; }
|
||||||
|
public IAsyncCommand TestGitCommand { get; }
|
||||||
|
|
||||||
|
public IAsyncCommand DetectPythonCommand { get; }
|
||||||
|
public IAsyncCommand RunPythonCodeCommand { get; }
|
||||||
|
public IAsyncCommand RunPythonScriptCommand { get; }
|
||||||
|
public IAsyncCommand TestPythonJsonCommand { get; }
|
||||||
|
public ICommand CreateTestScriptCommand { get; }
|
||||||
|
|
||||||
|
public ICommand ClearLogCommand { get; }
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Process 测试
|
||||||
|
|
||||||
|
private async Task RunProcessAsync()
|
||||||
|
{
|
||||||
|
ProcessStatus = "执行中...";
|
||||||
|
ProcessOutput = "";
|
||||||
|
AddLog($"[Process] 执行命令: {Command} {Arguments}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var options = new ProcessOptions
|
||||||
|
{
|
||||||
|
Command = Command,
|
||||||
|
Arguments = Arguments.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList(),
|
||||||
|
Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ProcessOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(ProcessOutput));
|
||||||
|
AddLog($"[输出] {line}");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
OnErrorReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
ProcessOutput += $"[错误] {line}\n";
|
||||||
|
OnPropertyChanged(nameof(ProcessOutput));
|
||||||
|
AddLog($"[错误] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _processExecutor.ExecuteAsync(options);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✓ 完成 (退出码: {result.Value.ExitCode}, 耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||||
|
ProcessOutput += $"\n=== 执行成功 ===\n退出码: {result.Value.ExitCode}\n耗时: {result.Value.Duration.TotalSeconds:F2}s";
|
||||||
|
AddLog($"[Process] 执行成功 (耗时: {result.Value.Duration.TotalMilliseconds}ms)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 失败: {result.ErrorMessage}";
|
||||||
|
ProcessOutput += $"\n=== 执行失败 ===\n{result.ErrorMessage}";
|
||||||
|
AddLog($"[Process] 执行失败: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
ProcessOutput = $"异常: {ex.Message}\n{ex.StackTrace}";
|
||||||
|
AddLog($"[Process] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunShellAsync()
|
||||||
|
{
|
||||||
|
ProcessStatus = "执行中...";
|
||||||
|
ProcessOutput = "";
|
||||||
|
|
||||||
|
var shellCmd = OperatingSystem.IsWindows()
|
||||||
|
? "dir | findstr .cs"
|
||||||
|
: "ls | grep .cs";
|
||||||
|
|
||||||
|
AddLog($"[Shell] 执行: {shellCmd}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await IProcessExecutor.RunShellAsync(shellCmd);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ProcessOutput = result.Value.StandardOutput;
|
||||||
|
ProcessStatus = $"✓ Shell 命令执行成功";
|
||||||
|
AddLog($"[Shell] 成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ProcessOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
ProcessStatus = $"✗ Shell 命令失败";
|
||||||
|
AddLog($"[Shell] 失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProcessStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
ProcessOutput = ex.Message;
|
||||||
|
AddLog($"[Shell] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestDotnetAsync()
|
||||||
|
{
|
||||||
|
Command = "dotnet";
|
||||||
|
Arguments = "--version";
|
||||||
|
await RunProcessAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestGitAsync()
|
||||||
|
{
|
||||||
|
Command = "git";
|
||||||
|
Arguments = "--version";
|
||||||
|
await RunProcessAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Python 测试
|
||||||
|
|
||||||
|
private async Task DetectPythonAsync()
|
||||||
|
{
|
||||||
|
PythonStatus = "检测中...";
|
||||||
|
AddLog("[Python] 正在检测 Python 环境...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var env = PythonEnvironment.Discover();
|
||||||
|
if (env != null)
|
||||||
|
{
|
||||||
|
PythonPath = env.PythonPath;
|
||||||
|
PythonVersion = env.Version ?? "未知版本";
|
||||||
|
PythonStatus = "✓ Python 环境已检测";
|
||||||
|
AddLog($"[Python] 找到 Python: {env.PythonPath}");
|
||||||
|
AddLog($"[Python] 版本: {env.Version}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonPath = "未找到";
|
||||||
|
PythonVersion = "未安装";
|
||||||
|
PythonStatus = "✗ 未找到 Python";
|
||||||
|
AddLog("[Python] 未找到 Python 环境,请安装 Python 或配置 PATH");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 检测失败: {ex.Message}";
|
||||||
|
AddLog($"[Python] 检测异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPythonCodeAsync()
|
||||||
|
{
|
||||||
|
PythonStatus = "执行中...";
|
||||||
|
PythonOutput = "";
|
||||||
|
AddLog($"[Python] 执行代码: {PythonCode}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _pythonExecutor.RunCodeAsync(PythonCode, new PythonOptions
|
||||||
|
{
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Python 输出] {line}");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
OnErrorReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += $"[错误] {line}\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Python 错误] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PythonOutput))
|
||||||
|
{
|
||||||
|
PythonOutput = result.Value.StandardOutput;
|
||||||
|
}
|
||||||
|
PythonStatus = $"✓ 执行成功 (耗时: {result.Value.Duration.TotalSeconds:F2}s)";
|
||||||
|
AddLog($"[Python] 执行成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
PythonStatus = $"✗ 执行失败";
|
||||||
|
AddLog($"[Python] 执行失败: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
PythonOutput = ex.Message;
|
||||||
|
AddLog($"[Python] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunPythonScriptAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(PythonScriptPath) || !File.Exists(PythonScriptPath))
|
||||||
|
{
|
||||||
|
PythonStatus = "✗ 请选择有效的脚本文件";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PythonStatus = "执行中...";
|
||||||
|
PythonOutput = "";
|
||||||
|
AddLog($"[Python] 执行脚本: {PythonScriptPath}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 解析参数(简单的 key=value 格式)
|
||||||
|
object? args = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(PythonArgs))
|
||||||
|
{
|
||||||
|
var argsDict = new Dictionary<string, string>();
|
||||||
|
var pairs = PythonArgs.Split(',', StringSplitOptions.TrimEntries);
|
||||||
|
foreach (var pair in pairs)
|
||||||
|
{
|
||||||
|
var kv = pair.Split('=', 2);
|
||||||
|
if (kv.Length == 2)
|
||||||
|
{
|
||||||
|
argsDict[kv[0].Trim()] = kv[1].Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
args = argsDict;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _pythonExecutor.RunScriptAsync(
|
||||||
|
PythonScriptPath,
|
||||||
|
args,
|
||||||
|
new PythonOptions
|
||||||
|
{
|
||||||
|
OnOutputReceived = line =>
|
||||||
|
{
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
PythonOutput += line + "\n";
|
||||||
|
OnPropertyChanged(nameof(PythonOutput));
|
||||||
|
AddLog($"[Script] {line}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(PythonOutput))
|
||||||
|
{
|
||||||
|
PythonOutput = result.Value.StandardOutput;
|
||||||
|
}
|
||||||
|
PythonStatus = $"✓ 脚本执行成功";
|
||||||
|
AddLog($"[Python] 脚本执行成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PythonOutput = result.ErrorMessage ?? "未知错误";
|
||||||
|
PythonStatus = $"✗ 脚本执行失败";
|
||||||
|
AddLog($"[Python] 脚本执行失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 异常: {ex.Message}";
|
||||||
|
PythonOutput = ex.Message;
|
||||||
|
AddLog($"[Python] 异常: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task TestPythonJsonAsync()
|
||||||
|
{
|
||||||
|
PythonCode = @"import json
|
||||||
|
result = {'status': 'ok', 'value': 42, 'message': '测试成功'}
|
||||||
|
print(json.dumps(result, ensure_ascii=False))";
|
||||||
|
await RunPythonCodeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateTestScript()
|
||||||
|
{
|
||||||
|
var tempPath = Path.Combine(Path.GetTempPath(), "sharp_test.py");
|
||||||
|
|
||||||
|
var scriptContent = @"import sys
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--name', default='World')
|
||||||
|
parser.add_argument('--count', type=int, default=1)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# 日志输出到 stderr
|
||||||
|
print(f'参数: name={args.name}, count={args.count}', file=sys.stderr)
|
||||||
|
|
||||||
|
# 处理
|
||||||
|
for i in range(args.count):
|
||||||
|
print(f'Hello, {args.name}! ({i+1})', file=sys.stderr)
|
||||||
|
|
||||||
|
# 结果输出到 stdout(JSON)
|
||||||
|
result = {
|
||||||
|
'message': f'Greeted {args.name} {args.count} times',
|
||||||
|
'success': True,
|
||||||
|
'timestamp': str(__import__('datetime').datetime.now())
|
||||||
|
}
|
||||||
|
print(json.dumps(result, ensure_ascii=False))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(tempPath, scriptContent);
|
||||||
|
PythonScriptPath = tempPath;
|
||||||
|
PythonArgs = "name=Sharp,count=3";
|
||||||
|
PythonStatus = $"✓ 测试脚本已创建: {tempPath}";
|
||||||
|
AddLog($"[Python] 测试脚本已创建: {tempPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
PythonStatus = $"✗ 创建脚本失败: {ex.Message}";
|
||||||
|
AddLog($"[Python] 创建脚本失败: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void AddLog(string message)
|
||||||
|
{
|
||||||
|
var timestamp = DateTime.Now.ToString("HH:mm:ss");
|
||||||
|
_dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
LogMessages.Add($"[{timestamp}] {message}");
|
||||||
|
|
||||||
|
// 保持日志数量在合理范围
|
||||||
|
while (LogMessages.Count > 200)
|
||||||
|
{
|
||||||
|
LogMessages.RemoveAt(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
<hc:GlowWindow x:Class="Sharp.Demo.Views.Windows.ProcessTestWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
xmlns:conv="clr-namespace:Sharp.Wpf.Converters;assembly=Sharp.Wpf"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Process / Python 测试工具" Height="700" Width="1000"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
<hc:GlowWindow.Resources>
|
||||||
|
<conv:BoolToVisibilityConverter x:Key="Bool2Vis" />
|
||||||
|
</hc:GlowWindow.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="300"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 主测试区域 -->
|
||||||
|
<TabControl Grid.Column="0" Margin="0,0,10,0">
|
||||||
|
<!-- Process 测试标签页 -->
|
||||||
|
<TabItem Header="Process 测试">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="15">
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<Border Background="{DynamicResource PrimaryBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||||
|
<TextBlock Text="{Binding ProcessStatus}" Foreground="White" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 命令输入 -->
|
||||||
|
<GroupBox Header="命令配置" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="命令:" VerticalAlignment="Center"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding Command}" Margin="10,0,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="参数:" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||||
|
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Arguments}" Margin="10,10,0,0"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Text="超时(秒):" VerticalAlignment="Center" Margin="0,10,0,0"/>
|
||||||
|
<hc:NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding TimeoutSeconds}"
|
||||||
|
Minimum="1" Maximum="600" Margin="10,10,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 快捷按钮 -->
|
||||||
|
<GroupBox Header="快速测试" Margin="0,0,0,15">
|
||||||
|
<WrapPanel Margin="10" Orientation="Horizontal">
|
||||||
|
<Button Content="执行命令" Command="{Binding RunProcessCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||||
|
<Button Content="Shell 命令" Command="{Binding RunShellCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8"/>
|
||||||
|
<Button Content="测试 dotnet" Command="{Binding TestDotnetCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
<Button Content="测试 Git" Command="{Binding TestGitCommand}"
|
||||||
|
Margin="0,0,10,10" Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 输出 -->
|
||||||
|
<GroupBox Header="执行输出">
|
||||||
|
<TextBox Text="{Binding ProcessOutput, Mode=OneWay}"
|
||||||
|
Height="300"
|
||||||
|
IsReadOnly="True"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="10"/>
|
||||||
|
</GroupBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Python 测试标签页 -->
|
||||||
|
<TabItem Header="Python 测试">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="15">
|
||||||
|
<!-- Python 环境信息 -->
|
||||||
|
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="Python 环境" FontSize="16" FontWeight="Bold"/>
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<TextBlock Text="路径: " FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding PythonPath}" Margin="5,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,5,0,0">
|
||||||
|
<TextBlock Text="版本: " FontWeight="SemiBold"/>
|
||||||
|
<TextBlock Text="{Binding PythonVersion}" Margin="5,0,20,0"/>
|
||||||
|
<Button Content="重新检测" Command="{Binding DetectPythonCommand}" Padding="10,4"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 状态栏 -->
|
||||||
|
<Border Background="{DynamicResource SuccessBrush}" Padding="10" CornerRadius="4" Margin="0,0,0,15">
|
||||||
|
<TextBlock Text="{Binding PythonStatus}" Foreground="White" FontWeight="Bold"/>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 代码执行 -->
|
||||||
|
<GroupBox Header="Python 代码执行" Margin="0,0,0,15">
|
||||||
|
<StackPanel Margin="10">
|
||||||
|
<TextBlock Text="Python 代码:" Margin="0,0,0,5"/>
|
||||||
|
<TextBox Text="{Binding PythonCode}"
|
||||||
|
Height="80"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
<WrapPanel>
|
||||||
|
<Button Content="执行代码" Command="{Binding RunPythonCodeCommand}"
|
||||||
|
Margin="0,0,10,0" Padding="15,8" Style="{StaticResource ButtonSuccess}"/>
|
||||||
|
<Button Content="测试 JSON 输出" Command="{Binding TestPythonJsonCommand}"
|
||||||
|
Padding="15,8" Style="{StaticResource ButtonInfo}"/>
|
||||||
|
</WrapPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 脚本执行 -->
|
||||||
|
<GroupBox Header="Python 脚本执行">
|
||||||
|
<StackPanel Margin="10">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Text="{Binding PythonScriptPath}"
|
||||||
|
hc:InfoElement.Placeholder="脚本文件路径"/>
|
||||||
|
<Button Grid.Column="1" Content="创建测试脚本"
|
||||||
|
Command="{Binding CreateTestScriptCommand}"
|
||||||
|
Margin="5,0,0,0" Padding="10,4"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="参数 (格式: key1=value1,key2=value2):" Margin="0,10,0,5"/>
|
||||||
|
<TextBox Text="{Binding PythonArgs}"
|
||||||
|
hc:InfoElement.Placeholder="例如: name=Sharp,count=3"
|
||||||
|
Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<Button Content="执行脚本" Command="{Binding RunPythonScriptCommand}"
|
||||||
|
Padding="15,8" Style="{StaticResource ButtonPrimary}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- Python 输出 -->
|
||||||
|
<GroupBox Header="Python 输出" Margin="0,15,0,0">
|
||||||
|
<TextBox Text="{Binding PythonOutput, Mode=OneWay}"
|
||||||
|
Height="200"
|
||||||
|
IsReadOnly="True"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
Margin="10"/>
|
||||||
|
</GroupBox>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
|
||||||
|
<!-- 日志面板 -->
|
||||||
|
<Border Grid.Column="1" Background="{DynamicResource RegionBrush}"
|
||||||
|
Padding="15" CornerRadius="8">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBlock Text="执行日志" FontSize="16" FontWeight="Bold" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<ListBox Grid.Row="1" ItemsSource="{Binding LogMessages}"
|
||||||
|
FontFamily="Consolas" FontSize="10"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto">
|
||||||
|
<ListBox.ItemContainerStyle>
|
||||||
|
<Style TargetType="ListBoxItem">
|
||||||
|
<Setter Property="Padding" Value="2"/>
|
||||||
|
</Style>
|
||||||
|
</ListBox.ItemContainerStyle>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<Button Grid.Row="2" Content="清空日志" Command="{Binding ClearLogCommand}"
|
||||||
|
Margin="0,10,0,0" HorizontalAlignment="Stretch"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</hc:GlowWindow>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Sharp.Demo.Views.Windows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ProcessTestWindow.xaml 的交互逻辑
|
||||||
|
/// </summary>
|
||||||
|
public partial class ProcessTestWindow
|
||||||
|
{
|
||||||
|
public ProcessTestWindow(Sharp.Demo.ViewModels.Windows.ProcessTestViewModel viewModel)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContext = viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
<hc:GlowWindow x:Class="Sharp.Demo.Views.Windows.SerialPortWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:hc="https://handyorg.github.io/handycontrol"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="串口通信调试工具" Height="750" Width="1200"
|
||||||
|
WindowStartupLocation="CenterOwner">
|
||||||
|
|
||||||
|
<Grid Margin="20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TabControl Grid.Row="0" Grid.RowSpan="2" Style="{StaticResource TabControlCapsule}">
|
||||||
|
<!-- Tab 1: 串口发现 -->
|
||||||
|
<TabItem Header="串口发现">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Content="刷新串口列表" Click="RefreshPorts_Click" Style="{StaticResource ButtonPrimary}" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="仅显示USB串口" Click="ShowUsbPorts_Click" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="测试连接" Click="TestConnection_Click" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock x:Name="PortCountText" Text="发现 0 个串口" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<DataGrid x:Name="PortsDataGrid" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
|
||||||
|
SelectionMode="Single" HeadersVisibility="Column" CanUserResizeColumns="True">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="端口" Binding="{Binding PortName}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="描述" Binding="{Binding Description}" Width="200"/>
|
||||||
|
<DataGridTextColumn Header="制造商" Binding="{Binding Manufacturer}" Width="150"/>
|
||||||
|
<DataGridCheckBoxColumn Header="USB设备" Binding="{Binding IsUsbSerial}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="VID" Binding="{Binding VendorId}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="PID" Binding="{Binding ProductId}" Width="80"/>
|
||||||
|
<DataGridCheckBoxColumn Header="被占用" Binding="{Binding IsInUse}" Width="80"/>
|
||||||
|
<DataGridTextColumn Header="设备ID" Binding="{Binding DeviceId}" Width="*"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" x:Name="DiscoveryStatusText" Text="点击'刷新串口列表'开始"
|
||||||
|
Foreground="{DynamicResource SecondaryTextBrush}" Margin="0,10,0,0"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Tab 2: 基础通信 -->
|
||||||
|
<TabItem Header="基础通信">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 连接配置 -->
|
||||||
|
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="PortNameCombo" Width="100" Margin="0,0,15,0"/>
|
||||||
|
|
||||||
|
<TextBlock Text="波特率:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="BaudRateCombo" Width="100" Margin="0,0,15,0" SelectedIndex="3">
|
||||||
|
<ComboBoxItem Content="4800"/>
|
||||||
|
<ComboBoxItem Content="9600"/>
|
||||||
|
<ComboBoxItem Content="19200"/>
|
||||||
|
<ComboBoxItem Content="38400"/>
|
||||||
|
<ComboBoxItem Content="57600"/>
|
||||||
|
<ComboBoxItem Content="115200"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="数据位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="DataBitsCombo" Width="60" Margin="0,0,15,0" SelectedIndex="3">
|
||||||
|
<ComboBoxItem Content="5"/>
|
||||||
|
<ComboBoxItem Content="6"/>
|
||||||
|
<ComboBoxItem Content="7"/>
|
||||||
|
<ComboBoxItem Content="8"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="校验位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="ParityCombo" Width="80" Margin="0,0,15,0" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="None"/>
|
||||||
|
<ComboBoxItem Content="Odd"/>
|
||||||
|
<ComboBoxItem Content="Even"/>
|
||||||
|
<ComboBoxItem Content="Mark"/>
|
||||||
|
<ComboBoxItem Content="Space"/>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<TextBlock Text="停止位:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="StopBitsCombo" Width="80" Margin="0,0,0,0" SelectedIndex="1">
|
||||||
|
<ComboBoxItem Content="None"/>
|
||||||
|
<ComboBoxItem Content="One"/>
|
||||||
|
<ComboBoxItem Content="Two"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<Button x:Name="OpenButton" Content="打开串口" Click="OpenPort_Click" Style="{StaticResource ButtonSuccess}" Margin="0,0,10,0"/>
|
||||||
|
<Button x:Name="CloseButton" Content="关闭串口" Click="ClosePort_Click" Style="{StaticResource ButtonDanger}" Margin="0,0,10,0" IsEnabled="False"/>
|
||||||
|
<TextBlock x:Name="ConnectionStatusText" Text="未连接" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 发送区域 -->
|
||||||
|
<GroupBox Header="发送数据" Grid.Row="1" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox x:Name="SendDataTextBox" Width="400" hc:InfoElement.Placeholder="输入要发送的数据 (十六进制,如: 01 02 03)" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="发送 (HEX)" Click="SendHex_Click" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="发送 (文本)" Click="SendText_Click" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="清空" Click="ClearSend_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<CheckBox x:Name="AutoAddNewLineCheckBox" Content="自动添加换行符" IsChecked="True" Margin="0,0,15,0"/>
|
||||||
|
<TextBlock Text="编码:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="EncodingCombo" Width="100" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="UTF-8"/>
|
||||||
|
<ComboBoxItem Content="ASCII"/>
|
||||||
|
<ComboBoxItem Content="GBK"/>
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 接收区域 -->
|
||||||
|
<GroupBox Header="接收数据" Grid.Row="2" Margin="0,0,0,10">
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="10">
|
||||||
|
<CheckBox x:Name="HexDisplayCheckBox" Content="HEX显示" IsChecked="True" Margin="0,0,15,0"/>
|
||||||
|
<CheckBox x:Name="AutoScrollCheckBox" Content="自动滚动" IsChecked="True" Margin="0,0,15,0"/>
|
||||||
|
<Button Content="清空接收区" Click="ClearReceive_Click" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock x:Name="ReceivedBytesText" Text="已接收: 0 字节" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<TextBox x:Name="ReceiveTextBox" Grid.Row="3" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="Wrap" FontFamily="Consolas" Background="{DynamicResource RegionBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Tab 3: 高级监听 -->
|
||||||
|
<TabItem Header="高级监听">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource RegionBrush}" Padding="15" CornerRadius="8" Margin="0,0,0,15">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
|
||||||
|
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="ServicePortCombo" Width="100" Margin="0,0,15,0"/>
|
||||||
|
<TextBlock Text="波特率:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="ServiceBaudRateCombo" Width="100" Margin="0,0,15,0" SelectedIndex="3">
|
||||||
|
<ComboBoxItem Content="9600"/>
|
||||||
|
<ComboBoxItem Content="19200"/>
|
||||||
|
<ComboBoxItem Content="38400"/>
|
||||||
|
<ComboBoxItem Content="57600"/>
|
||||||
|
<ComboBoxItem Content="115200"/>
|
||||||
|
</ComboBox>
|
||||||
|
<CheckBox x:Name="AutoReconnectCheckBox" Content="自动重连" IsChecked="True" Margin="0,0,15,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button x:Name="StartListeningButton" Content="启动监听" Click="StartListening_Click" Style="{StaticResource ButtonSuccess}" Margin="0,0,10,0"/>
|
||||||
|
<Button x:Name="StopListeningButton" Content="停止监听" Click="StopListening_Click" Style="{StaticResource ButtonDanger}" Margin="0,0,10,0" IsEnabled="False"/>
|
||||||
|
<TextBlock x:Name="ServiceStatusText" Text="未启动" VerticalAlignment="Center" Margin="20,0,0,0" FontWeight="Bold"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<DataGrid x:Name="EventLogDataGrid" Grid.Row="1" AutoGenerateColumns="False" IsReadOnly="True"
|
||||||
|
HeadersVisibility="Column" CanUserResizeColumns="True" Margin="0,0,0,15">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="时间" Binding="{Binding Time, StringFormat=HH:mm:ss.fff}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="类型" Binding="{Binding EventType}" Width="100"/>
|
||||||
|
<DataGridTextColumn Header="数据/消息" Binding="{Binding Data}" Width="*"/>
|
||||||
|
</DataGrid.Columns>
|
||||||
|
</DataGrid>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="2" Orientation="Horizontal">
|
||||||
|
<Button Content="清空日志" Click="ClearEventLog_Click" Margin="0,0,10,0"/>
|
||||||
|
<TextBlock x:Name="EventCountText" Text="事件数: 0" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
|
||||||
|
<!-- Tab 4: 数据工具 -->
|
||||||
|
<TabItem Header="数据工具">
|
||||||
|
<Grid Margin="15">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 数据转换 -->
|
||||||
|
<GroupBox Header="数据转换" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox x:Name="ConvertInputTextBox" Width="300" hc:InfoElement.Placeholder="输入数据" Margin="0,0,10,0"/>
|
||||||
|
<ComboBox x:Name="ConvertTypeCombo" Width="120" Margin="0,0,10,0" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="HEX -> 字节"/>
|
||||||
|
<ComboBoxItem Content="字节 -> HEX"/>
|
||||||
|
<ComboBoxItem Content="字节 -> Int16"/>
|
||||||
|
<ComboBoxItem Content="字节 -> Int32"/>
|
||||||
|
<ComboBoxItem Content="字节 -> Float"/>
|
||||||
|
<ComboBoxItem Content="Int32 -> 字节"/>
|
||||||
|
</ComboBox>
|
||||||
|
<CheckBox x:Name="BigEndianCheckBox" Content="大端序" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<Button Content="转换" Click="ConvertData_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Text="转换结果:" Margin="0,10,0,5"/>
|
||||||
|
<TextBox Grid.Row="2" x:Name="ConvertResultTextBox" IsReadOnly="True" Background="{DynamicResource RegionBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 校验计算 -->
|
||||||
|
<GroupBox Header="校验计算" Grid.Row="1" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBox x:Name="ChecksumInputTextBox" Width="400" hc:InfoElement.Placeholder="输入十六进制数据 (如: 01 02 03)" Margin="0,0,10,0"/>
|
||||||
|
<Button Content="CRC16" Click="CalculateCrc16_Click" Margin="0,0,5,0"/>
|
||||||
|
<Button Content="LRC" Click="CalculateLrc_Click" Margin="0,0,5,0"/>
|
||||||
|
<Button Content="XOR" Click="CalculateXor_Click" Margin="0,0,5,0"/>
|
||||||
|
<Button Content="累加和" Click="CalculateChecksum_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,10,0,0">
|
||||||
|
<TextBlock Text="校验结果:" VerticalAlignment="Center" Margin="0,0,10,0"/>
|
||||||
|
<TextBox x:Name="ChecksumResultTextBox" Width="200" IsReadOnly="True" Background="{DynamicResource RegionBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 诊断工具 -->
|
||||||
|
<GroupBox Header="诊断工具" Grid.Row="2" Margin="0,0,0,15">
|
||||||
|
<Grid Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="端口:" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||||
|
<ComboBox x:Name="DiagPortCombo" Width="100" Margin="0,0,10,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal">
|
||||||
|
<Button Content="测试连接" Click="DiagTestConnection_Click" Margin="0,0,5,0"/>
|
||||||
|
<Button Content="波特率扫描" Click="ScanBaudRates_Click" Margin="0,0,5,0"/>
|
||||||
|
<Button Content="流量监控(10s)" Click="MonitorTraffic_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</GroupBox>
|
||||||
|
|
||||||
|
<!-- 诊断结果 -->
|
||||||
|
<TextBox x:Name="DiagResultTextBox" Grid.Row="3" IsReadOnly="True" VerticalScrollBarVisibility="Auto"
|
||||||
|
TextWrapping="Wrap" FontFamily="Consolas" Background="{DynamicResource RegionBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</TabItem>
|
||||||
|
</TabControl>
|
||||||
|
</Grid>
|
||||||
|
</hc:GlowWindow>
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.IO.Ports;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using HandyControl.Controls;
|
||||||
|
using Ports.Abstractions;
|
||||||
|
using Ports.Discovery;
|
||||||
|
using Ports.Drivers;
|
||||||
|
using Ports.Models;
|
||||||
|
using Ports.Services;
|
||||||
|
using Ports.Utils;
|
||||||
|
|
||||||
|
namespace Sharp.Demo.Views.Windows;
|
||||||
|
|
||||||
|
public partial class SerialPortWindow
|
||||||
|
{
|
||||||
|
private ISerialPortDriver? _driver;
|
||||||
|
private ISerialPortService? _service;
|
||||||
|
private int _totalReceivedBytes;
|
||||||
|
private readonly ObservableCollection<EventLogItem> _eventLog = new();
|
||||||
|
|
||||||
|
public SerialPortWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
InitializeComboBoxes();
|
||||||
|
EventLogDataGrid.ItemsSource = _eventLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComboBoxes()
|
||||||
|
{
|
||||||
|
// 初始化波特率下拉框
|
||||||
|
BaudRateCombo.SelectedItem = BaudRateCombo.Items[4]; // 115200
|
||||||
|
|
||||||
|
// 加载串口列表
|
||||||
|
RefreshPortsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshPortsList()
|
||||||
|
{
|
||||||
|
var result = SerialPortDiscovery.GetAvailablePorts();
|
||||||
|
if (result.IsSuccess && result.Value != null)
|
||||||
|
{
|
||||||
|
var portNames = result.Value.Select(p => p.PortName).ToList();
|
||||||
|
|
||||||
|
PortNameCombo.ItemsSource = portNames;
|
||||||
|
ServicePortCombo.ItemsSource = portNames;
|
||||||
|
DiagPortCombo.ItemsSource = portNames;
|
||||||
|
|
||||||
|
if (portNames.Count > 0)
|
||||||
|
{
|
||||||
|
PortNameCombo.SelectedIndex = 0;
|
||||||
|
ServicePortCombo.SelectedIndex = 0;
|
||||||
|
DiagPortCombo.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Tab 1: 串口发现
|
||||||
|
|
||||||
|
private void RefreshPorts_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var result = SerialPortDiscovery.GetAvailablePorts();
|
||||||
|
if (result.IsSuccess && result.Value != null)
|
||||||
|
{
|
||||||
|
PortsDataGrid.ItemsSource = result.Value;
|
||||||
|
PortCountText.Text = $"发现 {result.Value.Count} 个串口";
|
||||||
|
DiscoveryStatusText.Text = $"最后刷新: {DateTime.Now:HH:mm:ss}";
|
||||||
|
RefreshPortsList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DiscoveryStatusText.Text = $"错误: {result.ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowUsbPorts_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var result = SerialPortDiscovery.GetUsbSerialPorts();
|
||||||
|
if (result.IsSuccess && result.Value != null)
|
||||||
|
{
|
||||||
|
PortsDataGrid.ItemsSource = result.Value;
|
||||||
|
PortCountText.Text = $"发现 {result.Value.Count} 个USB串口";
|
||||||
|
DiscoveryStatusText.Text = $"仅显示USB设备";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DiscoveryStatusText.Text = $"错误: {result.ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void TestConnection_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var selected = PortsDataGrid.SelectedItem as SerialPortInfo;
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请先选择一个串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiscoveryStatusText.Text = $"正在测试 {selected.PortName}...";
|
||||||
|
|
||||||
|
var options = new SerialPortOptions { PortName = selected.PortName };
|
||||||
|
var result = await SerialPortDiagnostics.TestConnectionAsync(selected.PortName, options);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
DiscoveryStatusText.Text = "测试完成";
|
||||||
|
HandyControl.Controls.MessageBox.Show(result.Value, "测试结果");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DiscoveryStatusText.Text = $"测试失败: {result.ErrorMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tab 2: 基础通信
|
||||||
|
|
||||||
|
private async void OpenPort_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (PortNameCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new SerialPortOptions
|
||||||
|
{
|
||||||
|
PortName = PortNameCombo.SelectedItem.ToString()!,
|
||||||
|
BaudRate = int.Parse(((ComboBoxItem)BaudRateCombo.SelectedItem).Content.ToString()!),
|
||||||
|
DataBits = int.Parse(((ComboBoxItem)DataBitsCombo.SelectedItem).Content.ToString()!),
|
||||||
|
Parity = Enum.Parse<Parity>(((ComboBoxItem)ParityCombo.SelectedItem).Content.ToString()!),
|
||||||
|
StopBits = Enum.Parse<StopBits>(((ComboBoxItem)StopBitsCombo.SelectedItem).Content.ToString()!)
|
||||||
|
};
|
||||||
|
|
||||||
|
_driver = new SerialPortDriver();
|
||||||
|
var result = await _driver.OpenAsync(options);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ConnectionStatusText.Text = "已连接";
|
||||||
|
OpenButton.IsEnabled = false;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
_totalReceivedBytes = 0;
|
||||||
|
|
||||||
|
// 启动接收线程
|
||||||
|
_ = Task.Run(ReceiveLoop);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConnectionStatusText.Text = $"连接失败: {result.ErrorMessage}";
|
||||||
|
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "连接失败", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ClosePort_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_driver != null)
|
||||||
|
{
|
||||||
|
await _driver.CloseAsync();
|
||||||
|
_driver.Dispose();
|
||||||
|
_driver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionStatusText.Text = "未连接";
|
||||||
|
OpenButton.IsEnabled = true;
|
||||||
|
CloseButton.IsEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SendHex_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_driver == null || !_driver.IsOpen)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请先打开串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hexResult = SerialDataConverter.HexStringToBytes(SendDataTextBox.Text);
|
||||||
|
if (hexResult.IsFailure)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show(hexResult.ErrorMessage ?? "数据格式错误", "错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _driver.WriteBytesAsync(hexResult.Value!);
|
||||||
|
if (result.IsFailure)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "发送失败", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SendText_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_driver == null || !_driver.IsOpen)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请先打开串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoding = GetSelectedEncoding();
|
||||||
|
var text = SendDataTextBox.Text;
|
||||||
|
|
||||||
|
if (AutoAddNewLineCheckBox.IsChecked == true)
|
||||||
|
text += "\r\n";
|
||||||
|
|
||||||
|
var bytesResult = SerialDataConverter.StringToBytes(text, encoding);
|
||||||
|
if (bytesResult.IsSuccess && bytesResult.Value != null)
|
||||||
|
{
|
||||||
|
await _driver.WriteBytesAsync(bytesResult.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearSend_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
SendDataTextBox.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearReceive_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ReceiveTextBox.Clear();
|
||||||
|
_totalReceivedBytes = 0;
|
||||||
|
ReceivedBytesText.Text = "已接收: 0 字节";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReceiveLoop()
|
||||||
|
{
|
||||||
|
while (_driver != null && _driver.IsOpen)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_driver.BytesToRead > 0)
|
||||||
|
{
|
||||||
|
var result = await _driver.ReadAvailableAsync();
|
||||||
|
if (result.IsSuccess && result.Value != null && result.Value.Length > 0)
|
||||||
|
{
|
||||||
|
_totalReceivedBytes += result.Value.Length;
|
||||||
|
Dispatcher.Invoke(() => DisplayReceivedData(result.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(10);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisplayReceivedData(byte[] data)
|
||||||
|
{
|
||||||
|
string display;
|
||||||
|
if (HexDisplayCheckBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
var hexResult = SerialDataConverter.BytesToHexString(data, " ");
|
||||||
|
display = hexResult.Value + "\r\n";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var encoding = GetSelectedEncoding();
|
||||||
|
var strResult = SerialDataConverter.BytesToString(data, encoding);
|
||||||
|
display = strResult.Value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReceiveTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {display}");
|
||||||
|
ReceivedBytesText.Text = $"已接收: {_totalReceivedBytes} 字节";
|
||||||
|
|
||||||
|
if (AutoScrollCheckBox.IsChecked == true)
|
||||||
|
{
|
||||||
|
ReceiveTextBox.ScrollToEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tab 3: 高级监听
|
||||||
|
|
||||||
|
private async void StartListening_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (ServicePortCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var options = new SerialPortOptions
|
||||||
|
{
|
||||||
|
PortName = ServicePortCombo.SelectedItem.ToString()!,
|
||||||
|
BaudRate = int.Parse(((ComboBoxItem)ServiceBaudRateCombo.SelectedItem).Content.ToString()!)
|
||||||
|
};
|
||||||
|
|
||||||
|
_service = new SerialPortService(new SerialPortDriver());
|
||||||
|
|
||||||
|
// 订阅事件
|
||||||
|
_service.DataReceived += OnServiceDataReceived;
|
||||||
|
_service.ErrorOccurred += OnServiceErrorOccurred;
|
||||||
|
_service.ConnectionStateChanged += OnServiceConnectionStateChanged;
|
||||||
|
|
||||||
|
var autoReconnect = AutoReconnectCheckBox.IsChecked == true;
|
||||||
|
var result = await _service.StartListeningAsync(options, autoReconnect);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
ServiceStatusText.Text = "监听中";
|
||||||
|
StartListeningButton.IsEnabled = false;
|
||||||
|
StopListeningButton.IsEnabled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ServiceStatusText.Text = $"启动失败";
|
||||||
|
HandyControl.Controls.MessageBox.Show(result.ErrorMessage ?? "启动失败", "错误");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void StopListening_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_service != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _service.StopListeningAsync();
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 任务取消是正常情况,忽略
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show($"停止监听时发生错误: {ex.Message}", "错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_service.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略清理异常
|
||||||
|
}
|
||||||
|
|
||||||
|
_service = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceStatusText.Text = "未启动";
|
||||||
|
StartListeningButton.IsEnabled = true;
|
||||||
|
StopListeningButton.IsEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServiceDataReceived(object? sender, DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var hexResult = SerialDataConverter.BytesToHexString(e.Data, " ");
|
||||||
|
_eventLog.Add(new EventLogItem
|
||||||
|
{
|
||||||
|
Time = e.ReceivedAt,
|
||||||
|
EventType = "数据接收",
|
||||||
|
Data = $"{e.Data.Length} 字节: {hexResult.Value}"
|
||||||
|
});
|
||||||
|
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServiceErrorOccurred(object? sender, Ports.Models.ErrorEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_eventLog.Add(new EventLogItem
|
||||||
|
{
|
||||||
|
Time = e.OccurredAt,
|
||||||
|
EventType = "错误",
|
||||||
|
Data = e.ErrorMessage
|
||||||
|
});
|
||||||
|
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnServiceConnectionStateChanged(object? sender, ConnectionStateChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_eventLog.Add(new EventLogItem
|
||||||
|
{
|
||||||
|
Time = e.ChangedAt,
|
||||||
|
EventType = "状态变化",
|
||||||
|
Data = $"{e.PreviousState} -> {e.CurrentState}"
|
||||||
|
});
|
||||||
|
ServiceStatusText.Text = e.CurrentState.ToString();
|
||||||
|
EventCountText.Text = $"事件数: {_eventLog.Count}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearEventLog_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_eventLog.Clear();
|
||||||
|
EventCountText.Text = "事件数: 0";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Tab 4: 数据工具
|
||||||
|
|
||||||
|
private void ConvertData_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var input = ConvertInputTextBox.Text;
|
||||||
|
var type = ((ComboBoxItem)ConvertTypeCombo.SelectedItem).Content.ToString();
|
||||||
|
var isBigEndian = BigEndianCheckBox.IsChecked == true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "HEX -> 字节":
|
||||||
|
var hexBytes = SerialDataConverter.HexStringToBytes(input);
|
||||||
|
ConvertResultTextBox.Text = hexBytes.IsSuccess ? $"字节数组长度: {hexBytes.Value!.Length}" : hexBytes.ErrorMessage;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "字节 -> HEX":
|
||||||
|
var bytes = SerialDataConverter.HexStringToBytes(input);
|
||||||
|
if (bytes.IsSuccess)
|
||||||
|
{
|
||||||
|
var hex = SerialDataConverter.BytesToHexString(bytes.Value!, " ");
|
||||||
|
ConvertResultTextBox.Text = hex.Value;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "字节 -> Int16":
|
||||||
|
var int16Bytes = SerialDataConverter.HexStringToBytes(input);
|
||||||
|
if (int16Bytes.IsSuccess)
|
||||||
|
{
|
||||||
|
var int16 = SerialDataConverter.BytesToInt16(int16Bytes.Value!, isBigEndian);
|
||||||
|
ConvertResultTextBox.Text = int16.IsSuccess ? int16.Value.ToString() : int16.ErrorMessage;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "字节 -> Int32":
|
||||||
|
var int32Bytes = SerialDataConverter.HexStringToBytes(input);
|
||||||
|
if (int32Bytes.IsSuccess)
|
||||||
|
{
|
||||||
|
var int32 = SerialDataConverter.BytesToInt32(int32Bytes.Value!, isBigEndian);
|
||||||
|
ConvertResultTextBox.Text = int32.IsSuccess ? int32.Value.ToString() : int32.ErrorMessage;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "字节 -> Float":
|
||||||
|
var floatBytes = SerialDataConverter.HexStringToBytes(input);
|
||||||
|
if (floatBytes.IsSuccess)
|
||||||
|
{
|
||||||
|
var floatValue = SerialDataConverter.BytesToFloat(floatBytes.Value!, isBigEndian);
|
||||||
|
ConvertResultTextBox.Text = floatValue.IsSuccess ? floatValue.Value.ToString("F4") : floatValue.ErrorMessage;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "Int32 -> 字节":
|
||||||
|
if (int.TryParse(input, out var intValue))
|
||||||
|
{
|
||||||
|
var intBytes = SerialDataConverter.Int32ToBytes(intValue, isBigEndian);
|
||||||
|
if (intBytes.IsSuccess)
|
||||||
|
{
|
||||||
|
var hex = SerialDataConverter.BytesToHexString(intBytes.Value!, " ");
|
||||||
|
ConvertResultTextBox.Text = hex.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConvertResultTextBox.Text = "输入不是有效的整数";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ConvertResultTextBox.Text = $"转换错误: {ex.Message}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateCrc16_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
||||||
|
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
||||||
|
{
|
||||||
|
var crc = SerialChecksumHelper.CalculateCrc16(hexBytes.Value);
|
||||||
|
ChecksumResultTextBox.Text = crc.IsSuccess ? $"0x{crc.Value:X4}" : crc.ErrorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateLrc_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
||||||
|
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
||||||
|
{
|
||||||
|
var lrc = SerialChecksumHelper.CalculateLrc(hexBytes.Value);
|
||||||
|
ChecksumResultTextBox.Text = lrc.IsSuccess ? $"0x{lrc.Value:X2}" : lrc.ErrorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateXor_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
||||||
|
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
||||||
|
{
|
||||||
|
var xor = SerialChecksumHelper.CalculateXor(hexBytes.Value);
|
||||||
|
ChecksumResultTextBox.Text = xor.IsSuccess ? $"0x{xor.Value:X2}" : xor.ErrorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CalculateChecksum_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var hexBytes = SerialDataConverter.HexStringToBytes(ChecksumInputTextBox.Text);
|
||||||
|
if (hexBytes.IsSuccess && hexBytes.Value != null)
|
||||||
|
{
|
||||||
|
var checksum = SerialChecksumHelper.CalculateChecksum(hexBytes.Value);
|
||||||
|
ChecksumResultTextBox.Text = checksum.IsSuccess ? $"0x{checksum.Value:X2}" : checksum.ErrorMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void DiagTestConnection_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DiagPortCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagResultTextBox.Text = "正在测试连接...\r\n";
|
||||||
|
|
||||||
|
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
||||||
|
var options = new SerialPortOptions { PortName = portName };
|
||||||
|
|
||||||
|
var result = await SerialPortDiagnostics.TestConnectionAsync(portName, options);
|
||||||
|
DiagResultTextBox.AppendText(result.IsSuccess ? result.Value! : $"错误: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ScanBaudRates_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DiagPortCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagResultTextBox.Text = "正在扫描波特率...\r\n";
|
||||||
|
|
||||||
|
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
||||||
|
var result = await SerialPortDiagnostics.TestBaudRatesAsync(
|
||||||
|
portName, 4800, 9600, 19200, 38400, 57600, 115200);
|
||||||
|
|
||||||
|
if (result.IsSuccess && result.Value != null)
|
||||||
|
{
|
||||||
|
DiagResultTextBox.AppendText("\r\n波特率测试结果:\r\n");
|
||||||
|
foreach (var kvp in result.Value)
|
||||||
|
{
|
||||||
|
DiagResultTextBox.AppendText($" {kvp.Key,6} bps: {(kvp.Value ? "✓ 成功" : "✗ 失败")}\r\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DiagResultTextBox.AppendText($"错误: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void MonitorTraffic_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DiagPortCombo.SelectedItem == null)
|
||||||
|
{
|
||||||
|
HandyControl.Controls.MessageBox.Show("请选择串口", "提示");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DiagResultTextBox.Text = "正在监控流量 (10秒)...\r\n";
|
||||||
|
|
||||||
|
var portName = DiagPortCombo.SelectedItem.ToString()!;
|
||||||
|
var options = new SerialPortOptions { PortName = portName };
|
||||||
|
|
||||||
|
var result = await SerialPortDiagnostics.MonitorTrafficAsync(
|
||||||
|
portName,
|
||||||
|
options,
|
||||||
|
(data, isSent) => Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var hexResult = SerialDataConverter.BytesToHexString(data, " ");
|
||||||
|
DiagResultTextBox.AppendText($"[{DateTime.Now:HH:mm:ss}] {(isSent ? "发送" : "接收")}: {hexResult.Value}\r\n");
|
||||||
|
}),
|
||||||
|
TimeSpan.FromSeconds(10));
|
||||||
|
|
||||||
|
if (result.IsSuccess && result.Value != null)
|
||||||
|
{
|
||||||
|
DiagResultTextBox.AppendText($"\r\n{result.Value}\r\n");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DiagResultTextBox.AppendText($"错误: {result.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 辅助方法
|
||||||
|
|
||||||
|
private Encoding GetSelectedEncoding()
|
||||||
|
{
|
||||||
|
var encoding = ((ComboBoxItem)EncodingCombo.SelectedItem)?.Content.ToString();
|
||||||
|
return encoding switch
|
||||||
|
{
|
||||||
|
"ASCII" => Encoding.ASCII,
|
||||||
|
"GBK" => Encoding.GetEncoding("GBK"),
|
||||||
|
_ => Encoding.UTF8
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosed(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnClosed(e);
|
||||||
|
|
||||||
|
// 清理资源
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_driver?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略清理异常
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_service?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 忽略清理异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EventLogItem
|
||||||
|
{
|
||||||
|
public DateTime Time { get; set; }
|
||||||
|
public string EventType { get; set; } = string.Empty;
|
||||||
|
public string Data { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user