将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace XP.Common.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog日志配置实体(从App.config读取)
|
||||
/// </summary>
|
||||
public class SerilogConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 日志输出根路径(默认:AppData/Files/Logs)
|
||||
/// </summary>
|
||||
public string LogPath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Files", "Logs");
|
||||
|
||||
/// <summary>
|
||||
/// 最低日志级别(Debug/Info/Warn/Error/Fatal)
|
||||
/// </summary>
|
||||
public string MinimumLevel { get; set; } = "Info";
|
||||
|
||||
/// <summary>
|
||||
/// 是否输出到控制台(调试环境=true,生产环境=false)
|
||||
/// </summary>
|
||||
public bool EnableConsole { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 日志文件分割规则(Day/Month/Hour/Size)
|
||||
/// </summary>
|
||||
public string RollingInterval { get; set; } = "Day";
|
||||
|
||||
/// <summary>
|
||||
/// 单个日志文件最大大小(MB,仅Size分割时生效)
|
||||
/// </summary>
|
||||
public long FileSizeLimitMB { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 保留日志文件数量(默认30天)
|
||||
/// </summary>
|
||||
public int RetainedFileCountLimit { get; set; } = 30;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite 配置实体
|
||||
/// </summary>
|
||||
public class SqliteConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库文件路径
|
||||
/// </summary>
|
||||
public string DbFilePath { get; set; } = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"Files", "Data", "XP.db");
|
||||
|
||||
/// <summary>
|
||||
/// 连接超时时间(秒,默认30)
|
||||
/// </summary>
|
||||
public int ConnectionTimeout { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// 数据库不存在时是否自动创建(默认true)
|
||||
/// </summary>
|
||||
public bool CreateIfNotExists { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用 WAL 模式(提升并发性能,默认true)
|
||||
/// </summary>
|
||||
public bool EnableWalMode { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启日志记录(记录所有SQL操作,默认false)
|
||||
/// </summary>
|
||||
public bool EnableSqlLogging { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 获取SQLite连接字符串
|
||||
/// </summary>
|
||||
public string GetConnectionString()
|
||||
{
|
||||
var builder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder
|
||||
{
|
||||
DataSource = DbFilePath,
|
||||
Cache = Microsoft.Data.Sqlite.SqliteCacheMode.Default,
|
||||
DefaultTimeout = ConnectionTimeout
|
||||
};
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Emgu.CV;
|
||||
using Emgu.CV.CvEnum;
|
||||
|
||||
namespace XP.Common.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像数据转换工具类,基于 Emgu CV 实现
|
||||
/// </summary>
|
||||
public static class ImageConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// 将 16 位无符号灰度数据转换为 RGB BitmapSource
|
||||
/// 使用 Emgu CV 的 Normalize 进行线性拉伸,CvtColor 转 BGR
|
||||
/// </summary>
|
||||
/// <param name="data">16 位灰度原始数据</param>
|
||||
/// <param name="width">图像宽度</param>
|
||||
/// <param name="height">图像高度</param>
|
||||
/// <returns>BGR24 格式的 BitmapSource</returns>
|
||||
public static BitmapSource ConvertGray16ToBitmapSource(ushort[] data, int width, int height)
|
||||
{
|
||||
if (data == null) throw new ArgumentNullException(nameof(data));
|
||||
if (width <= 0 || height <= 0) throw new ArgumentException("图像尺寸无效");
|
||||
|
||||
// 从 ushort[] 按字节拷贝构建 16 位灰度 Mat
|
||||
using var mat16 = new Mat(height, width, DepthType.Cv16U, 1);
|
||||
int byteCount = data.Length * sizeof(ushort);
|
||||
unsafe
|
||||
{
|
||||
fixed (ushort* pData = data)
|
||||
{
|
||||
Buffer.MemoryCopy(pData, mat16.DataPointer.ToPointer(), byteCount, byteCount);
|
||||
}
|
||||
}
|
||||
|
||||
// 归一化到 0~255 并转为 8 位
|
||||
using var mat8 = new Mat();
|
||||
CvInvoke.Normalize(mat16, mat8, 0, 255, NormType.MinMax, DepthType.Cv8U);
|
||||
|
||||
// 灰度转 BGR(立式 CT 显示需要 RGB 格式)
|
||||
using var matBgr = new Mat();
|
||||
CvInvoke.CvtColor(mat8, matBgr, ColorConversion.Gray2Bgr);
|
||||
|
||||
// Mat 转 BitmapSource
|
||||
return MatToBitmapSource(matBgr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Emgu CV Mat 转换为 WPF BitmapSource
|
||||
/// </summary>
|
||||
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||
{
|
||||
int width = mat.Width;
|
||||
int height = mat.Height;
|
||||
int channels = mat.NumberOfChannels;
|
||||
int stride = mat.Step;
|
||||
|
||||
byte[] pixels = new byte[height * stride];
|
||||
Marshal.Copy(mat.DataPointer, pixels, 0, pixels.Length);
|
||||
|
||||
var format = channels == 3 ? PixelFormats.Bgr24 : PixelFormats.Gray8;
|
||||
return BitmapSource.Create(width, height, 96, 96, format, null, pixels, stride);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Database.Models;
|
||||
|
||||
namespace XP.Common.Database.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 分页计算辅助工具
|
||||
/// </summary>
|
||||
public static class PaginationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算分页偏移量(SQLite OFFSET)
|
||||
/// </summary>
|
||||
public static int CalculateOffset(PaginationRequest pagination)
|
||||
{
|
||||
if (pagination.PageIndex < 1) pagination.PageIndex = 1;
|
||||
if (pagination.PageSize < 1) pagination.PageSize = 20;
|
||||
return (pagination.PageIndex - 1) * pagination.PageSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证并修正分页参数
|
||||
/// </summary>
|
||||
public static PaginationRequest ValidateAndFix(PaginationRequest pagination)
|
||||
{
|
||||
var fixedPagination = new PaginationRequest
|
||||
{
|
||||
PageIndex = pagination.PageIndex < 1 ? 1 : pagination.PageIndex,
|
||||
PageSize = pagination.PageSize < 1 ? 20 : (pagination.PageSize > 1000 ? 1000 : pagination.PageSize),
|
||||
OrderBy = pagination.OrderBy ?? string.Empty
|
||||
};
|
||||
return fixedPagination;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用CRUD SQL构建器(适配SQLite)
|
||||
/// </summary>
|
||||
public static class SqlBuilderHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 构建插入SQL
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
/// <param name="columns">列名集合</param>
|
||||
/// <returns>插入SQL(参数名:@列名)</returns>
|
||||
public static string BuildInsertSql(string tableName, IEnumerable<string> columns)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||
var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
|
||||
if (columnList.Count == 0) throw new ArgumentException("列名集合不能为空");
|
||||
|
||||
var columnsStr = string.Join(", ", columnList);
|
||||
var paramsStr = string.Join(", ", columnList.Select(c => $"@{c}"));
|
||||
return $"INSERT INTO {tableName} ({columnsStr}) VALUES ({paramsStr})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建更新SQL
|
||||
/// </summary>
|
||||
/// <param name="tableName">表名</param>
|
||||
/// <param name="updateColumns">更新列名</param>
|
||||
/// <param name="whereColumns">条件列名(如Id)</param>
|
||||
/// <returns>更新SQL</returns>
|
||||
public static string BuildUpdateSql(string tableName, IEnumerable<string> updateColumns, IEnumerable<string> whereColumns)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||
var updateList = updateColumns?.ToList() ?? throw new ArgumentNullException(nameof(updateColumns));
|
||||
var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
|
||||
if (updateList.Count == 0) throw new ArgumentException("更新列名集合不能为空");
|
||||
if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
|
||||
|
||||
var updateStr = string.Join(", ", updateList.Select(c => $"{c}=@{c}"));
|
||||
var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
|
||||
return $"UPDATE {tableName} SET {updateStr} WHERE {whereStr}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建删除SQL
|
||||
/// </summary>
|
||||
public static string BuildDeleteSql(string tableName, IEnumerable<string> whereColumns)
|
||||
{
|
||||
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||
var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
|
||||
if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
|
||||
|
||||
var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
|
||||
return $"DELETE FROM {tableName} WHERE {whereStr}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建查询SQL
|
||||
/// </summary>
|
||||
public static string BuildSelectSql(string tableName, IEnumerable<string> columns, IEnumerable<string>? whereColumns = null, string orderBy = "")
|
||||
{
|
||||
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||
var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
|
||||
if (columnList.Count == 0) columnList.Add("*");
|
||||
|
||||
var columnsStr = string.Join(", ", columnList);
|
||||
var sql = new StringBuilder($"SELECT {columnsStr} FROM {tableName}");
|
||||
|
||||
// 添加WHERE条件
|
||||
if (whereColumns != null && whereColumns.Any())
|
||||
{
|
||||
var whereStr = string.Join(" AND ", whereColumns.Select(c => $"{c}=@{c}"));
|
||||
sql.Append($" WHERE {whereStr}");
|
||||
}
|
||||
|
||||
// 添加排序
|
||||
if (!string.IsNullOrEmpty(orderBy))
|
||||
{
|
||||
sql.Append($" ORDER BY {orderBy}");
|
||||
}
|
||||
|
||||
return sql.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite参数化查询辅助工具
|
||||
/// </summary>
|
||||
public static class SqliteParameterHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建参数字典
|
||||
/// </summary>
|
||||
/// <param name="keyValues">参数名-值对(如 ("Id", 1), ("Name", "Test"))</param>
|
||||
/// <returns>参数字典</returns>
|
||||
public static Dictionary<string, object> CreateParameters(params (string Key, object Value)[] keyValues)
|
||||
{
|
||||
var parameters = new Dictionary<string, object>();
|
||||
foreach (var (key, value) in keyValues)
|
||||
{
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 合并参数字典
|
||||
/// </summary>
|
||||
public static Dictionary<string, object> MergeParameters(params Dictionary<string, object>[] paramLists)
|
||||
{
|
||||
var merged = new Dictionary<string, object>();
|
||||
foreach (var paramList in paramLists)
|
||||
{
|
||||
foreach (var (key, value) in paramList)
|
||||
{
|
||||
if (!merged.ContainsKey(key))
|
||||
{
|
||||
merged.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Database.Interfaces;
|
||||
|
||||
namespace XP.Common.Database.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库操作执行结果实体
|
||||
/// </summary>
|
||||
public class DbExecuteResult : IDbExecuteResult
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public int RowsAffected { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public Exception? Exception { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 快速创建成功结果
|
||||
/// </summary>
|
||||
public static DbExecuteResult Success(string message = "执行成功", int rowsAffected = 0)
|
||||
{
|
||||
return new DbExecuteResult
|
||||
{
|
||||
IsSuccess = true,
|
||||
Message = message,
|
||||
RowsAffected = rowsAffected
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 快速创建失败结果
|
||||
/// </summary>
|
||||
public static DbExecuteResult Fail(string message, Exception? ex = null, int rowsAffected = 0)
|
||||
{
|
||||
return new DbExecuteResult
|
||||
{
|
||||
IsSuccess = false,
|
||||
Message = message,
|
||||
Exception = ex,
|
||||
RowsAffected = rowsAffected
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,527 @@
|
||||
using Microsoft.Data.Sqlite;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.Database.Models;
|
||||
using XP.Common.Helpers;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
using CustomDbTransaction = XP.Common.Database.Interfaces.IDbTransaction;
|
||||
|
||||
namespace XP.Common.Database.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite 核心操作实现(适配IDbContext)
|
||||
/// </summary>
|
||||
public class SqliteContext : IDbContext
|
||||
{
|
||||
private readonly SqliteConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
private SqliteConnection? _connection;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(DI注入配置+日志)
|
||||
/// </summary>
|
||||
public SqliteContext(SqliteConfig config, ILoggerService logger)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_logger = logger.ForModule("Sqlite.Context");
|
||||
|
||||
// 自动创建数据库目录
|
||||
if (_config.CreateIfNotExists)
|
||||
{
|
||||
var dbDir = Path.GetDirectoryName(_config.DbFilePath);
|
||||
if (!string.IsNullOrEmpty(dbDir) && !Directory.Exists(dbDir))
|
||||
{
|
||||
Directory.CreateDirectory(dbDir);
|
||||
_logger.Info("自动创建数据库目录:{DirPath}", dbDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region 连接管理
|
||||
public IDbExecuteResult OpenConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_connection != null && _connection.State == ConnectionState.Open)
|
||||
{
|
||||
return DbExecuteResult.Success("连接已打开");
|
||||
}
|
||||
|
||||
_connection = new SqliteConnection(_config.GetConnectionString());
|
||||
_connection.Open();
|
||||
_logger.Debug("SQLite连接已打开:{DbPath}", _config.DbFilePath);
|
||||
|
||||
// 启用WAL模式提升性能
|
||||
if (_config.EnableWalMode)
|
||||
{
|
||||
using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
|
||||
cmd.ExecuteNonQuery();
|
||||
_logger.Debug("SQLite已启用WAL模式");
|
||||
}
|
||||
|
||||
return DbExecuteResult.Success("连接打开成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQLite连接打开失败:{DbPath}", _config.DbFilePath);
|
||||
return DbExecuteResult.Fail("连接打开失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IDbExecuteResult> OpenConnectionAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_connection != null && _connection.State == ConnectionState.Open)
|
||||
{
|
||||
return DbExecuteResult.Success("连接已打开");
|
||||
}
|
||||
|
||||
_connection = new SqliteConnection(_config.GetConnectionString());
|
||||
await _connection.OpenAsync();
|
||||
_logger.Debug("SQLite连接已异步打开:{DbPath}", _config.DbFilePath);
|
||||
|
||||
if (_config.EnableWalMode)
|
||||
{
|
||||
using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
|
||||
return DbExecuteResult.Success("连接异步打开成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQLite连接异步打开失败:{DbPath}", _config.DbFilePath);
|
||||
return DbExecuteResult.Fail("连接异步打开失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取有效连接(自动打开)
|
||||
/// </summary>
|
||||
private SqliteConnection GetValidConnection()
|
||||
{
|
||||
if (_connection == null || _connection.State != ConnectionState.Open)
|
||||
{
|
||||
var result = OpenConnection();
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
throw new InvalidOperationException($"获取SQLite连接失败:{result.Message}");
|
||||
}
|
||||
}
|
||||
return _connection!;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 事务管理
|
||||
public (IDbExecuteResult Result, CustomDbTransaction? Transaction) BeginTransaction()
|
||||
{
|
||||
try
|
||||
{
|
||||
var conn = GetValidConnection();
|
||||
// 系统的SqliteTransaction
|
||||
var innerSystemTrans = conn.BeginTransaction();
|
||||
// 封装为自定义的SqliteTransaction
|
||||
var customTrans = new SqliteTransaction(innerSystemTrans, _logger);
|
||||
_logger.Debug("SQLite事务已开始");
|
||||
// 返回:自定义IDbTransaction(解决返回类型不匹配)
|
||||
return (DbExecuteResult.Success("事务开始成功"), customTrans);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQLite事务开始失败");
|
||||
return (DbExecuteResult.Fail("事务开始失败", ex), null);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 通用SQL执行
|
||||
public IDbExecuteResult ExecuteNonQuery(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
var rowsAffected = cmd.ExecuteNonQuery();
|
||||
_logger.Debug("SQL执行成功,影响行数:{Rows}", rowsAffected);
|
||||
return DbExecuteResult.Success("执行成功", rowsAffected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL执行失败:{Sql}", sql);
|
||||
return DbExecuteResult.Fail("执行失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IDbExecuteResult> ExecuteNonQueryAsync(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
var rowsAffected = await cmd.ExecuteNonQueryAsync();
|
||||
_logger.Debug("SQL异步执行成功,影响行数:{Rows}", rowsAffected);
|
||||
return DbExecuteResult.Success("异步执行成功", (int)rowsAffected);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL异步执行失败:{Sql}", sql);
|
||||
return DbExecuteResult.Fail("异步执行失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public (IDbExecuteResult Result, T? Value) ExecuteScalar<T>(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
var resultObj = cmd.ExecuteScalar();
|
||||
T? value = default;
|
||||
if (resultObj != null && resultObj != DBNull.Value)
|
||||
{
|
||||
if (resultObj is T directValue)
|
||||
{
|
||||
value = directValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 类型不匹配时安全转换,处理值类型/引用类型
|
||||
value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
|
||||
}
|
||||
}
|
||||
var logValue = value ?? (object)"null";
|
||||
_logger.Debug("SQL标量查询成功,返回值:{Value}", logValue);
|
||||
return (DbExecuteResult.Success("标量查询成功"), value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL标量查询失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("标量查询失败", ex), default);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync<T>(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
var resultObj = await cmd.ExecuteScalarAsync();
|
||||
T? value = default;
|
||||
if (resultObj != null && resultObj != DBNull.Value)
|
||||
{
|
||||
if (resultObj is T directValue)
|
||||
{
|
||||
value = directValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 类型不匹配时安全转换,处理值类型/引用类型
|
||||
value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
|
||||
}
|
||||
}
|
||||
var logValue = value ?? (object)"null";
|
||||
_logger.Debug("SQL异步标量查询成功,返回值:{Value}", logValue);
|
||||
return (DbExecuteResult.Success("异步标量查询成功"), value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL异步标量查询失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("异步标量查询失败", ex), default);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 查询结果映射
|
||||
public (IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
using var reader = cmd.ExecuteReader();
|
||||
|
||||
var dt = new DataTable();
|
||||
// 添加列
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
|
||||
}
|
||||
// 添加行
|
||||
while (reader.Read())
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row[i] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
|
||||
}
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
|
||||
_logger.Debug("SQL查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
|
||||
return (DbExecuteResult.Success("查询成功"), dt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL查询DataTable失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("查询失败", ex), null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
LogSql(sql, parameters);
|
||||
using var cmd = CreateCommand(sql, parameters);
|
||||
using var reader = await cmd.ExecuteReaderAsync();
|
||||
|
||||
var dt = new DataTable();
|
||||
// 添加列
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
|
||||
}
|
||||
// 添加行(异步读取)
|
||||
while (await reader.ReadAsync())
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
row[i] = await reader.IsDBNullAsync(i) ? DBNull.Value : reader.GetValue(i);
|
||||
}
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
_logger.Debug("SQL异步查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
|
||||
return (DbExecuteResult.Success("异步查询成功"), dt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL异步查询DataTable失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("异步查询失败", ex), null);
|
||||
}
|
||||
}
|
||||
|
||||
public (IDbExecuteResult Result, List<T> Data) QueryList<T>(string sql, Dictionary<string, object>? parameters = null) where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (result, dt) = ExecuteDataTable(sql, parameters);
|
||||
if (!result.IsSuccess || dt == null)
|
||||
{
|
||||
return (result, new List<T>());
|
||||
}
|
||||
|
||||
var list = MapDataTableToEntityList<T>(dt);
|
||||
return (DbExecuteResult.Success("实体列表查询成功"), list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL查询实体列表失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("实体列表查询失败", ex), new List<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IDbExecuteResult Result, List<T> Data)> QueryListAsync<T>(string sql, Dictionary<string, object>? parameters = null) where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (result, dt) = await ExecuteDataTableAsync(sql, parameters);
|
||||
if (!result.IsSuccess || dt == null)
|
||||
{
|
||||
return (result, new List<T>());
|
||||
}
|
||||
|
||||
var list = MapDataTableToEntityList<T>(dt);
|
||||
return (DbExecuteResult.Success("异步实体列表查询成功"), list);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL异步查询实体列表失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("异步实体列表查询失败", ex), new List<T>());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 分页查询
|
||||
public (IDbExecuteResult Result, PaginationResponse<T> Data) QueryPaged<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 查询总条数
|
||||
var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
|
||||
var (countResult, totalCount) = ExecuteScalar<int>(countSql, parameters);
|
||||
if (!countResult.IsSuccess)
|
||||
{
|
||||
return (countResult, new PaginationResponse<T>());
|
||||
}
|
||||
|
||||
// 2. 构建分页SQL(SQLite分页:LIMIT/OFFSET)
|
||||
var offset = (pagination.PageIndex - 1) * pagination.PageSize;
|
||||
var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
|
||||
|
||||
// 3. 查询当前页数据
|
||||
var (dataResult, list) = QueryList<T>(pagedSql, parameters);
|
||||
if (!dataResult.IsSuccess)
|
||||
{
|
||||
return (dataResult, new PaginationResponse<T>());
|
||||
}
|
||||
|
||||
// 4. 封装分页结果
|
||||
var pagedResponse = new PaginationResponse<T>
|
||||
{
|
||||
PageIndex = pagination.PageIndex,
|
||||
PageSize = pagination.PageSize,
|
||||
TotalCount = totalCount,
|
||||
Data = list
|
||||
};
|
||||
|
||||
_logger.Debug("SQL分页查询成功:页码{Page},条数{Size},总条数{Total}",
|
||||
pagination.PageIndex, pagination.PageSize, totalCount);
|
||||
return (DbExecuteResult.Success("分页查询成功"), pagedResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL分页查询失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("分页查询失败", ex), new PaginationResponse<T>());
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(IDbExecuteResult Result, PaginationResponse<T> Data)> QueryPagedAsync<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 1. 查询总条数
|
||||
var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
|
||||
var (countResult, totalCount) = await ExecuteScalarAsync<int>(countSql, parameters);
|
||||
if (!countResult.IsSuccess)
|
||||
{
|
||||
return (countResult, new PaginationResponse<T>());
|
||||
}
|
||||
|
||||
// 2. 构建分页SQL
|
||||
var offset = (pagination.PageIndex - 1) * pagination.PageSize;
|
||||
var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
|
||||
|
||||
// 3. 查询当前页数据
|
||||
var (dataResult, list) = await QueryListAsync<T>(pagedSql, parameters);
|
||||
if (!dataResult.IsSuccess)
|
||||
{
|
||||
return (dataResult, new PaginationResponse<T>());
|
||||
}
|
||||
|
||||
// 4. 封装分页结果
|
||||
var pagedResponse = new PaginationResponse<T>
|
||||
{
|
||||
PageIndex = pagination.PageIndex,
|
||||
PageSize = pagination.PageSize,
|
||||
TotalCount = totalCount,
|
||||
Data = list
|
||||
};
|
||||
|
||||
_logger.Debug("SQL异步分页查询成功:页码{Page},条数{Size},总条数{Total}",
|
||||
pagination.PageIndex, pagination.PageSize, totalCount);
|
||||
return (DbExecuteResult.Success("异步分页查询成功"), pagedResponse);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQL异步分页查询失败:{Sql}", sql);
|
||||
return (DbExecuteResult.Fail("异步分页查询失败", ex), new PaginationResponse<T>());
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 私有辅助方法
|
||||
/// <summary>
|
||||
/// 创建SQLite命令(带参数)
|
||||
/// </summary>
|
||||
private SqliteCommand CreateCommand(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
var cmd = new SqliteCommand(sql, GetValidConnection());
|
||||
cmd.CommandTimeout = _config.ConnectionTimeout;
|
||||
|
||||
// 添加参数(防SQL注入)
|
||||
if (parameters != null && parameters.Count > 0)
|
||||
{
|
||||
foreach (var (key, value) in parameters)
|
||||
{
|
||||
var paramValue = value ?? DBNull.Value;
|
||||
cmd.Parameters.AddWithValue($"@{key}", paramValue);
|
||||
}
|
||||
}
|
||||
|
||||
return cmd;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录SQL日志(调试模式)
|
||||
/// </summary>
|
||||
private void LogSql(string sql, Dictionary<string, object>? parameters = null)
|
||||
{
|
||||
if (!_config.EnableSqlLogging) return;
|
||||
|
||||
var paramStr = parameters == null
|
||||
? "无参数"
|
||||
: string.Join(", ", parameters.Select(kv => $"{kv.Key}={kv.Value}"));
|
||||
_logger.Debug("执行SQL:{Sql} | 参数:{Params}", sql, paramStr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataTable映射为实体列表
|
||||
/// </summary>
|
||||
private List<T> MapDataTableToEntityList<T>(DataTable dt) where T : new()
|
||||
{
|
||||
var list = new List<T>();
|
||||
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
foreach (DataRow row in dt.Rows)
|
||||
{
|
||||
var entity = new T();
|
||||
foreach (var prop in props)
|
||||
{
|
||||
if (dt.Columns.Contains(prop.Name) && row[prop.Name] != DBNull.Value)
|
||||
{
|
||||
var value = Convert.ChangeType(row[prop.Name], prop.PropertyType);
|
||||
prop.SetValue(entity, value);
|
||||
}
|
||||
}
|
||||
list.Add(entity);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 资源释放
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
||||
// 关闭连接
|
||||
if (_connection != null)
|
||||
{
|
||||
if (_connection.State == ConnectionState.Open)
|
||||
{
|
||||
_connection.Close();
|
||||
_logger.Debug("SQLite连接已关闭(Dispose)");
|
||||
}
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
_logger.Debug("SQLiteContext资源已释放");
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using XP.Common.Database.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Database.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// SQLite 事务实现
|
||||
/// </summary>
|
||||
public class SqliteTransaction : IDbTransaction
|
||||
{
|
||||
private readonly Microsoft.Data.Sqlite.SqliteTransaction _innerTransaction;
|
||||
private readonly ILoggerService _logger;
|
||||
private bool _isDisposed = false;
|
||||
|
||||
public SqliteTransaction(Microsoft.Data.Sqlite.SqliteTransaction innerTrans, ILoggerService logger)
|
||||
{
|
||||
_innerTransaction = innerTrans ?? throw new ArgumentNullException(nameof(innerTrans));
|
||||
_logger = logger.ForModule("Sqlite.Transaction");
|
||||
}
|
||||
|
||||
public IDbExecuteResult Commit()
|
||||
{
|
||||
try
|
||||
{
|
||||
_innerTransaction.Commit();
|
||||
_logger.Debug("SQLite事务提交成功");
|
||||
return DbExecuteResult.Success("事务提交成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQLite事务提交失败");
|
||||
return DbExecuteResult.Fail("事务提交失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public IDbExecuteResult Rollback()
|
||||
{
|
||||
try
|
||||
{
|
||||
_innerTransaction.Rollback();
|
||||
_logger.Debug("SQLite事务回滚成功");
|
||||
return DbExecuteResult.Success("事务回滚成功");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "SQLite事务回滚失败");
|
||||
return DbExecuteResult.Fail("事务回滚失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_innerTransaction.Dispose();
|
||||
_isDisposed = true;
|
||||
_logger.Debug("SQLite事务资源已释放");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Database.Models;
|
||||
|
||||
namespace XP.Common.Database.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用数据库操作接口(适配任意数据库,SQLite为具体实现)
|
||||
/// </summary>
|
||||
public interface IDbContext : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 打开数据库连接
|
||||
/// </summary>
|
||||
IDbExecuteResult OpenConnection();
|
||||
|
||||
/// <summary>
|
||||
/// 异步打开数据库连接
|
||||
/// </summary>
|
||||
Task<IDbExecuteResult> OpenConnectionAsync();
|
||||
|
||||
/// <summary>
|
||||
/// 开始事务
|
||||
/// </summary>
|
||||
(IDbExecuteResult Result, IDbTransaction? Transaction) BeginTransaction();
|
||||
|
||||
/// <summary>
|
||||
/// 执行增删改SQL(无返回值)
|
||||
/// </summary>
|
||||
/// <param name="sql">SQL语句(参数化)</param>
|
||||
/// <param name="parameters">参数集合(key=参数名,value=参数值)</param>
|
||||
IDbExecuteResult ExecuteNonQuery(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行增删改SQL
|
||||
/// </summary>
|
||||
Task<IDbExecuteResult> ExecuteNonQueryAsync(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 执行查询并返回单个值(如Count/Sum)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">返回值类型</typeparam>
|
||||
/// <param name="sql">SQL语句</param>
|
||||
/// <param name="parameters">参数集合</param>
|
||||
(IDbExecuteResult Result, T? Value) ExecuteScalar<T>(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行查询并返回单个值
|
||||
/// </summary>
|
||||
Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync<T>(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 执行查询并返回DataTable
|
||||
/// </summary>
|
||||
(IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行查询并返回DataTable
|
||||
/// </summary>
|
||||
Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary<string, object>? parameters = null);
|
||||
|
||||
/// <summary>
|
||||
/// 执行查询并映射为实体列表
|
||||
/// </summary>
|
||||
/// <typeparam name="T">实体类型(需有无参构造函数)</typeparam>
|
||||
(IDbExecuteResult Result, List<T> Data) QueryList<T>(string sql, Dictionary<string, object>? parameters = null) where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行查询并映射为实体列表
|
||||
/// </summary>
|
||||
Task<(IDbExecuteResult Result, List<T> Data)> QueryListAsync<T>(string sql, Dictionary<string, object>? parameters = null) where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// 执行分页查询并返回分页结果
|
||||
/// </summary>
|
||||
/// <typeparam name="T">实体类型</typeparam>
|
||||
(IDbExecuteResult Result, PaginationResponse<T> Data) QueryPaged<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new();
|
||||
|
||||
/// <summary>
|
||||
/// 异步执行分页查询并返回分页结果
|
||||
/// </summary>
|
||||
Task<(IDbExecuteResult Result, PaginationResponse<T> Data)> QueryPagedAsync<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库操作通用执行结果
|
||||
/// </summary>
|
||||
public interface IDbExecuteResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否执行成功
|
||||
/// </summary>
|
||||
bool IsSuccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 影响行数(增删改)
|
||||
/// </summary>
|
||||
int RowsAffected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息(成功/失败提示)
|
||||
/// </summary>
|
||||
string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 异常信息(失败时非空)
|
||||
/// </summary>
|
||||
Exception? Exception { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 数据库事务接口
|
||||
/// </summary>
|
||||
public interface IDbTransaction : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 提交事务
|
||||
/// </summary>
|
||||
IDbExecuteResult Commit();
|
||||
|
||||
/// <summary>
|
||||
/// 回滚事务
|
||||
/// </summary>
|
||||
IDbExecuteResult Rollback();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用分页请求模型
|
||||
/// </summary>
|
||||
public class PaginationRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码(从1开始)
|
||||
/// </summary>
|
||||
public int PageIndex { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数
|
||||
/// </summary>
|
||||
public int PageSize { get; set; } = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 排序字段(如:CreateTime DESC)
|
||||
/// </summary>
|
||||
public string OrderBy { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace XP.Common.Database.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用分页响应模型
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型</typeparam>
|
||||
public class PaginationResponse<T>
|
||||
{
|
||||
/// <summary>
|
||||
/// 页码
|
||||
/// </summary>
|
||||
public int PageIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 每页条数
|
||||
/// </summary>
|
||||
public int PageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总条数
|
||||
/// </summary>
|
||||
public int TotalCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 总页数
|
||||
/// </summary>
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
|
||||
/// <summary>
|
||||
/// 当前页数据
|
||||
/// </summary>
|
||||
public List<T> Data { get; set; } = new List<T>();
|
||||
|
||||
/// <summary>
|
||||
/// 是否有下一页
|
||||
/// </summary>
|
||||
public bool HasNextPage => PageIndex < TotalPages;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# Dump 文件管理服务使用指南 | Dump File Management Service Usage Guide
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
XplorePlane 提供 Dump 文件管理功能,用于在应用程序崩溃或需要诊断时生成进程转储文件。支持三种触发方式:崩溃自动触发、定时触发和手动触发,并提供文件大小限制、自动清理和可配置存储路径等管理能力。
|
||||
|
||||
Dump 功能通过 Windows `MiniDumpWriteDump` API 实现,作为 `XP.Common` 的子模块集成到 `CommonModule` 中。
|
||||
|
||||
## 基本用法 | Basic Usage
|
||||
|
||||
### 通过依赖注入获取服务 | Get Service via DI
|
||||
|
||||
```csharp
|
||||
public class DiagnosticsService
|
||||
{
|
||||
private readonly IDumpService _dumpService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public DiagnosticsService(IDumpService dumpService, ILoggerService logger)
|
||||
{
|
||||
_dumpService = dumpService ?? throw new ArgumentNullException(nameof(dumpService));
|
||||
_logger = logger?.ForModule<DiagnosticsService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动生成 Mini Dump | Manually generate Mini Dump
|
||||
/// </summary>
|
||||
public void CaptureMiniDump()
|
||||
{
|
||||
var filePath = _dumpService.CreateMiniDump();
|
||||
if (filePath != null)
|
||||
{
|
||||
_logger.Info("Mini Dump 已生成:{FilePath} | Mini Dump generated: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动生成 Full Dump(包含完整内存)| Manually generate Full Dump (full memory)
|
||||
/// </summary>
|
||||
public void CaptureFullDump()
|
||||
{
|
||||
var filePath = _dumpService.CreateFullDump();
|
||||
if (filePath != null)
|
||||
{
|
||||
_logger.Info("Full Dump 已生成:{FilePath} | Full Dump generated: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 触发方式 | Trigger Modes
|
||||
|
||||
### 1. 崩溃自动触发 | Crash Auto Trigger
|
||||
|
||||
服务启动后自动订阅 `AppDomain.CurrentDomain.UnhandledException` 和 `TaskScheduler.UnobservedTaskException`,无需额外配置。崩溃时自动生成 Mini Dump。
|
||||
|
||||
### 2. 定时触发 | Scheduled Trigger
|
||||
|
||||
在 `App.config` 中启用定时触发后,服务按配置的时间间隔周期性生成 Mini Dump:
|
||||
|
||||
```xml
|
||||
<add key="Dump:EnableScheduledDump" value="true" />
|
||||
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||
```
|
||||
|
||||
### 3. 手动触发 | Manual Trigger
|
||||
|
||||
通过 `IDumpService` 接口的方法手动触发:
|
||||
|
||||
```csharp
|
||||
// 生成 Mini Dump(线程信息 + 数据段 + 句柄信息)
|
||||
// Generate Mini Dump (thread info + data segments + handle data)
|
||||
string? miniPath = _dumpService.CreateMiniDump();
|
||||
|
||||
// 生成 Full Dump(完整内存,仅手动触发允许)
|
||||
// Generate Full Dump (full memory, manual trigger only)
|
||||
string? fullPath = _dumpService.CreateFullDump();
|
||||
```
|
||||
|
||||
## Dump 类型说明 | Dump Type Description
|
||||
|
||||
| 类型 | 包含内容 | 文件大小 | 触发限制 |
|
||||
|------|----------|----------|----------|
|
||||
| Mini Dump | 线程信息、数据段、句柄信息 | 较小(受大小限制约束) | 所有触发方式 |
|
||||
| Full Dump | 进程完整内存 | 较大(无大小限制) | 仅手动触发 |
|
||||
|
||||
> 非手动触发(崩溃、定时)请求 Full Dump 时,系统会自动降级为 Mini Dump。
|
||||
|
||||
## 文件命名规则 | File Naming Convention
|
||||
|
||||
格式:`XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp`
|
||||
|
||||
示例:
|
||||
- `XplorePlane_20260317_1530_Crash.dmp` — 崩溃触发
|
||||
- `XplorePlane_20260317_1600_Scheduled.dmp` — 定时触发
|
||||
- `XplorePlane_20260317_1645_Manual.dmp` — 手动触发
|
||||
|
||||
## 自动清理 | Auto Cleanup
|
||||
|
||||
- 服务启动时立即执行一次清理
|
||||
- 运行期间每 24 小时执行一次清理
|
||||
- 超过保留天数(默认 7 天)的 `.dmp` 文件会被自动删除
|
||||
- 单个文件删除失败不影响其余文件的清理
|
||||
|
||||
## 配置 | Configuration
|
||||
|
||||
在 `App.config` 的 `<appSettings>` 中配置:
|
||||
|
||||
```xml
|
||||
<appSettings>
|
||||
<!-- Dump 文件存储路径 | Dump file storage path -->
|
||||
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||
<!-- 是否启用定时触发 | Enable scheduled trigger -->
|
||||
<add key="Dump:EnableScheduledDump" value="false" />
|
||||
<!-- 定时触发间隔(分钟)| Scheduled trigger interval (minutes) -->
|
||||
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||
<!-- Mini Dump 文件大小上限(MB)| Mini Dump file size limit (MB) -->
|
||||
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
|
||||
<!-- 文件保留天数 | File retention days -->
|
||||
<add key="Dump:RetentionDays" value="7" />
|
||||
</appSettings>
|
||||
```
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `Dump:StoragePath` | `D:\XplorePlane\Dump` | Dump 文件存储路径 |
|
||||
| `Dump:EnableScheduledDump` | `false` | 是否启用定时触发 |
|
||||
| `Dump:ScheduledIntervalMinutes` | `60` | 定时触发间隔(分钟) |
|
||||
| `Dump:MiniDumpSizeLimitMB` | `100` | Mini Dump 文件大小上限(MB),超过则删除 |
|
||||
| `Dump:RetentionDays` | `7` | 文件保留天数,超过则自动清理 |
|
||||
|
||||
## 错误处理 | Error Handling
|
||||
|
||||
Dump 功能遵循"记录并继续"原则,自身错误不影响主应用程序运行:
|
||||
|
||||
- Dump 文件写入失败 → 记录错误日志,返回 `null`,不抛出异常
|
||||
- Mini Dump 超过大小限制 → 删除文件,记录警告
|
||||
- 存储目录创建失败 → 回退到默认路径 `D:\XplorePlane\Dump`
|
||||
- 清理过程中文件删除失败 → 记录错误,继续清理其余文件
|
||||
|
||||
## IDumpService 接口 | IDumpService Interface
|
||||
|
||||
```csharp
|
||||
public interface IDumpService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
|
||||
/// </summary>
|
||||
string? CreateMiniDump();
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
|
||||
/// </summary>
|
||||
string? CreateFullDump();
|
||||
|
||||
/// <summary>
|
||||
/// 启动服务 | Start service
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务 | Stop service
|
||||
/// </summary>
|
||||
void Stop();
|
||||
}
|
||||
```
|
||||
|
||||
## 文件结构 | File Structure
|
||||
|
||||
```
|
||||
XP.Common/Dump/
|
||||
├── Interfaces/
|
||||
│ └── IDumpService.cs # 服务接口 | Service interface
|
||||
├── Implementations/
|
||||
│ ├── DumpService.cs # 服务实现 | Service implementation
|
||||
│ └── DumpCleaner.cs # 自动清理组件 | Auto cleanup component
|
||||
├── Configs/
|
||||
│ ├── DumpConfig.cs # 配置实体 | Config entity
|
||||
│ └── DumpTriggerType.cs # 触发类型枚举 | Trigger type enum
|
||||
└── Native/
|
||||
├── NativeMethods.cs # P/Invoke 声明 | P/Invoke declarations
|
||||
└── MiniDumpType.cs # Dump 类型标志枚举 | Dump type flags enum
|
||||
```
|
||||
@@ -0,0 +1,247 @@
|
||||
# 通用窗体使用指南 | General Form Usage Guide
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
`XP.Common.GeneralForm` 提供 XplorePlane 项目中可复用的通用 WPF 窗体组件。当前包含以下窗体:
|
||||
|
||||
| 窗体 | 说明 |
|
||||
|---|---|
|
||||
| `ProgressWindow` | 模态进度条窗口,支持线程安全的进度更新和关闭操作 |
|
||||
| `InputDialog` | 通用输入对话框,支持单行文本输入、可选验证和多语言按钮 |
|
||||
|
||||
## 目录结构 | Directory Structure
|
||||
|
||||
```
|
||||
XP.Common/GeneralForm/
|
||||
├── ViewModels/
|
||||
│ ├── InputDialogViewModel.cs # 输入对话框 ViewModel
|
||||
│ └── ProgressWindowViewModel.cs # 进度窗口 ViewModel
|
||||
└── Views/
|
||||
├── InputDialog.xaml # 输入对话框 XAML 视图
|
||||
├── InputDialog.xaml.cs # 输入对话框 Code-Behind
|
||||
├── ProgressWindow.xaml # 进度窗口 XAML 视图
|
||||
└── ProgressWindow.xaml.cs # 进度窗口 Code-Behind
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ProgressWindow 进度条窗口
|
||||
|
||||
### 功能特性 | Features
|
||||
|
||||
- 模态进度条窗口,居中显示,不可调整大小
|
||||
- 线程安全:`UpdateProgress()` 和 `Close()` 可从任意线程调用,内部自动通过 `Dispatcher` 调度
|
||||
- 可配置是否允许用户手动关闭窗口(`isCancelable` 参数)
|
||||
- 不可取消时,通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
|
||||
- 进度值自动 Clamp 到 `[0, 100]` 范围,超出范围时记录 Warn 日志
|
||||
- 自动继承主窗口图标
|
||||
- 使用 Telerik `RadProgressBar` 控件(Crystal 主题)
|
||||
|
||||
### 构造函数参数 | Constructor Parameters
|
||||
|
||||
```csharp
|
||||
public ProgressWindow(
|
||||
string title = "操作进行中", // 窗口标题
|
||||
string message = "请稍候...", // 提示信息
|
||||
bool isCancelable = true, // 是否允许用户关闭窗口
|
||||
ILoggerService? logger = null // 日志服务(可选)
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `title` | `string` | `"操作进行中"` | 窗口标题栏文本 |
|
||||
| `message` | `string` | `"请稍候..."` | 进度条上方的提示信息 |
|
||||
| `isCancelable` | `bool` | `true` | `true`:用户可手动关闭;`false`:禁用关闭按钮 |
|
||||
| `logger` | `ILoggerService?` | `null` | 传入日志服务后自动记录窗口生命周期日志 |
|
||||
|
||||
### 公开方法 | Public Methods
|
||||
|
||||
#### UpdateProgress - 更新进度
|
||||
|
||||
```csharp
|
||||
// 线程安全,可从任意线程调用
|
||||
void UpdateProgress(string message, double progress)
|
||||
```
|
||||
|
||||
- `message`:更新提示信息文本
|
||||
- `progress`:进度值(0-100),超出范围自动修正
|
||||
|
||||
#### Close - 关闭窗口
|
||||
|
||||
```csharp
|
||||
// 线程安全,可从任意线程调用(隐藏基类 Window.Close())
|
||||
new void Close()
|
||||
```
|
||||
|
||||
### 基本用法 | Basic Usage
|
||||
|
||||
```csharp
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
public class SomeService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public SomeService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<SomeService>()
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task ExecuteLongOperation()
|
||||
{
|
||||
// 创建进度窗口(不可取消)
|
||||
var progressWindow = new ProgressWindow(
|
||||
title: "数据处理中",
|
||||
message: "正在初始化...",
|
||||
isCancelable: false,
|
||||
logger: _logger);
|
||||
|
||||
// 显示模态窗口(需在 UI 线程调用)
|
||||
// 注意:ShowDialog() 会阻塞,通常配合 Task 使用
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
progressWindow.UpdateProgress("正在加载数据...", 20);
|
||||
await Task.Delay(1000); // 模拟耗时操作
|
||||
|
||||
progressWindow.UpdateProgress("正在处理数据...", 60);
|
||||
await Task.Delay(1000);
|
||||
|
||||
progressWindow.UpdateProgress("即将完成...", 90);
|
||||
await Task.Delay(500);
|
||||
|
||||
progressWindow.UpdateProgress("完成", 100);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 关闭窗口(线程安全)
|
||||
progressWindow.Close();
|
||||
}
|
||||
});
|
||||
|
||||
progressWindow.ShowDialog();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 允许用户取消的用法 | Cancelable Usage
|
||||
|
||||
```csharp
|
||||
// 创建可取消的进度窗口
|
||||
var progressWindow = new ProgressWindow(
|
||||
title: "文件导出",
|
||||
message: "正在导出文件...",
|
||||
isCancelable: true, // 用户可以点击关闭按钮取消
|
||||
logger: _logger);
|
||||
|
||||
progressWindow.ShowDialog();
|
||||
```
|
||||
|
||||
### ViewModel 绑定属性 | ViewModel Binding Properties
|
||||
|
||||
`ProgressWindowViewModel` 继承自 `BindableBase`,提供以下可绑定属性:
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|---|---|---|
|
||||
| `Title` | `string` | 窗口标题(只读) |
|
||||
| `Message` | `string` | 提示信息文本(可通知) |
|
||||
| `Progress` | `double` | 进度值 0-100(可通知) |
|
||||
| `ProgressText` | `string` | 百分比显示文本,如 `"75%"`(只读,自动计算) |
|
||||
| `IsCancelable` | `bool` | 是否允许用户关闭(只读) |
|
||||
|
||||
### 注意事项 | Notes
|
||||
|
||||
1. `ShowDialog()` 必须在 UI 线程调用,它会阻塞当前线程直到窗口关闭
|
||||
2. `UpdateProgress()` 和 `Close()` 内部已处理跨线程调度,可安全地从后台线程调用
|
||||
3. 当 `isCancelable = false` 时,窗口关闭按钮会被 Win32 API 禁用(灰色),用户无法通过 Alt+F4 或点击关闭
|
||||
4. 进度值超出 `[0, 100]` 范围时会自动修正并记录 Warn 级别日志
|
||||
5. `Close()` 使用 `new` 关键字隐藏基类方法(因 `Window.Close()` 非虚方法),确保通过 `ProgressWindow` 类型引用调用
|
||||
|
||||
---
|
||||
|
||||
## InputDialog 输入对话框
|
||||
|
||||
### 功能特性 | Features
|
||||
|
||||
- 模态输入对话框,居中于父窗口显示,不可调整大小
|
||||
- 自动继承主窗口图标
|
||||
- 按钮文本支持多语言(使用 `Button_OK` / `Button_Cancel` 资源键)
|
||||
- 可选的输入验证委托(`Func<string, string?>`),验证失败时在输入框下方显示红色错误提示
|
||||
- 输入内容变化时自动清除验证错误
|
||||
- 使用 Telerik `RadWatermarkTextBox` 和 `RadButton` 控件(Crystal 主题)
|
||||
- 提供静态 `Show()` 便捷方法,一行代码即可调用
|
||||
|
||||
### 静态方法 | Static Method
|
||||
|
||||
```csharp
|
||||
public static string? Show(
|
||||
string prompt, // 提示文本
|
||||
string title, // 窗口标题
|
||||
string defaultValue = "", // 默认值
|
||||
Func<string, string?>? validate = null, // 验证委托(可选)
|
||||
Window? owner = null // 父窗口(可选)
|
||||
)
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `prompt` | `string` | 必填 | 输入框上方的提示文本 |
|
||||
| `title` | `string` | 必填 | 窗口标题栏文本 |
|
||||
| `defaultValue` | `string` | `""` | 输入框的初始值 |
|
||||
| `validate` | `Func<string, string?>?` | `null` | 验证委托:返回 `null` 表示通过,返回错误信息则阻止确认 |
|
||||
| `owner` | `Window?` | `null` | 父窗口,设置后对话框居中于父窗口 |
|
||||
|
||||
返回值:用户输入的字符串,取消时返回 `null`。
|
||||
|
||||
### 基本用法 | Basic Usage
|
||||
|
||||
```csharp
|
||||
using XP.Common.GeneralForm.Views;
|
||||
|
||||
// 最简用法,无验证
|
||||
var name = InputDialog.Show("请输入名称:", "新建项目");
|
||||
if (name == null) return; // 用户取消
|
||||
|
||||
// 带默认值
|
||||
var port = InputDialog.Show("请输入端口号:", "配置", "8080");
|
||||
```
|
||||
|
||||
### 带验证的用法 | Usage with Validation
|
||||
|
||||
```csharp
|
||||
// 验证委托:返回 null 表示通过,返回错误信息则显示在输入框下方
|
||||
var groupId = InputDialog.Show(
|
||||
"请输入 Group ID:",
|
||||
"新增 Group",
|
||||
validate: input =>
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return "ID 不能为空";
|
||||
if (existingIds.Contains(input))
|
||||
return "ID 已存在,请使用不同的 ID";
|
||||
return null; // 验证通过
|
||||
});
|
||||
|
||||
// 验证非负整数
|
||||
var dbNumber = InputDialog.Show(
|
||||
"请输入 DB 块号(非负整数):",
|
||||
"新增 Group",
|
||||
"0",
|
||||
validate: input =>
|
||||
{
|
||||
if (!int.TryParse(input, out int val) || val < 0)
|
||||
return "必须为非负整数";
|
||||
return null;
|
||||
});
|
||||
```
|
||||
|
||||
### 注意事项 | Notes
|
||||
|
||||
1. `Show()` 必须在 UI 线程调用(内部使用 `ShowDialog()`)
|
||||
2. 验证委托在用户点击确定按钮时执行,验证失败不会关闭对话框
|
||||
3. 用户修改输入内容时会自动清除上一次的验证错误提示
|
||||
4. 按钮文本从 XP.Common 资源文件读取(`Button_OK` / `Button_Cancel`),自动跟随应用语言
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
# 多语言支持快速开始 | Localization Quick Start
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
|
||||
|
||||
### 核心特性 | Key Features
|
||||
|
||||
- ✅ 基于 .NET Resx 资源文件,编译时类型安全
|
||||
- ✅ 简洁的 XAML 标记扩展语法
|
||||
- ✅ 完整的 ViewModel 集成支持
|
||||
- ✅ 语言设置持久化到 App.config
|
||||
- ✅ 跨模块事件通知机制
|
||||
- ✅ 健壮的错误处理和回退机制
|
||||
- ✅ 多资源源 Fallback Chain 机制,支持模块级资源注册
|
||||
|
||||
---
|
||||
|
||||
## 快速开始 | Quick Start
|
||||
|
||||
### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
|
||||
|
||||
#### 基础用法 | Basic Usage
|
||||
|
||||
```xml
|
||||
<Window x:Class="XplorePlane.App.Views.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
|
||||
Title="{loc:Localization Key=App_Title}">
|
||||
|
||||
<Grid>
|
||||
<!-- 按钮文本本地化 | Localized button text -->
|
||||
<Button Content="{loc:Localization Key=Button_OK}" />
|
||||
|
||||
<!-- 标签文本本地化 | Localized label text -->
|
||||
<Label Content="{loc:Localization Key=Settings_Language}" />
|
||||
|
||||
<!-- 菜单项本地化 | Localized menu item -->
|
||||
<MenuItem Header="{loc:Localization Key=Menu_File}" />
|
||||
</Grid>
|
||||
</Window>
|
||||
```
|
||||
|
||||
#### 命名空间声明 | Namespace Declaration
|
||||
|
||||
在 XAML 文件顶部添加命名空间引用:
|
||||
|
||||
```xml
|
||||
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
|
||||
```
|
||||
|
||||
#### 语法说明 | Syntax Explanation
|
||||
|
||||
- `{loc:Localization Key=ResourceKey}` - 完整语法
|
||||
- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
|
||||
- 资源键不存在时,显示键名本身(便于调试)
|
||||
|
||||
---
|
||||
|
||||
### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
|
||||
|
||||
适用于不方便依赖注入的场景(静态方法、工具类等)。
|
||||
|
||||
```csharp
|
||||
using XP.Common.Localization;
|
||||
|
||||
// 基本用法 | Basic usage
|
||||
var title = LocalizationHelper.Get("App_Title");
|
||||
|
||||
// 带格式化参数 | With format arguments
|
||||
var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
|
||||
```
|
||||
|
||||
- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock)
|
||||
- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
|
||||
- 两者读取同一套 Resx 资源文件,结果一致
|
||||
|
||||
> **V1.4.1.1 变更:** `LocalizationHelper` 新增 `Initialize(ILocalizationService)` 方法。初始化后,`Get()` 会优先通过 `ILocalizationService` 获取字符串(支持 Fallback Chain);未初始化时仍兼容回退到原始 `ResourceManager`。建议在 `CommonModule` 或 App 启动时调用初始化。
|
||||
|
||||
```csharp
|
||||
// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
|
||||
LocalizationHelper.Initialize(localizationService);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
|
||||
|
||||
#### 依赖注入 | Dependency Injection
|
||||
|
||||
```csharp
|
||||
using XplorePlane.Common.Localization.Interfaces;
|
||||
using XplorePlane.Common.Localization.Enums;
|
||||
using Prism.Mvvm;
|
||||
|
||||
namespace XplorePlane.App.ViewModels
|
||||
{
|
||||
public class MyViewModel : BindableBase
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public MyViewModel(
|
||||
ILocalizationService localizationService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// 获取本地化字符串 | Get localized string
|
||||
public string GetWelcomeMessage()
|
||||
{
|
||||
return _localizationService.GetString("Welcome_Message");
|
||||
}
|
||||
|
||||
// 获取当前语言 | Get current language
|
||||
public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 动态文本绑定 | Dynamic Text Binding
|
||||
|
||||
```csharp
|
||||
public class StatusViewModel : BindableBase
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private string _statusMessage;
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public StatusViewModel(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
|
||||
// 订阅语言切换事件 | Subscribe to language changed event
|
||||
_localizationService.LanguageChanged += OnLanguageChanged;
|
||||
|
||||
// 初始化状态消息 | Initialize status message
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
|
||||
{
|
||||
// 语言切换时更新文本 | Update text when language changes
|
||||
UpdateStatusMessage();
|
||||
}
|
||||
|
||||
private void UpdateStatusMessage()
|
||||
{
|
||||
StatusMessage = _localizationService.GetString("Status_Ready");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 数据验证消息本地化 | Localized Validation Messages
|
||||
|
||||
```csharp
|
||||
public class FormViewModel : BindableBase, IDataErrorInfo
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private string _username;
|
||||
|
||||
public string Username
|
||||
{
|
||||
get => _username;
|
||||
set => SetProperty(ref _username, value);
|
||||
}
|
||||
|
||||
public string this[string columnName]
|
||||
{
|
||||
get
|
||||
{
|
||||
if (columnName == nameof(Username))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Username))
|
||||
{
|
||||
return _localizationService.GetString("Validation_UsernameRequired");
|
||||
}
|
||||
if (Username.Length < 3)
|
||||
{
|
||||
return _localizationService.GetString("Validation_UsernameTooShort");
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string Error => null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 多资源源 Fallback Chain | Multi-Source Fallback Chain
|
||||
|
||||
V1.1 版本引入了多资源源 Fallback Chain 机制,允许各模块注册自己的 Resx 资源文件。查找资源键时,从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。
|
||||
|
||||
#### 架构说明 | Architecture
|
||||
|
||||
```
|
||||
Fallback Chain(查找顺序从右到左):
|
||||
[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
|
||||
↑ 最高优先级
|
||||
```
|
||||
|
||||
- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
|
||||
- 后注册的模块优先级更高
|
||||
- 单个资源源查找异常时自动跳过,继续遍历下一个
|
||||
- 全部未找到时返回 key 本身并记录警告日志
|
||||
|
||||
#### 注册模块资源源 | Register Module Resource Source
|
||||
|
||||
在 Prism 模块的 `OnInitialized` 中注册:
|
||||
|
||||
```csharp
|
||||
using System.Resources;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
|
||||
public class ScanModule : IModule
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
|
||||
public ScanModule(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
public void OnInitialized(IContainerProvider containerProvider)
|
||||
{
|
||||
// 注册模块资源源到 Fallback Chain
|
||||
var resourceManager = new ResourceManager(
|
||||
"XP.Scan.Resources.Resources",
|
||||
typeof(ScanModule).Assembly);
|
||||
_localizationService.RegisterResourceSource("XP.Scan", resourceManager);
|
||||
}
|
||||
|
||||
public void RegisterTypes(IContainerRegistry containerRegistry) { }
|
||||
}
|
||||
```
|
||||
|
||||
#### 注销模块资源源 | Unregister Module Resource Source
|
||||
|
||||
```csharp
|
||||
// 注销指定资源源(不可注销默认的 "XP.Common")
|
||||
_localizationService.UnregisterResourceSource("XP.Scan");
|
||||
```
|
||||
|
||||
#### 注意事项 | Notes
|
||||
|
||||
- 资源源名称不可重复,重复注册会抛出 `InvalidOperationException`
|
||||
- 注销 `"XP.Common"` 会抛出 `InvalidOperationException`
|
||||
- 注销不存在的名称会静默忽略并记录警告日志
|
||||
- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
|
||||
|
||||
---
|
||||
|
||||
**版本 | Version:** 1.1
|
||||
**最后更新 | Last Updated:** 2026-04-01
|
||||
@@ -0,0 +1,233 @@
|
||||
# 日志服务使用示例 | Logger Service Usage Examples
|
||||
|
||||
## 示例 1:服务类中使用 | Example 1: Usage in Service Class
|
||||
|
||||
### 旧方式(手动传递字符串)| Old Way (Manual String)
|
||||
|
||||
```csharp
|
||||
public class PlcService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PlcService(ILoggerService logger)
|
||||
{
|
||||
// 需要手动输入类名,容易出错
|
||||
// Need to manually type class name, error-prone
|
||||
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
}
|
||||
|
||||
// 日志输出 | Log output:
|
||||
// [PlcService] 正在初始化 PLC 连接...
|
||||
```
|
||||
|
||||
### 新方式(自动类型推断)| New Way (Auto Type Inference)
|
||||
|
||||
```csharp
|
||||
public class PlcService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PlcService(ILoggerService logger)
|
||||
{
|
||||
// 自动获取完整类型名,重构安全
|
||||
// Automatically get full type name, refactoring-safe
|
||||
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
}
|
||||
|
||||
// 日志输出 | Log output:
|
||||
// [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接...
|
||||
```
|
||||
|
||||
## 示例 2:ViewModel 中使用 | Example 2: Usage in ViewModel
|
||||
|
||||
```csharp
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Hardware.RaySource.ViewModels
|
||||
{
|
||||
public class RaySourceOperateViewModel : BindableBase
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public RaySourceOperateViewModel(ILoggerService logger)
|
||||
{
|
||||
// 自动使用:XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
|
||||
// Automatically uses: XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
|
||||
_logger = logger?.ForModule<RaySourceOperateViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
_logger.Info("射线源操作视图模型已初始化 | Ray source operate view model initialized");
|
||||
}
|
||||
|
||||
public void StartXRay()
|
||||
{
|
||||
_logger.Info("用户请求启动射线 | User requested to start X-ray");
|
||||
// ... 业务逻辑 | business logic
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 3:工厂类中使用 | Example 3: Usage in Factory Class
|
||||
|
||||
```csharp
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Hardware.RaySource.Abstractions;
|
||||
|
||||
namespace XP.Hardware.RaySource.Factories
|
||||
{
|
||||
public class RaySourceFactory : IRaySourceFactory
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public RaySourceFactory(ILoggerService logger)
|
||||
{
|
||||
// 自动使用:XP.Hardware.RaySource.Factories.RaySourceFactory
|
||||
// Automatically uses: XP.Hardware.RaySource.Factories.RaySourceFactory
|
||||
_logger = logger?.ForModule<RaySourceFactory>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public IXRaySource CreateRaySource(string deviceType)
|
||||
{
|
||||
_logger.Info("创建射线源实例:类型={DeviceType} | Creating ray source instance: type={DeviceType}", deviceType);
|
||||
|
||||
switch (deviceType)
|
||||
{
|
||||
case "Comet225":
|
||||
return new Comet225RaySource(_logger);
|
||||
default:
|
||||
_logger.Error(null, "不支持的设备类型:{DeviceType} | Unsupported device type: {DeviceType}", deviceType);
|
||||
throw new NotSupportedException($"不支持的设备类型:{deviceType}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 4:静态方法中使用 | Example 4: Usage in Static Methods
|
||||
|
||||
```csharp
|
||||
public class ConfigLoader
|
||||
{
|
||||
public static PlcConfig LoadConfig(ILoggerService logger)
|
||||
{
|
||||
// 静态方法中也可以使用泛型
|
||||
// Can also use generics in static methods
|
||||
var log = logger.ForModule<ConfigLoader>();
|
||||
|
||||
log.Info("正在加载 PLC 配置 | Loading PLC configuration");
|
||||
|
||||
try
|
||||
{
|
||||
// ... 加载逻辑 | loading logic
|
||||
log.Info("PLC 配置加载成功 | PLC configuration loaded successfully");
|
||||
return config;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex, "PLC 配置加载失败 | PLC configuration loading failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 5:嵌套类中使用 | Example 5: Usage in Nested Classes
|
||||
|
||||
```csharp
|
||||
public class RaySourceService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public RaySourceService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<RaySourceService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
// 嵌套类 | Nested class
|
||||
public class ConnectionManager
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public ConnectionManager(ILoggerService logger)
|
||||
{
|
||||
// 自动使用:XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
|
||||
// Automatically uses: XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
|
||||
_logger = logger?.ForModule<ConnectionManager>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 示例 6:混合使用 | Example 6: Mixed Usage
|
||||
|
||||
有时你可能想要自定义模块名以保持简洁:
|
||||
|
||||
```csharp
|
||||
public class VeryLongNamespaceAndClassName
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public VeryLongNamespaceAndClassName(ILoggerService logger)
|
||||
{
|
||||
// 选项 1:使用完整类型名(详细但冗长)
|
||||
// Option 1: Use full type name (detailed but verbose)
|
||||
// _logger = logger?.ForModule<VeryLongNamespaceAndClassName>();
|
||||
// 输出 | Output: [XP.Some.Very.Long.Namespace.VeryLongNamespaceAndClassName]
|
||||
|
||||
// 选项 2:使用简短自定义名(简洁但需手动维护)
|
||||
// Option 2: Use short custom name (concise but needs manual maintenance)
|
||||
_logger = logger?.ForModule("VeryLong") ?? throw new ArgumentNullException(nameof(logger));
|
||||
// 输出 | Output: [VeryLong]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 优势对比 | Advantages Comparison
|
||||
|
||||
### ForModule<T>() 的优势 | Advantages of ForModule<T>()
|
||||
|
||||
✅ **重构安全**:重命名类时自动更新
|
||||
✅ **Refactoring-safe**: Automatically updates when renaming class
|
||||
|
||||
✅ **无拼写错误**:编译器检查类型
|
||||
✅ **No typos**: Compiler checks type
|
||||
|
||||
✅ **完整信息**:包含命名空间,便于定位
|
||||
✅ **Complete info**: Includes namespace, easy to locate
|
||||
|
||||
✅ **智能提示**:IDE 自动补全
|
||||
✅ **IntelliSense**: IDE auto-completion
|
||||
|
||||
### ForModule(string) 的优势 | Advantages of ForModule(string)
|
||||
|
||||
✅ **简洁输出**:日志文件更易读
|
||||
✅ **Concise output**: Log files more readable
|
||||
|
||||
✅ **自定义名称**:可以使用业务术语
|
||||
✅ **Custom names**: Can use business terms
|
||||
|
||||
✅ **灵活性**:可以为不同场景使用不同名称
|
||||
✅ **Flexibility**: Can use different names for different scenarios
|
||||
|
||||
## 推荐使用场景 | Recommended Usage Scenarios
|
||||
|
||||
| 场景 | 推荐方式 | 原因 |
|
||||
|------|---------|------|
|
||||
| 服务类 | `ForModule<T>()` | 需要完整追踪 |
|
||||
| ViewModel | `ForModule<T>()` | 需要完整追踪 |
|
||||
| 工厂类 | `ForModule<T>()` | 需要完整追踪 |
|
||||
| 简单工具类 | `ForModule("ToolName")` | 保持简洁 |
|
||||
| 临时调试 | `ForModule("Debug")` | 快速定位 |
|
||||
| 第三方集成 | `ForModule("ThirdParty.XXX")` | 明确标识 |
|
||||
|
||||
| Scenario | Recommended | Reason |
|
||||
|----------|------------|--------|
|
||||
| Service classes | `ForModule<T>()` | Need full tracing |
|
||||
| ViewModels | `ForModule<T>()` | Need full tracing |
|
||||
| Factory classes | `ForModule<T>()` | Need full tracing |
|
||||
| Simple utility classes | `ForModule("ToolName")` | Keep concise |
|
||||
| Temporary debugging | `ForModule("Debug")` | Quick location |
|
||||
| Third-party integration | `ForModule("ThirdParty.XXX")` | Clear identification |
|
||||
@@ -0,0 +1,177 @@
|
||||
# 日志服务使用指南 | Logger Service Usage Guide
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
XplorePlane 使用 Serilog 作为底层日志框架,通过 `ILoggerService` 接口提供统一的日志服务。
|
||||
|
||||
## 基本用法 | Basic Usage
|
||||
|
||||
### 方式 1:自动类型推断(推荐)| Method 1: Auto Type Inference (Recommended)
|
||||
|
||||
使用泛型方法 `ForModule<T>()` 自动获取类型的完整名称(命名空间 + 类名):
|
||||
|
||||
```csharp
|
||||
public class PlcService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PlcService(ILoggerService logger)
|
||||
{
|
||||
// 自动使用 "XP.Hardware.Plc.Services.PlcService" 作为模块名
|
||||
// Automatically uses "XP.Hardware.Plc.Services.PlcService" as module name
|
||||
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public void DoSomething()
|
||||
{
|
||||
_logger.Info("执行操作 | Performing operation");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式 2:手动指定模块名 | Method 2: Manual Module Name
|
||||
|
||||
如果需要自定义模块名,可以使用字符串参数:
|
||||
|
||||
```csharp
|
||||
public class PlcService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PlcService(ILoggerService logger)
|
||||
{
|
||||
// 手动指定简短的模块名
|
||||
// Manually specify a short module name
|
||||
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方式 3:使用 typeof 获取类型名 | Method 3: Using typeof for Type Name
|
||||
|
||||
在静态方法或无法使用泛型的场景:
|
||||
|
||||
```csharp
|
||||
public class PlcService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PlcService(ILoggerService logger)
|
||||
{
|
||||
// 使用 typeof 获取类型
|
||||
// Use typeof to get type
|
||||
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public static void StaticMethod(ILoggerService logger)
|
||||
{
|
||||
// 静态方法中也可以使用泛型
|
||||
// Can also use generics in static methods
|
||||
var log = logger.ForModule<PlcService>();
|
||||
log.Info("静态方法日志 | Static method log");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 日志级别 | Log Levels
|
||||
|
||||
```csharp
|
||||
// 调试信息(开发环境)| Debug information (development environment)
|
||||
_logger.Debug("调试信息:变量值={Value} | Debug info: variable value={Value}", someValue);
|
||||
|
||||
// 一般信息 | General information
|
||||
_logger.Info("操作成功 | Operation successful");
|
||||
|
||||
// 警告信息 | Warning information
|
||||
_logger.Warn("连接不稳定 | Connection unstable");
|
||||
|
||||
// 错误信息(带异常)| Error information (with exception)
|
||||
_logger.Error(ex, "操作失败:{Message} | Operation failed: {Message}", ex.Message);
|
||||
|
||||
// 致命错误 | Fatal error
|
||||
_logger.Fatal(ex, "系统崩溃 | System crash");
|
||||
```
|
||||
|
||||
## 日志输出格式 | Log Output Format
|
||||
|
||||
使用 `ForModule<T>()` 后,日志会自动包含完整的类型信息:
|
||||
|
||||
```
|
||||
2026-03-12 10:30:15.123 [INF] [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接... | Initializing PLC connection...
|
||||
2026-03-12 10:30:16.456 [INF] [XP.Hardware.Plc.Services.PlcService] PLC 连接成功 | PLC connection successful
|
||||
```
|
||||
|
||||
## 最佳实践 | Best Practices
|
||||
|
||||
### 1. 在构造函数中初始化日志器 | Initialize Logger in Constructor
|
||||
|
||||
```csharp
|
||||
public class MyService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public MyService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<MyService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 使用结构化日志 | Use Structured Logging
|
||||
|
||||
```csharp
|
||||
// 好的做法:使用占位符 | Good: use placeholders
|
||||
_logger.Info("用户 {UserId} 执行了操作 {Action} | User {UserId} performed action {Action}", userId, action);
|
||||
|
||||
// 不好的做法:字符串拼接 | Bad: string concatenation
|
||||
_logger.Info($"用户 {userId} 执行了操作 {action}");
|
||||
```
|
||||
|
||||
### 3. 异常日志包含上下文 | Exception Logs Include Context
|
||||
|
||||
```csharp
|
||||
try
|
||||
{
|
||||
await _plcClient.ConnectAsync(config);
|
||||
}
|
||||
catch (PlcException ex)
|
||||
{
|
||||
_logger.Error(ex, "PLC 连接失败:地址={Address}, 端口={Port} | PLC connection failed: address={Address}, port={Port}",
|
||||
config.Address, config.Port);
|
||||
throw;
|
||||
}
|
||||
```
|
||||
|
||||
## 对比:三种方式的输出 | Comparison: Output of Three Methods
|
||||
|
||||
```csharp
|
||||
// 方式 1:ForModule<T>() - 完整类型名
|
||||
// Method 1: ForModule<T>() - Full type name
|
||||
_logger = logger.ForModule<PlcService>();
|
||||
// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
|
||||
|
||||
// 方式 2:ForModule("PlcService") - 自定义名称
|
||||
// Method 2: ForModule("PlcService") - Custom name
|
||||
_logger = logger.ForModule("PlcService");
|
||||
// 输出 | Output: [PlcService]
|
||||
|
||||
// 方式 3:不调用 ForModule - 无模块标记
|
||||
// Method 3: Don't call ForModule - No module tag
|
||||
_logger = logger;
|
||||
// 输出 | Output: [] (空标记 | empty tag)
|
||||
```
|
||||
|
||||
## 配置 | Configuration
|
||||
|
||||
日志配置在 `App.config` 中设置,通过 `SerilogConfig` 加载:
|
||||
|
||||
```xml
|
||||
<appSettings>
|
||||
<add key="Serilog:LogPath" value="C:\Logs\XplorePlane" />
|
||||
<add key="Serilog:MinimumLevel" value="Information" />
|
||||
<add key="Serilog:EnableConsole" value="true" />
|
||||
<add key="Serilog:RollingInterval" value="Day" />
|
||||
<add key="Serilog:FileSizeLimitMB" value="100" />
|
||||
<add key="Serilog:RetainedFileCountLimit" value="30" />
|
||||
</appSettings>
|
||||
```
|
||||
@@ -0,0 +1,299 @@
|
||||
# PDF 查看与打印模块使用指南 | PDF Viewer & Printer Module Usage Guide
|
||||
|
||||
## 概述 | Overview
|
||||
|
||||
`XP.Common.PdfViewer` 提供基于 Telerik RadPdfViewer 的 PDF 文件查看与打印功能模块。模块作为 XP.Common 的通用可复用组件,通过 Prism DI 容器注册服务接口,供外部类库通过构造函数注入使用。
|
||||
|
||||
### 核心功能 | Core Features
|
||||
|
||||
- PDF 文件加载与显示(支持文件路径和文件流两种方式)
|
||||
- 内置 RadPdfViewerToolbar 提供页面导航、缩放、旋转、打印等完整工具栏 UI
|
||||
- 静默打印(指定打印机、页面范围、打印份数)
|
||||
- 打印设置对话框(用户交互式配置打印参数)
|
||||
- 打印预览功能
|
||||
- 多语言支持(简体中文、繁体中文、英文)
|
||||
- 结构化日志记录(使用 ILoggerService)
|
||||
- 资源自动释放(IDisposable + 终结器安全网)
|
||||
|
||||
---
|
||||
|
||||
## 目录结构 | Directory Structure
|
||||
|
||||
```
|
||||
XP.Common/PdfViewer/
|
||||
├── Exceptions/ # 自定义异常
|
||||
│ ├── PdfLoadException.cs # PDF 加载异常
|
||||
│ ├── PrinterNotFoundException.cs # 打印机未找到异常
|
||||
│ └── PrintException.cs # 打印异常
|
||||
├── Interfaces/ # 服务接口
|
||||
│ ├── IPdfViewerService.cs # PDF 查看服务接口
|
||||
│ └── IPdfPrintService.cs # PDF 打印服务接口
|
||||
├── Implementations/ # 服务实现
|
||||
│ ├── PdfViewerService.cs # PdfViewerService 实现
|
||||
│ └── PdfPrintService.cs # PdfPrintService 实现
|
||||
├── ViewModels/ # ViewModel
|
||||
│ └── PdfViewerWindowViewModel.cs # 阅读器窗口 ViewModel
|
||||
└── Views/ # 视图
|
||||
├── PdfViewerWindow.xaml # 阅读器窗口 XAML
|
||||
└── PdfViewerWindow.xaml.cs # 阅读器窗口 Code-Behind
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 服务接口 | Service Interfaces
|
||||
|
||||
### IPdfViewerService - PDF 查看服务
|
||||
|
||||
负责 PDF 文件加载和阅读器窗口管理。
|
||||
|
||||
```csharp
|
||||
public interface IPdfViewerService : IDisposable
|
||||
{
|
||||
/// 通过文件路径打开 PDF 阅读器窗口
|
||||
void OpenViewer(string filePath);
|
||||
|
||||
/// 通过文件流打开 PDF 阅读器窗口
|
||||
void OpenViewer(Stream stream, string? title = null);
|
||||
}
|
||||
```
|
||||
|
||||
### IPdfPrintService - PDF 打印服务
|
||||
|
||||
负责 PDF 打印功能,包括静默打印和交互式打印。
|
||||
|
||||
```csharp
|
||||
public interface IPdfPrintService
|
||||
{
|
||||
/// 使用指定打印机打印 PDF 文件
|
||||
void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
|
||||
|
||||
/// 打开打印设置对话框并打印
|
||||
bool PrintWithDialog(string filePath);
|
||||
|
||||
/// 打开打印预览对话框
|
||||
void PrintPreview(string filePath);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例 | Usage Examples
|
||||
|
||||
### 1. 通过文件路径打开 PDF
|
||||
|
||||
```csharp
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
|
||||
public class MyService
|
||||
{
|
||||
private readonly IPdfViewerService _pdfViewerService;
|
||||
|
||||
public MyService(IPdfViewerService pdfViewerService)
|
||||
{
|
||||
_pdfViewerService = pdfViewerService;
|
||||
}
|
||||
|
||||
public void OpenPdfByPath()
|
||||
{
|
||||
// 打开指定路径的 PDF 文件
|
||||
_pdfViewerService.OpenViewer(@"C:\Documents\UserManual.pdf");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 通过文件流打开 PDF
|
||||
|
||||
```csharp
|
||||
public void OpenPdfByStream()
|
||||
{
|
||||
// 从文件流打开 PDF(窗口标题可选)
|
||||
using var stream = File.OpenRead(@"C:\Documents\UserManual.pdf");
|
||||
_pdfViewerService.OpenViewer(stream, "用户手册.pdf");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 静默打印到指定打印机
|
||||
|
||||
```csharp
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
|
||||
public class MyService
|
||||
{
|
||||
private readonly IPdfPrintService _printService;
|
||||
|
||||
public MyService(IPdfPrintService printService)
|
||||
{
|
||||
_printService = printService;
|
||||
}
|
||||
|
||||
public void PrintPdf()
|
||||
{
|
||||
// 打印全部页面到指定打印机
|
||||
_printService.Print(
|
||||
filePath: @"C:\Documents\UserManual.pdf",
|
||||
printerName: "HP LaserJet Pro",
|
||||
pageFrom: null, // null 表示从第一页
|
||||
pageTo: null, // null 表示到最后一页
|
||||
copies: 1 // 打印 1 份
|
||||
);
|
||||
|
||||
// 打印指定范围(第 1-3 页)
|
||||
_printService.Print(
|
||||
filePath: @"C:\Documents\UserManual.pdf",
|
||||
printerName: "HP LaserJet Pro",
|
||||
pageFrom: 1,
|
||||
pageTo: 3,
|
||||
copies: 2
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 打开打印设置对话框
|
||||
|
||||
```csharp
|
||||
public void OpenPrintDialog()
|
||||
{
|
||||
// 显示打印设置对话框,用户确认后打印
|
||||
bool userConfirmed = _printService.PrintWithDialog(@"C:\Documents\UserManual.pdf");
|
||||
|
||||
if (userConfirmed)
|
||||
{
|
||||
// 用户点击了"确定"按钮
|
||||
}
|
||||
else
|
||||
{
|
||||
// 用户点击了"取消"按钮
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 打开打印预览
|
||||
|
||||
```csharp
|
||||
public void ShowPrintPreview()
|
||||
{
|
||||
// 打开打印预览对话框
|
||||
_printService.PrintPreview(@"C:\Documents\UserManual.pdf");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DI 注册 | DI Registration
|
||||
|
||||
在 `CommonModule.RegisterTypes` 中已注册为单例服务:
|
||||
|
||||
```csharp
|
||||
containerRegistry.RegisterSingleton<IPdfPrintService, PdfPrintService>();
|
||||
containerRegistry.RegisterSingleton<IPdfViewerService, PdfViewerService>();
|
||||
```
|
||||
|
||||
在 ViewModel 或 Service 中通过构造函数注入使用:
|
||||
|
||||
```csharp
|
||||
public class MyViewModel
|
||||
{
|
||||
private readonly IPdfViewerService _pdfViewerService;
|
||||
private readonly IPdfPrintService _printService;
|
||||
|
||||
public MyViewModel(
|
||||
IPdfViewerService pdfViewerService,
|
||||
IPdfPrintService printService)
|
||||
{
|
||||
_pdfViewerService = pdfViewerService;
|
||||
_printService = printService;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 多语言资源 | Localization Resources
|
||||
|
||||
PDF 模块支持多语言,资源键如下:
|
||||
|
||||
| 资源键 | zh-CN | zh-TW | en-US |
|
||||
|--------|-------|-------|-------|
|
||||
| `PdfViewer_Title` | PDF 阅读器 | PDF 閱讀器 | PDF Viewer |
|
||||
| `PdfViewer_TitleWithFile` | PDF 阅读器 - {0} | PDF 閱讀器 - {0} | PDF Viewer - {0} |
|
||||
| `PdfViewer_LoadSuccess` | PDF 文件加载成功:{0}({1} 页)| PDF 檔案載入成功:{0}({1} 頁)| PDF loaded: {0} ({1} pages) |
|
||||
| `PdfViewer_LoadFailed` | PDF 文件加载失败 | PDF 檔案載入失敗 | PDF file load failed |
|
||||
| `PdfViewer_PrintSuccess` | 打印任务已提交:{0} → {1} | 列印任務已提交:{0} → {1} | Print job submitted: {0} → {1} |
|
||||
| `PdfViewer_PrintFailed` | 打印失败 | 列印失敗 | Print failed |
|
||||
| `PdfViewer_PrinterNotFound` | 打印机未找到:{0} | 印表機未找到:{0} | Printer not found: {0} |
|
||||
|
||||
---
|
||||
|
||||
## 异常处理 | Exception Handling
|
||||
|
||||
| 异常类型 | 触发条件 | 说明 |
|
||||
|---------|---------|------|
|
||||
| `FileNotFoundException` | 文件路径不存在 | `OpenViewer(filePath)` 或 `Print(filePath, ...)` |
|
||||
| `ArgumentNullException` | 流参数为 null | `OpenViewer(null, ...)` |
|
||||
| `PdfLoadException` | PDF 格式无效或加载失败 | 文件损坏、非 PDF 格式等 |
|
||||
| `PrinterNotFoundException` | 指定打印机不存在 | `Print(filePath, printerName, ...)` |
|
||||
| `PrintException` | 打印过程中发生错误 | 打印机错误、驱动问题等 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项 | Notes
|
||||
|
||||
1. **RadPdfViewerToolbar 内置功能**:页面导航(首页/上一页/下一页/末页)、缩放(放大/缩小/适合宽度/适合整页)、旋转(顺时针/逆时针)等功能由 RadPdfViewerToolbar 自动提供,无需手动实现。
|
||||
|
||||
2. **资源释放**:`PdfViewerService` 实现 `IDisposable`,窗口��闭时会自动释放 PDF 文档资源。终结器作为安全网,确保未显式释放时也能清理资源。
|
||||
|
||||
3. **多语言支持**:RadPdfViewerToolbar 的内置按钮文本(如"首页"、"上一页"、"放大"等)由 Telerik 自身的本地化机制管理,无需在 XP.Common 的 Resources 中维护。
|
||||
|
||||
4. **打印设置**:Telerik 提供内置的 `PrintSettings` 类,无需自定义打印设置模型。
|
||||
|
||||
5. **日志记录**:所有关键操作(加载成功/失败、打印成功/失败)都会通过 `ILoggerService` 记录结构化日志。
|
||||
|
||||
---
|
||||
|
||||
## 典型应用场景 | Typical Use Cases
|
||||
|
||||
### 场景 1:主窗口添加"用户手册"按钮
|
||||
|
||||
```csharp
|
||||
// MainWindowViewModel.cs
|
||||
private void ExecuteOpenUserManual()
|
||||
{
|
||||
var manualPath = ConfigurationManager.AppSettings["UserManual"];
|
||||
var stream = File.OpenRead(manualPath);
|
||||
var fileName = Path.GetFileName(manualPath);
|
||||
_pdfViewerService.OpenViewer(stream, fileName);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2:导出报告后自动打开 PDF 预览
|
||||
|
||||
```csharp
|
||||
public void ExportAndPreview()
|
||||
{
|
||||
// 生成 PDF 报告到临时文件
|
||||
var tempPath = Path.Combine(Path.GetTempPath(), $"Report_{DateTime.Now:yyyyMMdd_HHmmss}.pdf");
|
||||
GenerateReport(tempPath);
|
||||
|
||||
// 自动打开 PDF 预览
|
||||
_pdfViewerService.OpenViewer(tempPath);
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 3:批量打印检测报告
|
||||
|
||||
```csharp
|
||||
public void BatchPrintReports(List<string> reportPaths, string printerName)
|
||||
{
|
||||
foreach (var path in reportPaths)
|
||||
{
|
||||
_printService.Print(
|
||||
filePath: path,
|
||||
printerName: printerName,
|
||||
pageFrom: 1,
|
||||
pageTo: null,
|
||||
copies: 1
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
# 实时日志查看器设计文档
|
||||
|
||||
#### 1. 核心目标
|
||||
|
||||
构建一个模态或非模态的 WPF 窗口,用于实时订阅 Serilog 事件,根据预定义的格式(如包含 `@l` 级别标记)自动渲染颜色,并提供过滤与自动滚动控制。
|
||||
|
||||
---
|
||||
|
||||
#### 2. 功能模块规划
|
||||
|
||||
为了保证代码的可维护性和复用性,建议采用 MVVM 模式,参考 `XP.Common.GeneralForm` 的目录结构进行扩展:
|
||||
|
||||
text
|
||||
|
||||
编辑
|
||||
|
||||
```
|
||||
XP.Common/
|
||||
└── GeneralForm/
|
||||
└── Views/
|
||||
├── RealTimeLogViewer.xaml // 视图:RichTextBox + 控制栏
|
||||
└── RealTimeLogViewer.xaml.cs
|
||||
└── ViewModels/
|
||||
└── RealTimeLogViewerViewModel.cs // 核心逻辑:处理颜色标记、过滤、滚动
|
||||
```
|
||||
|
||||
#### 3. 详细功能规格
|
||||
|
||||
| 功能模块 | 详细描述 | 交互逻辑 |
|
||||
| ------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| 颜色渲染引擎 | 基于 Serilog 标记<br>解析日志事件中的 Level 或 Message Template。 | * Error/Fatal: 红色/深红色<br>* Warning: 橙色/黄色<br>* Info: 默认色<br>* Debug: 浅灰色<br>利用 `RichTextBox` 的 `TextRange` 动态追加。 |
|
||||
| 动态过滤器 | 关键词黑白名单 | * 包含: 仅显示包含关键字的行。<br>* 排除: 隐藏包含关键字的行。<br>* 重置: 恢复显示所有。 |
|
||||
| 智能滚动 | 跟随开关 | * 开启: 新日志到达时,滚动条自动到底。<br>* 关闭: 用户可自由浏览历史,新日志仅在后台缓存/计数。 |
|
||||
| 行数限制 | 内存保护机制 | * 配置: 默认 2000 行,可调。<br>* 清理: 达到上限后,自动移除最旧的文本段落(`Paragraph`)。 |
|
||||
| 日志源接入 | 线程安全订阅 | 使用 `IObserver<LogEvent>` 或 `IObservable`,内部通过 `Dispatcher` 安全更新。 |
|
||||
|
||||
#### 4. 视觉交互 (UI/UX)
|
||||
|
||||
- **布局**:
|
||||
- **顶部工具栏**:包含“自动滚动”开关(带图标)、“清空日志”按钮、过滤输入框。
|
||||
- **中部显示区**:`RichTextBox` (只读),启用垂直滚动条。
|
||||
- **底部状态栏**:显示当前总日志条数、过滤后的条数。
|
||||
- **性能优化**:
|
||||
- 由于 WPF `RichTextBox` 在大量文本下性能较差,建议增加**最大行数限制**(如默认保留 4000 行,超出自动删除顶部旧日志)。
|
||||
|
||||
#### 6. 开发建议
|
||||
|
||||
1. **颜色解析**:如果 Serilog 输出的是纯文本(如 `[INF] User logged in`),你需要编写正则表达式来匹配 `[INF]`、`[ERR]` 等前缀并着色。
|
||||
2. **线程安全**:务必参考 `ProgressWindow` 中 `UpdateProgress` 的实现,使用 `Dispatcher.InvokeAsync` 或 `Dispatcher.CheckAccess` 来确保从后台线程(如 Serilog 的异步写入线程)更新 UI 时不会崩溃。
|
||||
3. **资源占用**:考虑到这是一个“基础设施”类库,建议在窗口关闭时(`OnClosed` 事件)取消对 `IObservable` 的订阅,防止内存泄漏。
|
||||
@@ -0,0 +1,34 @@
|
||||
namespace XP.Common.Dump.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 功能配置实体(从 App.config 读取)
|
||||
/// Dump feature configuration entity (loaded from App.config)
|
||||
/// </summary>
|
||||
public class DumpConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 文件存储路径 | Dump file storage path
|
||||
/// </summary>
|
||||
public string StoragePath { get; set; } = @"D:\XplorePlane\Dump";
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用定时触发 | Enable scheduled trigger
|
||||
/// </summary>
|
||||
public bool EnableScheduledDump { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 定时触发间隔(分钟)| Scheduled trigger interval (minutes)
|
||||
/// </summary>
|
||||
public int ScheduledIntervalMinutes { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Mini Dump 文件大小上限(MB)| Mini Dump file size limit (MB)
|
||||
/// </summary>
|
||||
public long MiniDumpSizeLimitMB { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 文件保留天数 | File retention days
|
||||
/// </summary>
|
||||
public int RetentionDays { get; set; } = 7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace XP.Common.Dump.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 触发类型 | Dump trigger type
|
||||
/// </summary>
|
||||
public enum DumpTriggerType
|
||||
{
|
||||
/// <summary>
|
||||
/// 崩溃触发 | Crash trigger
|
||||
/// </summary>
|
||||
Crash,
|
||||
|
||||
/// <summary>
|
||||
/// 定时触发 | Scheduled trigger
|
||||
/// </summary>
|
||||
Scheduled,
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发 | Manual trigger
|
||||
/// </summary>
|
||||
Manual
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using XP.Common.Dump.Configs;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Dump.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 文件自动清理组件,按保留天数策略删除过期文件
|
||||
/// Auto cleanup component for dump files, deletes expired files based on retention policy
|
||||
/// </summary>
|
||||
internal class DumpCleaner
|
||||
{
|
||||
private readonly DumpConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public DumpCleaner(DumpConfig config, ILoggerService logger)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_logger = logger?.ForModule<DumpCleaner>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行清理操作:删除超过保留天数的 .dmp 文件
|
||||
/// Execute cleanup: delete .dmp files older than retention days
|
||||
/// </summary>
|
||||
public void CleanExpiredFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(_config.StoragePath))
|
||||
{
|
||||
_logger.Debug("存储目录不存在,跳过清理:{Path} | Storage directory does not exist, skipping cleanup: {Path}", _config.StoragePath);
|
||||
return;
|
||||
}
|
||||
|
||||
var dmpFiles = Directory.GetFiles(_config.StoragePath, "*.dmp");
|
||||
var cutoffTime = DateTime.Now.AddDays(-_config.RetentionDays);
|
||||
var deletedCount = 0;
|
||||
|
||||
foreach (var filePath in dmpFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var creationTime = File.GetCreationTime(filePath);
|
||||
if (creationTime < cutoffTime)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
File.Delete(filePath);
|
||||
deletedCount++;
|
||||
_logger.Info("已删除过期 Dump 文件:{FileName} | Deleted expired dump file: {FileName}", fileName);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "删除 Dump 文件失败:{FilePath} | Failed to delete dump file: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Info("Dump 清理完成,共删除 {Count} 个文件 | Dump cleanup completed, {Count} files deleted", deletedCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Dump 清理过程发生异常:{Message} | Dump cleanup error: {Message}", ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using XP.Common.Dump.Configs;
|
||||
using XP.Common.Dump.Interfaces;
|
||||
using XP.Common.Dump.Native;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Dump.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 文件管理服务实现,负责 Dump 生成、崩溃事件订阅、定时调度和清理
|
||||
/// Dump file management service implementation, handles dump generation, crash event subscription, scheduled tasks and cleanup
|
||||
/// </summary>
|
||||
public class DumpService : IDumpService
|
||||
{
|
||||
private readonly DumpConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly DumpCleaner _cleaner;
|
||||
private Timer? _scheduledTimer;
|
||||
private Timer? _cleanupTimer;
|
||||
private bool _isStarted;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 默认存储路径 | Default storage path
|
||||
/// </summary>
|
||||
private const string DefaultStoragePath = @"D:\XplorePlane\Dump";
|
||||
|
||||
public DumpService(DumpConfig config, ILoggerService logger)
|
||||
{
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_logger = logger?.ForModule<DumpService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||
_cleaner = new DumpCleaner(config, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 Dump 文件名(纯函数)| Generate dump file name (pure function)
|
||||
/// 格式:XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp
|
||||
/// </summary>
|
||||
/// <param name="triggerType">触发类型 | Trigger type</param>
|
||||
/// <param name="timestamp">时间戳 | Timestamp</param>
|
||||
/// <returns>文件名 | File name</returns>
|
||||
internal static string GenerateFileName(DumpTriggerType triggerType, DateTime timestamp)
|
||||
{
|
||||
return $"XplorePlane_{timestamp:yyyyMMdd_HHmm}_{triggerType}.dmp";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 Mini Dump 文件大小是否在限制范围内 | Validate Mini Dump file size is within limit
|
||||
/// </summary>
|
||||
/// <param name="fileSizeBytes">文件大小(字节)| File size in bytes</param>
|
||||
/// <param name="sizeLimitMB">大小限制(MB)| Size limit in MB</param>
|
||||
/// <returns>true 表示在限制内 | true if within limit</returns>
|
||||
internal static bool ValidateFileSize(long fileSizeBytes, long sizeLimitMB)
|
||||
{
|
||||
return fileSizeBytes <= sizeLimitMB * 1024 * 1024;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入 Dump 文件的核心方法 | Core method to write dump file
|
||||
/// </summary>
|
||||
/// <param name="triggerType">触发类型 | Trigger type</param>
|
||||
/// <param name="isMiniDump">是否为 Mini Dump | Whether it is a Mini Dump</param>
|
||||
/// <returns>生成的文件完整路径,失败返回 null | Full path of generated file, null on failure</returns>
|
||||
private string? WriteDump(DumpTriggerType triggerType, bool isMiniDump)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 非手动触发请求 Full Dump 时,降级为 Mini Dump | Downgrade to Mini Dump for non-manual Full Dump requests
|
||||
if (!isMiniDump && triggerType != DumpTriggerType.Manual)
|
||||
{
|
||||
_logger.Warn("非手动触发不允许生成 Full Dump,已降级为 Mini Dump:{TriggerType} | Non-manual trigger cannot generate Full Dump, downgraded to Mini Dump: {TriggerType}", triggerType);
|
||||
isMiniDump = true;
|
||||
}
|
||||
|
||||
var fileName = GenerateFileName(triggerType, DateTime.Now);
|
||||
var filePath = Path.Combine(_config.StoragePath, fileName);
|
||||
|
||||
// 确定 Dump 类型标志 | Determine dump type flags
|
||||
uint dumpType = isMiniDump
|
||||
? (uint)(MiniDumpType.MiniDumpWithDataSegs | MiniDumpType.MiniDumpWithHandleData | MiniDumpType.MiniDumpWithThreadInfo)
|
||||
: (uint)MiniDumpType.MiniDumpWithFullMemory;
|
||||
|
||||
var process = Process.GetCurrentProcess();
|
||||
|
||||
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||
{
|
||||
bool success = NativeMethods.MiniDumpWriteDump(
|
||||
process.Handle,
|
||||
(uint)process.Id,
|
||||
fileStream.SafeFileHandle,
|
||||
dumpType,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero,
|
||||
IntPtr.Zero);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
var errorCode = Marshal.GetLastWin32Error();
|
||||
_logger.Error(null!, "MiniDumpWriteDump 调用失败,错误码:{ErrorCode} | MiniDumpWriteDump failed, error code: {ErrorCode}", errorCode);
|
||||
// 清理失败的文件 | Clean up failed file
|
||||
try { File.Delete(filePath); } catch { }
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Mini Dump 文件大小检查 | Mini Dump file size check
|
||||
if (isMiniDump)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (!ValidateFileSize(fileInfo.Length, _config.MiniDumpSizeLimitMB))
|
||||
{
|
||||
_logger.Warn("Mini Dump 文件超过大小限制({SizeMB}MB),已删除:{FilePath} | Mini Dump file exceeds size limit ({SizeMB}MB), deleted: {FilePath}",
|
||||
_config.MiniDumpSizeLimitMB, filePath);
|
||||
File.Delete(filePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var dumpTypeStr = isMiniDump ? "Mini" : "Full";
|
||||
_logger.Info("Dump 文件已生成:{FilePath},触发类型:{TriggerType},Dump 类型:{DumpType} | Dump file generated: {FilePath}, trigger: {TriggerType}, type: {DumpType}",
|
||||
filePath, triggerType, dumpTypeStr);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
_logger.Error(ex, "Dump 文件写入失败:{Message} | Dump file write failed: {Message}", ex.Message);
|
||||
return null;
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.Error(ex, "Dump 文件写入权限不足:{Message} | Dump file write permission denied: {Message}", ex.Message);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Dump 文件生成异常:{Message} | Dump file generation error: {Message}", ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#region 手动触发方法 | Manual trigger methods
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? CreateMiniDump()
|
||||
{
|
||||
_logger.Info("手动触发 Mini Dump 生成 | Manually triggering Mini Dump generation");
|
||||
var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: true);
|
||||
if (filePath != null)
|
||||
{
|
||||
_logger.Info("手动 Mini Dump 已生成:{FilePath} | Manual Mini Dump generated: {FilePath}", filePath);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? CreateFullDump()
|
||||
{
|
||||
_logger.Info("手动触发 Full Dump 生成 | Manually triggering Full Dump generation");
|
||||
var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: false);
|
||||
if (filePath != null)
|
||||
{
|
||||
_logger.Info("手动 Full Dump 已生成:{FilePath} | Manual Full Dump generated: {FilePath}", filePath);
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期管理 | Lifecycle management
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Start()
|
||||
{
|
||||
if (_isStarted) return;
|
||||
|
||||
_logger.Info("Dump 服务启动中 | Dump service starting");
|
||||
|
||||
// 确保存储目录存在 | Ensure storage directory exists
|
||||
EnsureStorageDirectory();
|
||||
|
||||
// 订阅崩溃事件 | Subscribe to crash events
|
||||
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||
|
||||
// 启动定时 Dump 计时器 | Start scheduled dump timer
|
||||
if (_config.EnableScheduledDump)
|
||||
{
|
||||
var intervalMs = _config.ScheduledIntervalMinutes * 60 * 1000;
|
||||
_scheduledTimer = new Timer(OnScheduledDumpCallback, null, intervalMs, intervalMs);
|
||||
_logger.Info("定时 Dump 已启用,间隔:{Interval} 分钟 | Scheduled dump enabled, interval: {Interval} minutes", _config.ScheduledIntervalMinutes);
|
||||
}
|
||||
|
||||
// 启动每日清理计时器(每24小时执行一次)| Start daily cleanup timer (every 24 hours)
|
||||
var dailyMs = 24 * 60 * 60 * 1000;
|
||||
_cleanupTimer = new Timer(OnCleanupCallback, null, dailyMs, dailyMs);
|
||||
|
||||
// 立即执行一次清理 | Execute cleanup immediately
|
||||
_cleaner.CleanExpiredFiles();
|
||||
|
||||
_isStarted = true;
|
||||
_logger.Info("Dump 服务已启动 | Dump service started");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Stop()
|
||||
{
|
||||
if (!_isStarted) return;
|
||||
|
||||
_logger.Info("Dump 服务停止中 | Dump service stopping");
|
||||
|
||||
// 取消事件订阅 | Unsubscribe from events
|
||||
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
|
||||
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
||||
|
||||
// 停止所有计时器 | Stop all timers
|
||||
_scheduledTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
|
||||
_isStarted = false;
|
||||
_logger.Info("Dump 服务已停止 | Dump service stopped");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
|
||||
Stop();
|
||||
|
||||
_scheduledTimer?.Dispose();
|
||||
_scheduledTimer = null;
|
||||
|
||||
_cleanupTimer?.Dispose();
|
||||
_cleanupTimer = null;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部方法 | Internal methods
|
||||
|
||||
/// <summary>
|
||||
/// 确保存储目录存在,不存在则创建,创建失败回退默认路径
|
||||
/// Ensure storage directory exists, create if not, fallback to default on failure
|
||||
/// </summary>
|
||||
private void EnsureStorageDirectory()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(_config.StoragePath))
|
||||
{
|
||||
Directory.CreateDirectory(_config.StoragePath);
|
||||
_logger.Info("已创建存储目录:{Path} | Created storage directory: {Path}", _config.StoragePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "存储目录创建失败:{Path},回退到默认路径 | Storage directory creation failed: {Path}, falling back to default", _config.StoragePath);
|
||||
_config.StoragePath = DefaultStoragePath;
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(DefaultStoragePath))
|
||||
{
|
||||
Directory.CreateDirectory(DefaultStoragePath);
|
||||
_logger.Info("已创建默认存储目录:{Path} | Created default storage directory: {Path}", DefaultStoragePath);
|
||||
}
|
||||
}
|
||||
catch (Exception fallbackEx)
|
||||
{
|
||||
_logger.Fatal(fallbackEx, "默认存储目录创建也失败:{Path} | Default storage directory creation also failed: {Path}", DefaultStoragePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 未处理异常回调 | Unhandled exception callback
|
||||
/// </summary>
|
||||
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
_logger.Info("检测到未处理异常,正在生成崩溃 Dump | Unhandled exception detected, generating crash dump");
|
||||
WriteDump(DumpTriggerType.Crash, isMiniDump: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 未观察到的 Task 异常回调 | Unobserved task exception callback
|
||||
/// </summary>
|
||||
private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||
{
|
||||
_logger.Info("检测到未观察的 Task 异常,正在生成崩溃 Dump | Unobserved task exception detected, generating crash dump");
|
||||
WriteDump(DumpTriggerType.Crash, isMiniDump: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时 Dump 回调 | Scheduled dump callback
|
||||
/// </summary>
|
||||
private void OnScheduledDumpCallback(object? state)
|
||||
{
|
||||
_logger.Debug("定时 Dump 触发 | Scheduled dump triggered");
|
||||
WriteDump(DumpTriggerType.Scheduled, isMiniDump: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理回调 | Cleanup callback
|
||||
/// </summary>
|
||||
private void OnCleanupCallback(object? state)
|
||||
{
|
||||
_logger.Debug("定时清理触发 | Scheduled cleanup triggered");
|
||||
_cleaner.CleanExpiredFiles();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Dump.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Dump 文件管理服务接口 | Dump file management service interface
|
||||
/// </summary>
|
||||
public interface IDumpService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
|
||||
/// </summary>
|
||||
/// <returns>生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure</returns>
|
||||
string? CreateMiniDump();
|
||||
|
||||
/// <summary>
|
||||
/// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
|
||||
/// </summary>
|
||||
/// <returns>生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure</returns>
|
||||
string? CreateFullDump();
|
||||
|
||||
/// <summary>
|
||||
/// 启动服务(订阅崩溃事件、启动定时任务和清理任务)| Start service (subscribe crash events, start scheduled and cleanup tasks)
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// 停止服务(取消定时任务、取消事件订阅)| Stop service (cancel scheduled tasks, unsubscribe events)
|
||||
/// </summary>
|
||||
void Stop();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Dump.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// MiniDump 类型标志,用于 MiniDumpWriteDump 的 dumpType 参数
|
||||
/// MiniDump type flags for MiniDumpWriteDump dumpType parameter
|
||||
/// </summary>
|
||||
[Flags]
|
||||
internal enum MiniDumpType : uint
|
||||
{
|
||||
/// <summary>
|
||||
/// 仅包含基本信息 | Basic information only
|
||||
/// </summary>
|
||||
MiniDumpNormal = 0x00000000,
|
||||
|
||||
/// <summary>
|
||||
/// 包含数据段信息 | Include data segment information
|
||||
/// </summary>
|
||||
MiniDumpWithDataSegs = 0x00000001,
|
||||
|
||||
/// <summary>
|
||||
/// 包含完整内存信息(Full Dump 使用)| Include full memory (used for Full Dump)
|
||||
/// </summary>
|
||||
MiniDumpWithFullMemory = 0x00000002,
|
||||
|
||||
/// <summary>
|
||||
/// 包含句柄信息 | Include handle information
|
||||
/// </summary>
|
||||
MiniDumpWithHandleData = 0x00000004,
|
||||
|
||||
/// <summary>
|
||||
/// 包含线程信息 | Include thread information
|
||||
/// </summary>
|
||||
MiniDumpWithThreadInfo = 0x00001000,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace XP.Common.Dump.Native
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows 原生方法 P/Invoke 声明 | Windows native method P/Invoke declarations
|
||||
/// </summary>
|
||||
internal static class NativeMethods
|
||||
{
|
||||
/// <summary>
|
||||
/// 将进程的 Mini Dump 写入指定文件 | Write a Mini Dump of the process to the specified file
|
||||
/// </summary>
|
||||
[DllImport("dbghelp.dll", SetLastError = true)]
|
||||
internal static extern bool MiniDumpWriteDump(
|
||||
IntPtr hProcess,
|
||||
uint processId,
|
||||
SafeHandle hFile,
|
||||
uint dumpType,
|
||||
IntPtr exceptionParam,
|
||||
IntPtr userStreamParam,
|
||||
IntPtr callbackParam);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Localization;
|
||||
|
||||
namespace XP.Common.GeneralForm.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 输入对话框 ViewModel | Input dialog ViewModel
|
||||
/// </summary>
|
||||
public class InputDialogViewModel : BindableBase
|
||||
{
|
||||
private readonly Func<string, string?>? _validate;
|
||||
private string _inputText;
|
||||
private string? _validationError;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口标题 | Window title
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示文本 | Prompt text
|
||||
/// </summary>
|
||||
public string Prompt { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 确定按钮文本(多语言)| OK button text (localized)
|
||||
/// </summary>
|
||||
public string OkText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 取消按钮文本(多语言)| Cancel button text (localized)
|
||||
/// </summary>
|
||||
public string CancelText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户输入的文本 | User input text
|
||||
/// </summary>
|
||||
public string InputText
|
||||
{
|
||||
get => _inputText;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _inputText, value))
|
||||
{
|
||||
// 输入变化时清除验证错误 | Clear validation error on input change
|
||||
ValidationError = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证错误信息,null 表示无错误 | Validation error message, null means no error
|
||||
/// </summary>
|
||||
public string? ValidationError
|
||||
{
|
||||
get => _validationError;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _validationError, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(HasValidationError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否存在验证错误 | Whether there is a validation error
|
||||
/// </summary>
|
||||
public bool HasValidationError => !string.IsNullOrEmpty(ValidationError);
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||
/// <param name="title">窗口标题 | Window title</param>
|
||||
/// <param name="defaultValue">默认值 | Default value</param>
|
||||
/// <param name="validate">验证委托,返回 null 表示通过,返回错误信息则显示 | Validation delegate</param>
|
||||
public InputDialogViewModel(
|
||||
string prompt,
|
||||
string title,
|
||||
string defaultValue = "",
|
||||
Func<string, string?>? validate = null)
|
||||
{
|
||||
Prompt = prompt;
|
||||
Title = title;
|
||||
_inputText = defaultValue;
|
||||
_validate = validate;
|
||||
|
||||
// 按钮文本使用多语言 | Button text uses localization
|
||||
OkText = LocalizationHelper.Get("Button_OK");
|
||||
CancelText = LocalizationHelper.Get("Button_Cancel");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行验证,返回是否通过 | Run validation, returns whether it passed
|
||||
/// </summary>
|
||||
/// <returns>true 表示验证通过 | true means validation passed</returns>
|
||||
public bool Validate()
|
||||
{
|
||||
if (_validate == null)
|
||||
return true;
|
||||
|
||||
var error = _validate(InputText);
|
||||
ValidationError = error;
|
||||
return error == null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.GeneralForm.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 进度条窗口 ViewModel,管理进度数据和窗口行为逻辑
|
||||
/// </summary>
|
||||
public class ProgressWindowViewModel : BindableBase
|
||||
{
|
||||
private readonly ILoggerService? _logger;
|
||||
private string _message;
|
||||
private double _progress;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口标题(只读)
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 提示信息文本
|
||||
/// </summary>
|
||||
public string Message
|
||||
{
|
||||
get => _message;
|
||||
set => SetProperty(ref _message, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进度值,范围 0-100
|
||||
/// </summary>
|
||||
public double Progress
|
||||
{
|
||||
get => _progress;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _progress, value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(ProgressText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 百分比显示文本,由 Progress 自动计算(只读)
|
||||
/// </summary>
|
||||
public string ProgressText => $"{Math.Floor(Progress)}%";
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许用户关闭窗口(只读)
|
||||
/// </summary>
|
||||
public bool IsCancelable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="title">窗口标题</param>
|
||||
/// <param name="message">提示信息</param>
|
||||
/// <param name="progress">初始进度值</param>
|
||||
/// <param name="isCancelable">是否允许取消</param>
|
||||
/// <param name="logger">日志服务(可选)</param>
|
||||
public ProgressWindowViewModel(
|
||||
string title = "操作进行中",
|
||||
string message = "请稍候...",
|
||||
double progress = 0,
|
||||
bool isCancelable = true,
|
||||
ILoggerService? logger = null)
|
||||
{
|
||||
Title = title;
|
||||
_message = message;
|
||||
_progress = progress;
|
||||
IsCancelable = isCancelable;
|
||||
|
||||
// 创建模块化日志实例(logger 为 null 时静默跳过)
|
||||
_logger = logger?.ForModule<ProgressWindowViewModel>();
|
||||
|
||||
// 构造时记录 Info 级别日志
|
||||
_logger?.Info("进度窗口已创建:Title={Title}, IsCancelable={IsCancelable}", Title, IsCancelable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新进度和提示信息(唯一的外部更新入口)
|
||||
/// </summary>
|
||||
/// <param name="message">提示信息文本</param>
|
||||
/// <param name="progress">进度值,超出 [0, 100] 范围时自动修正</param>
|
||||
public void UpdateProgress(string message, double progress)
|
||||
{
|
||||
// 检查 progress 是否超出有效范围,记录 Warn 日志
|
||||
if (progress < 0 || progress > 100)
|
||||
{
|
||||
_logger?.Warn("进度值超出有效范围 [0, 100]:{Progress},将自动修正", progress);
|
||||
}
|
||||
|
||||
// 将 progress 值 Clamp 到 [0, 100] 范围
|
||||
progress = Math.Clamp(progress, 0, 100);
|
||||
|
||||
// 更新属性,触发 PropertyChanged 通知
|
||||
Message = message;
|
||||
Progress = progress;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<Window x:Class="XP.Common.GeneralForm.Views.InputDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
Title="{Binding Title}"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ResizeMode="NoResize"
|
||||
Width="400" Height="180"
|
||||
ShowInTaskbar="False"
|
||||
Topmost="True"
|
||||
WindowStyle="SingleBorderWindow">
|
||||
|
||||
<Window.Resources>
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20,16,20,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 提示文本 | Prompt text -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding Prompt}"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="13"
|
||||
Foreground="#FF333333"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- 输入框 | Input text box -->
|
||||
<telerik:RadWatermarkTextBox Grid.Row="1"
|
||||
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
||||
Height="28"
|
||||
FontSize="13"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
Margin="0,0,0,2"/>
|
||||
|
||||
<!-- 验证错误提示 | Validation error message -->
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding ValidationError}"
|
||||
Foreground="#FFD32F2F"
|
||||
FontSize="11"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,7,0,0"
|
||||
Visibility="{Binding HasValidationError, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||
|
||||
<!-- 按钮区域 | Button area -->
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<telerik:RadButton Content="{Binding OkText}"
|
||||
Click="OnOkClick"
|
||||
Width="80" Height="28"
|
||||
Margin="0,0,8,0"
|
||||
IsDefault="True"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
<telerik:RadButton Content="{Binding CancelText}"
|
||||
Click="OnCancelClick"
|
||||
Width="80" Height="28"
|
||||
IsCancel="True"
|
||||
telerik:StyleManager.Theme="Crystal"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using XP.Common.GeneralForm.ViewModels;
|
||||
using XP.Common.Localization;
|
||||
|
||||
namespace XP.Common.GeneralForm.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用输入对话框,支持单行文本输入和可选的输入验证
|
||||
/// General input dialog with single-line text input and optional validation
|
||||
/// </summary>
|
||||
public partial class InputDialog : Window
|
||||
{
|
||||
private readonly InputDialogViewModel _viewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 用户输入的结果,取消时为 null | User input result, null if cancelled
|
||||
/// </summary>
|
||||
public string? Result { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||
/// <param name="title">窗口标题 | Window title</param>
|
||||
/// <param name="defaultValue">默认值 | Default value</param>
|
||||
/// <param name="validate">可选的验证委托,返回 null 表示通过,返回错误信息则阻止确认 | Optional validation delegate</param>
|
||||
public InputDialog(
|
||||
string prompt,
|
||||
string title,
|
||||
string defaultValue = "",
|
||||
Func<string, string?>? validate = null)
|
||||
{
|
||||
_viewModel = new InputDialogViewModel(prompt, title, defaultValue, validate);
|
||||
DataContext = _viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// 继承调用方窗口图标 | Inherit caller window icon
|
||||
if (Application.Current?.MainWindow != null)
|
||||
{
|
||||
Icon = Application.Current.MainWindow.Icon;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确定按钮点击事件 | OK button click handler
|
||||
/// </summary>
|
||||
private void OnOkClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 执行验证 | Run validation
|
||||
if (!_viewModel.Validate())
|
||||
return;
|
||||
|
||||
Result = _viewModel.InputText;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消按钮点击事件 | Cancel button click handler
|
||||
/// </summary>
|
||||
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Result = null;
|
||||
DialogResult = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 静态便捷方法,显示输入对话框并返回结果 | Static convenience method
|
||||
/// </summary>
|
||||
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||
/// <param name="title">窗口标题 | Window title</param>
|
||||
/// <param name="defaultValue">默认值 | Default value</param>
|
||||
/// <param name="validate">可选的验证委托 | Optional validation delegate</param>
|
||||
/// <param name="owner">父窗口(可选)| Owner window (optional)</param>
|
||||
/// <returns>用户输入的值,取消时返回 null | User input, null if cancelled</returns>
|
||||
public static string? Show(
|
||||
string prompt,
|
||||
string title,
|
||||
string defaultValue = "",
|
||||
Func<string, string?>? validate = null,
|
||||
Window? owner = null)
|
||||
{
|
||||
var dialog = new InputDialog(prompt, title, defaultValue, validate);
|
||||
// owner 未指定时自动回退到主窗口,确保对话框有父窗口约束不会被遮挡
|
||||
// Fall back to MainWindow if owner not specified, ensuring dialog stays in front
|
||||
dialog.Owner = owner ?? Application.Current?.MainWindow;
|
||||
dialog.ShowDialog();
|
||||
return dialog.Result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Window
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="XP.Common.GeneralForm.Views.ProgressWindow"
|
||||
Title="{Binding Title}"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="NoResize"
|
||||
Width="400"
|
||||
Height="150"
|
||||
ShowInTaskbar="False"
|
||||
WindowStyle="SingleBorderWindow">
|
||||
|
||||
<!-- 主布局:从上到下依次为提示信息、进度条、百分比文本 -->
|
||||
<Grid Margin="24,20,24,20">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 提示信息文本 -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding Message}"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,12" />
|
||||
|
||||
<!-- 进度条 -->
|
||||
<telerik:RadProgressBar Grid.Row="1"
|
||||
Value="{Binding Progress, Mode=OneWay}"
|
||||
Minimum="0"
|
||||
Maximum="100"
|
||||
Height="18"
|
||||
Margin="0,0,0,8" telerik:StyleManager.Theme="Crystal" />
|
||||
|
||||
<!-- 百分比文本 -->
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding ProgressText}"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="12"
|
||||
Foreground="#666666" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using XP.Common.GeneralForm.ViewModels;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.GeneralForm.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用进度条模态窗口,支持线程安全的进度更新和关闭操作
|
||||
/// </summary>
|
||||
public partial class ProgressWindow : Window
|
||||
{
|
||||
#region Win32 API
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
|
||||
|
||||
private const uint SC_CLOSE = 0xF060;
|
||||
private const uint MF_BYCOMMAND = 0x00000000;
|
||||
private const uint MF_GRAYED = 0x00000001;
|
||||
private const uint MF_ENABLED = 0x00000000;
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly ProgressWindowViewModel _viewModel;
|
||||
private readonly ILoggerService? _logger;
|
||||
private bool _isClosingByCode;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="title">窗口标题</param>
|
||||
/// <param name="message">提示信息</param>
|
||||
/// <param name="isCancelable">是否允许用户关闭窗口</param>
|
||||
/// <param name="logger">日志服务(可选)</param>
|
||||
public ProgressWindow(
|
||||
string title = "操作进行中",
|
||||
string message = "请稍候...",
|
||||
bool isCancelable = true,
|
||||
ILoggerService? logger = null)
|
||||
{
|
||||
// 创建 ViewModel 并设置为 DataContext
|
||||
_viewModel = new ProgressWindowViewModel(title, message, 0, isCancelable, logger);
|
||||
DataContext = _viewModel;
|
||||
|
||||
// 保存日志服务引用,用于 View 层日志记录
|
||||
_logger = logger?.ForModule<ProgressWindow>();
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// 继承主窗口图标
|
||||
if (Application.Current?.MainWindow != null)
|
||||
{
|
||||
Icon = Application.Current.MainWindow.Icon;
|
||||
}
|
||||
|
||||
// 订阅 Closing 事件,拦截用户手动关闭
|
||||
Closing += OnWindowClosing;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新进度和提示信息(线程安全,可从任意线程调用)
|
||||
/// </summary>
|
||||
/// <param name="message">提示信息文本</param>
|
||||
/// <param name="progress">进度值</param>
|
||||
public void UpdateProgress(string message, double progress)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Dispatcher.CheckAccess())
|
||||
{
|
||||
_viewModel.UpdateProgress(message, progress);
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.Invoke(() => _viewModel.UpdateProgress(message, progress));
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 其他调度异常静默处理,避免影响调用方
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 线程安全关闭窗口(使用 new 关键字隐藏基类 Close 方法,因为 Window.Close() 不是虚方法)
|
||||
/// </summary>
|
||||
public new void Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isClosingByCode = true;
|
||||
|
||||
if (Dispatcher.CheckAccess())
|
||||
{
|
||||
_logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
|
||||
base.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
|
||||
base.Close();
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// 其他调度异常静默处理,避免影响调用方
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口源初始化时,根据 IsCancelable 控制关闭按钮状态
|
||||
/// </summary>
|
||||
protected override void OnSourceInitialized(EventArgs e)
|
||||
{
|
||||
base.OnSourceInitialized(e);
|
||||
|
||||
if (!_viewModel.IsCancelable)
|
||||
{
|
||||
DisableCloseButton();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拦截窗口关闭事件
|
||||
/// </summary>
|
||||
private void OnWindowClosing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
// 当不可取消且非程序主动关闭时,阻止关闭
|
||||
if (!_viewModel.IsCancelable && !_isClosingByCode)
|
||||
{
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
|
||||
/// </summary>
|
||||
private void DisableCloseButton()
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var hMenu = GetSystemMenu(hwnd, false);
|
||||
if (hMenu != IntPtr.Zero)
|
||||
{
|
||||
EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Win32 API 启用窗口关闭按钮
|
||||
/// </summary>
|
||||
private void EnableCloseButton()
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
var hMenu = GetSystemMenu(hwnd, false);
|
||||
if (hMenu != IntPtr.Zero)
|
||||
{
|
||||
EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Configuration;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Dump.Configs;
|
||||
|
||||
namespace XP.Common.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用配置加载工具(读取App.config)
|
||||
/// </summary>
|
||||
public static class ConfigLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载Serilog配置
|
||||
/// </summary>
|
||||
public static SerilogConfig LoadSerilogConfig()
|
||||
{
|
||||
var config = new SerilogConfig();
|
||||
|
||||
var logPath = ConfigurationManager.AppSettings["Serilog:LogPath"];
|
||||
if (!string.IsNullOrEmpty(logPath)) config.LogPath = logPath;
|
||||
|
||||
var minLevel = ConfigurationManager.AppSettings["Serilog:MinimumLevel"];
|
||||
if (!string.IsNullOrEmpty(minLevel)) config.MinimumLevel = minLevel;
|
||||
|
||||
var enableConsole = ConfigurationManager.AppSettings["Serilog:EnableConsole"];
|
||||
if (bool.TryParse(enableConsole, out var console)) config.EnableConsole = console;
|
||||
|
||||
var rollingInterval = ConfigurationManager.AppSettings["Serilog:RollingInterval"];
|
||||
if (!string.IsNullOrEmpty(rollingInterval)) config.RollingInterval = rollingInterval;
|
||||
|
||||
var fileSize = ConfigurationManager.AppSettings["Serilog:FileSizeLimitMB"];
|
||||
if (long.TryParse(fileSize, out var size)) config.FileSizeLimitMB = size;
|
||||
|
||||
var retainCount = ConfigurationManager.AppSettings["Serilog:RetainedFileCountLimit"];
|
||||
if (int.TryParse(retainCount, out var count)) config.RetainedFileCountLimit = count;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载SQLite配置
|
||||
/// </summary>
|
||||
public static SqliteConfig LoadSqliteConfig()
|
||||
{
|
||||
var config = new SqliteConfig();
|
||||
|
||||
var dbPath = ConfigurationManager.AppSettings["Sqlite:DbFilePath"];
|
||||
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
|
||||
|
||||
var timeout = ConfigurationManager.AppSettings["Sqlite:ConnectionTimeout"];
|
||||
if (int.TryParse(timeout, out var t)) config.ConnectionTimeout = t;
|
||||
|
||||
var createIfNotExists = ConfigurationManager.AppSettings["Sqlite:CreateIfNotExists"];
|
||||
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
|
||||
|
||||
var enableWal = ConfigurationManager.AppSettings["Sqlite:EnableWalMode"];
|
||||
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
|
||||
|
||||
var enableSqlLog = ConfigurationManager.AppSettings["Sqlite:EnableSqlLogging"];
|
||||
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 Dump 配置 | Load Dump configuration
|
||||
/// </summary>
|
||||
public static DumpConfig LoadDumpConfig()
|
||||
{
|
||||
var config = new DumpConfig();
|
||||
|
||||
var storagePath = ConfigurationManager.AppSettings["Dump:StoragePath"];
|
||||
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
|
||||
|
||||
var enableScheduled = ConfigurationManager.AppSettings["Dump:EnableScheduledDump"];
|
||||
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
|
||||
|
||||
var interval = ConfigurationManager.AppSettings["Dump:ScheduledIntervalMinutes"];
|
||||
if (int.TryParse(interval, out var min)) config.ScheduledIntervalMinutes = min;
|
||||
|
||||
var sizeLimit = ConfigurationManager.AppSettings["Dump:MiniDumpSizeLimitMB"];
|
||||
if (long.TryParse(sizeLimit, out var size)) config.MiniDumpSizeLimitMB = size;
|
||||
|
||||
var retentionDays = ConfigurationManager.AppSettings["Dump:RetentionDays"];
|
||||
if (int.TryParse(retentionDays, out var days)) config.RetentionDays = days;
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace XP.Common.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 进程管理工具类 | Process Management Helper
|
||||
/// 提供启动外部程序、窗口置前等通用功能 | Provides common functions like launching external programs and bringing windows to front
|
||||
/// </summary>
|
||||
public static class ProcessHelper
|
||||
{
|
||||
#region Win32 API
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern bool IsIconic(IntPtr hWnd);
|
||||
|
||||
private const int SW_RESTORE = 9;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 启动或激活外部程序 | Start or activate an external program
|
||||
/// </summary>
|
||||
/// <param name="exePath">可执行文件完整路径 | Full path to the executable</param>
|
||||
/// <param name="singleInstance">是否单实例模式:true=已运行则置前,false=直接启动新实例 | Single instance mode: true=activate if running, false=always start new instance</param>
|
||||
/// <returns>操作结果 | Operation result</returns>
|
||||
public static ProcessResult StartOrActivate(string exePath, bool singleInstance = true)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exePath))
|
||||
{
|
||||
return ProcessResult.Fail("程序路径未配置 | Program path not configured");
|
||||
}
|
||||
|
||||
if (!File.Exists(exePath))
|
||||
{
|
||||
return ProcessResult.Fail($"程序文件不存在: {exePath} | Program file not found: {exePath}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 获取进程名(不含扩展名)| Get process name (without extension)
|
||||
var processName = Path.GetFileNameWithoutExtension(exePath);
|
||||
|
||||
if (singleInstance)
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
|
||||
if (processes.Length > 0)
|
||||
{
|
||||
// 程序已运行,将窗口置前 | Program already running, bring window to front
|
||||
var process = processes[0];
|
||||
var hWnd = process.MainWindowHandle;
|
||||
|
||||
if (hWnd != IntPtr.Zero)
|
||||
{
|
||||
BringToFront(hWnd);
|
||||
}
|
||||
|
||||
return ProcessResult.Activated();
|
||||
}
|
||||
}
|
||||
|
||||
// 启动程序 | Start program
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
UseShellExecute = true
|
||||
});
|
||||
|
||||
return ProcessResult.Started();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ProcessResult.Fail($"操作失败: {ex.Message} | Operation failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定进程是否正在运行 | Check if a process is running
|
||||
/// </summary>
|
||||
/// <param name="processName">进程名(不含扩展名)| Process name (without extension)</param>
|
||||
/// <returns>是否运行中 | Whether the process is running</returns>
|
||||
public static bool IsProcessRunning(string processName)
|
||||
{
|
||||
return Process.GetProcessesByName(processName).Length > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据可执行文件路径查找已运行的进程 | Find a running process by executable file path
|
||||
/// 通过进程名匹配并验证完整路径,确保找到的是同一个程序 | Matches by process name and verifies full path
|
||||
/// </summary>
|
||||
/// <param name="exePath">可执行文件完整路径 | Full path to the executable</param>
|
||||
/// <returns>找到的进程对象,未找到返回 null | The found process, or null if not found</returns>
|
||||
public static Process FindRunningProcess(string exePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exePath))
|
||||
return null;
|
||||
|
||||
var processName = Path.GetFileNameWithoutExtension(exePath);
|
||||
var normalizedTarget = NormalizePath(exePath);
|
||||
|
||||
try
|
||||
{
|
||||
var processes = Process.GetProcessesByName(processName);
|
||||
foreach (var proc in processes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 验证完整路径匹配,避免同名不同路径的进程误关联
|
||||
var procPath = proc.MainModule?.FileName;
|
||||
if (procPath != null && string.Equals(NormalizePath(procPath), normalizedTarget, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return proc;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 无法访问进程信息(权限不足等),跳过
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// GetProcessesByName 异常,返回 null
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标准化文件路径,用于路径比较 | Normalize file path for comparison
|
||||
/// </summary>
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将窗口置前显示 | Bring a window to the foreground
|
||||
/// </summary>
|
||||
/// <param name="hWnd">窗口句柄 | Window handle</param>
|
||||
public static void BringToFront(IntPtr hWnd)
|
||||
{
|
||||
if (hWnd == IntPtr.Zero) return;
|
||||
|
||||
// 如果窗口被最小化,先恢复 | If window is minimized, restore it first
|
||||
if (IsIconic(hWnd))
|
||||
{
|
||||
ShowWindow(hWnd, SW_RESTORE);
|
||||
}
|
||||
|
||||
SetForegroundWindow(hWnd);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 进程操作结果 | Process Operation Result
|
||||
/// </summary>
|
||||
public class ProcessResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否成功 | Whether the operation succeeded
|
||||
/// </summary>
|
||||
public bool Success { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为新启动(false表示激活已有窗口)| Whether it was newly started (false means activated existing window)
|
||||
/// </summary>
|
||||
public bool IsNewlyStarted { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误信息 | Error message
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建启动成功结果 | Create a started result
|
||||
/// </summary>
|
||||
public static ProcessResult Started() => new() { Success = true, IsNewlyStarted = true };
|
||||
|
||||
/// <summary>
|
||||
/// 创建激活成功结果 | Create an activated result
|
||||
/// </summary>
|
||||
public static ProcessResult Activated() => new() { Success = true, IsNewlyStarted = false };
|
||||
|
||||
/// <summary>
|
||||
/// 创建失败结果 | Create a failed result
|
||||
/// </summary>
|
||||
public static ProcessResult Fail(string errorMessage) => new() { Success = false, ErrorMessage = errorMessage };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using System;
|
||||
using System.Configuration;
|
||||
using System.Globalization;
|
||||
using XP.Common.Localization.Enums;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Localization.Configs
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化配置实现 | Localization configuration implementation
|
||||
/// 使用 App.config 存储语言设置 | Uses App.config to store language settings
|
||||
/// </summary>
|
||||
public class LocalizationConfig : ILocalizationConfig
|
||||
{
|
||||
private const string LanguageKey = "Language";
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public LocalizationConfig(ILoggerService logger)
|
||||
{
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<LocalizationConfig>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取保存的语言设置 | Get saved language setting
|
||||
/// </summary>
|
||||
/// <returns>语言设置,如果未设置则返回 null | Language setting, or null if not set</returns>
|
||||
public SupportedLanguage? GetSavedLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var languageString = ConfigurationManager.AppSettings[LanguageKey];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(languageString))
|
||||
{
|
||||
_logger.Info("No saved language setting found in configuration");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Enum.TryParse<SupportedLanguage>(languageString, true, out var language))
|
||||
{
|
||||
_logger.Info($"Loaded saved language setting: {language}");
|
||||
return language;
|
||||
}
|
||||
|
||||
_logger.Warn($"Invalid language setting in configuration: '{languageString}'. Expected values: ZhCN, ZhTW, EnUS");
|
||||
return null;
|
||||
}
|
||||
catch (ConfigurationErrorsException ex)
|
||||
{
|
||||
_logger.Error(ex, "Configuration error while reading language setting");
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Unexpected error while reading saved language setting");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存语言设置 | Save language setting
|
||||
/// </summary>
|
||||
/// <param name="language">要保存的语言 | Language to save</param>
|
||||
public void SaveLanguage(SupportedLanguage language)
|
||||
{
|
||||
try
|
||||
{
|
||||
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
|
||||
var appSettings = config.AppSettings.Settings;
|
||||
|
||||
var languageValue = language.ToString();
|
||||
|
||||
if (appSettings[LanguageKey] == null)
|
||||
{
|
||||
appSettings.Add(LanguageKey, languageValue);
|
||||
_logger.Info($"Added new language setting to configuration: {language}");
|
||||
}
|
||||
else
|
||||
{
|
||||
appSettings[LanguageKey].Value = languageValue;
|
||||
_logger.Info($"Updated language setting in configuration: {language}");
|
||||
}
|
||||
|
||||
config.Save(ConfigurationSaveMode.Modified);
|
||||
ConfigurationManager.RefreshSection("appSettings");
|
||||
|
||||
_logger.Info($"Successfully saved language setting: {language}");
|
||||
}
|
||||
catch (ConfigurationErrorsException ex)
|
||||
{
|
||||
_logger.Error(ex, $"Configuration error while saving language setting: {language}");
|
||||
throw new InvalidOperationException($"Failed to save language setting to configuration file: {ex.Message}", ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
_logger.Error(ex, $"Access denied while saving language setting: {language}");
|
||||
throw new InvalidOperationException("Access denied to configuration file. Please check file permissions.", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Unexpected error while saving language setting: {language}");
|
||||
throw new InvalidOperationException($"Failed to save language setting: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统默认语言 | Get system default language
|
||||
/// </summary>
|
||||
/// <returns>系统默认语言 | System default language</returns>
|
||||
public SupportedLanguage GetSystemDefaultLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
var systemCulture = CultureInfo.CurrentUICulture.Name;
|
||||
_logger.Info($"Detecting system default language from culture: {systemCulture}");
|
||||
|
||||
var defaultLanguage = systemCulture switch
|
||||
{
|
||||
"zh-CN" => SupportedLanguage.ZhCN,
|
||||
"zh-TW" or "zh-HK" or "zh-MO" => SupportedLanguage.ZhTW,
|
||||
"en-US" or "en" => SupportedLanguage.EnUS,
|
||||
_ when systemCulture.StartsWith("zh", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.ZhCN,
|
||||
_ when systemCulture.StartsWith("en", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.EnUS,
|
||||
_ => SupportedLanguage.ZhCN // 默认简体中文 | Default to Simplified Chinese
|
||||
};
|
||||
|
||||
_logger.Info($"System default language determined: {defaultLanguage} (from culture: {systemCulture})");
|
||||
return defaultLanguage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Error while detecting system default language, falling back to ZhCN");
|
||||
return SupportedLanguage.ZhCN; // 默认简体中文 | Default to Simplified Chinese
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace XP.Common.Localization.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 支持的语言枚举 | Supported language enumeration
|
||||
/// </summary>
|
||||
public enum SupportedLanguage
|
||||
{
|
||||
/// <summary>
|
||||
/// 简体中文 | Simplified Chinese
|
||||
/// </summary>
|
||||
[Description("zh-CN")]
|
||||
ZhCN,
|
||||
|
||||
/// <summary>
|
||||
/// 繁体中文 | Traditional Chinese
|
||||
/// </summary>
|
||||
[Description("zh-TW")]
|
||||
ZhTW,
|
||||
|
||||
/// <summary>
|
||||
/// 美式英语 | American English
|
||||
/// </summary>
|
||||
[Description("en-US")]
|
||||
EnUS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using Prism.Events;
|
||||
|
||||
namespace XP.Common.Localization.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Prism 语言切换事件 | Prism language changed event
|
||||
/// 用于跨模块通知语言切换 | Used for cross-module language change notification
|
||||
/// </summary>
|
||||
public class LanguageChangedEvent : PubSubEvent<LanguageChangedEventArgs>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using XP.Common.Localization.Enums;
|
||||
|
||||
namespace XP.Common.Localization.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言切换事件参数 | Language changed event arguments
|
||||
/// </summary>
|
||||
public class LanguageChangedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 旧语言 | Old language
|
||||
/// </summary>
|
||||
public SupportedLanguage OldLanguage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 新语言 | New language
|
||||
/// </summary>
|
||||
public SupportedLanguage NewLanguage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 切换时间 | Change timestamp
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="oldLanguage">旧语言 | Old language</param>
|
||||
/// <param name="newLanguage">新语言 | New language</param>
|
||||
public LanguageChangedEventArgs(SupportedLanguage oldLanguage, SupportedLanguage newLanguage)
|
||||
{
|
||||
OldLanguage = oldLanguage;
|
||||
NewLanguage = newLanguage;
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Localization.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化异常基类 | Localization exception base class
|
||||
/// </summary>
|
||||
public class LocalizationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息 | Exception message</param>
|
||||
public LocalizationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息 | Exception message</param>
|
||||
/// <param name="innerException">内部异常 | Inner exception</param>
|
||||
public LocalizationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Localization.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化初始化异常 | Localization initialization exception
|
||||
/// 当资源文件无法加载时抛出 | Thrown when resource files cannot be loaded
|
||||
/// </summary>
|
||||
public class LocalizationInitializationException : LocalizationException
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息 | Exception message</param>
|
||||
public LocalizationInitializationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="message">异常消息 | Exception message</param>
|
||||
/// <param name="innerException">内部异常 | Inner exception</param>
|
||||
public LocalizationInitializationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Localization.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源键未找到异常 | Resource key not found exception
|
||||
/// </summary>
|
||||
public class ResourceKeyNotFoundException : LocalizationException
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源键 | Resource key
|
||||
/// </summary>
|
||||
public string ResourceKey { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="resourceKey">资源键 | Resource key</param>
|
||||
public ResourceKeyNotFoundException(string resourceKey)
|
||||
: base($"Resource key not found: {resourceKey}")
|
||||
{
|
||||
ResourceKey = resourceKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using System.Windows.Markup;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
|
||||
namespace XP.Common.Localization.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化标记扩展 | Localization markup extension
|
||||
/// 用法: {loc:Localization Key=ResourceKey} | Usage: {loc:Localization Key=ResourceKey}
|
||||
/// </summary>
|
||||
[MarkupExtensionReturnType(typeof(string))]
|
||||
public class LocalizationExtension : MarkupExtension
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
private string _key = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 资源键 | Resource key
|
||||
/// </summary>
|
||||
[ConstructorArgument("key")]
|
||||
public string Key
|
||||
{
|
||||
get => _key;
|
||||
set => _key = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认构造函数 | Default constructor
|
||||
/// </summary>
|
||||
public LocalizationExtension()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带资源键的构造函数 | Constructor with resource key
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
public LocalizationExtension(string key)
|
||||
{
|
||||
_key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化服务(由 CommonModule 调用)| Initialize localization service (called by CommonModule)
|
||||
/// </summary>
|
||||
/// <param name="localizationService">本地化服务实例 | Localization service instance</param>
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提供本地化字符串值 | Provide localized string value
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供者 | Service provider</param>
|
||||
/// <returns>本地化字符串 | Localized string</returns>
|
||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_key))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (_localizationService == null)
|
||||
{
|
||||
// 设计时回退 | Design-time fallback
|
||||
return $"[{_key}]";
|
||||
}
|
||||
|
||||
// 直接返回当前语言的翻译字符串 | Return translated text for current language
|
||||
return _localizationService.GetString(_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Resources;
|
||||
|
||||
namespace XP.Common.Localization
|
||||
{
|
||||
/// <summary>
|
||||
/// 静态本地化帮助类 | Static localization helper
|
||||
/// 用法:Loc.Get("ResourceKey") 或 Loc.Get("ResourceKey", param1, param2)
|
||||
/// Usage: Loc.Get("ResourceKey") or Loc.Get("ResourceKey", param1, param2)
|
||||
/// </summary>
|
||||
public static class LocalizationHelper
|
||||
{
|
||||
private static ILocalizationService? _localizationService;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化本地化帮助类(由 CommonModule 或 App 启动时调用)
|
||||
/// Initialize the localization helper (called by CommonModule or App at startup)
|
||||
/// </summary>
|
||||
/// <param name="localizationService">本地化服务实例 | Localization service instance</param>
|
||||
public static void Initialize(ILocalizationService localizationService)
|
||||
{
|
||||
_localizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(使用当前 UI 语言)| Get localized string (using current UI culture)
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <returns>本地化字符串,找不到时返回键本身 | Localized string, returns key itself if not found</returns>
|
||||
public static string Get(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
return string.Empty;
|
||||
|
||||
if (_localizationService != null)
|
||||
return _localizationService.GetString(key);
|
||||
|
||||
// 兼容回退:未初始化时仍使用原始 ResourceManager
|
||||
// Fallback: use original ResourceManager when not initialized
|
||||
return Resources.Resources.ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串并格式化 | Get localized string with formatting
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <param name="args">格式化参数 | Format arguments</param>
|
||||
/// <returns>格式化后的本地化字符串 | Formatted localized string</returns>
|
||||
public static string Get(string key, params object[] args)
|
||||
{
|
||||
var template = Get(key);
|
||||
return args.Length > 0 ? string.Format(template, args) : template;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Resources;
|
||||
|
||||
namespace XP.Common.Localization.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源源条目,封装名称与 ResourceManager 的映射
|
||||
/// Resource source entry, encapsulating the mapping between name and ResourceManager
|
||||
/// </summary>
|
||||
internal class ResourceSource
|
||||
{
|
||||
/// <summary>
|
||||
/// 资源源唯一标识 | Resource source unique identifier
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// .NET 资源管理器实例 | .NET ResourceManager instance
|
||||
/// </summary>
|
||||
public ResourceManager ResourceManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="name">资源源名称(如 "XP.Scan")| Resource source name (e.g. "XP.Scan")</param>
|
||||
/// <param name="resourceManager">模块的 ResourceManager 实例 | Module's ResourceManager instance</param>
|
||||
/// <exception cref="ArgumentNullException">当 name 或 resourceManager 为 null 时抛出 | Thrown when name or resourceManager is null</exception>
|
||||
public ResourceSource(string name, ResourceManager resourceManager)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
ResourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Threading;
|
||||
using Prism.Events;
|
||||
using XP.Common.Localization.Enums;
|
||||
using XP.Common.Localization.Events;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Localization.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Resx 本地化服务实现 | Resx localization service implementation
|
||||
/// 使用 .NET ResourceManager 加载 Resx 资源文件 | Uses .NET ResourceManager to load Resx resource files
|
||||
/// 支持多资源源 Fallback Chain 机制 | Supports multi-source Fallback Chain mechanism
|
||||
/// </summary>
|
||||
public class ResxLocalizationService : ILocalizationService
|
||||
{
|
||||
private readonly List<ResourceSource> _resourceSources = new List<ResourceSource>();
|
||||
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ILocalizationConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
private SupportedLanguage _currentLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前语言 | Get current language
|
||||
/// </summary>
|
||||
public SupportedLanguage CurrentLanguage => _currentLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 语言切换事件 | Language changed event
|
||||
/// </summary>
|
||||
public event EventHandler<LanguageChangedEventArgs>? LanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
|
||||
/// <param name="config">本地化配置 | Localization configuration</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public ResxLocalizationService(
|
||||
IEventAggregator eventAggregator,
|
||||
ILocalizationConfig config,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<ResxLocalizationService>();
|
||||
|
||||
// 初始化默认资源源(XP.Common),注册到 Fallback Chain[0]
|
||||
// Initialize default resource source (XP.Common), registered at Fallback Chain[0]
|
||||
var defaultResourceManager = new ResourceManager(
|
||||
"XP.Common.Resources.Resources",
|
||||
Assembly.GetExecutingAssembly());
|
||||
_resourceSources.Add(new ResourceSource("XP.Common", defaultResourceManager));
|
||||
|
||||
// 加载保存的语言或使用默认语言 | Load saved language or use default language
|
||||
InitializeLanguage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <returns>本地化字符串 | Localized string</returns>
|
||||
public string GetString(string key)
|
||||
{
|
||||
return GetString(key, CultureInfo.CurrentUICulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(指定语言)| Get localized string (specified language)
|
||||
/// 从 Fallback Chain 末尾向前遍历,第一个返回非 null 值的 ResourceManager 即为结果
|
||||
/// Traverses the Fallback Chain from end to beginning, first ResourceManager returning non-null wins
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <param name="culture">文化信息 | Culture info</param>
|
||||
/// <returns>本地化字符串 | Localized string</returns>
|
||||
public string GetString(string key, CultureInfo culture)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
_rwLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// 从末尾向前遍历(后注册 = 高优先级)
|
||||
// Traverse from end to beginning (last registered = highest priority)
|
||||
for (int i = _resourceSources.Count - 1; i >= 0; i--)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = _resourceSources[i].ResourceManager.GetString(key, culture);
|
||||
if (value != null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 单个 ResourceManager 抛出异常时捕获并继续遍历下一个
|
||||
// Catch exception from individual ResourceManager and continue to next
|
||||
_logger.Error(ex, $"资源源 '{_resourceSources[i].Name}' 查找键 '{key}' 时出错 | Error looking up key '{key}' in resource source '{_resourceSources[i].Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_rwLock.ExitReadLock();
|
||||
}
|
||||
|
||||
// 全部未找到则返回 key 本身并记录警告日志
|
||||
// If all sources fail to find the key, return the key itself and log warning
|
||||
_logger.Warn($"资源键未找到 | Resource key not found: '{key}' for culture '{culture?.Name ?? "null"}'");
|
||||
return key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
|
||||
/// Register a module resource source to the end of the Fallback Chain (highest priority)
|
||||
/// </summary>
|
||||
/// <param name="name">资源源名称 | Resource source name</param>
|
||||
/// <param name="resourceManager">模块的 ResourceManager 实例 | Module's ResourceManager instance</param>
|
||||
public void RegisterResourceSource(string name, ResourceManager resourceManager)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
if (resourceManager == null) throw new ArgumentNullException(nameof(resourceManager));
|
||||
|
||||
_rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// 检查重复名称 | Check for duplicate name
|
||||
if (_resourceSources.Any(s => s.Name == name))
|
||||
{
|
||||
_logger.Warn($"资源源 '{name}' 已存在,注册被拒绝 | Resource source '{name}' already exists, registration rejected");
|
||||
throw new InvalidOperationException($"Resource source '{name}' is already registered.");
|
||||
}
|
||||
|
||||
_resourceSources.Add(new ResourceSource(name, resourceManager));
|
||||
_logger.Info($"资源源 '{name}' 已注册到 Fallback Chain | Resource source '{name}' registered to Fallback Chain");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Fallback Chain 中注销指定资源源
|
||||
/// Unregister the specified resource source from the Fallback Chain
|
||||
/// </summary>
|
||||
/// <param name="name">资源源名称 | Resource source name</param>
|
||||
public void UnregisterResourceSource(string name)
|
||||
{
|
||||
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||
|
||||
// 禁止注销默认资源源 | Prevent unregistering default resource source
|
||||
if (name == "XP.Common")
|
||||
{
|
||||
throw new InvalidOperationException("Cannot unregister the default resource source 'XP.Common'.");
|
||||
}
|
||||
|
||||
_rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var source = _resourceSources.FirstOrDefault(s => s.Name == name);
|
||||
if (source != null)
|
||||
{
|
||||
_resourceSources.Remove(source);
|
||||
_logger.Info($"资源源 '{name}' 已从 Fallback Chain 注销 | Resource source '{name}' unregistered from Fallback Chain");
|
||||
}
|
||||
else
|
||||
{
|
||||
// 注销不存在的名称时静默忽略,记录警告日志
|
||||
// Silently ignore non-existent name, log warning
|
||||
_logger.Warn($"资源源 '{name}' 不存在,注销操作被忽略 | Resource source '{name}' does not exist, unregister ignored");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前语言(仅保存配置,重启后生效)| Set current language (save config only, takes effect after restart)
|
||||
/// </summary>
|
||||
/// <param name="language">目标语言 | Target language</param>
|
||||
public void SetLanguage(SupportedLanguage language)
|
||||
{
|
||||
_rwLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// 如果语言相同,无需保存 | If language is the same, no need to save
|
||||
if (_currentLanguage == language)
|
||||
{
|
||||
_logger.Info($"语言已经是 {language},无需切换 | Language is already {language}, no need to switch");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Info($"保存语言设置,重启后生效 | Saving language setting, takes effect after restart: {_currentLanguage} -> {language}");
|
||||
|
||||
// 仅保存到配置,不更新运行时状态 | Only save to config, do not update runtime state
|
||||
_config.SaveLanguage(language);
|
||||
|
||||
_logger.Info($"语言设置已保存 | Language setting saved: {language}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"语言设置保存失败 | Failed to save language setting: {language}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_rwLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有支持的语言 | Get all supported languages
|
||||
/// </summary>
|
||||
/// <returns>支持的语言列表 | List of supported languages</returns>
|
||||
public IEnumerable<SupportedLanguage> GetSupportedLanguages()
|
||||
{
|
||||
return Enum.GetValues(typeof(SupportedLanguage))
|
||||
.Cast<SupportedLanguage>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化语言设置 | Initialize language settings
|
||||
/// </summary>
|
||||
private void InitializeLanguage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 尝试获取保存的语言 | Try to get saved language
|
||||
var savedLanguage = _config.GetSavedLanguage();
|
||||
var language = savedLanguage ?? _config.GetSystemDefaultLanguage();
|
||||
|
||||
_currentLanguage = language;
|
||||
var culture = GetCultureInfo(language);
|
||||
CultureInfo.CurrentUICulture = culture;
|
||||
CultureInfo.CurrentCulture = culture;
|
||||
|
||||
_logger.Info($"语言初始化完成 | Language initialized: {language} (Culture: {culture.Name})");
|
||||
|
||||
// 验证资源文件是否可加载 | Verify resource file can be loaded
|
||||
ValidateResourceFile(culture);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "语言初始化失败 | Language initialization failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取语言对应的 CultureInfo | Get CultureInfo for language
|
||||
/// </summary>
|
||||
/// <param name="language">支持的语言 | Supported language</param>
|
||||
/// <returns>文化信息 | Culture info</returns>
|
||||
private CultureInfo GetCultureInfo(SupportedLanguage language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
|
||||
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
|
||||
SupportedLanguage.EnUS => new CultureInfo("en-US"),
|
||||
_ => new CultureInfo("zh-CN") // 默认简体中文 | Default to Simplified Chinese
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证资源文件是否可加载 | Validate resource file can be loaded
|
||||
/// 使用默认资源源(XP.Common)进行验证 | Uses default resource source (XP.Common) for validation
|
||||
/// </summary>
|
||||
/// <param name="culture">文化信息 | Culture info</param>
|
||||
private void ValidateResourceFile(CultureInfo culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 使用第一个资源源(XP.Common)进行验证
|
||||
// Use the first resource source (XP.Common) for validation
|
||||
var testValue = _resourceSources[0].ResourceManager.GetString("App_Title", culture);
|
||||
|
||||
if (testValue != null)
|
||||
{
|
||||
_logger.Info($"资源文件验证成功 | Resource file validated for culture: {culture.Name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warn($"资源文件可能不完整 | Resource file may be incomplete for culture: {culture.Name}");
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.Warn($"资源文件验证失败,将使用默认资源 | Resource file validation failed, will use default resources for culture: {culture.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using XP.Common.Localization.Enums;
|
||||
|
||||
namespace XP.Common.Localization.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化配置接口 | Localization configuration interface
|
||||
/// 负责语言设置的加载和保存 | Responsible for loading and saving language settings
|
||||
/// </summary>
|
||||
public interface ILocalizationConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取保存的语言设置 | Get saved language setting
|
||||
/// </summary>
|
||||
/// <returns>语言设置,如果未设置则返回 null | Language setting, or null if not set</returns>
|
||||
SupportedLanguage? GetSavedLanguage();
|
||||
|
||||
/// <summary>
|
||||
/// 保存语言设置 | Save language setting
|
||||
/// </summary>
|
||||
/// <param name="language">要保存的语言 | Language to save</param>
|
||||
void SaveLanguage(SupportedLanguage language);
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统默认语言 | Get system default language
|
||||
/// </summary>
|
||||
/// <returns>系统默认语言 | System default language</returns>
|
||||
SupportedLanguage GetSystemDefaultLanguage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Resources;
|
||||
using XP.Common.Localization.Enums;
|
||||
using XP.Common.Localization.Events;
|
||||
|
||||
namespace XP.Common.Localization.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 本地化服务接口 | Localization service interface
|
||||
/// 提供多语言资源访问和语言切换功能 | Provides multilingual resource access and language switching
|
||||
/// </summary>
|
||||
public interface ILocalizationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取当前语言 | Get current language
|
||||
/// </summary>
|
||||
SupportedLanguage CurrentLanguage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言切换事件 | Language changed event
|
||||
/// </summary>
|
||||
event EventHandler<LanguageChangedEventArgs> LanguageChanged;
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <returns>本地化字符串 | Localized string</returns>
|
||||
string GetString(string key);
|
||||
|
||||
/// <summary>
|
||||
/// 获取本地化字符串(指定语言)| Get localized string (specified language)
|
||||
/// </summary>
|
||||
/// <param name="key">资源键 | Resource key</param>
|
||||
/// <param name="culture">文化信息 | Culture info</param>
|
||||
/// <returns>本地化字符串 | Localized string</returns>
|
||||
string GetString(string key, CultureInfo culture);
|
||||
|
||||
/// <summary>
|
||||
/// 设置当前语言 | Set current language
|
||||
/// </summary>
|
||||
/// <param name="language">目标语言 | Target language</param>
|
||||
void SetLanguage(SupportedLanguage language);
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有支持的语言 | Get all supported languages
|
||||
/// </summary>
|
||||
/// <returns>支持的语言列表 | List of supported languages</returns>
|
||||
IEnumerable<SupportedLanguage> GetSupportedLanguages();
|
||||
|
||||
/// <summary>
|
||||
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
|
||||
/// Register a module resource source to the end of the Fallback Chain (highest priority)
|
||||
/// </summary>
|
||||
/// <param name="name">资源源名称(如 "XP.Scan"),用于标识和注销 | Resource source name (e.g. "XP.Scan"), used for identification and unregistration</param>
|
||||
/// <param name="resourceManager">模块的 ResourceManager 实例 | The module's ResourceManager instance</param>
|
||||
void RegisterResourceSource(string name, ResourceManager resourceManager);
|
||||
|
||||
/// <summary>
|
||||
/// 从 Fallback Chain 中注销指定资源源
|
||||
/// Unregister the specified resource source from the Fallback Chain
|
||||
/// </summary>
|
||||
/// <param name="name">资源源名称 | Resource source name</param>
|
||||
void UnregisterResourceSource(string name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using XP.Common.Localization.Enums;
|
||||
|
||||
namespace XP.Common.Localization.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言选项数据模型 | Language option data model
|
||||
/// 用于在 UI 中显示可选语言 | Used to display available languages in UI
|
||||
/// </summary>
|
||||
public class LanguageOption
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言枚举值 | Language enum value
|
||||
/// </summary>
|
||||
public SupportedLanguage Language { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 显示名称 | Display name
|
||||
/// </summary>
|
||||
public string DisplayName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 语言标志/图标 | Language flag/icon
|
||||
/// </summary>
|
||||
public string Flag { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="language">语言 | Language</param>
|
||||
/// <param name="displayName">显示名称 | Display name</param>
|
||||
/// <param name="flag">标志 | Flag</param>
|
||||
public LanguageOption(SupportedLanguage language, string displayName, string flag = "")
|
||||
{
|
||||
Language = language;
|
||||
DisplayName = displayName;
|
||||
Flag = flag;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Localization.Enums;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
|
||||
namespace XP.Common.Localization.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 语言切换器 ViewModel | Language switcher ViewModel
|
||||
/// 提供语言选择和切换功能 | Provides language selection and switching functionality
|
||||
/// </summary>
|
||||
public class LanguageSwitcherViewModel : BindableBase
|
||||
{
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly ILocalizationConfig _config;
|
||||
private readonly ILoggerService _logger;
|
||||
private LanguageOption? _selectedLanguage;
|
||||
|
||||
/// <summary>
|
||||
/// 选中的语言选项 | Selected language option
|
||||
/// </summary>
|
||||
public LanguageOption? SelectedLanguage
|
||||
{
|
||||
get => _selectedLanguage;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedLanguage, value))
|
||||
{
|
||||
ApplyCommand.RaiseCanExecuteChanged();
|
||||
UpdatePreviewTexts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可用语言列表 | Available languages list
|
||||
/// </summary>
|
||||
public IEnumerable<LanguageOption> AvailableLanguages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 应用按钮命令 | Apply button command
|
||||
/// </summary>
|
||||
public DelegateCommand ApplyCommand { get; }
|
||||
|
||||
private string _previewRestartNotice = string.Empty;
|
||||
/// <summary>
|
||||
/// 预览重启提示文本 | Preview restart notice text
|
||||
/// </summary>
|
||||
public string PreviewRestartNotice
|
||||
{
|
||||
get => _previewRestartNotice;
|
||||
set => SetProperty(ref _previewRestartNotice, value);
|
||||
}
|
||||
|
||||
private string _previewApplyButtonText = string.Empty;
|
||||
/// <summary>
|
||||
/// 预览应用按钮文本 | Preview apply button text
|
||||
/// </summary>
|
||||
public string PreviewApplyButtonText
|
||||
{
|
||||
get => _previewApplyButtonText;
|
||||
set => SetProperty(ref _previewApplyButtonText, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="localizationService">本地化服务 | Localization service</param>
|
||||
/// <param name="config">本地化配置 | Localization configuration</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public LanguageSwitcherViewModel(
|
||||
ILocalizationService localizationService,
|
||||
ILocalizationConfig config,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
_logger = logger.ForModule<LanguageSwitcherViewModel>();
|
||||
|
||||
// 初始化可用语言列表 | Initialize available languages list
|
||||
AvailableLanguages = new[]
|
||||
{
|
||||
new LanguageOption(SupportedLanguage.ZhCN, "简体中文", "🇨🇳"),
|
||||
new LanguageOption(SupportedLanguage.ZhTW, "繁體中文", "🇹🇼"),
|
||||
new LanguageOption(SupportedLanguage.EnUS, "English", "🇺🇸")
|
||||
};
|
||||
|
||||
// 设置当前选中的语言 | Set currently selected language
|
||||
var currentLang = localizationService.CurrentLanguage;
|
||||
foreach (var lang in AvailableLanguages)
|
||||
{
|
||||
if (lang.Language == currentLang)
|
||||
{
|
||||
_selectedLanguage = lang;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化命令 | Initialize commands
|
||||
ApplyCommand = new DelegateCommand(OnApply, CanApply);
|
||||
|
||||
// 初始化预览文本 | Initialize preview texts
|
||||
UpdatePreviewTexts();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新预览文本 | Update preview texts
|
||||
/// </summary>
|
||||
private void UpdatePreviewTexts()
|
||||
{
|
||||
if (_selectedLanguage == null)
|
||||
return;
|
||||
|
||||
// 获取选中语言的 CultureInfo | Get CultureInfo for selected language
|
||||
var previewCulture = GetCultureInfo(_selectedLanguage.Language);
|
||||
|
||||
// 使用选中的语言获取本地化文本 | Get localized text using selected language
|
||||
PreviewRestartNotice = _localizationService.GetString("Settings_Language_RestartNotice", previewCulture);
|
||||
PreviewApplyButtonText = _localizationService.GetString("Button_Apply", previewCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以应用 | Determine if can apply
|
||||
/// 与配置文件中已保存的语言比较 | Compare with saved language in config
|
||||
/// </summary>
|
||||
private bool CanApply()
|
||||
{
|
||||
if (_selectedLanguage == null)
|
||||
return false;
|
||||
|
||||
// 获取配置文件中已保存的语言 | Get saved language from config
|
||||
var savedLanguage = _config.GetSavedLanguage() ?? _localizationService.CurrentLanguage;
|
||||
return _selectedLanguage.Language != savedLanguage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用语言切换 | Apply language change
|
||||
/// </summary>
|
||||
private void OnApply()
|
||||
{
|
||||
if (_selectedLanguage == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var newLanguage = _selectedLanguage.Language;
|
||||
_localizationService.SetLanguage(newLanguage);
|
||||
|
||||
_logger.Info($"Language switched to {newLanguage}");
|
||||
|
||||
// 显示重启提示对话框 | Show restart prompt dialog
|
||||
MessageBox.Show(
|
||||
_localizationService.GetString("Settings_Language_SavedRestartRequired"),
|
||||
_localizationService.GetString("Dialog_Notice"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Information);
|
||||
|
||||
// 更新命令状态 | Update command state
|
||||
ApplyCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, $"Failed to switch language to {_selectedLanguage.Language}");
|
||||
|
||||
// 显示错误对话框 | Show error dialog
|
||||
var errorMsg = string.Format(
|
||||
_localizationService.GetString("Settings_Language_SwitchFailed"),
|
||||
ex.Message);
|
||||
MessageBox.Show(
|
||||
errorMsg,
|
||||
_localizationService.GetString("Dialog_Error"),
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
|
||||
// 恢复到之前的语言 | Restore previous language
|
||||
var currentLang = _localizationService.CurrentLanguage;
|
||||
foreach (var lang in AvailableLanguages)
|
||||
{
|
||||
if (lang.Language == currentLang)
|
||||
{
|
||||
_selectedLanguage = lang;
|
||||
RaisePropertyChanged(nameof(SelectedLanguage));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取语言对应的 CultureInfo | Get CultureInfo for language
|
||||
/// </summary>
|
||||
private CultureInfo GetCultureInfo(SupportedLanguage language)
|
||||
{
|
||||
return language switch
|
||||
{
|
||||
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
|
||||
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
|
||||
SupportedLanguage.EnUS => new CultureInfo("en-US"),
|
||||
_ => new CultureInfo("zh-CN")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<Window x:Class="XP.Common.Localization.Views.LanguageSwitcherWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||
xmlns:vm="clr-namespace:XP.Common.Localization.ViewModels"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance Type=vm:LanguageSwitcherViewModel}"
|
||||
Title="{loc:Localization Key=Settings_Language}"
|
||||
Height="315" Width="500"
|
||||
MinHeight="300" MinWidth="450"
|
||||
ResizeMode="CanResize"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="White">
|
||||
|
||||
<!-- 主容器,带边距 | Main container with margins -->
|
||||
<Grid Margin="24,16,24,16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 标题 | Title -->
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{loc:Localization Key=Settings_Language}"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"/>
|
||||
|
||||
<!-- 副标题/描述 | Subtitle/Description -->
|
||||
<TextBlock Grid.Row="1"
|
||||
Text="{loc:Localization Key=Settings_Language_Description}"
|
||||
FontSize="12"
|
||||
Foreground="#757575"
|
||||
Margin="0,0,0,20"/>
|
||||
|
||||
<!-- 直接显示下拉选择框(无卡片)| Direct ComboBox display (no card) -->
|
||||
<telerik:RadComboBox Grid.Row="2"
|
||||
ItemsSource="{Binding AvailableLanguages}"
|
||||
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Height="36"
|
||||
Margin="0,0,0,16"
|
||||
telerik:StyleManager.Theme="Crystal">
|
||||
<telerik:RadComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="4,2">
|
||||
<!-- 语言标志 | Language flag -->
|
||||
<TextBlock Text="{Binding Flag}"
|
||||
Margin="0,0,8,0"
|
||||
FontSize="16"
|
||||
VerticalAlignment="Center"/>
|
||||
<!-- 语言名称 | Language name -->
|
||||
<TextBlock Text="{Binding DisplayName}"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="13"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</telerik:RadComboBox.ItemTemplate>
|
||||
</telerik:RadComboBox>
|
||||
|
||||
<!-- 提示信息区域 | Notice information area -->
|
||||
<Border Grid.Row="3"
|
||||
Background="#E3F2FD"
|
||||
BorderBrush="#2196F3"
|
||||
BorderThickness="1"
|
||||
CornerRadius="4"
|
||||
Padding="12"
|
||||
Margin="0,0,0,16">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 信息图标 | Information icon -->
|
||||
<Path Grid.Column="0"
|
||||
Data="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13V17M13,9H11V7H13V9Z"
|
||||
Fill="#2196F3"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Stretch="Uniform"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,2,12,0"/>
|
||||
|
||||
<!-- 提示文本 | Notice text -->
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding PreviewRestartNotice}"
|
||||
FontSize="12"
|
||||
FontWeight="Medium"
|
||||
Foreground="#1976D2"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 占位空间 | Spacer -->
|
||||
<Grid Grid.Row="4"/>
|
||||
|
||||
<!-- 底部操作区 | Bottom action area -->
|
||||
<Grid Grid.Row="5">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 应用按钮(绿色)| Apply button (green) -->
|
||||
<telerik:RadButton Grid.Column="1"
|
||||
Content="{Binding PreviewApplyButtonText}"
|
||||
Command="{Binding ApplyCommand}"
|
||||
Width="100"
|
||||
Height="36"
|
||||
telerik:StyleManager.Theme="Crystal">
|
||||
<telerik:RadButton.Background>
|
||||
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
|
||||
<GradientStop Color="White"/>
|
||||
<GradientStop Color="#FF77E062" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</telerik:RadButton.Background>
|
||||
</telerik:RadButton>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Windows;
|
||||
using XP.Common.Localization.ViewModels;
|
||||
|
||||
namespace XP.Common.Localization.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// LanguageSwitcherWindow.xaml 的交互逻辑 | Interaction logic for LanguageSwitcherWindow.xaml
|
||||
/// </summary>
|
||||
public partial class LanguageSwitcherWindow : Window
|
||||
{
|
||||
public LanguageSwitcherWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数(带 ViewModel)| Constructor (with ViewModel)
|
||||
/// </summary>
|
||||
/// <param name="viewModel">语言切换器 ViewModel | Language switcher ViewModel</param>
|
||||
public LanguageSwitcherWindow(LanguageSwitcherViewModel viewModel) : this()
|
||||
{
|
||||
DataContext = viewModel;
|
||||
|
||||
if (Application.Current?.MainWindow != null)
|
||||
Icon = Application.Current.MainWindow.Icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using System;
|
||||
using Serilog;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
|
||||
namespace XP.Common.Logging.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog日志服务实现(适配ILoggerService接口)| Serilog logger service implementation (adapts ILoggerService interface)
|
||||
/// </summary>
|
||||
public class SerilogLoggerService : ILoggerService
|
||||
{
|
||||
private ILogger _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数:初始化全局日志实例 | Constructor: initialize global logger instance
|
||||
/// </summary>
|
||||
public SerilogLoggerService()
|
||||
{
|
||||
_logger = Log.Logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 私有构造函数:用于模块标记 | Private constructor: for module tagging
|
||||
/// </summary>
|
||||
private SerilogLoggerService(ILogger logger)
|
||||
{
|
||||
_logger = logger ?? Log.Logger;
|
||||
}
|
||||
|
||||
public void Debug(string message, params object[] args)
|
||||
{
|
||||
_logger.Debug(message, args);
|
||||
}
|
||||
|
||||
public void Info(string message, params object[] args)
|
||||
{
|
||||
_logger.Information(message, args);
|
||||
}
|
||||
|
||||
public void Warn(string message, params object[] args)
|
||||
{
|
||||
_logger.Warning(message, args);
|
||||
}
|
||||
|
||||
public void Error(Exception ex, string message, params object[] args)
|
||||
{
|
||||
_logger.Error(ex, message, args);
|
||||
}
|
||||
|
||||
public void Fatal(Exception ex, string message, params object[] args)
|
||||
{
|
||||
_logger.Fatal(ex, message, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动指定模块名 | Manually specify module name
|
||||
/// </summary>
|
||||
public ILoggerService ForModule(string moduleName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(moduleName)) return this;
|
||||
return new SerilogLoggerService(_logger.ForContext("SourceContext", moduleName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动使用类型全名作为模块名 | Automatically use type full name as module name
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型参数 | Type parameter</typeparam>
|
||||
public ILoggerService ForModule<T>()
|
||||
{
|
||||
var typeName = typeof(T).FullName ?? typeof(T).Name;
|
||||
return new SerilogLoggerService(_logger.ForContext("SourceContext", typeName));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.Logging.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用日志服务接口(与具体日志框架解耦)| Generic logger service interface (decoupled from specific logging framework)
|
||||
/// </summary>
|
||||
public interface ILoggerService
|
||||
{
|
||||
/// <summary>
|
||||
/// 调试日志 | Debug log
|
||||
/// </summary>
|
||||
void Debug(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 信息日志 | Information log
|
||||
/// </summary>
|
||||
void Info(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 警告日志 | Warning log
|
||||
/// </summary>
|
||||
void Warn(string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 错误日志(带异常)| Error log (with exception)
|
||||
/// </summary>
|
||||
void Error(Exception ex, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 致命错误日志(带异常)| Fatal error log (with exception)
|
||||
/// </summary>
|
||||
void Fatal(Exception ex, string message, params object[] args);
|
||||
|
||||
/// <summary>
|
||||
/// 标记日志所属模块(手动指定模块名)| Mark logger module (manually specify module name)
|
||||
/// </summary>
|
||||
/// <param name="moduleName">模块名称 | Module name</param>
|
||||
ILoggerService ForModule(string moduleName);
|
||||
|
||||
/// <summary>
|
||||
/// 标记日志所属模块(自动使用类型全名)| Mark logger module (automatically use type full name)
|
||||
/// </summary>
|
||||
/// <typeparam name="T">类型参数(自动推断命名空间+类名)| Type parameter (automatically infer namespace + class name)</typeparam>
|
||||
ILoggerService ForModule<T>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using Serilog;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Configs;
|
||||
using XP.Common.Logging.ViewModels;
|
||||
|
||||
namespace XP.Common.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog全局初始化工具(应用启动时调用一次)
|
||||
/// </summary>
|
||||
public static class SerilogInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化Serilog日志
|
||||
/// </summary>
|
||||
public static void Initialize(SerilogConfig config)
|
||||
{
|
||||
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||
|
||||
// 确保日志目录存在
|
||||
if (!System.IO.Directory.Exists(config.LogPath))
|
||||
System.IO.Directory.CreateDirectory(config.LogPath);
|
||||
|
||||
// 解析日志级别
|
||||
if (!Enum.TryParse<LogEventLevel>(config.MinimumLevel, true, out var minLevel))
|
||||
minLevel = LogEventLevel.Information;
|
||||
|
||||
// 解析文件分割规则
|
||||
if (!Enum.TryParse<RollingInterval>(config.RollingInterval, true, out var rollingInterval))
|
||||
rollingInterval = RollingInterval.Day;
|
||||
|
||||
// 构建Serilog配置
|
||||
var loggerConfig = new LoggerConfiguration()
|
||||
.MinimumLevel.Is(minLevel)
|
||||
.Enrich.FromLogContext() // 启用上下文(模块标记)
|
||||
.WriteTo.File(
|
||||
path: System.IO.Path.Combine(config.LogPath, "app_.log"), // 最终文件名:app_20260307.log
|
||||
rollingInterval: rollingInterval,
|
||||
fileSizeLimitBytes: config.FileSizeLimitMB * 1024 * 1024,
|
||||
retainedFileCountLimit: config.RetainedFileCountLimit,
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
|
||||
);
|
||||
|
||||
// 调试环境输出到控制台
|
||||
if (config.EnableConsole)
|
||||
loggerConfig.WriteTo.Console(
|
||||
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
|
||||
|
||||
// 注册实时日志查看器 Sink(接收所有级别日志)| Register real-time log viewer sink (receive all levels)
|
||||
loggerConfig.WriteTo.Sink(RealTimeLogSink.Instance, Serilog.Events.LogEventLevel.Verbose);
|
||||
|
||||
// 全局初始化
|
||||
Log.Logger = loggerConfig.CreateLogger();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
|
||||
namespace XP.Common.Logging.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// Serilog 自定义 Sink,将日志事件转发到 RealTimeLogViewerViewModel,并维护环形缓冲区
|
||||
/// Custom Serilog Sink that forwards log events and maintains a ring buffer for history
|
||||
/// </summary>
|
||||
public class RealTimeLogSink : ILogEventSink
|
||||
{
|
||||
private const int BufferCapacity = 500;
|
||||
private readonly ConcurrentQueue<LogEvent> _buffer = new();
|
||||
|
||||
/// <summary>
|
||||
/// 全局单例实例 | Global singleton instance
|
||||
/// </summary>
|
||||
public static RealTimeLogSink Instance { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// 日志事件到达时触发 | Fired when a log event arrives
|
||||
/// </summary>
|
||||
public event Action<LogEvent>? LogEventReceived;
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
// 入队缓冲区 | Enqueue to buffer
|
||||
_buffer.Enqueue(logEvent);
|
||||
while (_buffer.Count > BufferCapacity)
|
||||
{
|
||||
_buffer.TryDequeue(out _);
|
||||
}
|
||||
|
||||
LogEventReceived?.Invoke(logEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取缓冲区中的历史日志快照 | Get a snapshot of buffered history logs
|
||||
/// </summary>
|
||||
public List<LogEvent> GetBufferedHistory()
|
||||
{
|
||||
return _buffer.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Localization;
|
||||
|
||||
namespace XP.Common.Logging.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑
|
||||
/// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc.
|
||||
/// </summary>
|
||||
public class RealTimeLogViewerViewModel : BindableBase
|
||||
{
|
||||
private readonly Dispatcher _dispatcher;
|
||||
private readonly ObservableCollection<LogDisplayEntry> _allEntries = new();
|
||||
private string _filterText = string.Empty;
|
||||
private bool _isAutoScroll = true;
|
||||
private int _maxLines = 2000;
|
||||
private int _totalCount;
|
||||
private int _filteredCount;
|
||||
private bool _showDebug = true;
|
||||
private bool _showInfo = true;
|
||||
private bool _showWarning = true;
|
||||
private bool _showError = true;
|
||||
private bool _showFatal = true;
|
||||
|
||||
public RealTimeLogViewerViewModel()
|
||||
{
|
||||
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||
|
||||
FilteredEntries = new ObservableCollection<LogDisplayEntry>();
|
||||
ClearCommand = new DelegateCommand(ClearAll);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤后的日志条目集合(绑定到 UI)| Filtered log entries (bound to UI)
|
||||
/// </summary>
|
||||
public ObservableCollection<LogDisplayEntry> FilteredEntries { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 过滤关键词 | Filter keyword
|
||||
/// </summary>
|
||||
public string FilterText
|
||||
{
|
||||
get => _filterText;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _filterText, value))
|
||||
{
|
||||
ApplyFilter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否自动滚动到底部 | Whether to auto-scroll to bottom
|
||||
/// </summary>
|
||||
public bool IsAutoScroll
|
||||
{
|
||||
get => _isAutoScroll;
|
||||
set => SetProperty(ref _isAutoScroll, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 最大行数限制 | Maximum line count limit
|
||||
/// </summary>
|
||||
public int MaxLines
|
||||
{
|
||||
get => _maxLines;
|
||||
set => SetProperty(ref _maxLines, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 总日志条数 | Total log entry count
|
||||
/// </summary>
|
||||
public int TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
private set => SetProperty(ref _totalCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 过滤后的日志条数 | Filtered log entry count
|
||||
/// </summary>
|
||||
public int FilteredCount
|
||||
{
|
||||
get => _filteredCount;
|
||||
private set => SetProperty(ref _filteredCount, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Debug 级别日志 | Whether to show Debug level logs
|
||||
/// </summary>
|
||||
public bool ShowDebug
|
||||
{
|
||||
get => _showDebug;
|
||||
set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Info 级别日志 | Whether to show Information level logs
|
||||
/// </summary>
|
||||
public bool ShowInfo
|
||||
{
|
||||
get => _showInfo;
|
||||
set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Warning 级别日志 | Whether to show Warning level logs
|
||||
/// </summary>
|
||||
public bool ShowWarning
|
||||
{
|
||||
get => _showWarning;
|
||||
set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Error 级别日志 | Whether to show Error level logs
|
||||
/// </summary>
|
||||
public bool ShowError
|
||||
{
|
||||
get => _showError;
|
||||
set { if (SetProperty(ref _showError, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs
|
||||
/// </summary>
|
||||
public bool ShowFatal
|
||||
{
|
||||
get => _showFatal;
|
||||
set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态栏文本 | Status bar text
|
||||
/// </summary>
|
||||
public string StatusText
|
||||
{
|
||||
get
|
||||
{
|
||||
bool hasLevelFilter = !(_showDebug && _showInfo && _showWarning && _showError && _showFatal);
|
||||
bool hasTextFilter = !string.IsNullOrEmpty(_filterText);
|
||||
if (hasLevelFilter || hasTextFilter)
|
||||
return LocalizationHelper.Get("LogViewer_StatusFiltered", TotalCount, FilteredCount);
|
||||
return LocalizationHelper.Get("LogViewer_StatusTotal", TotalCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空日志命令 | Clear log command
|
||||
/// </summary>
|
||||
public DelegateCommand ClearCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event
|
||||
/// </summary>
|
||||
public event Action? ScrollToBottomRequested;
|
||||
|
||||
/// <summary>
|
||||
/// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe)
|
||||
/// </summary>
|
||||
public void AddLogEvent(LogEvent logEvent)
|
||||
{
|
||||
if (logEvent == null) return;
|
||||
|
||||
var entry = new LogDisplayEntry(logEvent);
|
||||
|
||||
if (_dispatcher.CheckAccess())
|
||||
{
|
||||
AddEntryInternal(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dispatcher.InvokeAsync(() => AddEntryInternal(entry));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 内部添加日志条目 | Internal add log entry
|
||||
/// </summary>
|
||||
private void AddEntryInternal(LogDisplayEntry entry)
|
||||
{
|
||||
_allEntries.Add(entry);
|
||||
|
||||
// 超出最大行数时移除最旧的条目 | Remove oldest entries when exceeding max lines
|
||||
while (_allEntries.Count > _maxLines)
|
||||
{
|
||||
var removed = _allEntries[0];
|
||||
_allEntries.RemoveAt(0);
|
||||
FilteredEntries.Remove(removed);
|
||||
}
|
||||
|
||||
// 判断是否匹配过滤条件 | Check if entry matches filter
|
||||
if (MatchesFilter(entry))
|
||||
{
|
||||
FilteredEntries.Add(entry);
|
||||
}
|
||||
|
||||
TotalCount = _allEntries.Count;
|
||||
FilteredCount = FilteredEntries.Count;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
|
||||
// 请求自动滚动 | Request auto-scroll
|
||||
if (_isAutoScroll)
|
||||
{
|
||||
ScrollToBottomRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用过滤器 | Apply filter
|
||||
/// </summary>
|
||||
private void ApplyFilter()
|
||||
{
|
||||
FilteredEntries.Clear();
|
||||
|
||||
foreach (var entry in _allEntries)
|
||||
{
|
||||
if (MatchesFilter(entry))
|
||||
{
|
||||
FilteredEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
FilteredCount = FilteredEntries.Count;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断条目是否匹配过滤条件 | Check if entry matches filter
|
||||
/// </summary>
|
||||
private bool MatchesFilter(LogDisplayEntry entry)
|
||||
{
|
||||
// 级别过滤 | Level filter
|
||||
var levelMatch = entry.LevelFull switch
|
||||
{
|
||||
"Debug" => _showDebug,
|
||||
"Information" => _showInfo,
|
||||
"Warning" => _showWarning,
|
||||
"Error" => _showError,
|
||||
"Fatal" => _showFatal,
|
||||
_ => true
|
||||
};
|
||||
if (!levelMatch) return false;
|
||||
|
||||
// 关键词过滤 | Keyword filter
|
||||
if (string.IsNullOrWhiteSpace(_filterText)) return true;
|
||||
return entry.Message.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||
|| entry.Level.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||
|| entry.Source.Contains(_filterText, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有日志 | Clear all logs
|
||||
/// </summary>
|
||||
private void ClearAll()
|
||||
{
|
||||
_allEntries.Clear();
|
||||
FilteredEntries.Clear();
|
||||
TotalCount = 0;
|
||||
FilteredCount = 0;
|
||||
RaisePropertyChanged(nameof(StatusText));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 日志显示条目模型 | Log display entry model
|
||||
/// </summary>
|
||||
public class LogDisplayEntry
|
||||
{
|
||||
public LogDisplayEntry(LogEvent logEvent)
|
||||
{
|
||||
Timestamp = logEvent.Timestamp.LocalDateTime;
|
||||
TimestampDisplay = Timestamp.ToString("HH:mm:ss.fff");
|
||||
Level = logEvent.Level.ToString().Substring(0, 3).ToUpper();
|
||||
LevelFull = logEvent.Level.ToString();
|
||||
|
||||
// 渲染消息文本 | Render message text
|
||||
Message = logEvent.RenderMessage();
|
||||
|
||||
// 如果有异常,追加异常信息 | Append exception info if present
|
||||
if (logEvent.Exception != null)
|
||||
{
|
||||
Message += Environment.NewLine + logEvent.Exception.ToString();
|
||||
}
|
||||
|
||||
// 提取 SourceContext | Extract SourceContext
|
||||
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceValue))
|
||||
{
|
||||
Source = sourceValue.ToString().Trim('"');
|
||||
}
|
||||
else
|
||||
{
|
||||
Source = string.Empty;
|
||||
}
|
||||
|
||||
// 根据日志级别设置颜色 | Set color based on log level
|
||||
LevelColor = logEvent.Level switch
|
||||
{
|
||||
LogEventLevel.Fatal => new SolidColorBrush(Color.FromRgb(180, 0, 0)),
|
||||
LogEventLevel.Error => new SolidColorBrush(Colors.Red),
|
||||
LogEventLevel.Warning => new SolidColorBrush(Color.FromRgb(200, 130, 0)),
|
||||
LogEventLevel.Debug => new SolidColorBrush(Colors.Gray),
|
||||
LogEventLevel.Verbose => new SolidColorBrush(Colors.LightGray),
|
||||
_ => new SolidColorBrush(Color.FromRgb(30, 30, 30))
|
||||
};
|
||||
|
||||
// Freeze 画刷,使其可跨线程访问(避免 DependencySource 线程异常)
|
||||
// Freeze the brush so it can be accessed from any thread
|
||||
LevelColor.Freeze();
|
||||
}
|
||||
|
||||
public DateTime Timestamp { get; }
|
||||
public string TimestampDisplay { get; }
|
||||
public string Level { get; }
|
||||
public string LevelFull { get; }
|
||||
public string Message { get; }
|
||||
public string Source { get; }
|
||||
public SolidColorBrush LevelColor { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<Window x:Class="XP.Common.GeneralForm.Views.RealTimeLogViewer"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||
Title="{loc:Localization LogViewer_Title}"
|
||||
Width="960" Height="600"
|
||||
MinWidth="700" MinHeight="400"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="True"
|
||||
WindowStyle="SingleBorderWindow">
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 顶部工具栏 | Top toolbar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 级别筛选栏 | Level filter bar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- 日志显示区 | Log display area -->
|
||||
<RowDefinition Height="*"/>
|
||||
<!-- 底部状态栏 | Bottom status bar -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- === 顶部工具栏 | Top Toolbar === -->
|
||||
<DockPanel Grid.Row="0" Margin="8,6" LastChildFill="True">
|
||||
<!-- 左侧:自动滚动开关 + 清空按钮 | Left: Auto-scroll toggle + Clear button -->
|
||||
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<telerik:RadToggleButton x:Name="AutoScrollToggle"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
IsChecked="{Binding IsAutoScroll, Mode=TwoWay}"
|
||||
Content="{loc:Localization LogViewer_AutoScroll}"
|
||||
Padding="10,4" Margin="0,0,6,0"/>
|
||||
<telerik:RadButton telerik:StyleManager.Theme="Crystal"
|
||||
Command="{Binding ClearCommand}"
|
||||
Content="{loc:Localization LogViewer_ClearLog}"
|
||||
Padding="10,4" Margin="0,0,12,0"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧:过滤输入框 | Right: Filter input -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock Text="{loc:Localization LogViewer_Filter}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||
<telerik:RadWatermarkTextBox telerik:StyleManager.Theme="Crystal"
|
||||
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, Delay=300}"
|
||||
WatermarkContent="{loc:Localization LogViewer_FilterWatermark}"
|
||||
Width="260" Padding="4,3"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
<!-- === 级别筛选栏 | Level Filter Bar === -->
|
||||
<Border Grid.Row="1" Background="#FFF8F8F8" BorderBrush="#FFDDDDDD" BorderThickness="0,0,0,1" Padding="8,4">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="{loc:Localization LogViewer_LevelFilter}" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="#FF666666" FontSize="12"/>
|
||||
<CheckBox Content="Debug" IsChecked="{Binding ShowDebug}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Gray"/>
|
||||
<CheckBox Content="Info" IsChecked="{Binding ShowInfo}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FF1E1E1E"/>
|
||||
<CheckBox Content="Warning" IsChecked="{Binding ShowWarning}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFC88200"/>
|
||||
<CheckBox Content="Error" IsChecked="{Binding ShowError}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Red"/>
|
||||
<CheckBox Content="Fatal" IsChecked="{Binding ShowFatal}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFB40000"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- === 日志显示区(RadGridView)| Log Display Area === -->
|
||||
<telerik:RadGridView Grid.Row="2"
|
||||
x:Name="LogGridView"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
ItemsSource="{Binding FilteredEntries}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
RowIndicatorVisibility="Collapsed"
|
||||
ShowGroupPanel="False"
|
||||
ShowColumnHeaders="True"
|
||||
CanUserFreezeColumns="False"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserSortColumns="False"
|
||||
CanUserResizeColumns="True"
|
||||
IsFilteringAllowed="False"
|
||||
SelectionMode="Extended"
|
||||
FontFamily="Consolas"
|
||||
FontSize="12"
|
||||
Margin="4,0,4,0">
|
||||
<telerik:RadGridView.Columns>
|
||||
<!-- 时间戳列 | Timestamp column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColTime}"
|
||||
DataMemberBinding="{Binding TimestampDisplay}"
|
||||
Width="100"
|
||||
IsReadOnly="True"/>
|
||||
|
||||
<!-- 级别列 | Level column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColLevel}"
|
||||
DataMemberBinding="{Binding Level}"
|
||||
Width="60"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
|
||||
<!-- 来源列 | Source column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColSource}"
|
||||
DataMemberBinding="{Binding Source}"
|
||||
Width="200"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
|
||||
<!-- 消息列 | Message column -->
|
||||
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColMessage}"
|
||||
DataMemberBinding="{Binding Message}"
|
||||
Width="*"
|
||||
IsReadOnly="True">
|
||||
<telerik:GridViewDataColumn.CellStyle>
|
||||
<Style TargetType="telerik:GridViewCell">
|
||||
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||
</Style>
|
||||
</telerik:GridViewDataColumn.CellStyle>
|
||||
</telerik:GridViewDataColumn>
|
||||
</telerik:RadGridView.Columns>
|
||||
</telerik:RadGridView>
|
||||
|
||||
<!-- === 底部状态栏 | Bottom Status Bar === -->
|
||||
<StatusBar Grid.Row="3" Background="#FFF0F0F0" BorderBrush="#FFCCCCCC" BorderThickness="0,1,0,0">
|
||||
<StatusBarItem>
|
||||
<TextBlock Text="{Binding StatusText}" Foreground="#FF666666" FontSize="12"/>
|
||||
</StatusBarItem>
|
||||
<Separator/>
|
||||
<StatusBarItem HorizontalAlignment="Right">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{loc:Localization LogViewer_MaxLines}" Foreground="#FF999999" FontSize="11" VerticalAlignment="Center"/>
|
||||
<telerik:RadNumericUpDown telerik:StyleManager.Theme="Crystal"
|
||||
Value="{Binding MaxLines, Mode=TwoWay}"
|
||||
Minimum="100" Maximum="10000"
|
||||
NumberDecimalDigits="0"
|
||||
SmallChange="500"
|
||||
Width="90" Height="22"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
</StatusBarItem>
|
||||
</StatusBar>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using Serilog.Events;
|
||||
using XP.Common.Logging.ViewModels;
|
||||
|
||||
namespace XP.Common.GeneralForm.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// 实时日志查看器窗口,订阅 Serilog 事件并实时显示
|
||||
/// Real-time log viewer window, subscribes to Serilog events and displays in real-time
|
||||
/// </summary>
|
||||
public partial class RealTimeLogViewer : Window
|
||||
{
|
||||
private readonly RealTimeLogViewerViewModel _viewModel;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="maxLines">最大行数限制,默认 2000 | Max line count, default 2000</param>
|
||||
public RealTimeLogViewer(int maxLines = 2000)
|
||||
{
|
||||
_viewModel = new RealTimeLogViewerViewModel { MaxLines = maxLines };
|
||||
DataContext = _viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// 继承主窗口图标 | Inherit main window icon
|
||||
if (Application.Current?.MainWindow != null)
|
||||
{
|
||||
Icon = Application.Current.MainWindow.Icon;
|
||||
}
|
||||
|
||||
// 订阅自动滚动事件 | Subscribe to auto-scroll event
|
||||
_viewModel.ScrollToBottomRequested += OnScrollToBottomRequested;
|
||||
|
||||
// 加载缓冲区中的历史日志 | Load buffered history logs
|
||||
var history = RealTimeLogSink.Instance.GetBufferedHistory();
|
||||
foreach (var logEvent in history)
|
||||
{
|
||||
_viewModel.AddLogEvent(logEvent);
|
||||
}
|
||||
|
||||
// 订阅 Serilog Sink 事件(在历史加载之后,避免重复)| Subscribe after history load
|
||||
RealTimeLogSink.Instance.LogEventReceived += OnLogEventReceived;
|
||||
|
||||
Closed += OnWindowClosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 接收 Serilog 日志事件 | Receive Serilog log event
|
||||
/// </summary>
|
||||
private void OnLogEventReceived(LogEvent logEvent)
|
||||
{
|
||||
_viewModel.AddLogEvent(logEvent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动滚动到底部 | Auto-scroll to bottom
|
||||
/// </summary>
|
||||
private void OnScrollToBottomRequested()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (LogGridView.Items.Count > 0)
|
||||
{
|
||||
var lastItem = LogGridView.Items[LogGridView.Items.Count - 1];
|
||||
LogGridView.ScrollIntoView(lastItem);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 滚动失败时静默处理 | Silently handle scroll failures
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭时取消订阅,防止内存泄漏 | Unsubscribe on close to prevent memory leaks
|
||||
/// </summary>
|
||||
private void OnWindowClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_viewModel.ScrollToBottomRequested -= OnScrollToBottomRequested;
|
||||
RealTimeLogSink.Instance.LogEventReceived -= OnLogEventReceived;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using Prism.Ioc;
|
||||
using Prism.Modularity;
|
||||
using XP.Common.Dump.Configs;
|
||||
using XP.Common.Dump.Implementations;
|
||||
using XP.Common.Dump.Interfaces;
|
||||
using XP.Common.Helpers;
|
||||
using XP.Common.Localization.Configs;
|
||||
using XP.Common.Localization.Extensions;
|
||||
using XP.Common.Localization.Implementations;
|
||||
using XP.Common.Localization.Interfaces;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Implementations;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
|
||||
namespace XP.Common.Module
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用模块 | Common module
|
||||
/// 提供通用基础设施服务,包括本地化支持 | Provides common infrastructure services including localization support
|
||||
/// </summary>
|
||||
public class CommonModule : IModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 模块初始化 | Module initialization
|
||||
/// 在所有类型注册完成后调用 | Called after all types are registered
|
||||
/// </summary>
|
||||
public void OnInitialized(IContainerProvider containerProvider)
|
||||
{
|
||||
// 获取日志服务 | Get logger service
|
||||
var logger = containerProvider.Resolve<ILoggerService>().ForModule("XP.Common.Module.CommonModule");
|
||||
|
||||
try
|
||||
{
|
||||
// 解析本地化服务 | Resolve localization service
|
||||
var localizationService = containerProvider.Resolve<ILocalizationService>();
|
||||
|
||||
// 初始化 LocalizationExtension | Initialize LocalizationExtension
|
||||
LocalizationExtension.Initialize(localizationService);
|
||||
|
||||
logger.Info("本地化系统初始化成功 | Localization system initialized successfully");
|
||||
logger.Info($"当前语言 | Current language: {localizationService.CurrentLanguage}");
|
||||
|
||||
// 启动 Dump 服务 | Start Dump service
|
||||
var dumpService = containerProvider.Resolve<IDumpService>();
|
||||
dumpService.Start();
|
||||
logger.Info("Dump 服务初始化成功 | Dump service initialized successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "本地化系统初始化失败 | Localization system initialization failed");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册类型到 DI 容器 | Register types to DI container
|
||||
/// </summary>
|
||||
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||
{
|
||||
// 注册本地化配置服务为单例 | Register localization config service as singleton
|
||||
containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>();
|
||||
|
||||
// 注册本地化服务为单例 | Register localization service as singleton
|
||||
containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>();
|
||||
|
||||
// 注册 Dump 配置为单例(通过工厂方法加载)| Register Dump config as singleton (via factory method)
|
||||
containerRegistry.RegisterSingleton<DumpConfig>(() => ConfigLoader.LoadDumpConfig());
|
||||
|
||||
// 注册 Dump 服务为单例 | Register Dump service as singleton
|
||||
containerRegistry.RegisterSingleton<IDumpService, DumpService>();
|
||||
|
||||
// 注册 PDF 打印服务为单例 | Register PDF print service as singleton
|
||||
containerRegistry.RegisterSingleton<IPdfPrintService, PdfPrintService>();
|
||||
|
||||
// 注册 PDF 查看服务为单例 | Register PDF viewer service as singleton
|
||||
containerRegistry.RegisterSingleton<IPdfViewerService, PdfViewerService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.PdfViewer.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 加载异常 | PDF load exception
|
||||
/// 当 PDF 文件格式无效或加载失败时抛出 | Thrown when PDF format is invalid or loading fails
|
||||
/// </summary>
|
||||
public class PdfLoadException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载失败的文件路径 | File path that failed to load
|
||||
/// </summary>
|
||||
public string? FilePath { get; }
|
||||
|
||||
public PdfLoadException(string message, string? filePath = null, Exception? innerException = null)
|
||||
: base(message, innerException)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.PdfViewer.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 打印异常 | Print exception
|
||||
/// 当打印过程中发生错误时抛出 | Thrown when an error occurs during printing
|
||||
/// </summary>
|
||||
public class PrintException : Exception
|
||||
{
|
||||
public PrintException(string message, Exception? innerException = null)
|
||||
: base(message, innerException) { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Common.PdfViewer.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 打印机未找到异常 | Printer not found exception
|
||||
/// 当指定的打印机名称不存在或不可用时抛出 | Thrown when specified printer is not found or unavailable
|
||||
/// </summary>
|
||||
public class PrinterNotFoundException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// 未找到的打印机名称 | Name of the printer that was not found
|
||||
/// </summary>
|
||||
public string PrinterName { get; }
|
||||
|
||||
public PrinterNotFoundException(string printerName)
|
||||
: base($"打印机未找到 | Printer not found: {printerName}")
|
||||
{
|
||||
PrinterName = printerName;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Drawing.Printing;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Telerik.Windows.Controls;
|
||||
using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
|
||||
using Telerik.Windows.Documents.Fixed.Print;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Exceptions;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
|
||||
namespace XP.Common.PdfViewer.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 打印服务实现 | PDF print service implementation
|
||||
/// 基于 Telerik RadPdfViewer.Print() 实现 | Based on Telerik RadPdfViewer.Print()
|
||||
/// </summary>
|
||||
public class PdfPrintService : IPdfPrintService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
public PdfPrintService(ILoggerService logger)
|
||||
{
|
||||
_logger = logger?.ForModule<PdfPrintService>()
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
|
||||
/// </summary>
|
||||
public void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1)
|
||||
{
|
||||
// 1. 验证文件路径存在 | Validate file path exists
|
||||
ValidateFilePath(filePath);
|
||||
|
||||
// 2. 验证打印机名称有效 | Validate printer name is valid
|
||||
ValidatePrinterName(printerName);
|
||||
|
||||
RadPdfViewer? pdfViewer = null;
|
||||
try
|
||||
{
|
||||
// 3. 创建隐藏的 RadPdfViewer 并加载 PDF | Create hidden RadPdfViewer and load PDF
|
||||
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||
|
||||
// 4. 创建 PrintDialog 并配置打印机名称 | Create PrintDialog and configure printer name
|
||||
var printDialog = new PrintDialog();
|
||||
printDialog.PrintQueue = new System.Printing.PrintQueue(
|
||||
new System.Printing.LocalPrintServer(), printerName);
|
||||
|
||||
// 5. 配置页面范围 | Configure page range
|
||||
if (pageFrom.HasValue || pageTo.HasValue)
|
||||
{
|
||||
printDialog.PageRangeSelection = PageRangeSelection.UserPages;
|
||||
printDialog.PageRange = new PageRange(
|
||||
pageFrom ?? 1,
|
||||
pageTo ?? int.MaxValue);
|
||||
}
|
||||
|
||||
// 6. 配置打印份数 | Configure copies
|
||||
if (copies > 1)
|
||||
{
|
||||
printDialog.PrintTicket.CopyCount = copies;
|
||||
}
|
||||
|
||||
// 7. 配置 Telerik PrintSettings | Configure Telerik PrintSettings
|
||||
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||
{
|
||||
DocumentName = Path.GetFileName(filePath),
|
||||
PageMargins = new Thickness(0),
|
||||
UseDefaultPrinter = false
|
||||
};
|
||||
|
||||
// 8. 调用 RadPdfViewer.Print() 静默打印 | Call RadPdfViewer.Print() for silent printing
|
||||
pdfViewer.Print(printDialog, printSettings);
|
||||
|
||||
// 9. 记录 Info 日志 | Log info
|
||||
var pageRange = FormatPageRange(pageFrom, pageTo);
|
||||
_logger.Info("打印任务已提交 | Print job submitted: {FileName} → {PrinterName}, 页面范围 | Pages: {PageRange}, 份数 | Copies: {Copies}",
|
||||
Path.GetFileName(filePath), printerName, pageRange, copies);
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException
|
||||
and not PrinterNotFoundException
|
||||
and not PdfLoadException)
|
||||
{
|
||||
_logger.Error(ex, "打印失败 | Print failed: {FileName} → {PrinterName}", Path.GetFileName(filePath), printerName);
|
||||
throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 10. 释放资源 | Release resources
|
||||
DisposePdfViewer(pdfViewer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开打印设置对话框并打印 | Open print settings dialog and print
|
||||
/// </summary>
|
||||
public bool PrintWithDialog(string filePath)
|
||||
{
|
||||
// 1. 验证文件路径 | Validate file path
|
||||
ValidateFilePath(filePath);
|
||||
|
||||
RadPdfViewer? pdfViewer = null;
|
||||
try
|
||||
{
|
||||
// 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
|
||||
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||
|
||||
// 3. 配置 PrintSettings | Configure PrintSettings
|
||||
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||
{
|
||||
DocumentName = Path.GetFileName(filePath),
|
||||
PageMargins = new Thickness(0),
|
||||
UseDefaultPrinter = false
|
||||
};
|
||||
|
||||
// 4. 调用 RadPdfViewer.Print() 弹出 PrintDialog | Call RadPdfViewer.Print() to show PrintDialog
|
||||
pdfViewer.Print(printSettings);
|
||||
|
||||
_logger.Info("用户通过对话框打印 | User printed via dialog: {FileName}", Path.GetFileName(filePath));
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
|
||||
{
|
||||
_logger.Error(ex, "打印失败 | Print failed: {FileName}", Path.GetFileName(filePath));
|
||||
throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 5. 释放资源 | Release resources
|
||||
DisposePdfViewer(pdfViewer);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开打印预览对话框 | Open print preview dialog
|
||||
/// </summary>
|
||||
public void PrintPreview(string filePath)
|
||||
{
|
||||
// 1. 验证文件路径 | Validate file path
|
||||
ValidateFilePath(filePath);
|
||||
|
||||
RadPdfViewer? pdfViewer = null;
|
||||
try
|
||||
{
|
||||
// 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
|
||||
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||
|
||||
// 3. 通过 PrintDialog 显示预览 | Show preview via PrintDialog
|
||||
var printDialog = new PrintDialog();
|
||||
|
||||
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||
{
|
||||
DocumentName = Path.GetFileName(filePath),
|
||||
PageMargins = new Thickness(0),
|
||||
UseDefaultPrinter = false
|
||||
};
|
||||
|
||||
// 显示打印对话框(含预览功能)| Show print dialog (with preview capability)
|
||||
pdfViewer.Print(printDialog, printSettings);
|
||||
|
||||
_logger.Info("打印预览已显示 | Print preview shown: {FileName}", Path.GetFileName(filePath));
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
|
||||
{
|
||||
_logger.Error(ex, "打印预览失败 | Print preview failed: {FileName}", Path.GetFileName(filePath));
|
||||
throw new PrintException($"打印预览失败 | Print preview failed: {Path.GetFileName(filePath)}", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 4. 释放资源 | Release resources
|
||||
DisposePdfViewer(pdfViewer);
|
||||
}
|
||||
}
|
||||
|
||||
#region 私有辅助方法 | Private Helper Methods
|
||||
|
||||
/// <summary>
|
||||
/// 验证文件路径是否存在 | Validate file path exists
|
||||
/// </summary>
|
||||
private void ValidateFilePath(string filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath), "文件路径不能为空 | File path cannot be null or empty");
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_logger.Error(new FileNotFoundException(filePath), "文件不存在 | File not found: {FilePath}", filePath);
|
||||
throw new FileNotFoundException($"文件不存在 | File not found: {filePath}", filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证打印机名称是否有效 | Validate printer name is valid
|
||||
/// </summary>
|
||||
private void ValidatePrinterName(string printerName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(printerName))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(printerName), "打印机名称不能为空 | Printer name cannot be null or empty");
|
||||
}
|
||||
|
||||
// 通过 PrinterSettings.InstalledPrinters 查询系统已安装的打印机 | Query installed printers
|
||||
bool found = false;
|
||||
foreach (string installedPrinter in PrinterSettings.InstalledPrinters)
|
||||
{
|
||||
if (string.Equals(installedPrinter, printerName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
{
|
||||
_logger.Error(new PrinterNotFoundException(printerName),
|
||||
"打印机未找到 | Printer not found: {PrinterName}", printerName);
|
||||
throw new PrinterNotFoundException(printerName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建隐藏的 RadPdfViewer 并加载 PDF 文档 | Create hidden RadPdfViewer and load PDF document
|
||||
/// </summary>
|
||||
private RadPdfViewer CreatePdfViewerWithDocument(string filePath)
|
||||
{
|
||||
var pdfViewer = new RadPdfViewer();
|
||||
|
||||
try
|
||||
{
|
||||
var provider = new PdfFormatProvider();
|
||||
using var fileStream = File.OpenRead(filePath);
|
||||
pdfViewer.Document = provider.Import(fileStream);
|
||||
return pdfViewer;
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||
{
|
||||
// 释放已创建的 viewer | Dispose created viewer
|
||||
DisposePdfViewer(pdfViewer);
|
||||
_logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
|
||||
throw new PdfLoadException("PDF 文件加载失败 | PDF file load failed", filePath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 RadPdfViewer 资源 | Dispose RadPdfViewer resources
|
||||
/// </summary>
|
||||
private static void DisposePdfViewer(RadPdfViewer? pdfViewer)
|
||||
{
|
||||
if (pdfViewer != null)
|
||||
{
|
||||
pdfViewer.Document = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化页面范围字符串 | Format page range string
|
||||
/// </summary>
|
||||
private static string FormatPageRange(int? pageFrom, int? pageTo)
|
||||
{
|
||||
if (!pageFrom.HasValue && !pageTo.HasValue)
|
||||
{
|
||||
return "全部 | All";
|
||||
}
|
||||
|
||||
var from = pageFrom?.ToString() ?? "1";
|
||||
var to = pageTo?.ToString() ?? "末页 | Last";
|
||||
return $"{from}-{to}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using XP.Common.Localization;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Exceptions;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
using XP.Common.PdfViewer.ViewModels;
|
||||
using XP.Common.PdfViewer.Views;
|
||||
|
||||
namespace XP.Common.PdfViewer.Implementations
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 查看服务实现 | PDF viewer service implementation
|
||||
/// 基于 Telerik RadPdfViewer 实现 | Based on Telerik RadPdfViewer
|
||||
/// </summary>
|
||||
public class PdfViewerService : IPdfViewerService
|
||||
{
|
||||
private readonly ILoggerService _logger;
|
||||
private readonly IPdfPrintService _printService;
|
||||
private bool _disposed;
|
||||
|
||||
public PdfViewerService(ILoggerService logger, IPdfPrintService printService)
|
||||
{
|
||||
_logger = logger?.ForModule<PdfViewerService>()
|
||||
?? throw new ArgumentNullException(nameof(logger));
|
||||
_printService = printService
|
||||
?? throw new ArgumentNullException(nameof(printService));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||
public void OpenViewer(string filePath)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
// 1. 验证文件路径存在 | Validate file path exists
|
||||
if (string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(filePath),
|
||||
"文件路径不能为空 | File path cannot be null or empty");
|
||||
}
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
_logger.Error(new FileNotFoundException(filePath),
|
||||
"文件不存在 | File not found: {FilePath}", filePath);
|
||||
throw new FileNotFoundException(
|
||||
$"文件不存在 | File not found: {filePath}", filePath);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 创建 PdfViewerWindowViewModel | Create PdfViewerWindowViewModel
|
||||
var viewModel = new PdfViewerWindowViewModel(filePath, _printService, _logger);
|
||||
|
||||
// 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
|
||||
var window = new PdfViewerWindow
|
||||
{
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
||||
// 4. 记录 Info 日志 | Log info
|
||||
_logger.Info("打开 PDF 阅读器窗口 | Opening PDF viewer window: {FileName}",
|
||||
Path.GetFileName(filePath));
|
||||
|
||||
// 5. 显示窗口(非模态)| Show window (non-modal)
|
||||
window.Show();
|
||||
}
|
||||
catch (Exception ex) when (ex is not FileNotFoundException
|
||||
and not ArgumentNullException
|
||||
and not PdfLoadException)
|
||||
{
|
||||
_logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
|
||||
throw new PdfLoadException(
|
||||
"PDF 文件加载失败 | PDF file load failed", filePath, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
|
||||
/// </summary>
|
||||
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||
/// <exception cref="ArgumentNullException">流为 null | Stream is null</exception>
|
||||
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||
public void OpenViewer(Stream stream, string? title = null)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
// 1. 验证 stream 非 null | Validate stream is not null
|
||||
ArgumentNullException.ThrowIfNull(stream, nameof(stream));
|
||||
|
||||
try
|
||||
{
|
||||
// 2. 创建 ViewModel | Create ViewModel
|
||||
var viewModel = new PdfViewerWindowViewModel(stream, title, _printService, _logger);
|
||||
|
||||
// 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
|
||||
var window = new PdfViewerWindow
|
||||
{
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
||||
// 4. 记录 Info 日志 | Log info
|
||||
var displayTitle = title ?? LocalizationHelper.Get("PdfViewer_Title");
|
||||
_logger.Info("打开 PDF 阅读器窗口(流模式)| Opening PDF viewer window (stream mode): {Title}",
|
||||
displayTitle);
|
||||
|
||||
// 5. 显示窗口(非模态)| Show window (non-modal)
|
||||
window.Show();
|
||||
}
|
||||
catch (Exception ex) when (ex is not ArgumentNullException
|
||||
and not PdfLoadException)
|
||||
{
|
||||
_logger.Error(ex, "PDF 文件加载失败(流模式)| PDF file load failed (stream mode)");
|
||||
throw new PdfLoadException(
|
||||
"PDF 文件加载失败 | PDF file load failed", null, ex);
|
||||
}
|
||||
}
|
||||
|
||||
#region IDisposable 模式 | IDisposable Pattern
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Dispose resources
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源的核心方法 | Core dispose method
|
||||
/// </summary>
|
||||
/// <param name="disposing">是否由 Dispose() 调用 | Whether called by Dispose()</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
// 释放托管资源 | Dispose managed resources
|
||||
_logger.Info("PdfViewerService 已释放 | PdfViewerService disposed");
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 终结器安全网 | Finalizer safety net
|
||||
/// 确保未显式释放时仍能清理非托管资源 | Ensures unmanaged resources are cleaned up if not explicitly disposed
|
||||
/// </summary>
|
||||
~PdfViewerService()
|
||||
{
|
||||
Dispose(disposing: false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using XP.Common.PdfViewer.Exceptions;
|
||||
|
||||
namespace XP.Common.PdfViewer.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 打印服务接口 | PDF print service interface
|
||||
/// 提供 PDF 文件打印功能 | Provides PDF file printing functionality
|
||||
/// </summary>
|
||||
public interface IPdfPrintService
|
||||
{
|
||||
/// <summary>
|
||||
/// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
/// <param name="printerName">打印机名称 | Printer name</param>
|
||||
/// <param name="pageFrom">起始页码(从 1 开始,null 表示从第一页)| Start page (1-based, null for first page)</param>
|
||||
/// <param name="pageTo">结束页码(从 1 开始,null 表示到最后一页)| End page (1-based, null for last page)</param>
|
||||
/// <param name="copies">打印份数(默认 1)| Number of copies (default 1)</param>
|
||||
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||
/// <exception cref="PrinterNotFoundException">打印机不存在 | Printer not found</exception>
|
||||
/// <exception cref="PrintException">打印失败 | Print failed</exception>
|
||||
void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
|
||||
|
||||
/// <summary>
|
||||
/// 打开打印设置对话框并打印 | Open print settings dialog and print
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
/// <returns>用户是否确认打印 | Whether user confirmed printing</returns>
|
||||
bool PrintWithDialog(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 打开打印预览对话框 | Open print preview dialog
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
void PrintPreview(string filePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using XP.Common.PdfViewer.Exceptions;
|
||||
|
||||
namespace XP.Common.PdfViewer.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 查看服务接口 | PDF viewer service interface
|
||||
/// 提供 PDF 文件加载和阅读器窗口管理功能 | Provides PDF file loading and viewer window management
|
||||
/// </summary>
|
||||
public interface IPdfViewerService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||
void OpenViewer(string filePath);
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
|
||||
/// </summary>
|
||||
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||
/// <exception cref="ArgumentNullException">流为 null | Stream is null</exception>
|
||||
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||
void OpenViewer(Stream stream, string? title = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using Prism.Commands;
|
||||
using Prism.Mvvm;
|
||||
using XP.Common.Localization;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
|
||||
namespace XP.Common.PdfViewer.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 阅读器窗口 ViewModel | PDF viewer window ViewModel
|
||||
/// 轻量级 ViewModel,核心功能由 RadPdfViewer 控件内置处理 | Lightweight ViewModel, core features handled by RadPdfViewer
|
||||
/// </summary>
|
||||
public class PdfViewerWindowViewModel : BindableBase
|
||||
{
|
||||
private readonly IPdfPrintService _printService;
|
||||
private readonly ILoggerService _logger;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口标题(含文件名)| Window title (with file name)
|
||||
/// </summary>
|
||||
public string Title { get; }
|
||||
|
||||
/// <summary>
|
||||
/// PDF 文件路径(用于打印)| PDF file path (for printing)
|
||||
/// </summary>
|
||||
public string? FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// PDF 文件流(用于流加载场景)| PDF file stream (for stream loading scenario)
|
||||
/// </summary>
|
||||
public Stream? PdfStream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打印命令(打开打印设置对话框)| Print command (open print settings dialog)
|
||||
/// </summary>
|
||||
public DelegateCommand PrintCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 打印预览命令 | Print preview command
|
||||
/// </summary>
|
||||
public DelegateCommand PrintPreviewCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件路径创建 ViewModel | Create ViewModel by file path
|
||||
/// </summary>
|
||||
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||
/// <param name="printService">打印服务 | Print service</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public PdfViewerWindowViewModel(
|
||||
string filePath,
|
||||
IPdfPrintService printService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_printService = printService ?? throw new ArgumentNullException(nameof(printService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
||||
PdfStream = null;
|
||||
|
||||
// 使用多语言标题,包含文件名 | Use localized title with file name
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
Title = LocalizationHelper.Get("PdfViewer_TitleWithFile", fileName);
|
||||
|
||||
// 初始化命令 | Initialize commands
|
||||
PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
|
||||
PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过文件流创建 ViewModel | Create ViewModel by stream
|
||||
/// </summary>
|
||||
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||
/// <param name="printService">打印服务 | Print service</param>
|
||||
/// <param name="logger">日志服务 | Logger service</param>
|
||||
public PdfViewerWindowViewModel(
|
||||
Stream stream,
|
||||
string? title,
|
||||
IPdfPrintService printService,
|
||||
ILoggerService logger)
|
||||
{
|
||||
_printService = printService ?? throw new ArgumentNullException(nameof(printService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
PdfStream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||
FilePath = null;
|
||||
|
||||
// 使用自定义标题或默认多语言标题 | Use custom title or default localized title
|
||||
Title = !string.IsNullOrWhiteSpace(title)
|
||||
? title
|
||||
: LocalizationHelper.Get("PdfViewer_Title");
|
||||
|
||||
// 初始化命令(流模式下无文件路径,命令不可用)| Initialize commands (no file path in stream mode, commands disabled)
|
||||
PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
|
||||
PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行打印(打开打印设置对话框)| Execute print (open print settings dialog)
|
||||
/// </summary>
|
||||
private void ExecutePrint()
|
||||
{
|
||||
try
|
||||
{
|
||||
_printService.PrintWithDialog(FilePath!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "打印失败 | Print failed: {FilePath}", FilePath ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行打印预览 | Execute print preview
|
||||
/// </summary>
|
||||
private void ExecutePrintPreview()
|
||||
{
|
||||
try
|
||||
{
|
||||
_printService.PrintPreview(FilePath!);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "打印预览失败 | Print preview failed: {FilePath}", FilePath ?? string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断打印命令是否可用(仅当 FilePath 有效时)| Check if print command is available (only when FilePath is valid)
|
||||
/// </summary>
|
||||
private bool CanExecutePrint()
|
||||
{
|
||||
return !string.IsNullOrEmpty(FilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<Window x:Class="XP.Common.PdfViewer.Views.PdfViewerWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||
xmlns:converters="clr-namespace:Telerik.Windows.Documents.Converters;assembly=Telerik.Windows.Controls.FixedDocumentViewers"
|
||||
Title="{Binding Title}"
|
||||
Width="1024" Height="768"
|
||||
MinWidth="800" MinHeight="600"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ShowInTaskbar="True"
|
||||
WindowStyle="SingleBorderWindow">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 缩略图转换器:将 RadFixedDocument 转换为缩略图集合 | Thumbnail converter: converts RadFixedDocument to thumbnails collection -->
|
||||
<converters:ThumbnailsConverter x:Key="ThumbnailsConverter" ThumbnailsHeight="200"/>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 工具栏(RadPdfViewerToolbar 内置导航/缩放/旋转/打印)| Toolbar (built-in navigation/zoom/rotate/print) -->
|
||||
<RowDefinition Height="Auto"/>
|
||||
<!-- PDF 显示区域 | PDF display area -->
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Telerik 内置工具栏 | Built-in toolbar -->
|
||||
<telerik:RadPdfViewerToolBar Grid.Row="0"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
RadPdfViewer="{Binding ElementName=pdfViewer, Mode=OneTime}"/>
|
||||
|
||||
<!-- 主内容区域:左侧缩略图面板 + 右侧 PDF 显示 | Main content: left thumbnails + right PDF display -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<!-- 左侧缩略图面板 | Left thumbnails panel -->
|
||||
<ColumnDefinition Width="180"/>
|
||||
<!-- PDF 显示区域 | PDF display area -->
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 页面缩略图面板 | Page thumbnails panel -->
|
||||
<Border Grid.Column="0" BorderBrush="#DDDDDD" BorderThickness="0,0,1,0" Background="#F5F5F5">
|
||||
<ListBox x:Name="thumbnailListBox"
|
||||
ItemsSource="{Binding ElementName=pdfViewer, Path=Document, Converter={StaticResource ThumbnailsConverter}}"
|
||||
SelectionChanged="ThumbnailListBox_SelectionChanged"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="4">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="2,4" HorizontalAlignment="Center">
|
||||
<!-- 缩略图图片 | Thumbnail image -->
|
||||
<Border BorderBrush="#CCCCCC" BorderThickness="1" Background="White"
|
||||
HorizontalAlignment="Center">
|
||||
<Image Source="{Binding ImageSource}" Stretch="Uniform"
|
||||
MaxWidth="150" MaxHeight="200"/>
|
||||
</Border>
|
||||
<!-- 页码 | Page number -->
|
||||
<TextBlock Text="{Binding PageNumber}"
|
||||
HorizontalAlignment="Center" Margin="0,2,0,0"
|
||||
FontSize="11" Foreground="#666666"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<!-- PDF 渲染区域 | PDF rendering area -->
|
||||
<telerik:RadPdfViewer Grid.Column="1"
|
||||
x:Name="pdfViewer"
|
||||
telerik:StyleManager.Theme="Crystal"
|
||||
DataContext="{Binding ElementName=pdfViewer, Path=CommandDescriptors}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
|
||||
using XP.Common.PdfViewer.Exceptions;
|
||||
using XP.Common.PdfViewer.ViewModels;
|
||||
|
||||
namespace XP.Common.PdfViewer.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// PDF 阅读器窗口 code-behind | PDF viewer window code-behind
|
||||
/// 负责在 Loaded 事件中加载 PDF 文档,在 Closed 事件中释放资源
|
||||
/// Responsible for loading PDF document on Loaded event and releasing resources on Closed event
|
||||
/// </summary>
|
||||
public partial class PdfViewerWindow : Window
|
||||
{
|
||||
// 防止缩略图选择和页面跳转之间的循环触发 | Prevent circular trigger between thumbnail selection and page navigation
|
||||
private bool _isSyncingSelection;
|
||||
|
||||
public PdfViewerWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += OnLoaded;
|
||||
Closed += OnClosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口加载事件:通过 PdfFormatProvider 加载 PDF 文档到 RadPdfViewer
|
||||
/// Window loaded event: load PDF document into RadPdfViewer via PdfFormatProvider
|
||||
/// </summary>
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var vm = DataContext as PdfViewerWindowViewModel;
|
||||
if (vm == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var provider = new PdfFormatProvider();
|
||||
|
||||
if (vm.PdfStream != null)
|
||||
{
|
||||
// 从文件流加载 PDF | Load PDF from stream
|
||||
pdfViewer.Document = provider.Import(vm.PdfStream);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(vm.FilePath))
|
||||
{
|
||||
// 从文件路径加载 PDF | Load PDF from file path
|
||||
using var stream = File.OpenRead(vm.FilePath);
|
||||
pdfViewer.Document = provider.Import(stream);
|
||||
}
|
||||
|
||||
// 文档加载后,监听页面变化以同步缩略图选中状态 | After document loaded, listen for page changes to sync thumbnail selection
|
||||
pdfViewer.CurrentPageChanged += PdfViewer_CurrentPageChanged;
|
||||
|
||||
// 初始选中第一页缩略图 | Initially select first page thumbnail
|
||||
if (thumbnailListBox.Items.Count > 0)
|
||||
{
|
||||
thumbnailListBox.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not PdfLoadException)
|
||||
{
|
||||
// 捕获加载异常并包装为 PdfLoadException | Catch loading exceptions and wrap as PdfLoadException
|
||||
throw new PdfLoadException(
|
||||
"PDF 文件加载失败 | PDF file load failed",
|
||||
vm.FilePath,
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缩略图列表选择变化:跳转到对应页面 | Thumbnail selection changed: navigate to corresponding page
|
||||
/// ListBox.SelectedIndex 从 0 开始,RadPdfViewer.CurrentPageNumber 从 1 开始
|
||||
/// ListBox.SelectedIndex is 0-based, RadPdfViewer.CurrentPageNumber is 1-based
|
||||
/// </summary>
|
||||
private void ThumbnailListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_isSyncingSelection) return;
|
||||
if (thumbnailListBox.SelectedIndex < 0) return;
|
||||
|
||||
_isSyncingSelection = true;
|
||||
try
|
||||
{
|
||||
// SelectedIndex(0-based) + 1 = CurrentPageNumber(1-based)
|
||||
pdfViewer.CurrentPageNumber = thumbnailListBox.SelectedIndex + 1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSyncingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PDF 阅读器页面变化:同步缩略图选中状态 | PDF viewer page changed: sync thumbnail selection
|
||||
/// </summary>
|
||||
private void PdfViewer_CurrentPageChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_isSyncingSelection) return;
|
||||
|
||||
_isSyncingSelection = true;
|
||||
try
|
||||
{
|
||||
// CurrentPageNumber(1-based) - 1 = SelectedIndex(0-based)
|
||||
var index = pdfViewer.CurrentPageNumber - 1;
|
||||
if (index >= 0 && index < thumbnailListBox.Items.Count)
|
||||
{
|
||||
thumbnailListBox.SelectedIndex = index;
|
||||
thumbnailListBox.ScrollIntoView(thumbnailListBox.SelectedItem);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSyncingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口关闭事件:释放 RadPdfViewer 文档资源
|
||||
/// Window closed event: release RadPdfViewer document resources
|
||||
/// </summary>
|
||||
private void OnClosed(object? sender, EventArgs e)
|
||||
{
|
||||
// 取消事件订阅 | Unsubscribe events
|
||||
pdfViewer.CurrentPageChanged -= PdfViewer_CurrentPageChanged;
|
||||
|
||||
// RadPdfViewer 内部管理文档资源,设置 Document = null 触发释放
|
||||
// RadPdfViewer internally manages document resources, setting Document = null triggers release
|
||||
pdfViewer.Document = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
Telerik.Windows.Controls.RadProgressBar, Telerik.Windows.Controls, Version=2024.1.408.310, Culture=neutral, PublicKeyToken=5803cfa389c90ce7
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1497
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,378 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>XplorePlane X-Ray Inspection System</value>
|
||||
</data>
|
||||
<data name="Menu_File" xml:space="preserve">
|
||||
<value>File</value>
|
||||
</data>
|
||||
<data name="Menu_Settings" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="Button_OK" xml:space="preserve">
|
||||
<value>OK</value>
|
||||
</data>
|
||||
<data name="Button_Cancel" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="Language_ZhCN" xml:space="preserve">
|
||||
<value>Simplified Chinese</value>
|
||||
</data>
|
||||
<data name="Language_ZhTW" xml:space="preserve">
|
||||
<value>Traditional Chinese</value>
|
||||
</data>
|
||||
<data name="Language_EnUS" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="Settings_Language" xml:space="preserve">
|
||||
<value>Language Settings</value>
|
||||
</data>
|
||||
<data name="Settings_Language_Description" xml:space="preserve">
|
||||
<value>Select your preferred display language</value>
|
||||
</data>
|
||||
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||
<value>Language changes will take effect on the next application startup.</value>
|
||||
</data>
|
||||
<data name="Button_Apply" xml:space="preserve">
|
||||
<value>Apply</value>
|
||||
</data>
|
||||
<data name="Button_Close" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
<data name="Button_Save" xml:space="preserve">
|
||||
<value>Save</value>
|
||||
</data>
|
||||
<data name="Button_Delete" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="Button_Edit" xml:space="preserve">
|
||||
<value>Edit</value>
|
||||
</data>
|
||||
<data name="Button_Add" xml:space="preserve">
|
||||
<value>Add</value>
|
||||
</data>
|
||||
<data name="Button_Refresh" xml:space="preserve">
|
||||
<value>Refresh</value>
|
||||
</data>
|
||||
<data name="Button_Search" xml:space="preserve">
|
||||
<value>Search</value>
|
||||
</data>
|
||||
<data name="Button_Reset" xml:space="preserve">
|
||||
<value>Reset</value>
|
||||
</data>
|
||||
<data name="Status_Ready" xml:space="preserve">
|
||||
<value>Ready</value>
|
||||
</data>
|
||||
<data name="Status_Loading" xml:space="preserve">
|
||||
<value>Loading...</value>
|
||||
</data>
|
||||
<data name="Status_Saving" xml:space="preserve">
|
||||
<value>Saving...</value>
|
||||
</data>
|
||||
<data name="Status_Processing" xml:space="preserve">
|
||||
<value>Processing...</value>
|
||||
</data>
|
||||
<data name="Status_Success" xml:space="preserve">
|
||||
<value>Success</value>
|
||||
</data>
|
||||
<data name="Status_Failed" xml:space="preserve">
|
||||
<value>Failed</value>
|
||||
</data>
|
||||
<data name="Status_Error" xml:space="preserve">
|
||||
<value>Error</value>
|
||||
</data>
|
||||
<data name="Status_Warning" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="Status_Connected" xml:space="preserve">
|
||||
<value>Connected</value>
|
||||
</data>
|
||||
<data name="Status_Disconnected" xml:space="preserve">
|
||||
<value>Disconnected</value>
|
||||
</data>
|
||||
<data name="Welcome_Message" xml:space="preserve">
|
||||
<value>Welcome to XplorePlane X-Ray Inspection System</value>
|
||||
</data>
|
||||
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||
<value>Operation completed successfully</value>
|
||||
</data>
|
||||
<data name="Message_OperationFailed" xml:space="preserve">
|
||||
<value>Operation failed</value>
|
||||
</data>
|
||||
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||
<value>Are you sure you want to delete?</value>
|
||||
</data>
|
||||
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||
<value>There are unsaved changes. Do you want to save?</value>
|
||||
</data>
|
||||
<data name="Message_InvalidInput" xml:space="preserve">
|
||||
<value>Invalid input. Please check and try again</value>
|
||||
</data>
|
||||
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||
<value>Connection lost</value>
|
||||
</data>
|
||||
<data name="Message_RestartRequired" xml:space="preserve">
|
||||
<value>Language setting saved. Please restart the application to apply the new language.</value>
|
||||
</data>
|
||||
<data name="Menu_View" xml:space="preserve">
|
||||
<value>View</value>
|
||||
</data>
|
||||
<data name="Menu_Tools" xml:space="preserve">
|
||||
<value>Tools</value>
|
||||
</data>
|
||||
<data name="Menu_Help" xml:space="preserve">
|
||||
<value>Help</value>
|
||||
</data>
|
||||
<data name="Menu_Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
</data>
|
||||
<data name="Menu_About" xml:space="preserve">
|
||||
<value>About</value>
|
||||
</data>
|
||||
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||
<value>Confirmation</value>
|
||||
</data>
|
||||
<data name="Dialog_Information" xml:space="preserve">
|
||||
<value>Information</value>
|
||||
</data>
|
||||
<data name="Dialog_Warning" xml:space="preserve">
|
||||
<value>Warning</value>
|
||||
</data>
|
||||
<data name="Dialog_Error" xml:space="preserve">
|
||||
<value>Error</value>
|
||||
</data>
|
||||
<data name="Dialog_Notice" xml:space="preserve">
|
||||
<value>Notice</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||
<value>Language setting saved. Please restart the application to apply the new language.</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||
<value>Failed to switch language: {0}</value>
|
||||
</data>
|
||||
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||
<value>Scan Mode:</value>
|
||||
<comment>Scan - Scan mode label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||
<value>Frame Merge:</value>
|
||||
<comment>Scan - Frame merge label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||
<value>Acquisition Count:</value>
|
||||
<comment>Scan - Acquisition count label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||
<value>Rotation Angle:</value>
|
||||
<comment>Scan - Rotation angle label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||
<value>Progress:</value>
|
||||
<comment>Scan - Acquisition progress label</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Start" xml:space="preserve">
|
||||
<value>Start</value>
|
||||
<comment>Scan - Start acquisition button</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
<comment>Scan - Stop acquisition button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||
<value>Total {0} logs</value>
|
||||
<comment>LogViewer - Status bar total count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||
<value>Total {0} logs, {1} after filtering</value>
|
||||
<comment>LogViewer - Status bar filtered count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Title" xml:space="preserve">
|
||||
<value>Real-Time Log Viewer</value>
|
||||
<comment>LogViewer - Window title</comment>
|
||||
</data>
|
||||
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||
<value>Auto Scroll</value>
|
||||
<comment>LogViewer - Auto-scroll button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||
<value>Clear Log</value>
|
||||
<comment>LogViewer - Clear log button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Filter" xml:space="preserve">
|
||||
<value>Filter:</value>
|
||||
<comment>LogViewer - Filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||
<value>Enter keyword to filter logs...</value>
|
||||
<comment>LogViewer - Filter watermark</comment>
|
||||
</data>
|
||||
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||
<value>Level Filter:</value>
|
||||
<comment>LogViewer - Level filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||
<value>Time</value>
|
||||
<comment>LogViewer - Time column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||
<value>Level</value>
|
||||
<comment>LogViewer - Level column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||
<value>Source</value>
|
||||
<comment>LogViewer - Source column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||
<value>Message</value>
|
||||
<comment>LogViewer - Message column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||
<value>Max Lines:</value>
|
||||
<comment>LogViewer - Max lines label</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_Title" xml:space="preserve">
|
||||
<value>PDF Viewer</value>
|
||||
<comment>PdfViewer - Window title</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||
<value>PDF Viewer - {0}</value>
|
||||
<comment>PdfViewer - Window title with file name</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||
<value>PDF loaded: {0} ({1} pages)</value>
|
||||
<comment>PdfViewer - Load success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||
<value>PDF file load failed</value>
|
||||
<comment>PdfViewer - Load failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||
<value>Print job submitted: {0} → {1}</value>
|
||||
<comment>PdfViewer - Print success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||
<value>Print failed</value>
|
||||
<comment>PdfViewer - Print failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||
<value>Printer not found: {0}</value>
|
||||
<comment>PdfViewer - Printer not found</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,378 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>XplorePlane X射线检测系统</value>
|
||||
</data>
|
||||
<data name="Menu_File" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
<data name="Menu_Settings" xml:space="preserve">
|
||||
<value>设置</value>
|
||||
</data>
|
||||
<data name="Button_OK" xml:space="preserve">
|
||||
<value>确定</value>
|
||||
</data>
|
||||
<data name="Button_Cancel" xml:space="preserve">
|
||||
<value>取消</value>
|
||||
</data>
|
||||
<data name="Language_ZhCN" xml:space="preserve">
|
||||
<value>简体中文</value>
|
||||
</data>
|
||||
<data name="Language_ZhTW" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="Language_EnUS" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="Settings_Language" xml:space="preserve">
|
||||
<value>语言设置</value>
|
||||
</data>
|
||||
<data name="Settings_Language_Description" xml:space="preserve">
|
||||
<value>选择您偏好的显示语言</value>
|
||||
</data>
|
||||
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||
<value>语言切换将在下次启动应用程序时生效。</value>
|
||||
</data>
|
||||
<data name="Button_Apply" xml:space="preserve">
|
||||
<value>应用</value>
|
||||
</data>
|
||||
<data name="Button_Close" xml:space="preserve">
|
||||
<value>关闭</value>
|
||||
</data>
|
||||
<data name="Button_Save" xml:space="preserve">
|
||||
<value>保存</value>
|
||||
</data>
|
||||
<data name="Button_Delete" xml:space="preserve">
|
||||
<value>删除</value>
|
||||
</data>
|
||||
<data name="Button_Edit" xml:space="preserve">
|
||||
<value>编辑</value>
|
||||
</data>
|
||||
<data name="Button_Add" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="Button_Refresh" xml:space="preserve">
|
||||
<value>刷新</value>
|
||||
</data>
|
||||
<data name="Button_Search" xml:space="preserve">
|
||||
<value>搜索</value>
|
||||
</data>
|
||||
<data name="Button_Reset" xml:space="preserve">
|
||||
<value>重置</value>
|
||||
</data>
|
||||
<data name="Status_Ready" xml:space="preserve">
|
||||
<value>就绪</value>
|
||||
</data>
|
||||
<data name="Status_Loading" xml:space="preserve">
|
||||
<value>加载中...</value>
|
||||
</data>
|
||||
<data name="Status_Saving" xml:space="preserve">
|
||||
<value>保存中...</value>
|
||||
</data>
|
||||
<data name="Status_Processing" xml:space="preserve">
|
||||
<value>处理中...</value>
|
||||
</data>
|
||||
<data name="Status_Success" xml:space="preserve">
|
||||
<value>成功</value>
|
||||
</data>
|
||||
<data name="Status_Failed" xml:space="preserve">
|
||||
<value>失败</value>
|
||||
</data>
|
||||
<data name="Status_Error" xml:space="preserve">
|
||||
<value>错误</value>
|
||||
</data>
|
||||
<data name="Status_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Status_Connected" xml:space="preserve">
|
||||
<value>已连接</value>
|
||||
</data>
|
||||
<data name="Status_Disconnected" xml:space="preserve">
|
||||
<value>已断开</value>
|
||||
</data>
|
||||
<data name="Welcome_Message" xml:space="preserve">
|
||||
<value>欢迎使用 XplorePlane X射线检测系统</value>
|
||||
</data>
|
||||
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||
<value>操作成功完成</value>
|
||||
</data>
|
||||
<data name="Message_OperationFailed" xml:space="preserve">
|
||||
<value>操作失败</value>
|
||||
</data>
|
||||
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||
<value>确定要删除吗?</value>
|
||||
</data>
|
||||
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||
<value>有未保存的更改,是否保存?</value>
|
||||
</data>
|
||||
<data name="Message_InvalidInput" xml:space="preserve">
|
||||
<value>输入无效,请检查后重试</value>
|
||||
</data>
|
||||
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||
<value>连接已断开</value>
|
||||
</data>
|
||||
<data name="Message_RestartRequired" xml:space="preserve">
|
||||
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||
</data>
|
||||
<data name="Menu_View" xml:space="preserve">
|
||||
<value>视图</value>
|
||||
</data>
|
||||
<data name="Menu_Tools" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
<data name="Menu_Help" xml:space="preserve">
|
||||
<value>帮助</value>
|
||||
</data>
|
||||
<data name="Menu_Exit" xml:space="preserve">
|
||||
<value>退出</value>
|
||||
</data>
|
||||
<data name="Menu_About" xml:space="preserve">
|
||||
<value>关于</value>
|
||||
</data>
|
||||
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||
<value>确认</value>
|
||||
</data>
|
||||
<data name="Dialog_Information" xml:space="preserve">
|
||||
<value>信息</value>
|
||||
</data>
|
||||
<data name="Dialog_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Dialog_Error" xml:space="preserve">
|
||||
<value>错误</value>
|
||||
</data>
|
||||
<data name="Dialog_Notice" xml:space="preserve">
|
||||
<value>提示</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||
<value>语言切换失败:{0}</value>
|
||||
</data>
|
||||
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||
<value>采集模式:</value>
|
||||
<comment>Scan - 采集模式标签 | Scan mode label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||
<value>帧合并:</value>
|
||||
<comment>Scan - 帧合并标签 | Frame merge label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||
<value>采集张数:</value>
|
||||
<comment>Scan - 采集张数标签 | Acquisition count label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||
<value>旋转角度:</value>
|
||||
<comment>Scan - 旋转角度标签 | Rotation angle label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||
<value>采集进度:</value>
|
||||
<comment>Scan - 采集进度标签 | Acquisition progress label</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Start" xml:space="preserve">
|
||||
<value>开始采集</value>
|
||||
<comment>Scan - 开始采集按钮 | Start acquisition button</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||
<value>停止采集</value>
|
||||
<comment>Scan - 停止采集按钮 | Stop acquisition button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||
<value>共 {0} 条日志</value>
|
||||
<comment>LogViewer - 状态栏总数 | Status bar total count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||
<value>共 {0} 条日志,过滤后 {1} 条</value>
|
||||
<comment>LogViewer - 状态栏过滤后 | Status bar filtered count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Title" xml:space="preserve">
|
||||
<value>实时日志查看器</value>
|
||||
<comment>LogViewer - 窗口标题 | Window title</comment>
|
||||
</data>
|
||||
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||
<value>自动滚动</value>
|
||||
<comment>LogViewer - 自动滚动按钮 | Auto-scroll button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||
<value>清空日志</value>
|
||||
<comment>LogViewer - 清空日志按钮 | Clear log button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Filter" xml:space="preserve">
|
||||
<value>过滤:</value>
|
||||
<comment>LogViewer - 过滤标签 | Filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||
<value>输入关键词过滤日志...</value>
|
||||
<comment>LogViewer - 过滤水印 | Filter watermark</comment>
|
||||
</data>
|
||||
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||
<value>级别筛选:</value>
|
||||
<comment>LogViewer - 级别筛选标签 | Level filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||
<value>时间</value>
|
||||
<comment>LogViewer - 时间列头 | Time column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||
<value>级别</value>
|
||||
<comment>LogViewer - 级别列头 | Level column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||
<value>来源</value>
|
||||
<comment>LogViewer - 来源列头 | Source column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||
<value>消息</value>
|
||||
<comment>LogViewer - 消息列头 | Message column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||
<value>最大行数:</value>
|
||||
<comment>LogViewer - 最大行数标签 | Max lines label</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_Title" xml:space="preserve">
|
||||
<value>PDF 阅读器</value>
|
||||
<comment>PdfViewer - 窗口标题 | Window title</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||
<value>PDF 阅读器 - {0}</value>
|
||||
<comment>PdfViewer - 带文件名的窗口标题 | Window title with file name</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||
<value>PDF 文件加载成功:{0}({1} 页)</value>
|
||||
<comment>PdfViewer - 加载成功日志 | Load success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||
<value>PDF 文件加载失败</value>
|
||||
<comment>PdfViewer - 加载失败日志 | Load failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||
<value>打印任务已提交:{0} → {1}</value>
|
||||
<comment>PdfViewer - 打印成功日志 | Print success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||
<value>打印失败</value>
|
||||
<comment>PdfViewer - 打印失败日志 | Print failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||
<value>打印机未找到:{0}</value>
|
||||
<comment>PdfViewer - 打印机未找到 | Printer not found</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,378 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>XplorePlane X射线检测系统</value>
|
||||
</data>
|
||||
<data name="Menu_File" xml:space="preserve">
|
||||
<value>文件</value>
|
||||
</data>
|
||||
<data name="Menu_Settings" xml:space="preserve">
|
||||
<value>设置</value>
|
||||
</data>
|
||||
<data name="Button_OK" xml:space="preserve">
|
||||
<value>确定</value>
|
||||
</data>
|
||||
<data name="Button_Cancel" xml:space="preserve">
|
||||
<value>取消</value>
|
||||
</data>
|
||||
<data name="Language_ZhCN" xml:space="preserve">
|
||||
<value>简体中文</value>
|
||||
</data>
|
||||
<data name="Language_ZhTW" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="Language_EnUS" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="Settings_Language" xml:space="preserve">
|
||||
<value>语言设置</value>
|
||||
</data>
|
||||
<data name="Settings_Language_Description" xml:space="preserve">
|
||||
<value>选择您偏好的显示语言</value>
|
||||
</data>
|
||||
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||
<value>语言切换将在下次启动应用程序时生效。</value>
|
||||
</data>
|
||||
<data name="Button_Apply" xml:space="preserve">
|
||||
<value>应用</value>
|
||||
</data>
|
||||
<data name="Button_Close" xml:space="preserve">
|
||||
<value>关闭</value>
|
||||
</data>
|
||||
<data name="Button_Save" xml:space="preserve">
|
||||
<value>保存</value>
|
||||
</data>
|
||||
<data name="Button_Delete" xml:space="preserve">
|
||||
<value>删除</value>
|
||||
</data>
|
||||
<data name="Button_Edit" xml:space="preserve">
|
||||
<value>编辑</value>
|
||||
</data>
|
||||
<data name="Button_Add" xml:space="preserve">
|
||||
<value>添加</value>
|
||||
</data>
|
||||
<data name="Button_Refresh" xml:space="preserve">
|
||||
<value>刷新</value>
|
||||
</data>
|
||||
<data name="Button_Search" xml:space="preserve">
|
||||
<value>搜索</value>
|
||||
</data>
|
||||
<data name="Button_Reset" xml:space="preserve">
|
||||
<value>重置</value>
|
||||
</data>
|
||||
<data name="Status_Ready" xml:space="preserve">
|
||||
<value>就绪</value>
|
||||
</data>
|
||||
<data name="Status_Loading" xml:space="preserve">
|
||||
<value>加载中...</value>
|
||||
</data>
|
||||
<data name="Status_Saving" xml:space="preserve">
|
||||
<value>保存中...</value>
|
||||
</data>
|
||||
<data name="Status_Processing" xml:space="preserve">
|
||||
<value>处理中...</value>
|
||||
</data>
|
||||
<data name="Status_Success" xml:space="preserve">
|
||||
<value>成功</value>
|
||||
</data>
|
||||
<data name="Status_Failed" xml:space="preserve">
|
||||
<value>失败</value>
|
||||
</data>
|
||||
<data name="Status_Error" xml:space="preserve">
|
||||
<value>错误</value>
|
||||
</data>
|
||||
<data name="Status_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Status_Connected" xml:space="preserve">
|
||||
<value>已连接</value>
|
||||
</data>
|
||||
<data name="Status_Disconnected" xml:space="preserve">
|
||||
<value>已断开</value>
|
||||
</data>
|
||||
<data name="Welcome_Message" xml:space="preserve">
|
||||
<value>欢迎使用 XplorePlane X射线检测系统</value>
|
||||
</data>
|
||||
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||
<value>操作成功完成</value>
|
||||
</data>
|
||||
<data name="Message_OperationFailed" xml:space="preserve">
|
||||
<value>操作失败</value>
|
||||
</data>
|
||||
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||
<value>确定要删除吗?</value>
|
||||
</data>
|
||||
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||
<value>有未保存的更改,是否保存?</value>
|
||||
</data>
|
||||
<data name="Message_InvalidInput" xml:space="preserve">
|
||||
<value>输入无效,请检查后重试</value>
|
||||
</data>
|
||||
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||
<value>连接已断开</value>
|
||||
</data>
|
||||
<data name="Message_RestartRequired" xml:space="preserve">
|
||||
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||
</data>
|
||||
<data name="Menu_View" xml:space="preserve">
|
||||
<value>视图</value>
|
||||
</data>
|
||||
<data name="Menu_Tools" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
<data name="Menu_Help" xml:space="preserve">
|
||||
<value>帮助</value>
|
||||
</data>
|
||||
<data name="Menu_Exit" xml:space="preserve">
|
||||
<value>退出</value>
|
||||
</data>
|
||||
<data name="Menu_About" xml:space="preserve">
|
||||
<value>关于</value>
|
||||
</data>
|
||||
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||
<value>确认</value>
|
||||
</data>
|
||||
<data name="Dialog_Information" xml:space="preserve">
|
||||
<value>信息</value>
|
||||
</data>
|
||||
<data name="Dialog_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Dialog_Error" xml:space="preserve">
|
||||
<value>错误</value>
|
||||
</data>
|
||||
<data name="Dialog_Notice" xml:space="preserve">
|
||||
<value>提示</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||
<value>语言切换失败:{0}</value>
|
||||
</data>
|
||||
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||
<value>采集模式:</value>
|
||||
<comment>Scan - 采集模式标签 | Scan mode label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||
<value>帧合并:</value>
|
||||
<comment>Scan - 帧合并标签 | Frame merge label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||
<value>采集张数:</value>
|
||||
<comment>Scan - 采集张数标签 | Acquisition count label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||
<value>旋转角度:</value>
|
||||
<comment>Scan - 旋转角度标签 | Rotation angle label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||
<value>采集进度:</value>
|
||||
<comment>Scan - 采集进度标签 | Acquisition progress label</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Start" xml:space="preserve">
|
||||
<value>开始采集</value>
|
||||
<comment>Scan - 开始采集按钮 | Start acquisition button</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||
<value>停止采集</value>
|
||||
<comment>Scan - 停止采集按钮 | Stop acquisition button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||
<value>共 {0} 条日志</value>
|
||||
<comment>LogViewer - 状态栏总数 | Status bar total count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||
<value>共 {0} 条日志,过滤后 {1} 条</value>
|
||||
<comment>LogViewer - 状态栏过滤后 | Status bar filtered count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Title" xml:space="preserve">
|
||||
<value>实时日志查看器</value>
|
||||
<comment>LogViewer - 窗口标题 | Window title</comment>
|
||||
</data>
|
||||
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||
<value>自动滚动</value>
|
||||
<comment>LogViewer - 自动滚动按钮 | Auto-scroll button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||
<value>清空日志</value>
|
||||
<comment>LogViewer - 清空日志按钮 | Clear log button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Filter" xml:space="preserve">
|
||||
<value>过滤:</value>
|
||||
<comment>LogViewer - 过滤标签 | Filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||
<value>输入关键词过滤日志...</value>
|
||||
<comment>LogViewer - 过滤水印 | Filter watermark</comment>
|
||||
</data>
|
||||
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||
<value>级别筛选:</value>
|
||||
<comment>LogViewer - 级别筛选标签 | Level filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||
<value>时间</value>
|
||||
<comment>LogViewer - 时间列头 | Time column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||
<value>级别</value>
|
||||
<comment>LogViewer - 级别列头 | Level column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||
<value>来源</value>
|
||||
<comment>LogViewer - 来源列头 | Source column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||
<value>消息</value>
|
||||
<comment>LogViewer - 消息列头 | Message column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||
<value>最大行数:</value>
|
||||
<comment>LogViewer - 最大行数标签 | Max lines label</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_Title" xml:space="preserve">
|
||||
<value>PDF 阅读器</value>
|
||||
<comment>PdfViewer - 窗口标题 | Window title</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||
<value>PDF 阅读器 - {0}</value>
|
||||
<comment>PdfViewer - 带文件名的窗口标题 | Window title with file name</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||
<value>PDF 文件加载成功:{0}({1} 页)</value>
|
||||
<comment>PdfViewer - 加载成功日志 | Load success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||
<value>PDF 文件加载失败</value>
|
||||
<comment>PdfViewer - 加载失败日志 | Load failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||
<value>打印任务已提交:{0} → {1}</value>
|
||||
<comment>PdfViewer - 打印成功日志 | Print success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||
<value>打印失败</value>
|
||||
<comment>PdfViewer - 打印失败日志 | Print failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||
<value>打印机未找到:{0}</value>
|
||||
<comment>PdfViewer - 打印机未找到 | Printer not found</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,378 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="App_Title" xml:space="preserve">
|
||||
<value>XplorePlane X射線檢測系統</value>
|
||||
</data>
|
||||
<data name="Menu_File" xml:space="preserve">
|
||||
<value>檔案</value>
|
||||
</data>
|
||||
<data name="Menu_Settings" xml:space="preserve">
|
||||
<value>設定</value>
|
||||
</data>
|
||||
<data name="Button_OK" xml:space="preserve">
|
||||
<value>確定</value>
|
||||
</data>
|
||||
<data name="Button_Cancel" xml:space="preserve">
|
||||
<value>取消</value>
|
||||
</data>
|
||||
<data name="Language_ZhCN" xml:space="preserve">
|
||||
<value>簡體中文</value>
|
||||
</data>
|
||||
<data name="Language_ZhTW" xml:space="preserve">
|
||||
<value>繁體中文</value>
|
||||
</data>
|
||||
<data name="Language_EnUS" xml:space="preserve">
|
||||
<value>English</value>
|
||||
</data>
|
||||
<data name="Settings_Language" xml:space="preserve">
|
||||
<value>語言設定</value>
|
||||
</data>
|
||||
<data name="Settings_Language_Description" xml:space="preserve">
|
||||
<value>選擇您偏好的顯示語言</value>
|
||||
</data>
|
||||
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||
<value>語言切換將在下次啟動應用程式時生效。</value>
|
||||
</data>
|
||||
<data name="Button_Apply" xml:space="preserve">
|
||||
<value>套用</value>
|
||||
</data>
|
||||
<data name="Button_Close" xml:space="preserve">
|
||||
<value>關閉</value>
|
||||
</data>
|
||||
<data name="Button_Save" xml:space="preserve">
|
||||
<value>儲存</value>
|
||||
</data>
|
||||
<data name="Button_Delete" xml:space="preserve">
|
||||
<value>刪除</value>
|
||||
</data>
|
||||
<data name="Button_Edit" xml:space="preserve">
|
||||
<value>編輯</value>
|
||||
</data>
|
||||
<data name="Button_Add" xml:space="preserve">
|
||||
<value>新增</value>
|
||||
</data>
|
||||
<data name="Button_Refresh" xml:space="preserve">
|
||||
<value>重新整理</value>
|
||||
</data>
|
||||
<data name="Button_Search" xml:space="preserve">
|
||||
<value>搜尋</value>
|
||||
</data>
|
||||
<data name="Button_Reset" xml:space="preserve">
|
||||
<value>重設</value>
|
||||
</data>
|
||||
<data name="Status_Ready" xml:space="preserve">
|
||||
<value>就緒</value>
|
||||
</data>
|
||||
<data name="Status_Loading" xml:space="preserve">
|
||||
<value>載入中...</value>
|
||||
</data>
|
||||
<data name="Status_Saving" xml:space="preserve">
|
||||
<value>儲存中...</value>
|
||||
</data>
|
||||
<data name="Status_Processing" xml:space="preserve">
|
||||
<value>處理中...</value>
|
||||
</data>
|
||||
<data name="Status_Success" xml:space="preserve">
|
||||
<value>成功</value>
|
||||
</data>
|
||||
<data name="Status_Failed" xml:space="preserve">
|
||||
<value>失敗</value>
|
||||
</data>
|
||||
<data name="Status_Error" xml:space="preserve">
|
||||
<value>錯誤</value>
|
||||
</data>
|
||||
<data name="Status_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Status_Connected" xml:space="preserve">
|
||||
<value>已連線</value>
|
||||
</data>
|
||||
<data name="Status_Disconnected" xml:space="preserve">
|
||||
<value>已中斷連線</value>
|
||||
</data>
|
||||
<data name="Welcome_Message" xml:space="preserve">
|
||||
<value>歡迎使用 XplorePlane X射線檢測系統</value>
|
||||
</data>
|
||||
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||
<value>操作成功完成</value>
|
||||
</data>
|
||||
<data name="Message_OperationFailed" xml:space="preserve">
|
||||
<value>操作失敗</value>
|
||||
</data>
|
||||
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||
<value>確定要刪除嗎?</value>
|
||||
</data>
|
||||
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||
<value>有未儲存的變更,是否儲存?</value>
|
||||
</data>
|
||||
<data name="Message_InvalidInput" xml:space="preserve">
|
||||
<value>輸入無效,請檢查後重試</value>
|
||||
</data>
|
||||
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||
<value>連線已中斷</value>
|
||||
</data>
|
||||
<data name="Message_RestartRequired" xml:space="preserve">
|
||||
<value>語言設定已儲存,請重新啟動應用程式以套用新語言。</value>
|
||||
</data>
|
||||
<data name="Menu_View" xml:space="preserve">
|
||||
<value>檢視</value>
|
||||
</data>
|
||||
<data name="Menu_Tools" xml:space="preserve">
|
||||
<value>工具</value>
|
||||
</data>
|
||||
<data name="Menu_Help" xml:space="preserve">
|
||||
<value>說明</value>
|
||||
</data>
|
||||
<data name="Menu_Exit" xml:space="preserve">
|
||||
<value>結束</value>
|
||||
</data>
|
||||
<data name="Menu_About" xml:space="preserve">
|
||||
<value>關於</value>
|
||||
</data>
|
||||
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||
<value>確認</value>
|
||||
</data>
|
||||
<data name="Dialog_Information" xml:space="preserve">
|
||||
<value>資訊</value>
|
||||
</data>
|
||||
<data name="Dialog_Warning" xml:space="preserve">
|
||||
<value>警告</value>
|
||||
</data>
|
||||
<data name="Dialog_Error" xml:space="preserve">
|
||||
<value>錯誤</value>
|
||||
</data>
|
||||
<data name="Dialog_Notice" xml:space="preserve">
|
||||
<value>提示</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||
<value>語言設定已儲存,請重新啟動應用程式以套用新語言。</value>
|
||||
</data>
|
||||
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||
<value>語言切換失敗:{0}</value>
|
||||
</data>
|
||||
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||
<value>採集模式:</value>
|
||||
<comment>Scan - 採集模式標籤 | Scan mode label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||
<value>幀合併:</value>
|
||||
<comment>Scan - 幀合併標籤 | Frame merge label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||
<value>採集張數:</value>
|
||||
<comment>Scan - 採集張數標籤 | Acquisition count label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||
<value>旋轉角度:</value>
|
||||
<comment>Scan - 旋轉角度標籤 | Rotation angle label</comment>
|
||||
</data>
|
||||
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||
<value>採集進度:</value>
|
||||
<comment>Scan - 採集進度標籤 | Acquisition progress label</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Start" xml:space="preserve">
|
||||
<value>開始採集</value>
|
||||
<comment>Scan - 開始採集按鈕 | Start acquisition button</comment>
|
||||
</data>
|
||||
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||
<value>停止採集</value>
|
||||
<comment>Scan - 停止採集按鈕 | Stop acquisition button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||
<value>共 {0} 筆日誌</value>
|
||||
<comment>LogViewer - 狀態列總數 | Status bar total count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||
<value>共 {0} 筆日誌,篩選後 {1} 筆</value>
|
||||
<comment>LogViewer - 狀態列篩選後 | Status bar filtered count</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Title" xml:space="preserve">
|
||||
<value>即時日誌檢視器</value>
|
||||
<comment>LogViewer - 視窗標題 | Window title</comment>
|
||||
</data>
|
||||
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||
<value>自動捲動</value>
|
||||
<comment>LogViewer - 自動捲動按鈕 | Auto-scroll button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||
<value>清空日誌</value>
|
||||
<comment>LogViewer - 清空日誌按鈕 | Clear log button</comment>
|
||||
</data>
|
||||
<data name="LogViewer_Filter" xml:space="preserve">
|
||||
<value>篩選:</value>
|
||||
<comment>LogViewer - 篩選標籤 | Filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||
<value>輸入關鍵詞篩選日誌...</value>
|
||||
<comment>LogViewer - 篩選浮水印 | Filter watermark</comment>
|
||||
</data>
|
||||
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||
<value>級別篩選:</value>
|
||||
<comment>LogViewer - 級別篩選標籤 | Level filter label</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||
<value>時間</value>
|
||||
<comment>LogViewer - 時間欄頭 | Time column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||
<value>級別</value>
|
||||
<comment>LogViewer - 級別欄頭 | Level column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||
<value>來源</value>
|
||||
<comment>LogViewer - 來源欄頭 | Source column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||
<value>訊息</value>
|
||||
<comment>LogViewer - 訊息欄頭 | Message column header</comment>
|
||||
</data>
|
||||
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||
<value>最大行數:</value>
|
||||
<comment>LogViewer - 最大行數標籤 | Max lines label</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_Title" xml:space="preserve">
|
||||
<value>PDF 閱讀器</value>
|
||||
<comment>PdfViewer - 視窗標題 | Window title</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||
<value>PDF 閱讀器 - {0}</value>
|
||||
<comment>PdfViewer - 帶檔案名稱的視窗標題 | Window title with file name</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||
<value>PDF 檔案載入成功:{0}({1} 頁)</value>
|
||||
<comment>PdfViewer - 載入成功日誌 | Load success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||
<value>PDF 檔案載入失敗</value>
|
||||
<comment>PdfViewer - 載入失敗日誌 | Load failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||
<value>列印任務已提交:{0} → {1}</value>
|
||||
<comment>PdfViewer - 列印成功日誌 | Print success log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||
<value>列印失敗</value>
|
||||
<comment>PdfViewer - 列印失敗日誌 | Print failed log</comment>
|
||||
</data>
|
||||
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||
<value>印表機未找到:{0}</value>
|
||||
<comment>PdfViewer - 印表機未找到 | Printer not found</comment>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,67 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<!-- NuGet 包版本 (用于包管理) -->
|
||||
<Version>1.0.0</Version>
|
||||
<!-- 程序集版本 (用于代码引用) -->
|
||||
<AssemblyVersion>1.4.16.1</AssemblyVersion>
|
||||
<!-- 文件版本 (用于 Windows 文件属性显示) -->
|
||||
<FileVersion>1.4.16.1</FileVersion>
|
||||
<!-- 版权声明 -->
|
||||
<Copyright>Copyright © 2026 Hexagon. All rights reserved.</Copyright>
|
||||
<!-- 公司名称 -->
|
||||
<Company>Hexagon Manufacturing Intelligence (Qingdao) Co., Ltd.</Company>
|
||||
<!-- 产品名称 -->
|
||||
<Product>XplorePlane XP.Common Infrastructure</Product>
|
||||
<!-- 标题 -->
|
||||
<AssemblyTitle>XP.Common Core Library</AssemblyTitle>
|
||||
<UseWPF>true</UseWPF>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
||||
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.10.0.5680" />
|
||||
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageReference Include="Telerik.UI.for.Wpf.NetCore.Xaml" Version="2024.1.408" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Controls\ViewModels\" />
|
||||
<Folder Include="Controls\Views\" />
|
||||
<Folder Include="Extensions\" />
|
||||
<Folder Include="Serialization\" />
|
||||
<Folder Include="Converters\" />
|
||||
<Folder Include="PdfViewer\Exceptions\" />
|
||||
<Folder Include="PdfViewer\Interfaces\" />
|
||||
<Folder Include="PdfViewer\Implementations\" />
|
||||
<Folder Include="PdfViewer\ViewModels\" />
|
||||
<Folder Include="PdfViewer\Views\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Resources\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resources\Resources.zh-CN.resx">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resources\Resources.zh-TW.resx">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Update="Resources\Resources.en-US.resx">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Resources\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,533 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Prism.Events;
|
||||
using XP.Hardware.Detector.Abstractions.Enums;
|
||||
using XP.Hardware.Detector.Abstractions.Events;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 面阵探测器抽象基类 | Area detector abstract base class
|
||||
/// 封装通用逻辑:参数校验、状态更新、事件发布
|
||||
/// 使用模板方法模式,子类实现具体的硬件操作
|
||||
/// </summary>
|
||||
public abstract class AreaDetectorBase : IAreaDetector
|
||||
{
|
||||
protected readonly IEventAggregator _eventAggregator;
|
||||
protected readonly object _statusLock = new object();
|
||||
protected DetectorStatus _status = DetectorStatus.Uninitialized;
|
||||
protected bool _disposed = false;
|
||||
|
||||
/// <summary>
|
||||
/// 探测器状态 | Detector status
|
||||
/// </summary>
|
||||
public DetectorStatus Status
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_statusLock)
|
||||
{
|
||||
return _status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 探测器类型 | Detector type
|
||||
/// </summary>
|
||||
public abstract DetectorType Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
|
||||
protected AreaDetectorBase(IEventAggregator eventAggregator)
|
||||
{
|
||||
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化探测器 | Initialize detector
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> InitializeAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Uninitialized)
|
||||
{
|
||||
return DetectorResult.Failure("探测器已初始化 | Detector already initialized");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Initializing);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await InitializeInternalAsync(cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"初始化异常 | Initialization exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 启动连续采集 | Start continuous acquisition
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> StartAcquisitionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Acquiring);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await StartAcquisitionInternalAsync(cancellationToken);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"启动采集异常 | Start acquisition exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止采集 | Stop acquisition
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> StopAcquisitionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
// 未初始化时不允许停止采集操作 | Stop acquisition not allowed when uninitialized
|
||||
if (Status == DetectorStatus.Uninitialized)
|
||||
{
|
||||
return DetectorResult.Failure("探测器未初始化,无法执行停止采集操作 | Detector not initialized, cannot stop acquisition");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await StopAcquisitionInternalAsync(cancellationToken);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"停止采集异常 | Stop acquisition exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单帧采集 | Single frame acquisition
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> AcquireSingleFrameAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Acquiring);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await AcquireSingleFrameInternalAsync(cancellationToken);
|
||||
|
||||
// 恢复状态 | Restore status
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"单帧采集异常 | Single frame acquisition exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暗场校正 | Dark field correction
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> DarkCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Correcting);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await DarkCorrectionInternalAsync(frameCount, cancellationToken);
|
||||
|
||||
// 恢复状态 | Restore status
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
PublishCorrectionCompleted(CorrectionType.Dark, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"暗场校正异常 | Dark correction exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 增益校正 | Gain correction
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> GainCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Correcting);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await GainCorrectionInternalAsync(frameCount, cancellationToken);
|
||||
|
||||
// 恢复状态 | Restore status
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
PublishCorrectionCompleted(CorrectionType.Gain, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"增益校正异常 | Gain correction exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动校正 | Auto correction
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> AutoCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
if (frameCount <= 0)
|
||||
{
|
||||
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Correcting);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await AutoCorrectionInternalAsync(frameCount, cancellationToken);
|
||||
|
||||
// 恢复状态 | Restore status
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
PublishCorrectionCompleted(CorrectionType.Auto, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"自动校正异常 | Auto correction exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 坏像素校正 | Bad pixel correction
|
||||
/// </summary>
|
||||
public async Task<DetectorResult> BadPixelCorrectionAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 参数校验 | Parameter validation
|
||||
if (Status != DetectorStatus.Ready)
|
||||
{
|
||||
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 更新状态 | Update status
|
||||
UpdateStatus(DetectorStatus.Correcting);
|
||||
|
||||
// 调用子类实现 | Call derived class implementation
|
||||
var result = await BadPixelCorrectionInternalAsync(cancellationToken);
|
||||
|
||||
// 恢复状态 | Restore status
|
||||
UpdateStatus(DetectorStatus.Ready);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
PublishCorrectionCompleted(CorrectionType.BadPixel, result);
|
||||
}
|
||||
else
|
||||
{
|
||||
PublishError(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
UpdateStatus(DetectorStatus.Error);
|
||||
var errorResult = DetectorResult.Failure($"坏像素校正异常 | Bad pixel correction exception: {ex.Message}", ex);
|
||||
PublishError(errorResult);
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取探测器信息 | Get detector information
|
||||
/// </summary>
|
||||
public abstract DetectorInfo GetInfo();
|
||||
|
||||
// 模板方法,由子类实现 | Template methods, implemented by derived classes
|
||||
protected abstract Task<DetectorResult> InitializeInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> StartAcquisitionInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> StopAcquisitionInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> AcquireSingleFrameInternalAsync(CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> DarkCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> GainCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> AutoCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||
protected abstract Task<DetectorResult> BadPixelCorrectionInternalAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 更新状态并发布事件 | Update status and publish event
|
||||
/// </summary>
|
||||
/// <param name="newStatus">新状态 | New status</param>
|
||||
/// <summary>
|
||||
/// 更新状态并发布事件 | Update status and publish event
|
||||
/// 验证状态转换的合法性 | Validate state transition legality
|
||||
/// </summary>
|
||||
/// <param name="newStatus">新状态 | New status</param>
|
||||
protected void UpdateStatus(DetectorStatus newStatus)
|
||||
{
|
||||
lock (_statusLock)
|
||||
{
|
||||
// 验证状态转换的合法性 | Validate state transition legality
|
||||
if (!IsValidStateTransition(_status, newStatus))
|
||||
{
|
||||
var errorMsg = $"非法的状态转换:{_status} -> {newStatus} | Invalid state transition: {_status} -> {newStatus}";
|
||||
PublishError(DetectorResult.Failure(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_status != newStatus)
|
||||
{
|
||||
var oldStatus = _status;
|
||||
_status = newStatus;
|
||||
_eventAggregator.GetEvent<StatusChangedEvent>().Publish(newStatus);
|
||||
|
||||
// 记录状态转换日志(如果需要)| Log state transition (if needed)
|
||||
System.Diagnostics.Debug.WriteLine($"状态转换 | State transition: {oldStatus} -> {newStatus}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证状态转换是否合法 | Validate if state transition is legal
|
||||
/// </summary>
|
||||
/// <param name="currentStatus">当前状态 | Current status</param>
|
||||
/// <param name="newStatus">新状态 | New status</param>
|
||||
/// <returns>是否合法 | Whether it's legal</returns>
|
||||
private bool IsValidStateTransition(DetectorStatus currentStatus, DetectorStatus newStatus)
|
||||
{
|
||||
// 相同状态总是允许的 | Same state is always allowed
|
||||
if (currentStatus == newStatus)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 定义合法的状态转换规则 | Define legal state transition rules
|
||||
return (currentStatus, newStatus) switch
|
||||
{
|
||||
// 从未初始化状态只能转到初始化中 | From Uninitialized can only go to Initializing
|
||||
(DetectorStatus.Uninitialized, DetectorStatus.Initializing) => true,
|
||||
|
||||
// 从初始化中可以转到就绪或错误 | From Initializing can go to Ready or Error
|
||||
(DetectorStatus.Initializing, DetectorStatus.Ready) => true,
|
||||
(DetectorStatus.Initializing, DetectorStatus.Error) => true,
|
||||
|
||||
// 从就绪状态可以转到采集中、校正中 | From Ready can go to Acquiring or Correcting
|
||||
(DetectorStatus.Ready, DetectorStatus.Acquiring) => true,
|
||||
(DetectorStatus.Ready, DetectorStatus.Correcting) => true,
|
||||
|
||||
// 从采集中可以转到就绪或错误 | From Acquiring can go to Ready or Error
|
||||
(DetectorStatus.Acquiring, DetectorStatus.Ready) => true,
|
||||
(DetectorStatus.Acquiring, DetectorStatus.Error) => true,
|
||||
|
||||
// 从校正中可以转到就绪或错误 | From Correcting can go to Ready or Error
|
||||
(DetectorStatus.Correcting, DetectorStatus.Ready) => true,
|
||||
(DetectorStatus.Correcting, DetectorStatus.Error) => true,
|
||||
|
||||
// 从错误状态可以转到初始化中(重新初始化)| From Error can go to Initializing (re-initialize)
|
||||
(DetectorStatus.Error, DetectorStatus.Initializing) => true,
|
||||
|
||||
// 任何状态都可以转到错误状态 | Any state can go to Error
|
||||
(_, DetectorStatus.Error) => true,
|
||||
|
||||
// 其他转换都是非法的 | All other transitions are illegal
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 发布错误事件 | Publish error event
|
||||
/// </summary>
|
||||
/// <param name="result">错误结果 | Error result</param>
|
||||
protected void PublishError(DetectorResult result)
|
||||
{
|
||||
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布图像采集事件 | Publish image captured event
|
||||
/// </summary>
|
||||
/// <param name="args">事件参数 | Event arguments</param>
|
||||
protected void PublishImageCaptured(ImageCapturedEventArgs args)
|
||||
{
|
||||
_eventAggregator.GetEvent<ImageCapturedEvent>().Publish(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发布校正完成事件 | Publish correction completed event
|
||||
/// </summary>
|
||||
/// <param name="type">校正类型 | Correction type</param>
|
||||
/// <param name="result">校正结果 | Correction result</param>
|
||||
protected void PublishCorrectionCompleted(CorrectionType type, DetectorResult result)
|
||||
{
|
||||
_eventAggregator.GetEvent<CorrectionCompletedEvent>().Publish(
|
||||
new CorrectionCompletedEventArgs(type, result));
|
||||
}
|
||||
|
||||
// IDisposable 实现 | IDisposable implementation
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源 | Dispose resources
|
||||
/// </summary>
|
||||
/// <param name="disposing">是否释放托管资源 | Whether to dispose managed resources</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
// 释放托管资源 | Release managed resources
|
||||
}
|
||||
// 释放非托管资源 | Release unmanaged resources
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using XP.Hardware.Detector.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器信息 | Detector information
|
||||
/// 包含探测器的硬件参数和配置信息
|
||||
/// </summary>
|
||||
public class DetectorInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器类型 | Detector type
|
||||
/// </summary>
|
||||
public DetectorType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 探测器型号 | Detector model
|
||||
/// </summary>
|
||||
public string Model { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 序列号 | Serial number
|
||||
/// </summary>
|
||||
public string SerialNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 固件版本 | Firmware version
|
||||
/// </summary>
|
||||
public string FirmwareVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大分辨率宽度 | Maximum resolution width
|
||||
/// </summary>
|
||||
public uint MaxWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大分辨率高度 | Maximum resolution height
|
||||
/// </summary>
|
||||
public uint MaxHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 像素尺寸(微米)| Pixel size (micrometers)
|
||||
/// </summary>
|
||||
public double PixelSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 位深度 | Bit depth
|
||||
/// </summary>
|
||||
public int BitDepth { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器操作结果封装 | Detector operation result wrapper
|
||||
/// 统一封装成功/失败状态、数据和错误信息
|
||||
/// </summary>
|
||||
public class DetectorResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作是否成功 | Whether the operation succeeded
|
||||
/// </summary>
|
||||
public bool IsSuccess { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误消息 | Error message
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 异常对象 | Exception object
|
||||
/// </summary>
|
||||
public Exception Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误码 | Error code
|
||||
/// </summary>
|
||||
public int ErrorCode { get; }
|
||||
|
||||
protected DetectorResult(bool isSuccess, string errorMessage = null, Exception exception = null, int errorCode = 0)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
ErrorMessage = errorMessage;
|
||||
Exception = exception;
|
||||
ErrorCode = errorCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建成功结果 | Create success result
|
||||
/// </summary>
|
||||
/// <param name="message">成功消息 | Success message</param>
|
||||
/// <returns>成功结果 | Success result</returns>
|
||||
public static DetectorResult Success(string message = null)
|
||||
{
|
||||
return new DetectorResult(true, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建失败结果 | Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">错误消息 | Error message</param>
|
||||
/// <param name="exception">异常对象 | Exception object</param>
|
||||
/// <param name="errorCode">错误码 | Error code</param>
|
||||
/// <returns>失败结果 | Failure result</returns>
|
||||
public static DetectorResult Failure(string errorMessage, Exception exception = null, int errorCode = -1)
|
||||
{
|
||||
return new DetectorResult(false, errorMessage, exception, errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带数据的探测器操作结果 | Detector operation result with data
|
||||
/// </summary>
|
||||
/// <typeparam name="T">数据类型 | Data type</typeparam>
|
||||
public class DetectorResult<T> : DetectorResult
|
||||
{
|
||||
/// <summary>
|
||||
/// 结果数据 | Result data
|
||||
/// </summary>
|
||||
public T Data { get; }
|
||||
|
||||
private DetectorResult(bool isSuccess, T data, string errorMessage = null, Exception exception = null, int errorCode = 0)
|
||||
: base(isSuccess, errorMessage, exception, errorCode)
|
||||
{
|
||||
Data = data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建成功结果 | Create success result
|
||||
/// </summary>
|
||||
/// <param name="data">结果数据 | Result data</param>
|
||||
/// <param name="message">成功消息 | Success message</param>
|
||||
/// <returns>成功结果 | Success result</returns>
|
||||
public static DetectorResult<T> Success(T data, string message = null)
|
||||
{
|
||||
return new DetectorResult<T>(true, data, message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建失败结果 | Create failure result
|
||||
/// </summary>
|
||||
/// <param name="errorMessage">错误消息 | Error message</param>
|
||||
/// <param name="exception">异常对象 | Exception object</param>
|
||||
/// <param name="errorCode">错误码 | Error code</param>
|
||||
/// <returns>失败结果 | Failure result</returns>
|
||||
public new static DetectorResult<T> Failure(string errorMessage, Exception exception = null, int errorCode = -1)
|
||||
{
|
||||
return new DetectorResult<T>(false, default(T), errorMessage, exception, errorCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 采集模式枚举 | Acquisition mode enumeration
|
||||
/// 定义图像采集的工作模式
|
||||
/// </summary>
|
||||
public enum AcquisitionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 连续采集模式 | Continuous acquisition mode
|
||||
/// </summary>
|
||||
Continuous = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 单帧采集模式 | Single frame acquisition mode
|
||||
/// </summary>
|
||||
SingleFrame = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// Binning 模式枚举 | Binning mode enumeration
|
||||
/// 定义像素合并模式,用于提高信噪比或采集速度
|
||||
/// </summary>
|
||||
public enum BinningMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 1x1 模式(无合并)| 1x1 mode (no binning)
|
||||
/// </summary>
|
||||
Bin1x1 = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 2x2 模式(2x2 像素合并)| 2x2 mode (2x2 pixel binning)
|
||||
/// </summary>
|
||||
Bin2x2 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 4x4 模式(4x4 像素合并)| 4x4 mode (4x4 pixel binning)
|
||||
/// </summary>
|
||||
Bin4x4 = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正类型枚举 | Correction type enumeration
|
||||
/// 定义探测器支持的校正类型
|
||||
/// </summary>
|
||||
public enum CorrectionType
|
||||
{
|
||||
/// <summary>
|
||||
/// 暗场校正 | Dark field correction
|
||||
/// </summary>
|
||||
Dark = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 增益校正(亮场校正)| Gain correction (bright field correction)
|
||||
/// </summary>
|
||||
Gain = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 自动校正(暗场+增益+坏像素)| Auto correction (dark + gain + bad pixel)
|
||||
/// </summary>
|
||||
Auto = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 坏像素校正 | Bad pixel correction
|
||||
/// </summary>
|
||||
BadPixel = 3
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器状态枚举 | Detector status enumeration
|
||||
/// 定义探测器的运行状态
|
||||
/// </summary>
|
||||
public enum DetectorStatus
|
||||
{
|
||||
/// <summary>
|
||||
/// 未初始化 | Uninitialized
|
||||
/// </summary>
|
||||
Uninitialized = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 初始化中 | Initializing
|
||||
/// </summary>
|
||||
Initializing = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 就绪 | Ready
|
||||
/// </summary>
|
||||
Ready = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 采集中 | Acquiring
|
||||
/// </summary>
|
||||
Acquiring = 3,
|
||||
|
||||
/// <summary>
|
||||
/// 校正中 | Correcting
|
||||
/// </summary>
|
||||
Correcting = 4,
|
||||
|
||||
/// <summary>
|
||||
/// 错误 | Error
|
||||
/// </summary>
|
||||
Error = 5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 探测器类型枚举 | Detector type enumeration
|
||||
/// 定义支持的探测器厂商类型
|
||||
/// </summary>
|
||||
public enum DetectorType
|
||||
{
|
||||
/// <summary>
|
||||
/// Varex 探测器 | Varex detector
|
||||
/// </summary>
|
||||
Varex = 0,
|
||||
|
||||
/// <summary>
|
||||
/// iRay 探测器 | iRay detector (预留)
|
||||
/// </summary>
|
||||
IRay = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Hamamatsu 探测器 | Hamamatsu detector (预留)
|
||||
/// </summary>
|
||||
Hamamatsu = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// 增益模式枚举 | Gain mode enumeration
|
||||
/// 定义探测器的信号放大倍数
|
||||
/// </summary>
|
||||
public enum GainMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 低增益模式 | Low gain mode
|
||||
/// </summary>
|
||||
Low = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 高增益模式 | High gain mode
|
||||
/// </summary>
|
||||
High = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Prism.Events;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正完成事件 | Correction completed event
|
||||
/// 当探测器校正操作完成时触发
|
||||
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||
/// </summary>
|
||||
public class CorrectionCompletedEvent : PubSubEvent<CorrectionCompletedEventArgs>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using XP.Hardware.Detector.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正完成事件参数 | Correction completed event arguments
|
||||
/// 携带校正类型和结果信息
|
||||
/// </summary>
|
||||
public class CorrectionCompletedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 校正类型 | Correction type
|
||||
/// </summary>
|
||||
public CorrectionType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 校正结果 | Correction result
|
||||
/// </summary>
|
||||
public DetectorResult Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数 | Constructor
|
||||
/// </summary>
|
||||
/// <param name="type">校正类型 | Correction type</param>
|
||||
/// <param name="result">校正结果 | Correction result</param>
|
||||
public CorrectionCompletedEventArgs(CorrectionType type, DetectorResult result)
|
||||
{
|
||||
Type = type;
|
||||
Result = result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Prism.Events;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 错误发生事件 | Error occurred event
|
||||
/// 当探测器操作发生错误时触发
|
||||
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||
/// </summary>
|
||||
public class ErrorOccurredEvent : PubSubEvent<DetectorResult>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Prism.Events;
|
||||
using XP.Hardware.Detector.Abstractions;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像采集事件 | Image captured event
|
||||
/// 当探测器采集到新图像时触发
|
||||
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||
/// </summary>
|
||||
public class ImageCapturedEvent : PubSubEvent<ImageCapturedEventArgs>
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions
|
||||
{
|
||||
/// <summary>
|
||||
/// 图像采集事件参数 | Image captured event arguments
|
||||
/// 携带图像数据和元信息
|
||||
/// </summary>
|
||||
public class ImageCapturedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// 16 位图像原始数据(无符号)| 16-bit raw image data (unsigned)
|
||||
/// </summary>
|
||||
public ushort[] ImageData { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图像宽度 | Image width
|
||||
/// </summary>
|
||||
public uint Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 图像高度 | Image height
|
||||
/// </summary>
|
||||
public uint Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 帧号 | Frame number
|
||||
/// </summary>
|
||||
public int FrameNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 存储路径 | Save path
|
||||
/// </summary>
|
||||
public string SavePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 曝光时间(毫秒)| Exposure time (milliseconds)
|
||||
/// </summary>
|
||||
public uint ExposureTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 采集时间戳 | Capture timestamp
|
||||
/// </summary>
|
||||
public DateTime CaptureTime { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using Prism.Events;
|
||||
using XP.Hardware.Detector.Abstractions.Enums;
|
||||
|
||||
namespace XP.Hardware.Detector.Abstractions.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态变更事件 | Status changed event
|
||||
/// 当探测器状态发生变化时触发
|
||||
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||
/// </summary>
|
||||
public class StatusChangedEvent : PubSubEvent<DetectorStatus>
|
||||
{
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user