Merged PR 20: XP.Common及Feature/XP.Hardware合并至Develop/XP

将Feature/XP.Common和Feature/XP.Hardware分支整合为Develop/XP.forHardwareAndCommon,申请合并至Develop/XP,除了合并外,还完善Develop/XP对通用类库和硬件类库的注册和界面及功能整合。
This commit is contained in:
QI Mingxuan
2026-04-17 09:12:37 +08:00
committed by SONG Tian
581 changed files with 74600 additions and 222 deletions
+43
View File
@@ -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;
}
}
+56
View File
@@ -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();
}
}
}
+68
View File
@@ -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. 构建分页SQLSQLite分页: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;
}
}
+183
View File
@@ -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
```
+247
View File
@@ -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
+265
View File
@@ -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
+233
View File
@@ -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 连接...
```
## 示例 2ViewModel 中使用 | 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 |
+177
View File
@@ -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
// 方式 1ForModule<T>() - 完整类型名
// Method 1: ForModule<T>() - Full type name
_logger = logger.ForModule<PlcService>();
// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
// 方式 2ForModule("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>
```
+299
View File
@@ -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` 的订阅,防止内存泄漏。
+34
View File
@@ -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;
}
}
+23
View File
@@ -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
}
}
+32
View File
@@ -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();
}
}
+37
View File
@@ -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,
}
}
+25
View File
@@ -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);
}
}
}
}
+91
View File
@@ -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;
}
}
}
+199
View File
@@ -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>();
}
}
+57
View File
@@ -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;
}
}
}
+80
View File
@@ -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;
}
}
}
+1
View File
@@ -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.
File diff suppressed because it is too large Load Diff
+378
View File
@@ -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>
+378
View File
@@ -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>
+378
View File
@@ -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>
+378
View File
@@ -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>
+67
View File
@@ -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