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:
@@ -0,0 +1,43 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace XP.Common.Configs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog日志配置实体(从App.config读取)
|
||||||
|
/// </summary>
|
||||||
|
public class SerilogConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 日志输出根路径(默认:AppData/Files/Logs)
|
||||||
|
/// </summary>
|
||||||
|
public string LogPath { get; set; } = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Files", "Logs");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最低日志级别(Debug/Info/Warn/Error/Fatal)
|
||||||
|
/// </summary>
|
||||||
|
public string MinimumLevel { get; set; } = "Info";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否输出到控制台(调试环境=true,生产环境=false)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableConsole { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志文件分割规则(Day/Month/Hour/Size)
|
||||||
|
/// </summary>
|
||||||
|
public string RollingInterval { get; set; } = "Day";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单个日志文件最大大小(MB,仅Size分割时生效)
|
||||||
|
/// </summary>
|
||||||
|
public long FileSizeLimitMB { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保留日志文件数量(默认30天)
|
||||||
|
/// </summary>
|
||||||
|
public int RetainedFileCountLimit { get; set; } = 30;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Configs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite 配置实体
|
||||||
|
/// </summary>
|
||||||
|
public class SqliteConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库文件路径
|
||||||
|
/// </summary>
|
||||||
|
public string DbFilePath { get; set; } = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Files", "Data", "XP.db");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 连接超时时间(秒,默认30)
|
||||||
|
/// </summary>
|
||||||
|
public int ConnectionTimeout { get; set; } = 30;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库不存在时是否自动创建(默认true)
|
||||||
|
/// </summary>
|
||||||
|
public bool CreateIfNotExists { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用 WAL 模式(提升并发性能,默认true)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableWalMode { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否开启日志记录(记录所有SQL操作,默认false)
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSqlLogging { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取SQLite连接字符串
|
||||||
|
/// </summary>
|
||||||
|
public string GetConnectionString()
|
||||||
|
{
|
||||||
|
var builder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder
|
||||||
|
{
|
||||||
|
DataSource = DbFilePath,
|
||||||
|
Cache = Microsoft.Data.Sqlite.SqliteCacheMode.Default,
|
||||||
|
DefaultTimeout = ConnectionTimeout
|
||||||
|
};
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using Emgu.CV;
|
||||||
|
using Emgu.CV.CvEnum;
|
||||||
|
|
||||||
|
namespace XP.Common.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 图像数据转换工具类,基于 Emgu CV 实现
|
||||||
|
/// </summary>
|
||||||
|
public static class ImageConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将 16 位无符号灰度数据转换为 RGB BitmapSource
|
||||||
|
/// 使用 Emgu CV 的 Normalize 进行线性拉伸,CvtColor 转 BGR
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">16 位灰度原始数据</param>
|
||||||
|
/// <param name="width">图像宽度</param>
|
||||||
|
/// <param name="height">图像高度</param>
|
||||||
|
/// <returns>BGR24 格式的 BitmapSource</returns>
|
||||||
|
public static BitmapSource ConvertGray16ToBitmapSource(ushort[] data, int width, int height)
|
||||||
|
{
|
||||||
|
if (data == null) throw new ArgumentNullException(nameof(data));
|
||||||
|
if (width <= 0 || height <= 0) throw new ArgumentException("图像尺寸无效");
|
||||||
|
|
||||||
|
// 从 ushort[] 按字节拷贝构建 16 位灰度 Mat
|
||||||
|
using var mat16 = new Mat(height, width, DepthType.Cv16U, 1);
|
||||||
|
int byteCount = data.Length * sizeof(ushort);
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (ushort* pData = data)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(pData, mat16.DataPointer.ToPointer(), byteCount, byteCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 归一化到 0~255 并转为 8 位
|
||||||
|
using var mat8 = new Mat();
|
||||||
|
CvInvoke.Normalize(mat16, mat8, 0, 255, NormType.MinMax, DepthType.Cv8U);
|
||||||
|
|
||||||
|
// 灰度转 BGR(立式 CT 显示需要 RGB 格式)
|
||||||
|
using var matBgr = new Mat();
|
||||||
|
CvInvoke.CvtColor(mat8, matBgr, ColorConversion.Gray2Bgr);
|
||||||
|
|
||||||
|
// Mat 转 BitmapSource
|
||||||
|
return MatToBitmapSource(matBgr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将 Emgu CV Mat 转换为 WPF BitmapSource
|
||||||
|
/// </summary>
|
||||||
|
private static BitmapSource MatToBitmapSource(Mat mat)
|
||||||
|
{
|
||||||
|
int width = mat.Width;
|
||||||
|
int height = mat.Height;
|
||||||
|
int channels = mat.NumberOfChannels;
|
||||||
|
int stride = mat.Step;
|
||||||
|
|
||||||
|
byte[] pixels = new byte[height * stride];
|
||||||
|
Marshal.Copy(mat.DataPointer, pixels, 0, pixels.Length);
|
||||||
|
|
||||||
|
var format = channels == 3 ? PixelFormats.Bgr24 : PixelFormats.Gray8;
|
||||||
|
return BitmapSource.Create(width, height, 96, 96, format, null, pixels, stride);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Database.Models;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分页计算辅助工具
|
||||||
|
/// </summary>
|
||||||
|
public static class PaginationHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 计算分页偏移量(SQLite OFFSET)
|
||||||
|
/// </summary>
|
||||||
|
public static int CalculateOffset(PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
if (pagination.PageIndex < 1) pagination.PageIndex = 1;
|
||||||
|
if (pagination.PageSize < 1) pagination.PageSize = 20;
|
||||||
|
return (pagination.PageIndex - 1) * pagination.PageSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证并修正分页参数
|
||||||
|
/// </summary>
|
||||||
|
public static PaginationRequest ValidateAndFix(PaginationRequest pagination)
|
||||||
|
{
|
||||||
|
var fixedPagination = new PaginationRequest
|
||||||
|
{
|
||||||
|
PageIndex = pagination.PageIndex < 1 ? 1 : pagination.PageIndex,
|
||||||
|
PageSize = pagination.PageSize < 1 ? 20 : (pagination.PageSize > 1000 ? 1000 : pagination.PageSize),
|
||||||
|
OrderBy = pagination.OrderBy ?? string.Empty
|
||||||
|
};
|
||||||
|
return fixedPagination;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用CRUD SQL构建器(适配SQLite)
|
||||||
|
/// </summary>
|
||||||
|
public static class SqlBuilderHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 构建插入SQL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">表名</param>
|
||||||
|
/// <param name="columns">列名集合</param>
|
||||||
|
/// <returns>插入SQL(参数名:@列名)</returns>
|
||||||
|
public static string BuildInsertSql(string tableName, IEnumerable<string> columns)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||||
|
var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
|
||||||
|
if (columnList.Count == 0) throw new ArgumentException("列名集合不能为空");
|
||||||
|
|
||||||
|
var columnsStr = string.Join(", ", columnList);
|
||||||
|
var paramsStr = string.Join(", ", columnList.Select(c => $"@{c}"));
|
||||||
|
return $"INSERT INTO {tableName} ({columnsStr}) VALUES ({paramsStr})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建更新SQL
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tableName">表名</param>
|
||||||
|
/// <param name="updateColumns">更新列名</param>
|
||||||
|
/// <param name="whereColumns">条件列名(如Id)</param>
|
||||||
|
/// <returns>更新SQL</returns>
|
||||||
|
public static string BuildUpdateSql(string tableName, IEnumerable<string> updateColumns, IEnumerable<string> whereColumns)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||||
|
var updateList = updateColumns?.ToList() ?? throw new ArgumentNullException(nameof(updateColumns));
|
||||||
|
var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
|
||||||
|
if (updateList.Count == 0) throw new ArgumentException("更新列名集合不能为空");
|
||||||
|
if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
|
||||||
|
|
||||||
|
var updateStr = string.Join(", ", updateList.Select(c => $"{c}=@{c}"));
|
||||||
|
var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
|
||||||
|
return $"UPDATE {tableName} SET {updateStr} WHERE {whereStr}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建删除SQL
|
||||||
|
/// </summary>
|
||||||
|
public static string BuildDeleteSql(string tableName, IEnumerable<string> whereColumns)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||||
|
var whereList = whereColumns?.ToList() ?? throw new ArgumentNullException(nameof(whereColumns));
|
||||||
|
if (whereList.Count == 0) throw new ArgumentException("条件列名集合不能为空");
|
||||||
|
|
||||||
|
var whereStr = string.Join(" AND ", whereList.Select(c => $"{c}=@{c}"));
|
||||||
|
return $"DELETE FROM {tableName} WHERE {whereStr}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构建查询SQL
|
||||||
|
/// </summary>
|
||||||
|
public static string BuildSelectSql(string tableName, IEnumerable<string> columns, IEnumerable<string>? whereColumns = null, string orderBy = "")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(tableName)) throw new ArgumentNullException(nameof(tableName));
|
||||||
|
var columnList = columns?.ToList() ?? throw new ArgumentNullException(nameof(columns));
|
||||||
|
if (columnList.Count == 0) columnList.Add("*");
|
||||||
|
|
||||||
|
var columnsStr = string.Join(", ", columnList);
|
||||||
|
var sql = new StringBuilder($"SELECT {columnsStr} FROM {tableName}");
|
||||||
|
|
||||||
|
// 添加WHERE条件
|
||||||
|
if (whereColumns != null && whereColumns.Any())
|
||||||
|
{
|
||||||
|
var whereStr = string.Join(" AND ", whereColumns.Select(c => $"{c}=@{c}"));
|
||||||
|
sql.Append($" WHERE {whereStr}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加排序
|
||||||
|
if (!string.IsNullOrEmpty(orderBy))
|
||||||
|
{
|
||||||
|
sql.Append($" ORDER BY {orderBy}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite参数化查询辅助工具
|
||||||
|
/// </summary>
|
||||||
|
public static class SqliteParameterHelper
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建参数字典
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="keyValues">参数名-值对(如 ("Id", 1), ("Name", "Test"))</param>
|
||||||
|
/// <returns>参数字典</returns>
|
||||||
|
public static Dictionary<string, object> CreateParameters(params (string Key, object Value)[] keyValues)
|
||||||
|
{
|
||||||
|
var parameters = new Dictionary<string, object>();
|
||||||
|
foreach (var (key, value) in keyValues)
|
||||||
|
{
|
||||||
|
parameters.Add(key, value);
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 合并参数字典
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, object> MergeParameters(params Dictionary<string, object>[] paramLists)
|
||||||
|
{
|
||||||
|
var merged = new Dictionary<string, object>();
|
||||||
|
foreach (var paramList in paramLists)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in paramList)
|
||||||
|
{
|
||||||
|
if (!merged.ContainsKey(key))
|
||||||
|
{
|
||||||
|
merged.Add(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Database.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库操作执行结果实体
|
||||||
|
/// </summary>
|
||||||
|
public class DbExecuteResult : IDbExecuteResult
|
||||||
|
{
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
public int RowsAffected { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
public Exception? Exception { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速创建成功结果
|
||||||
|
/// </summary>
|
||||||
|
public static DbExecuteResult Success(string message = "执行成功", int rowsAffected = 0)
|
||||||
|
{
|
||||||
|
return new DbExecuteResult
|
||||||
|
{
|
||||||
|
IsSuccess = true,
|
||||||
|
Message = message,
|
||||||
|
RowsAffected = rowsAffected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 快速创建失败结果
|
||||||
|
/// </summary>
|
||||||
|
public static DbExecuteResult Fail(string message, Exception? ex = null, int rowsAffected = 0)
|
||||||
|
{
|
||||||
|
return new DbExecuteResult
|
||||||
|
{
|
||||||
|
IsSuccess = false,
|
||||||
|
Message = message,
|
||||||
|
Exception = ex,
|
||||||
|
RowsAffected = rowsAffected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,527 @@
|
|||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Configs;
|
||||||
|
using XP.Common.Database.Interfaces;
|
||||||
|
using XP.Common.Database.Models;
|
||||||
|
using XP.Common.Helpers;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
using CustomDbTransaction = XP.Common.Database.Interfaces.IDbTransaction;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite 核心操作实现(适配IDbContext)
|
||||||
|
/// </summary>
|
||||||
|
public class SqliteContext : IDbContext
|
||||||
|
{
|
||||||
|
private readonly SqliteConfig _config;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private SqliteConnection? _connection;
|
||||||
|
private bool _isDisposed = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数(DI注入配置+日志)
|
||||||
|
/// </summary>
|
||||||
|
public SqliteContext(SqliteConfig config, ILoggerService logger)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
_logger = logger.ForModule("Sqlite.Context");
|
||||||
|
|
||||||
|
// 自动创建数据库目录
|
||||||
|
if (_config.CreateIfNotExists)
|
||||||
|
{
|
||||||
|
var dbDir = Path.GetDirectoryName(_config.DbFilePath);
|
||||||
|
if (!string.IsNullOrEmpty(dbDir) && !Directory.Exists(dbDir))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dbDir);
|
||||||
|
_logger.Info("自动创建数据库目录:{DirPath}", dbDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 连接管理
|
||||||
|
public IDbExecuteResult OpenConnection()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_connection != null && _connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
return DbExecuteResult.Success("连接已打开");
|
||||||
|
}
|
||||||
|
|
||||||
|
_connection = new SqliteConnection(_config.GetConnectionString());
|
||||||
|
_connection.Open();
|
||||||
|
_logger.Debug("SQLite连接已打开:{DbPath}", _config.DbFilePath);
|
||||||
|
|
||||||
|
// 启用WAL模式提升性能
|
||||||
|
if (_config.EnableWalMode)
|
||||||
|
{
|
||||||
|
using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
_logger.Debug("SQLite已启用WAL模式");
|
||||||
|
}
|
||||||
|
|
||||||
|
return DbExecuteResult.Success("连接打开成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQLite连接打开失败:{DbPath}", _config.DbFilePath);
|
||||||
|
return DbExecuteResult.Fail("连接打开失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDbExecuteResult> OpenConnectionAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_connection != null && _connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
return DbExecuteResult.Success("连接已打开");
|
||||||
|
}
|
||||||
|
|
||||||
|
_connection = new SqliteConnection(_config.GetConnectionString());
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
_logger.Debug("SQLite连接已异步打开:{DbPath}", _config.DbFilePath);
|
||||||
|
|
||||||
|
if (_config.EnableWalMode)
|
||||||
|
{
|
||||||
|
using var cmd = new SqliteCommand("PRAGMA journal_mode=WAL;", _connection);
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DbExecuteResult.Success("连接异步打开成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQLite连接异步打开失败:{DbPath}", _config.DbFilePath);
|
||||||
|
return DbExecuteResult.Fail("连接异步打开失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取有效连接(自动打开)
|
||||||
|
/// </summary>
|
||||||
|
private SqliteConnection GetValidConnection()
|
||||||
|
{
|
||||||
|
if (_connection == null || _connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
var result = OpenConnection();
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"获取SQLite连接失败:{result.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _connection!;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 事务管理
|
||||||
|
public (IDbExecuteResult Result, CustomDbTransaction? Transaction) BeginTransaction()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var conn = GetValidConnection();
|
||||||
|
// 系统的SqliteTransaction
|
||||||
|
var innerSystemTrans = conn.BeginTransaction();
|
||||||
|
// 封装为自定义的SqliteTransaction
|
||||||
|
var customTrans = new SqliteTransaction(innerSystemTrans, _logger);
|
||||||
|
_logger.Debug("SQLite事务已开始");
|
||||||
|
// 返回:自定义IDbTransaction(解决返回类型不匹配)
|
||||||
|
return (DbExecuteResult.Success("事务开始成功"), customTrans);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQLite事务开始失败");
|
||||||
|
return (DbExecuteResult.Fail("事务开始失败", ex), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 通用SQL执行
|
||||||
|
public IDbExecuteResult ExecuteNonQuery(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
var rowsAffected = cmd.ExecuteNonQuery();
|
||||||
|
_logger.Debug("SQL执行成功,影响行数:{Rows}", rowsAffected);
|
||||||
|
return DbExecuteResult.Success("执行成功", rowsAffected);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL执行失败:{Sql}", sql);
|
||||||
|
return DbExecuteResult.Fail("执行失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDbExecuteResult> ExecuteNonQueryAsync(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
var rowsAffected = await cmd.ExecuteNonQueryAsync();
|
||||||
|
_logger.Debug("SQL异步执行成功,影响行数:{Rows}", rowsAffected);
|
||||||
|
return DbExecuteResult.Success("异步执行成功", (int)rowsAffected);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL异步执行失败:{Sql}", sql);
|
||||||
|
return DbExecuteResult.Fail("异步执行失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (IDbExecuteResult Result, T? Value) ExecuteScalar<T>(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
var resultObj = cmd.ExecuteScalar();
|
||||||
|
T? value = default;
|
||||||
|
if (resultObj != null && resultObj != DBNull.Value)
|
||||||
|
{
|
||||||
|
if (resultObj is T directValue)
|
||||||
|
{
|
||||||
|
value = directValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 类型不匹配时安全转换,处理值类型/引用类型
|
||||||
|
value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var logValue = value ?? (object)"null";
|
||||||
|
_logger.Debug("SQL标量查询成功,返回值:{Value}", logValue);
|
||||||
|
return (DbExecuteResult.Success("标量查询成功"), value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL标量查询失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("标量查询失败", ex), default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync<T>(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
var resultObj = await cmd.ExecuteScalarAsync();
|
||||||
|
T? value = default;
|
||||||
|
if (resultObj != null && resultObj != DBNull.Value)
|
||||||
|
{
|
||||||
|
if (resultObj is T directValue)
|
||||||
|
{
|
||||||
|
value = directValue;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 类型不匹配时安全转换,处理值类型/引用类型
|
||||||
|
value = (T)Convert.ChangeType(resultObj, Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var logValue = value ?? (object)"null";
|
||||||
|
_logger.Debug("SQL异步标量查询成功,返回值:{Value}", logValue);
|
||||||
|
return (DbExecuteResult.Success("异步标量查询成功"), value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL异步标量查询失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("异步标量查询失败", ex), default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 查询结果映射
|
||||||
|
public (IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
|
||||||
|
var dt = new DataTable();
|
||||||
|
// 添加列
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
|
||||||
|
}
|
||||||
|
// 添加行
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var row = dt.NewRow();
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
row[i] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
|
||||||
|
}
|
||||||
|
dt.Rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Debug("SQL查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
|
||||||
|
return (DbExecuteResult.Success("查询成功"), dt);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL查询DataTable失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("查询失败", ex), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
LogSql(sql, parameters);
|
||||||
|
using var cmd = CreateCommand(sql, parameters);
|
||||||
|
using var reader = await cmd.ExecuteReaderAsync();
|
||||||
|
|
||||||
|
var dt = new DataTable();
|
||||||
|
// 添加列
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
dt.Columns.Add(reader.GetName(i), reader.GetFieldType(i));
|
||||||
|
}
|
||||||
|
// 添加行(异步读取)
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
var row = dt.NewRow();
|
||||||
|
for (int i = 0; i < reader.FieldCount; i++)
|
||||||
|
{
|
||||||
|
row[i] = await reader.IsDBNullAsync(i) ? DBNull.Value : reader.GetValue(i);
|
||||||
|
}
|
||||||
|
dt.Rows.Add(row);
|
||||||
|
}
|
||||||
|
_logger.Debug("SQL异步查询成功,返回DataTable行数:{Rows}", dt.Rows.Count);
|
||||||
|
return (DbExecuteResult.Success("异步查询成功"), dt);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL异步查询DataTable失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("异步查询失败", ex), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public (IDbExecuteResult Result, List<T> Data) QueryList<T>(string sql, Dictionary<string, object>? parameters = null) where T : new()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (result, dt) = ExecuteDataTable(sql, parameters);
|
||||||
|
if (!result.IsSuccess || dt == null)
|
||||||
|
{
|
||||||
|
return (result, new List<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = MapDataTableToEntityList<T>(dt);
|
||||||
|
return (DbExecuteResult.Success("实体列表查询成功"), list);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL查询实体列表失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("实体列表查询失败", ex), new List<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IDbExecuteResult Result, List<T> Data)> QueryListAsync<T>(string sql, Dictionary<string, object>? parameters = null) where T : new()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (result, dt) = await ExecuteDataTableAsync(sql, parameters);
|
||||||
|
if (!result.IsSuccess || dt == null)
|
||||||
|
{
|
||||||
|
return (result, new List<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
var list = MapDataTableToEntityList<T>(dt);
|
||||||
|
return (DbExecuteResult.Success("异步实体列表查询成功"), list);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL异步查询实体列表失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("异步实体列表查询失败", ex), new List<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 分页查询
|
||||||
|
public (IDbExecuteResult Result, PaginationResponse<T> Data) QueryPaged<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 查询总条数
|
||||||
|
var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
|
||||||
|
var (countResult, totalCount) = ExecuteScalar<int>(countSql, parameters);
|
||||||
|
if (!countResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return (countResult, new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建分页SQL(SQLite分页:LIMIT/OFFSET)
|
||||||
|
var offset = (pagination.PageIndex - 1) * pagination.PageSize;
|
||||||
|
var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
|
||||||
|
|
||||||
|
// 3. 查询当前页数据
|
||||||
|
var (dataResult, list) = QueryList<T>(pagedSql, parameters);
|
||||||
|
if (!dataResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return (dataResult, new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 封装分页结果
|
||||||
|
var pagedResponse = new PaginationResponse<T>
|
||||||
|
{
|
||||||
|
PageIndex = pagination.PageIndex,
|
||||||
|
PageSize = pagination.PageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Data = list
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.Debug("SQL分页查询成功:页码{Page},条数{Size},总条数{Total}",
|
||||||
|
pagination.PageIndex, pagination.PageSize, totalCount);
|
||||||
|
return (DbExecuteResult.Success("分页查询成功"), pagedResponse);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL分页查询失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("分页查询失败", ex), new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<(IDbExecuteResult Result, PaginationResponse<T> Data)> QueryPagedAsync<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 1. 查询总条数
|
||||||
|
var countSql = $"SELECT COUNT(*) FROM ({sql}) AS TotalCountQuery";
|
||||||
|
var (countResult, totalCount) = await ExecuteScalarAsync<int>(countSql, parameters);
|
||||||
|
if (!countResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return (countResult, new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 构建分页SQL
|
||||||
|
var offset = (pagination.PageIndex - 1) * pagination.PageSize;
|
||||||
|
var pagedSql = $"{sql} {(!string.IsNullOrEmpty(pagination.OrderBy) ? $"ORDER BY {pagination.OrderBy}" : "")} LIMIT {pagination.PageSize} OFFSET {offset}";
|
||||||
|
|
||||||
|
// 3. 查询当前页数据
|
||||||
|
var (dataResult, list) = await QueryListAsync<T>(pagedSql, parameters);
|
||||||
|
if (!dataResult.IsSuccess)
|
||||||
|
{
|
||||||
|
return (dataResult, new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 封装分页结果
|
||||||
|
var pagedResponse = new PaginationResponse<T>
|
||||||
|
{
|
||||||
|
PageIndex = pagination.PageIndex,
|
||||||
|
PageSize = pagination.PageSize,
|
||||||
|
TotalCount = totalCount,
|
||||||
|
Data = list
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.Debug("SQL异步分页查询成功:页码{Page},条数{Size},总条数{Total}",
|
||||||
|
pagination.PageIndex, pagination.PageSize, totalCount);
|
||||||
|
return (DbExecuteResult.Success("异步分页查询成功"), pagedResponse);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQL异步分页查询失败:{Sql}", sql);
|
||||||
|
return (DbExecuteResult.Fail("异步分页查询失败", ex), new PaginationResponse<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 私有辅助方法
|
||||||
|
/// <summary>
|
||||||
|
/// 创建SQLite命令(带参数)
|
||||||
|
/// </summary>
|
||||||
|
private SqliteCommand CreateCommand(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
var cmd = new SqliteCommand(sql, GetValidConnection());
|
||||||
|
cmd.CommandTimeout = _config.ConnectionTimeout;
|
||||||
|
|
||||||
|
// 添加参数(防SQL注入)
|
||||||
|
if (parameters != null && parameters.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in parameters)
|
||||||
|
{
|
||||||
|
var paramValue = value ?? DBNull.Value;
|
||||||
|
cmd.Parameters.AddWithValue($"@{key}", paramValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录SQL日志(调试模式)
|
||||||
|
/// </summary>
|
||||||
|
private void LogSql(string sql, Dictionary<string, object>? parameters = null)
|
||||||
|
{
|
||||||
|
if (!_config.EnableSqlLogging) return;
|
||||||
|
|
||||||
|
var paramStr = parameters == null
|
||||||
|
? "无参数"
|
||||||
|
: string.Join(", ", parameters.Select(kv => $"{kv.Key}={kv.Value}"));
|
||||||
|
_logger.Debug("执行SQL:{Sql} | 参数:{Params}", sql, paramStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DataTable映射为实体列表
|
||||||
|
/// </summary>
|
||||||
|
private List<T> MapDataTableToEntityList<T>(DataTable dt) where T : new()
|
||||||
|
{
|
||||||
|
var list = new List<T>();
|
||||||
|
var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
|
||||||
|
foreach (DataRow row in dt.Rows)
|
||||||
|
{
|
||||||
|
var entity = new T();
|
||||||
|
foreach (var prop in props)
|
||||||
|
{
|
||||||
|
if (dt.Columns.Contains(prop.Name) && row[prop.Name] != DBNull.Value)
|
||||||
|
{
|
||||||
|
var value = Convert.ChangeType(row[prop.Name], prop.PropertyType);
|
||||||
|
prop.SetValue(entity, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.Add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 资源释放
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed) return;
|
||||||
|
|
||||||
|
// 关闭连接
|
||||||
|
if (_connection != null)
|
||||||
|
{
|
||||||
|
if (_connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
_connection.Close();
|
||||||
|
_logger.Debug("SQLite连接已关闭(Dispose)");
|
||||||
|
}
|
||||||
|
_connection.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDisposed = true;
|
||||||
|
_logger.Debug("SQLiteContext资源已释放");
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using XP.Common.Database.Interfaces;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite 事务实现
|
||||||
|
/// </summary>
|
||||||
|
public class SqliteTransaction : IDbTransaction
|
||||||
|
{
|
||||||
|
private readonly Microsoft.Data.Sqlite.SqliteTransaction _innerTransaction;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private bool _isDisposed = false;
|
||||||
|
|
||||||
|
public SqliteTransaction(Microsoft.Data.Sqlite.SqliteTransaction innerTrans, ILoggerService logger)
|
||||||
|
{
|
||||||
|
_innerTransaction = innerTrans ?? throw new ArgumentNullException(nameof(innerTrans));
|
||||||
|
_logger = logger.ForModule("Sqlite.Transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDbExecuteResult Commit()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_innerTransaction.Commit();
|
||||||
|
_logger.Debug("SQLite事务提交成功");
|
||||||
|
return DbExecuteResult.Success("事务提交成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQLite事务提交失败");
|
||||||
|
return DbExecuteResult.Fail("事务提交失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDbExecuteResult Rollback()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_innerTransaction.Rollback();
|
||||||
|
_logger.Debug("SQLite事务回滚成功");
|
||||||
|
return DbExecuteResult.Success("事务回滚成功");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "SQLite事务回滚失败");
|
||||||
|
return DbExecuteResult.Fail("事务回滚失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_isDisposed) return;
|
||||||
|
_innerTransaction.Dispose();
|
||||||
|
_isDisposed = true;
|
||||||
|
_logger.Debug("SQLite事务资源已释放");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Database.Models;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用数据库操作接口(适配任意数据库,SQLite为具体实现)
|
||||||
|
/// </summary>
|
||||||
|
public interface IDbContext : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 打开数据库连接
|
||||||
|
/// </summary>
|
||||||
|
IDbExecuteResult OpenConnection();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步打开数据库连接
|
||||||
|
/// </summary>
|
||||||
|
Task<IDbExecuteResult> OpenConnectionAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开始事务
|
||||||
|
/// </summary>
|
||||||
|
(IDbExecuteResult Result, IDbTransaction? Transaction) BeginTransaction();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行增删改SQL(无返回值)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sql">SQL语句(参数化)</param>
|
||||||
|
/// <param name="parameters">参数集合(key=参数名,value=参数值)</param>
|
||||||
|
IDbExecuteResult ExecuteNonQuery(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行增删改SQL
|
||||||
|
/// </summary>
|
||||||
|
Task<IDbExecuteResult> ExecuteNonQueryAsync(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行查询并返回单个值(如Count/Sum)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">返回值类型</typeparam>
|
||||||
|
/// <param name="sql">SQL语句</param>
|
||||||
|
/// <param name="parameters">参数集合</param>
|
||||||
|
(IDbExecuteResult Result, T? Value) ExecuteScalar<T>(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行查询并返回单个值
|
||||||
|
/// </summary>
|
||||||
|
Task<(IDbExecuteResult Result, T? Value)> ExecuteScalarAsync<T>(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行查询并返回DataTable
|
||||||
|
/// </summary>
|
||||||
|
(IDbExecuteResult Result, DataTable? Data) ExecuteDataTable(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行查询并返回DataTable
|
||||||
|
/// </summary>
|
||||||
|
Task<(IDbExecuteResult Result, DataTable? Data)> ExecuteDataTableAsync(string sql, Dictionary<string, object>? parameters = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行查询并映射为实体列表
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型(需有无参构造函数)</typeparam>
|
||||||
|
(IDbExecuteResult Result, List<T> Data) QueryList<T>(string sql, Dictionary<string, object>? parameters = null) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行查询并映射为实体列表
|
||||||
|
/// </summary>
|
||||||
|
Task<(IDbExecuteResult Result, List<T> Data)> QueryListAsync<T>(string sql, Dictionary<string, object>? parameters = null) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行分页查询并返回分页结果
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">实体类型</typeparam>
|
||||||
|
(IDbExecuteResult Result, PaginationResponse<T> Data) QueryPaged<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步执行分页查询并返回分页结果
|
||||||
|
/// </summary>
|
||||||
|
Task<(IDbExecuteResult Result, PaginationResponse<T> Data)> QueryPagedAsync<T>(string sql, PaginationRequest pagination, Dictionary<string, object>? parameters = null) where T : new();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库操作通用执行结果
|
||||||
|
/// </summary>
|
||||||
|
public interface IDbExecuteResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否执行成功
|
||||||
|
/// </summary>
|
||||||
|
bool IsSuccess { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 影响行数(增删改)
|
||||||
|
/// </summary>
|
||||||
|
int RowsAffected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 消息(成功/失败提示)
|
||||||
|
/// </summary>
|
||||||
|
string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常信息(失败时非空)
|
||||||
|
/// </summary>
|
||||||
|
Exception? Exception { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库事务接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IDbTransaction : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 提交事务
|
||||||
|
/// </summary>
|
||||||
|
IDbExecuteResult Commit();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 回滚事务
|
||||||
|
/// </summary>
|
||||||
|
IDbExecuteResult Rollback();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用分页请求模型
|
||||||
|
/// </summary>
|
||||||
|
public class PaginationRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码(从1开始)
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序字段(如:CreateTime DESC)
|
||||||
|
/// </summary>
|
||||||
|
public string OrderBy { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace XP.Common.Database.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用分页响应模型
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型</typeparam>
|
||||||
|
public class PaginationResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 页码
|
||||||
|
/// </summary>
|
||||||
|
public int PageIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 每页条数
|
||||||
|
/// </summary>
|
||||||
|
public int PageSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总条数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总页数
|
||||||
|
/// </summary>
|
||||||
|
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前页数据
|
||||||
|
/// </summary>
|
||||||
|
public List<T> Data { get; set; } = new List<T>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否有下一页
|
||||||
|
/// </summary>
|
||||||
|
public bool HasNextPage => PageIndex < TotalPages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# Dump 文件管理服务使用指南 | Dump File Management Service Usage Guide
|
||||||
|
|
||||||
|
## 概述 | Overview
|
||||||
|
|
||||||
|
XplorePlane 提供 Dump 文件管理功能,用于在应用程序崩溃或需要诊断时生成进程转储文件。支持三种触发方式:崩溃自动触发、定时触发和手动触发,并提供文件大小限制、自动清理和可配置存储路径等管理能力。
|
||||||
|
|
||||||
|
Dump 功能通过 Windows `MiniDumpWriteDump` API 实现,作为 `XP.Common` 的子模块集成到 `CommonModule` 中。
|
||||||
|
|
||||||
|
## 基本用法 | Basic Usage
|
||||||
|
|
||||||
|
### 通过依赖注入获取服务 | Get Service via DI
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class DiagnosticsService
|
||||||
|
{
|
||||||
|
private readonly IDumpService _dumpService;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public DiagnosticsService(IDumpService dumpService, ILoggerService logger)
|
||||||
|
{
|
||||||
|
_dumpService = dumpService ?? throw new ArgumentNullException(nameof(dumpService));
|
||||||
|
_logger = logger?.ForModule<DiagnosticsService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动生成 Mini Dump | Manually generate Mini Dump
|
||||||
|
/// </summary>
|
||||||
|
public void CaptureMiniDump()
|
||||||
|
{
|
||||||
|
var filePath = _dumpService.CreateMiniDump();
|
||||||
|
if (filePath != null)
|
||||||
|
{
|
||||||
|
_logger.Info("Mini Dump 已生成:{FilePath} | Mini Dump generated: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动生成 Full Dump(包含完整内存)| Manually generate Full Dump (full memory)
|
||||||
|
/// </summary>
|
||||||
|
public void CaptureFullDump()
|
||||||
|
{
|
||||||
|
var filePath = _dumpService.CreateFullDump();
|
||||||
|
if (filePath != null)
|
||||||
|
{
|
||||||
|
_logger.Info("Full Dump 已生成:{FilePath} | Full Dump generated: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 触发方式 | Trigger Modes
|
||||||
|
|
||||||
|
### 1. 崩溃自动触发 | Crash Auto Trigger
|
||||||
|
|
||||||
|
服务启动后自动订阅 `AppDomain.CurrentDomain.UnhandledException` 和 `TaskScheduler.UnobservedTaskException`,无需额外配置。崩溃时自动生成 Mini Dump。
|
||||||
|
|
||||||
|
### 2. 定时触发 | Scheduled Trigger
|
||||||
|
|
||||||
|
在 `App.config` 中启用定时触发后,服务按配置的时间间隔周期性生成 Mini Dump:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<add key="Dump:EnableScheduledDump" value="true" />
|
||||||
|
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 手动触发 | Manual Trigger
|
||||||
|
|
||||||
|
通过 `IDumpService` 接口的方法手动触发:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 生成 Mini Dump(线程信息 + 数据段 + 句柄信息)
|
||||||
|
// Generate Mini Dump (thread info + data segments + handle data)
|
||||||
|
string? miniPath = _dumpService.CreateMiniDump();
|
||||||
|
|
||||||
|
// 生成 Full Dump(完整内存,仅手动触发允许)
|
||||||
|
// Generate Full Dump (full memory, manual trigger only)
|
||||||
|
string? fullPath = _dumpService.CreateFullDump();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dump 类型说明 | Dump Type Description
|
||||||
|
|
||||||
|
| 类型 | 包含内容 | 文件大小 | 触发限制 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| Mini Dump | 线程信息、数据段、句柄信息 | 较小(受大小限制约束) | 所有触发方式 |
|
||||||
|
| Full Dump | 进程完整内存 | 较大(无大小限制) | 仅手动触发 |
|
||||||
|
|
||||||
|
> 非手动触发(崩溃、定时)请求 Full Dump 时,系统会自动降级为 Mini Dump。
|
||||||
|
|
||||||
|
## 文件命名规则 | File Naming Convention
|
||||||
|
|
||||||
|
格式:`XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp`
|
||||||
|
|
||||||
|
示例:
|
||||||
|
- `XplorePlane_20260317_1530_Crash.dmp` — 崩溃触发
|
||||||
|
- `XplorePlane_20260317_1600_Scheduled.dmp` — 定时触发
|
||||||
|
- `XplorePlane_20260317_1645_Manual.dmp` — 手动触发
|
||||||
|
|
||||||
|
## 自动清理 | Auto Cleanup
|
||||||
|
|
||||||
|
- 服务启动时立即执行一次清理
|
||||||
|
- 运行期间每 24 小时执行一次清理
|
||||||
|
- 超过保留天数(默认 7 天)的 `.dmp` 文件会被自动删除
|
||||||
|
- 单个文件删除失败不影响其余文件的清理
|
||||||
|
|
||||||
|
## 配置 | Configuration
|
||||||
|
|
||||||
|
在 `App.config` 的 `<appSettings>` 中配置:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<appSettings>
|
||||||
|
<!-- Dump 文件存储路径 | Dump file storage path -->
|
||||||
|
<add key="Dump:StoragePath" value="D:\XplorePlane\Dump" />
|
||||||
|
<!-- 是否启用定时触发 | Enable scheduled trigger -->
|
||||||
|
<add key="Dump:EnableScheduledDump" value="false" />
|
||||||
|
<!-- 定时触发间隔(分钟)| Scheduled trigger interval (minutes) -->
|
||||||
|
<add key="Dump:ScheduledIntervalMinutes" value="60" />
|
||||||
|
<!-- Mini Dump 文件大小上限(MB)| Mini Dump file size limit (MB) -->
|
||||||
|
<add key="Dump:MiniDumpSizeLimitMB" value="100" />
|
||||||
|
<!-- 文件保留天数 | File retention days -->
|
||||||
|
<add key="Dump:RetentionDays" value="7" />
|
||||||
|
</appSettings>
|
||||||
|
```
|
||||||
|
|
||||||
|
| 配置项 | 默认值 | 说明 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `Dump:StoragePath` | `D:\XplorePlane\Dump` | Dump 文件存储路径 |
|
||||||
|
| `Dump:EnableScheduledDump` | `false` | 是否启用定时触发 |
|
||||||
|
| `Dump:ScheduledIntervalMinutes` | `60` | 定时触发间隔(分钟) |
|
||||||
|
| `Dump:MiniDumpSizeLimitMB` | `100` | Mini Dump 文件大小上限(MB),超过则删除 |
|
||||||
|
| `Dump:RetentionDays` | `7` | 文件保留天数,超过则自动清理 |
|
||||||
|
|
||||||
|
## 错误处理 | Error Handling
|
||||||
|
|
||||||
|
Dump 功能遵循"记录并继续"原则,自身错误不影响主应用程序运行:
|
||||||
|
|
||||||
|
- Dump 文件写入失败 → 记录错误日志,返回 `null`,不抛出异常
|
||||||
|
- Mini Dump 超过大小限制 → 删除文件,记录警告
|
||||||
|
- 存储目录创建失败 → 回退到默认路径 `D:\XplorePlane\Dump`
|
||||||
|
- 清理过程中文件删除失败 → 记录错误,继续清理其余文件
|
||||||
|
|
||||||
|
## IDumpService 接口 | IDumpService Interface
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IDumpService : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
|
||||||
|
/// </summary>
|
||||||
|
string? CreateMiniDump();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
|
||||||
|
/// </summary>
|
||||||
|
string? CreateFullDump();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动服务 | Start service
|
||||||
|
/// </summary>
|
||||||
|
void Start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止服务 | Stop service
|
||||||
|
/// </summary>
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文件结构 | File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
XP.Common/Dump/
|
||||||
|
├── Interfaces/
|
||||||
|
│ └── IDumpService.cs # 服务接口 | Service interface
|
||||||
|
├── Implementations/
|
||||||
|
│ ├── DumpService.cs # 服务实现 | Service implementation
|
||||||
|
│ └── DumpCleaner.cs # 自动清理组件 | Auto cleanup component
|
||||||
|
├── Configs/
|
||||||
|
│ ├── DumpConfig.cs # 配置实体 | Config entity
|
||||||
|
│ └── DumpTriggerType.cs # 触发类型枚举 | Trigger type enum
|
||||||
|
└── Native/
|
||||||
|
├── NativeMethods.cs # P/Invoke 声明 | P/Invoke declarations
|
||||||
|
└── MiniDumpType.cs # Dump 类型标志枚举 | Dump type flags enum
|
||||||
|
```
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# 通用窗体使用指南 | General Form Usage Guide
|
||||||
|
|
||||||
|
## 概述 | Overview
|
||||||
|
|
||||||
|
`XP.Common.GeneralForm` 提供 XplorePlane 项目中可复用的通用 WPF 窗体组件。当前包含以下窗体:
|
||||||
|
|
||||||
|
| 窗体 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `ProgressWindow` | 模态进度条窗口,支持线程安全的进度更新和关闭操作 |
|
||||||
|
| `InputDialog` | 通用输入对话框,支持单行文本输入、可选验证和多语言按钮 |
|
||||||
|
|
||||||
|
## 目录结构 | Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
XP.Common/GeneralForm/
|
||||||
|
├── ViewModels/
|
||||||
|
│ ├── InputDialogViewModel.cs # 输入对话框 ViewModel
|
||||||
|
│ └── ProgressWindowViewModel.cs # 进度窗口 ViewModel
|
||||||
|
└── Views/
|
||||||
|
├── InputDialog.xaml # 输入对话框 XAML 视图
|
||||||
|
├── InputDialog.xaml.cs # 输入对话框 Code-Behind
|
||||||
|
├── ProgressWindow.xaml # 进度窗口 XAML 视图
|
||||||
|
└── ProgressWindow.xaml.cs # 进度窗口 Code-Behind
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ProgressWindow 进度条窗口
|
||||||
|
|
||||||
|
### 功能特性 | Features
|
||||||
|
|
||||||
|
- 模态进度条窗口,居中显示,不可调整大小
|
||||||
|
- 线程安全:`UpdateProgress()` 和 `Close()` 可从任意线程调用,内部自动通过 `Dispatcher` 调度
|
||||||
|
- 可配置是否允许用户手动关闭窗口(`isCancelable` 参数)
|
||||||
|
- 不可取消时,通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
|
||||||
|
- 进度值自动 Clamp 到 `[0, 100]` 范围,超出范围时记录 Warn 日志
|
||||||
|
- 自动继承主窗口图标
|
||||||
|
- 使用 Telerik `RadProgressBar` 控件(Crystal 主题)
|
||||||
|
|
||||||
|
### 构造函数参数 | Constructor Parameters
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public ProgressWindow(
|
||||||
|
string title = "操作进行中", // 窗口标题
|
||||||
|
string message = "请稍候...", // 提示信息
|
||||||
|
bool isCancelable = true, // 是否允许用户关闭窗口
|
||||||
|
ILoggerService? logger = null // 日志服务(可选)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `title` | `string` | `"操作进行中"` | 窗口标题栏文本 |
|
||||||
|
| `message` | `string` | `"请稍候..."` | 进度条上方的提示信息 |
|
||||||
|
| `isCancelable` | `bool` | `true` | `true`:用户可手动关闭;`false`:禁用关闭按钮 |
|
||||||
|
| `logger` | `ILoggerService?` | `null` | 传入日志服务后自动记录窗口生命周期日志 |
|
||||||
|
|
||||||
|
### 公开方法 | Public Methods
|
||||||
|
|
||||||
|
#### UpdateProgress - 更新进度
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 线程安全,可从任意线程调用
|
||||||
|
void UpdateProgress(string message, double progress)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `message`:更新提示信息文本
|
||||||
|
- `progress`:进度值(0-100),超出范围自动修正
|
||||||
|
|
||||||
|
#### Close - 关闭窗口
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 线程安全,可从任意线程调用(隐藏基类 Window.Close())
|
||||||
|
new void Close()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 基本用法 | Basic Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.GeneralForm.Views;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
public class SomeService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public SomeService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
_logger = logger?.ForModule<SomeService>()
|
||||||
|
?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteLongOperation()
|
||||||
|
{
|
||||||
|
// 创建进度窗口(不可取消)
|
||||||
|
var progressWindow = new ProgressWindow(
|
||||||
|
title: "数据处理中",
|
||||||
|
message: "正在初始化...",
|
||||||
|
isCancelable: false,
|
||||||
|
logger: _logger);
|
||||||
|
|
||||||
|
// 显示模态窗口(需在 UI 线程调用)
|
||||||
|
// 注意:ShowDialog() 会阻塞,通常配合 Task 使用
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
progressWindow.UpdateProgress("正在加载数据...", 20);
|
||||||
|
await Task.Delay(1000); // 模拟耗时操作
|
||||||
|
|
||||||
|
progressWindow.UpdateProgress("正在处理数据...", 60);
|
||||||
|
await Task.Delay(1000);
|
||||||
|
|
||||||
|
progressWindow.UpdateProgress("即将完成...", 90);
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
progressWindow.UpdateProgress("完成", 100);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 关闭窗口(线程安全)
|
||||||
|
progressWindow.Close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
progressWindow.ShowDialog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 允许用户取消的用法 | Cancelable Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 创建可取消的进度窗口
|
||||||
|
var progressWindow = new ProgressWindow(
|
||||||
|
title: "文件导出",
|
||||||
|
message: "正在导出文件...",
|
||||||
|
isCancelable: true, // 用户可以点击关闭按钮取消
|
||||||
|
logger: _logger);
|
||||||
|
|
||||||
|
progressWindow.ShowDialog();
|
||||||
|
```
|
||||||
|
|
||||||
|
### ViewModel 绑定属性 | ViewModel Binding Properties
|
||||||
|
|
||||||
|
`ProgressWindowViewModel` 继承自 `BindableBase`,提供以下可绑定属性:
|
||||||
|
|
||||||
|
| 属性 | 类型 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Title` | `string` | 窗口标题(只读) |
|
||||||
|
| `Message` | `string` | 提示信息文本(可通知) |
|
||||||
|
| `Progress` | `double` | 进度值 0-100(可通知) |
|
||||||
|
| `ProgressText` | `string` | 百分比显示文本,如 `"75%"`(只读,自动计算) |
|
||||||
|
| `IsCancelable` | `bool` | 是否允许用户关闭(只读) |
|
||||||
|
|
||||||
|
### 注意事项 | Notes
|
||||||
|
|
||||||
|
1. `ShowDialog()` 必须在 UI 线程调用,它会阻塞当前线程直到窗口关闭
|
||||||
|
2. `UpdateProgress()` 和 `Close()` 内部已处理跨线程调度,可安全地从后台线程调用
|
||||||
|
3. 当 `isCancelable = false` 时,窗口关闭按钮会被 Win32 API 禁用(灰色),用户无法通过 Alt+F4 或点击关闭
|
||||||
|
4. 进度值超出 `[0, 100]` 范围时会自动修正并记录 Warn 级别日志
|
||||||
|
5. `Close()` 使用 `new` 关键字隐藏基类方法(因 `Window.Close()` 非虚方法),确保通过 `ProgressWindow` 类型引用调用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## InputDialog 输入对话框
|
||||||
|
|
||||||
|
### 功能特性 | Features
|
||||||
|
|
||||||
|
- 模态输入对话框,居中于父窗口显示,不可调整大小
|
||||||
|
- 自动继承主窗口图标
|
||||||
|
- 按钮文本支持多语言(使用 `Button_OK` / `Button_Cancel` 资源键)
|
||||||
|
- 可选的输入验证委托(`Func<string, string?>`),验证失败时在输入框下方显示红色错误提示
|
||||||
|
- 输入内容变化时自动清除验证错误
|
||||||
|
- 使用 Telerik `RadWatermarkTextBox` 和 `RadButton` 控件(Crystal 主题)
|
||||||
|
- 提供静态 `Show()` 便捷方法,一行代码即可调用
|
||||||
|
|
||||||
|
### 静态方法 | Static Method
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public static string? Show(
|
||||||
|
string prompt, // 提示文本
|
||||||
|
string title, // 窗口标题
|
||||||
|
string defaultValue = "", // 默认值
|
||||||
|
Func<string, string?>? validate = null, // 验证委托(可选)
|
||||||
|
Window? owner = null // 父窗口(可选)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 类型 | 默认值 | 说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `prompt` | `string` | 必填 | 输入框上方的提示文本 |
|
||||||
|
| `title` | `string` | 必填 | 窗口标题栏文本 |
|
||||||
|
| `defaultValue` | `string` | `""` | 输入框的初始值 |
|
||||||
|
| `validate` | `Func<string, string?>?` | `null` | 验证委托:返回 `null` 表示通过,返回错误信息则阻止确认 |
|
||||||
|
| `owner` | `Window?` | `null` | 父窗口,设置后对话框居中于父窗口 |
|
||||||
|
|
||||||
|
返回值:用户输入的字符串,取消时返回 `null`。
|
||||||
|
|
||||||
|
### 基本用法 | Basic Usage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.GeneralForm.Views;
|
||||||
|
|
||||||
|
// 最简用法,无验证
|
||||||
|
var name = InputDialog.Show("请输入名称:", "新建项目");
|
||||||
|
if (name == null) return; // 用户取消
|
||||||
|
|
||||||
|
// 带默认值
|
||||||
|
var port = InputDialog.Show("请输入端口号:", "配置", "8080");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 带验证的用法 | Usage with Validation
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 验证委托:返回 null 表示通过,返回错误信息则显示在输入框下方
|
||||||
|
var groupId = InputDialog.Show(
|
||||||
|
"请输入 Group ID:",
|
||||||
|
"新增 Group",
|
||||||
|
validate: input =>
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(input))
|
||||||
|
return "ID 不能为空";
|
||||||
|
if (existingIds.Contains(input))
|
||||||
|
return "ID 已存在,请使用不同的 ID";
|
||||||
|
return null; // 验证通过
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证非负整数
|
||||||
|
var dbNumber = InputDialog.Show(
|
||||||
|
"请输入 DB 块号(非负整数):",
|
||||||
|
"新增 Group",
|
||||||
|
"0",
|
||||||
|
validate: input =>
|
||||||
|
{
|
||||||
|
if (!int.TryParse(input, out int val) || val < 0)
|
||||||
|
return "必须为非负整数";
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项 | Notes
|
||||||
|
|
||||||
|
1. `Show()` 必须在 UI 线程调用(内部使用 `ShowDialog()`)
|
||||||
|
2. 验证委托在用户点击确定按钮时执行,验证失败不会关闭对话框
|
||||||
|
3. 用户修改输入内容时会自动清除上一次的验证错误提示
|
||||||
|
4. 按钮文本从 XP.Common 资源文件读取(`Button_OK` / `Button_Cancel`),自动跟随应用语言
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
|||||||
|
# 多语言支持快速开始 | Localization Quick Start
|
||||||
|
|
||||||
|
## 概述 | Overview
|
||||||
|
|
||||||
|
XplorePlane 多语言支持系统基于 .NET 原生 Resx 资源文件实现,与 Prism MVVM 架构无缝集成。系统支持简体中文(zh-CN)、繁体中文(zh-TW)和美式英语(en-US)三种语言。
|
||||||
|
|
||||||
|
### 核心特性 | Key Features
|
||||||
|
|
||||||
|
- ✅ 基于 .NET Resx 资源文件,编译时类型安全
|
||||||
|
- ✅ 简洁的 XAML 标记扩展语法
|
||||||
|
- ✅ 完整的 ViewModel 集成支持
|
||||||
|
- ✅ 语言设置持久化到 App.config
|
||||||
|
- ✅ 跨模块事件通知机制
|
||||||
|
- ✅ 健壮的错误处理和回退机制
|
||||||
|
- ✅ 多资源源 Fallback Chain 机制,支持模块级资源注册
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始 | Quick Start
|
||||||
|
|
||||||
|
### 1. 在 XAML 中使用本地化资源 | Using Localization in XAML
|
||||||
|
|
||||||
|
#### 基础用法 | Basic Usage
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<Window x:Class="XplorePlane.App.Views.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
|
||||||
|
Title="{loc:Localization Key=App_Title}">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<!-- 按钮文本本地化 | Localized button text -->
|
||||||
|
<Button Content="{loc:Localization Key=Button_OK}" />
|
||||||
|
|
||||||
|
<!-- 标签文本本地化 | Localized label text -->
|
||||||
|
<Label Content="{loc:Localization Key=Settings_Language}" />
|
||||||
|
|
||||||
|
<!-- 菜单项本地化 | Localized menu item -->
|
||||||
|
<MenuItem Header="{loc:Localization Key=Menu_File}" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 命名空间声明 | Namespace Declaration
|
||||||
|
|
||||||
|
在 XAML 文件顶部添加命名空间引用:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
xmlns:loc="clr-namespace:XplorePlane.Common.Localization.Extensions;assembly=XP.Common"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 语法说明 | Syntax Explanation
|
||||||
|
|
||||||
|
- `{loc:Localization Key=ResourceKey}` - 完整语法
|
||||||
|
- `{loc:Localization App_Title}` - 简化语法(Key 可省略)
|
||||||
|
- 资源键不存在时,显示键名本身(便于调试)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 在 C# 代码中使用静态帮助类 | Using Static Helper in C# Code
|
||||||
|
|
||||||
|
适用于不方便依赖注入的场景(静态方法、工具类等)。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.Localization;
|
||||||
|
|
||||||
|
// 基本用法 | Basic usage
|
||||||
|
var title = LocalizationHelper.Get("App_Title");
|
||||||
|
|
||||||
|
// 带格式化参数 | With format arguments
|
||||||
|
var errorMsg = LocalizationHelper.Get("Settings_Language_SwitchFailed", ex.Message);
|
||||||
|
```
|
||||||
|
|
||||||
|
- 在 ViewModel / Service 中优先使用 `ILocalizationService`(可测试、可 Mock)
|
||||||
|
- 在静态方法、工具类、或不方便注入的地方使用 `LocalizationHelper`
|
||||||
|
- 两者读取同一套 Resx 资源文件,结果一致
|
||||||
|
|
||||||
|
> **V1.4.1.1 变更:** `LocalizationHelper` 新增 `Initialize(ILocalizationService)` 方法。初始化后,`Get()` 会优先通过 `ILocalizationService` 获取字符串(支持 Fallback Chain);未初始化时仍兼容回退到原始 `ResourceManager`。建议在 `CommonModule` 或 App 启动时调用初始化。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 在 CommonModule 或 App 启动时调用 | Call at CommonModule or App startup
|
||||||
|
LocalizationHelper.Initialize(localizationService);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 在 ViewModel 中使用本地化服务 | Using Localization Service in ViewModel
|
||||||
|
|
||||||
|
#### 依赖注入 | Dependency Injection
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XplorePlane.Common.Localization.Interfaces;
|
||||||
|
using XplorePlane.Common.Localization.Enums;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
|
||||||
|
namespace XplorePlane.App.ViewModels
|
||||||
|
{
|
||||||
|
public class MyViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public MyViewModel(
|
||||||
|
ILocalizationService localizationService,
|
||||||
|
ILoggerService logger)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本地化字符串 | Get localized string
|
||||||
|
public string GetWelcomeMessage()
|
||||||
|
{
|
||||||
|
return _localizationService.GetString("Welcome_Message");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前语言 | Get current language
|
||||||
|
public SupportedLanguage CurrentLanguage => _localizationService.CurrentLanguage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 动态文本绑定 | Dynamic Text Binding
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class StatusViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private string _statusMessage;
|
||||||
|
|
||||||
|
public string StatusMessage
|
||||||
|
{
|
||||||
|
get => _statusMessage;
|
||||||
|
set => SetProperty(ref _statusMessage, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatusViewModel(ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
|
||||||
|
// 订阅语言切换事件 | Subscribe to language changed event
|
||||||
|
_localizationService.LanguageChanged += OnLanguageChanged;
|
||||||
|
|
||||||
|
// 初始化状态消息 | Initialize status message
|
||||||
|
UpdateStatusMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLanguageChanged(object sender, LanguageChangedEventArgs e)
|
||||||
|
{
|
||||||
|
// 语言切换时更新文本 | Update text when language changes
|
||||||
|
UpdateStatusMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStatusMessage()
|
||||||
|
{
|
||||||
|
StatusMessage = _localizationService.GetString("Status_Ready");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 数据验证消息本地化 | Localized Validation Messages
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class FormViewModel : BindableBase, IDataErrorInfo
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private string _username;
|
||||||
|
|
||||||
|
public string Username
|
||||||
|
{
|
||||||
|
get => _username;
|
||||||
|
set => SetProperty(ref _username, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string columnName]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (columnName == nameof(Username))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Username))
|
||||||
|
{
|
||||||
|
return _localizationService.GetString("Validation_UsernameRequired");
|
||||||
|
}
|
||||||
|
if (Username.Length < 3)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString("Validation_UsernameTooShort");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Error => null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 多资源源 Fallback Chain | Multi-Source Fallback Chain
|
||||||
|
|
||||||
|
V1.1 版本引入了多资源源 Fallback Chain 机制,允许各模块注册自己的 Resx 资源文件。查找资源键时,从最后注册的资源源开始向前遍历,第一个返回非 null 值的即为结果。
|
||||||
|
|
||||||
|
#### 架构说明 | Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Fallback Chain(查找顺序从右到左):
|
||||||
|
[XP.Common (默认)] → [XP.Scan (模块注册)] → [XP.Hardware (模块注册)]
|
||||||
|
↑ 最高优先级
|
||||||
|
```
|
||||||
|
|
||||||
|
- `XP.Common` 为默认资源源,始终位于 Chain[0],不可注销
|
||||||
|
- 后注册的模块优先级更高
|
||||||
|
- 单个资源源查找异常时自动跳过,继续遍历下一个
|
||||||
|
- 全部未找到时返回 key 本身并记录警告日志
|
||||||
|
|
||||||
|
#### 注册模块资源源 | Register Module Resource Source
|
||||||
|
|
||||||
|
在 Prism 模块的 `OnInitialized` 中注册:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Resources;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
|
||||||
|
public class ScanModule : IModule
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
|
||||||
|
public ScanModule(ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnInitialized(IContainerProvider containerProvider)
|
||||||
|
{
|
||||||
|
// 注册模块资源源到 Fallback Chain
|
||||||
|
var resourceManager = new ResourceManager(
|
||||||
|
"XP.Scan.Resources.Resources",
|
||||||
|
typeof(ScanModule).Assembly);
|
||||||
|
_localizationService.RegisterResourceSource("XP.Scan", resourceManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterTypes(IContainerRegistry containerRegistry) { }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注销模块资源源 | Unregister Module Resource Source
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 注销指定资源源(不可注销默认的 "XP.Common")
|
||||||
|
_localizationService.UnregisterResourceSource("XP.Scan");
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注意事项 | Notes
|
||||||
|
|
||||||
|
- 资源源名称不可重复,重复注册会抛出 `InvalidOperationException`
|
||||||
|
- 注销 `"XP.Common"` 会抛出 `InvalidOperationException`
|
||||||
|
- 注销不存在的名称会静默忽略并记录警告日志
|
||||||
|
- 线程安全:内部使用 `ReaderWriterLockSlim` 保护读写操作
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**版本 | Version:** 1.1
|
||||||
|
**最后更新 | Last Updated:** 2026-04-01
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
# 日志服务使用示例 | Logger Service Usage Examples
|
||||||
|
|
||||||
|
## 示例 1:服务类中使用 | Example 1: Usage in Service Class
|
||||||
|
|
||||||
|
### 旧方式(手动传递字符串)| Old Way (Manual String)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PlcService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 需要手动输入类名,容易出错
|
||||||
|
// Need to manually type class name, error-prone
|
||||||
|
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志输出 | Log output:
|
||||||
|
// [PlcService] 正在初始化 PLC 连接...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 新方式(自动类型推断)| New Way (Auto Type Inference)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PlcService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 自动获取完整类型名,重构安全
|
||||||
|
// Automatically get full type name, refactoring-safe
|
||||||
|
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日志输出 | Log output:
|
||||||
|
// [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例 2:ViewModel 中使用 | Example 2: Usage in ViewModel
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Hardware.RaySource.ViewModels
|
||||||
|
{
|
||||||
|
public class RaySourceOperateViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public RaySourceOperateViewModel(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 自动使用:XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
|
||||||
|
// Automatically uses: XP.Hardware.RaySource.ViewModels.RaySourceOperateViewModel
|
||||||
|
_logger = logger?.ForModule<RaySourceOperateViewModel>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
_logger.Info("射线源操作视图模型已初始化 | Ray source operate view model initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartXRay()
|
||||||
|
{
|
||||||
|
_logger.Info("用户请求启动射线 | User requested to start X-ray");
|
||||||
|
// ... 业务逻辑 | business logic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例 3:工厂类中使用 | Example 3: Usage in Factory Class
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Hardware.RaySource.Abstractions;
|
||||||
|
|
||||||
|
namespace XP.Hardware.RaySource.Factories
|
||||||
|
{
|
||||||
|
public class RaySourceFactory : IRaySourceFactory
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public RaySourceFactory(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 自动使用:XP.Hardware.RaySource.Factories.RaySourceFactory
|
||||||
|
// Automatically uses: XP.Hardware.RaySource.Factories.RaySourceFactory
|
||||||
|
_logger = logger?.ForModule<RaySourceFactory>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IXRaySource CreateRaySource(string deviceType)
|
||||||
|
{
|
||||||
|
_logger.Info("创建射线源实例:类型={DeviceType} | Creating ray source instance: type={DeviceType}", deviceType);
|
||||||
|
|
||||||
|
switch (deviceType)
|
||||||
|
{
|
||||||
|
case "Comet225":
|
||||||
|
return new Comet225RaySource(_logger);
|
||||||
|
default:
|
||||||
|
_logger.Error(null, "不支持的设备类型:{DeviceType} | Unsupported device type: {DeviceType}", deviceType);
|
||||||
|
throw new NotSupportedException($"不支持的设备类型:{deviceType}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例 4:静态方法中使用 | Example 4: Usage in Static Methods
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class ConfigLoader
|
||||||
|
{
|
||||||
|
public static PlcConfig LoadConfig(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 静态方法中也可以使用泛型
|
||||||
|
// Can also use generics in static methods
|
||||||
|
var log = logger.ForModule<ConfigLoader>();
|
||||||
|
|
||||||
|
log.Info("正在加载 PLC 配置 | Loading PLC configuration");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ... 加载逻辑 | loading logic
|
||||||
|
log.Info("PLC 配置加载成功 | PLC configuration loaded successfully");
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.Error(ex, "PLC 配置加载失败 | PLC configuration loading failed");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例 5:嵌套类中使用 | Example 5: Usage in Nested Classes
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class RaySourceService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public RaySourceService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
_logger = logger?.ForModule<RaySourceService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 嵌套类 | Nested class
|
||||||
|
public class ConnectionManager
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public ConnectionManager(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 自动使用:XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
|
||||||
|
// Automatically uses: XP.Hardware.RaySource.Services.RaySourceService+ConnectionManager
|
||||||
|
_logger = logger?.ForModule<ConnectionManager>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 示例 6:混合使用 | Example 6: Mixed Usage
|
||||||
|
|
||||||
|
有时你可能想要自定义模块名以保持简洁:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class VeryLongNamespaceAndClassName
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public VeryLongNamespaceAndClassName(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 选项 1:使用完整类型名(详细但冗长)
|
||||||
|
// Option 1: Use full type name (detailed but verbose)
|
||||||
|
// _logger = logger?.ForModule<VeryLongNamespaceAndClassName>();
|
||||||
|
// 输出 | Output: [XP.Some.Very.Long.Namespace.VeryLongNamespaceAndClassName]
|
||||||
|
|
||||||
|
// 选项 2:使用简短自定义名(简洁但需手动维护)
|
||||||
|
// Option 2: Use short custom name (concise but needs manual maintenance)
|
||||||
|
_logger = logger?.ForModule("VeryLong") ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
// 输出 | Output: [VeryLong]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 优势对比 | Advantages Comparison
|
||||||
|
|
||||||
|
### ForModule<T>() 的优势 | Advantages of ForModule<T>()
|
||||||
|
|
||||||
|
✅ **重构安全**:重命名类时自动更新
|
||||||
|
✅ **Refactoring-safe**: Automatically updates when renaming class
|
||||||
|
|
||||||
|
✅ **无拼写错误**:编译器检查类型
|
||||||
|
✅ **No typos**: Compiler checks type
|
||||||
|
|
||||||
|
✅ **完整信息**:包含命名空间,便于定位
|
||||||
|
✅ **Complete info**: Includes namespace, easy to locate
|
||||||
|
|
||||||
|
✅ **智能提示**:IDE 自动补全
|
||||||
|
✅ **IntelliSense**: IDE auto-completion
|
||||||
|
|
||||||
|
### ForModule(string) 的优势 | Advantages of ForModule(string)
|
||||||
|
|
||||||
|
✅ **简洁输出**:日志文件更易读
|
||||||
|
✅ **Concise output**: Log files more readable
|
||||||
|
|
||||||
|
✅ **自定义名称**:可以使用业务术语
|
||||||
|
✅ **Custom names**: Can use business terms
|
||||||
|
|
||||||
|
✅ **灵活性**:可以为不同场景使用不同名称
|
||||||
|
✅ **Flexibility**: Can use different names for different scenarios
|
||||||
|
|
||||||
|
## 推荐使用场景 | Recommended Usage Scenarios
|
||||||
|
|
||||||
|
| 场景 | 推荐方式 | 原因 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 服务类 | `ForModule<T>()` | 需要完整追踪 |
|
||||||
|
| ViewModel | `ForModule<T>()` | 需要完整追踪 |
|
||||||
|
| 工厂类 | `ForModule<T>()` | 需要完整追踪 |
|
||||||
|
| 简单工具类 | `ForModule("ToolName")` | 保持简洁 |
|
||||||
|
| 临时调试 | `ForModule("Debug")` | 快速定位 |
|
||||||
|
| 第三方集成 | `ForModule("ThirdParty.XXX")` | 明确标识 |
|
||||||
|
|
||||||
|
| Scenario | Recommended | Reason |
|
||||||
|
|----------|------------|--------|
|
||||||
|
| Service classes | `ForModule<T>()` | Need full tracing |
|
||||||
|
| ViewModels | `ForModule<T>()` | Need full tracing |
|
||||||
|
| Factory classes | `ForModule<T>()` | Need full tracing |
|
||||||
|
| Simple utility classes | `ForModule("ToolName")` | Keep concise |
|
||||||
|
| Temporary debugging | `ForModule("Debug")` | Quick location |
|
||||||
|
| Third-party integration | `ForModule("ThirdParty.XXX")` | Clear identification |
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# 日志服务使用指南 | Logger Service Usage Guide
|
||||||
|
|
||||||
|
## 概述 | Overview
|
||||||
|
|
||||||
|
XplorePlane 使用 Serilog 作为底层日志框架,通过 `ILoggerService` 接口提供统一的日志服务。
|
||||||
|
|
||||||
|
## 基本用法 | Basic Usage
|
||||||
|
|
||||||
|
### 方式 1:自动类型推断(推荐)| Method 1: Auto Type Inference (Recommended)
|
||||||
|
|
||||||
|
使用泛型方法 `ForModule<T>()` 自动获取类型的完整名称(命名空间 + 类名):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PlcService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 自动使用 "XP.Hardware.Plc.Services.PlcService" 作为模块名
|
||||||
|
// Automatically uses "XP.Hardware.Plc.Services.PlcService" as module name
|
||||||
|
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DoSomething()
|
||||||
|
{
|
||||||
|
_logger.Info("执行操作 | Performing operation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式 2:手动指定模块名 | Method 2: Manual Module Name
|
||||||
|
|
||||||
|
如果需要自定义模块名,可以使用字符串参数:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PlcService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 手动指定简短的模块名
|
||||||
|
// Manually specify a short module name
|
||||||
|
_logger = logger?.ForModule("PlcService") ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式 3:使用 typeof 获取类型名 | Method 3: Using typeof for Type Name
|
||||||
|
|
||||||
|
在静态方法或无法使用泛型的场景:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PlcService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PlcService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 使用 typeof 获取类型
|
||||||
|
// Use typeof to get type
|
||||||
|
_logger = logger?.ForModule<PlcService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void StaticMethod(ILoggerService logger)
|
||||||
|
{
|
||||||
|
// 静态方法中也可以使用泛型
|
||||||
|
// Can also use generics in static methods
|
||||||
|
var log = logger.ForModule<PlcService>();
|
||||||
|
log.Info("静态方法日志 | Static method log");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志级别 | Log Levels
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 调试信息(开发环境)| Debug information (development environment)
|
||||||
|
_logger.Debug("调试信息:变量值={Value} | Debug info: variable value={Value}", someValue);
|
||||||
|
|
||||||
|
// 一般信息 | General information
|
||||||
|
_logger.Info("操作成功 | Operation successful");
|
||||||
|
|
||||||
|
// 警告信息 | Warning information
|
||||||
|
_logger.Warn("连接不稳定 | Connection unstable");
|
||||||
|
|
||||||
|
// 错误信息(带异常)| Error information (with exception)
|
||||||
|
_logger.Error(ex, "操作失败:{Message} | Operation failed: {Message}", ex.Message);
|
||||||
|
|
||||||
|
// 致命错误 | Fatal error
|
||||||
|
_logger.Fatal(ex, "系统崩溃 | System crash");
|
||||||
|
```
|
||||||
|
|
||||||
|
## 日志输出格式 | Log Output Format
|
||||||
|
|
||||||
|
使用 `ForModule<T>()` 后,日志会自动包含完整的类型信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-03-12 10:30:15.123 [INF] [XP.Hardware.Plc.Services.PlcService] 正在初始化 PLC 连接... | Initializing PLC connection...
|
||||||
|
2026-03-12 10:30:16.456 [INF] [XP.Hardware.Plc.Services.PlcService] PLC 连接成功 | PLC connection successful
|
||||||
|
```
|
||||||
|
|
||||||
|
## 最佳实践 | Best Practices
|
||||||
|
|
||||||
|
### 1. 在构造函数中初始化日志器 | Initialize Logger in Constructor
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public MyService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
_logger = logger?.ForModule<MyService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用结构化日志 | Use Structured Logging
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 好的做法:使用占位符 | Good: use placeholders
|
||||||
|
_logger.Info("用户 {UserId} 执行了操作 {Action} | User {UserId} performed action {Action}", userId, action);
|
||||||
|
|
||||||
|
// 不好的做法:字符串拼接 | Bad: string concatenation
|
||||||
|
_logger.Info($"用户 {userId} 执行了操作 {action}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 异常日志包含上下文 | Exception Logs Include Context
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _plcClient.ConnectAsync(config);
|
||||||
|
}
|
||||||
|
catch (PlcException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "PLC 连接失败:地址={Address}, 端口={Port} | PLC connection failed: address={Address}, port={Port}",
|
||||||
|
config.Address, config.Port);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 对比:三种方式的输出 | Comparison: Output of Three Methods
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 方式 1:ForModule<T>() - 完整类型名
|
||||||
|
// Method 1: ForModule<T>() - Full type name
|
||||||
|
_logger = logger.ForModule<PlcService>();
|
||||||
|
// 输出 | Output: [XP.Hardware.Plc.Services.PlcService]
|
||||||
|
|
||||||
|
// 方式 2:ForModule("PlcService") - 自定义名称
|
||||||
|
// Method 2: ForModule("PlcService") - Custom name
|
||||||
|
_logger = logger.ForModule("PlcService");
|
||||||
|
// 输出 | Output: [PlcService]
|
||||||
|
|
||||||
|
// 方式 3:不调用 ForModule - 无模块标记
|
||||||
|
// Method 3: Don't call ForModule - No module tag
|
||||||
|
_logger = logger;
|
||||||
|
// 输出 | Output: [] (空标记 | empty tag)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置 | Configuration
|
||||||
|
|
||||||
|
日志配置在 `App.config` 中设置,通过 `SerilogConfig` 加载:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<appSettings>
|
||||||
|
<add key="Serilog:LogPath" value="C:\Logs\XplorePlane" />
|
||||||
|
<add key="Serilog:MinimumLevel" value="Information" />
|
||||||
|
<add key="Serilog:EnableConsole" value="true" />
|
||||||
|
<add key="Serilog:RollingInterval" value="Day" />
|
||||||
|
<add key="Serilog:FileSizeLimitMB" value="100" />
|
||||||
|
<add key="Serilog:RetainedFileCountLimit" value="30" />
|
||||||
|
</appSettings>
|
||||||
|
```
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
# PDF 查看与打印模块使用指南 | PDF Viewer & Printer Module Usage Guide
|
||||||
|
|
||||||
|
## 概述 | Overview
|
||||||
|
|
||||||
|
`XP.Common.PdfViewer` 提供基于 Telerik RadPdfViewer 的 PDF 文件查看与打印功能模块。模块作为 XP.Common 的通用可复用组件,通过 Prism DI 容器注册服务接口,供外部类库通过构造函数注入使用。
|
||||||
|
|
||||||
|
### 核心功能 | Core Features
|
||||||
|
|
||||||
|
- PDF 文件加载与显示(支持文件路径和文件流两种方式)
|
||||||
|
- 内置 RadPdfViewerToolbar 提供页面导航、缩放、旋转、打印等完整工具栏 UI
|
||||||
|
- 静默打印(指定打印机、页面范围、打印份数)
|
||||||
|
- 打印设置对话框(用户交互式配置打印参数)
|
||||||
|
- 打印预览功能
|
||||||
|
- 多语言支持(简体中文、繁体中文、英文)
|
||||||
|
- 结构化日志记录(使用 ILoggerService)
|
||||||
|
- 资源自动释放(IDisposable + 终结器安全网)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录结构 | Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
XP.Common/PdfViewer/
|
||||||
|
├── Exceptions/ # 自定义异常
|
||||||
|
│ ├── PdfLoadException.cs # PDF 加载异常
|
||||||
|
│ ├── PrinterNotFoundException.cs # 打印机未找到异常
|
||||||
|
│ └── PrintException.cs # 打印异常
|
||||||
|
├── Interfaces/ # 服务接口
|
||||||
|
│ ├── IPdfViewerService.cs # PDF 查看服务接口
|
||||||
|
│ └── IPdfPrintService.cs # PDF 打印服务接口
|
||||||
|
├── Implementations/ # 服务实现
|
||||||
|
│ ├── PdfViewerService.cs # PdfViewerService 实现
|
||||||
|
│ └── PdfPrintService.cs # PdfPrintService 实现
|
||||||
|
├── ViewModels/ # ViewModel
|
||||||
|
│ └── PdfViewerWindowViewModel.cs # 阅读器窗口 ViewModel
|
||||||
|
└── Views/ # 视图
|
||||||
|
├── PdfViewerWindow.xaml # 阅读器窗口 XAML
|
||||||
|
└── PdfViewerWindow.xaml.cs # 阅读器窗口 Code-Behind
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 服务接口 | Service Interfaces
|
||||||
|
|
||||||
|
### IPdfViewerService - PDF 查看服务
|
||||||
|
|
||||||
|
负责 PDF 文件加载和阅读器窗口管理。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPdfViewerService : IDisposable
|
||||||
|
{
|
||||||
|
/// 通过文件路径打开 PDF 阅读器窗口
|
||||||
|
void OpenViewer(string filePath);
|
||||||
|
|
||||||
|
/// 通过文件流打开 PDF 阅读器窗口
|
||||||
|
void OpenViewer(Stream stream, string? title = null);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IPdfPrintService - PDF 打印服务
|
||||||
|
|
||||||
|
负责 PDF 打印功能,包括静默打印和交互式打印。
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public interface IPdfPrintService
|
||||||
|
{
|
||||||
|
/// 使用指定打印机打印 PDF 文件
|
||||||
|
void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
|
||||||
|
|
||||||
|
/// 打开打印设置对话框并打印
|
||||||
|
bool PrintWithDialog(string filePath);
|
||||||
|
|
||||||
|
/// 打开打印预览对话框
|
||||||
|
void PrintPreview(string filePath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例 | Usage Examples
|
||||||
|
|
||||||
|
### 1. 通过文件路径打开 PDF
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
|
||||||
|
public class MyService
|
||||||
|
{
|
||||||
|
private readonly IPdfViewerService _pdfViewerService;
|
||||||
|
|
||||||
|
public MyService(IPdfViewerService pdfViewerService)
|
||||||
|
{
|
||||||
|
_pdfViewerService = pdfViewerService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenPdfByPath()
|
||||||
|
{
|
||||||
|
// 打开指定路径的 PDF 文件
|
||||||
|
_pdfViewerService.OpenViewer(@"C:\Documents\UserManual.pdf");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 通过文件流打开 PDF
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void OpenPdfByStream()
|
||||||
|
{
|
||||||
|
// 从文件流打开 PDF(窗口标题可选)
|
||||||
|
using var stream = File.OpenRead(@"C:\Documents\UserManual.pdf");
|
||||||
|
_pdfViewerService.OpenViewer(stream, "用户手册.pdf");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 静默打印到指定打印机
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
|
||||||
|
public class MyService
|
||||||
|
{
|
||||||
|
private readonly IPdfPrintService _printService;
|
||||||
|
|
||||||
|
public MyService(IPdfPrintService printService)
|
||||||
|
{
|
||||||
|
_printService = printService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PrintPdf()
|
||||||
|
{
|
||||||
|
// 打印全部页面到指定打印机
|
||||||
|
_printService.Print(
|
||||||
|
filePath: @"C:\Documents\UserManual.pdf",
|
||||||
|
printerName: "HP LaserJet Pro",
|
||||||
|
pageFrom: null, // null 表示从第一页
|
||||||
|
pageTo: null, // null 表示到最后一页
|
||||||
|
copies: 1 // 打印 1 份
|
||||||
|
);
|
||||||
|
|
||||||
|
// 打印指定范围(第 1-3 页)
|
||||||
|
_printService.Print(
|
||||||
|
filePath: @"C:\Documents\UserManual.pdf",
|
||||||
|
printerName: "HP LaserJet Pro",
|
||||||
|
pageFrom: 1,
|
||||||
|
pageTo: 3,
|
||||||
|
copies: 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 打开打印设置对话框
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void OpenPrintDialog()
|
||||||
|
{
|
||||||
|
// 显示打印设置对话框,用户确认后打印
|
||||||
|
bool userConfirmed = _printService.PrintWithDialog(@"C:\Documents\UserManual.pdf");
|
||||||
|
|
||||||
|
if (userConfirmed)
|
||||||
|
{
|
||||||
|
// 用户点击了"确定"按钮
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 用户点击了"取消"按钮
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 打开打印预览
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void ShowPrintPreview()
|
||||||
|
{
|
||||||
|
// 打开打印预览对话框
|
||||||
|
_printService.PrintPreview(@"C:\Documents\UserManual.pdf");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DI 注册 | DI Registration
|
||||||
|
|
||||||
|
在 `CommonModule.RegisterTypes` 中已注册为单例服务:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
containerRegistry.RegisterSingleton<IPdfPrintService, PdfPrintService>();
|
||||||
|
containerRegistry.RegisterSingleton<IPdfViewerService, PdfViewerService>();
|
||||||
|
```
|
||||||
|
|
||||||
|
在 ViewModel 或 Service 中通过构造函数注入使用:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class MyViewModel
|
||||||
|
{
|
||||||
|
private readonly IPdfViewerService _pdfViewerService;
|
||||||
|
private readonly IPdfPrintService _printService;
|
||||||
|
|
||||||
|
public MyViewModel(
|
||||||
|
IPdfViewerService pdfViewerService,
|
||||||
|
IPdfPrintService printService)
|
||||||
|
{
|
||||||
|
_pdfViewerService = pdfViewerService;
|
||||||
|
_printService = printService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 多语言资源 | Localization Resources
|
||||||
|
|
||||||
|
PDF 模块支持多语言,资源键如下:
|
||||||
|
|
||||||
|
| 资源键 | zh-CN | zh-TW | en-US |
|
||||||
|
|--------|-------|-------|-------|
|
||||||
|
| `PdfViewer_Title` | PDF 阅读器 | PDF 閱讀器 | PDF Viewer |
|
||||||
|
| `PdfViewer_TitleWithFile` | PDF 阅读器 - {0} | PDF 閱讀器 - {0} | PDF Viewer - {0} |
|
||||||
|
| `PdfViewer_LoadSuccess` | PDF 文件加载成功:{0}({1} 页)| PDF 檔案載入成功:{0}({1} 頁)| PDF loaded: {0} ({1} pages) |
|
||||||
|
| `PdfViewer_LoadFailed` | PDF 文件加载失败 | PDF 檔案載入失敗 | PDF file load failed |
|
||||||
|
| `PdfViewer_PrintSuccess` | 打印任务已提交:{0} → {1} | 列印任務已提交:{0} → {1} | Print job submitted: {0} → {1} |
|
||||||
|
| `PdfViewer_PrintFailed` | 打印失败 | 列印失敗 | Print failed |
|
||||||
|
| `PdfViewer_PrinterNotFound` | 打印机未找到:{0} | 印表機未找到:{0} | Printer not found: {0} |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 异常处理 | Exception Handling
|
||||||
|
|
||||||
|
| 异常类型 | 触发条件 | 说明 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| `FileNotFoundException` | 文件路径不存在 | `OpenViewer(filePath)` 或 `Print(filePath, ...)` |
|
||||||
|
| `ArgumentNullException` | 流参数为 null | `OpenViewer(null, ...)` |
|
||||||
|
| `PdfLoadException` | PDF 格式无效或加载失败 | 文件损坏、非 PDF 格式等 |
|
||||||
|
| `PrinterNotFoundException` | 指定打印机不存在 | `Print(filePath, printerName, ...)` |
|
||||||
|
| `PrintException` | 打印过程中发生错误 | 打印机错误、驱动问题等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 注意事项 | Notes
|
||||||
|
|
||||||
|
1. **RadPdfViewerToolbar 内置功能**:页面导航(首页/上一页/下一页/末页)、缩放(放大/缩小/适合宽度/适合整页)、旋转(顺时针/逆时针)等功能由 RadPdfViewerToolbar 自动提供,无需手动实现。
|
||||||
|
|
||||||
|
2. **资源释放**:`PdfViewerService` 实现 `IDisposable`,窗口��闭时会自动释放 PDF 文档资源。终结器作为安全网,确保未显式释放时也能清理资源。
|
||||||
|
|
||||||
|
3. **多语言支持**:RadPdfViewerToolbar 的内置按钮文本(如"首页"、"上一页"、"放大"等)由 Telerik 自身的本地化机制管理,无需在 XP.Common 的 Resources 中维护。
|
||||||
|
|
||||||
|
4. **打印设置**:Telerik 提供内置的 `PrintSettings` 类,无需自定义打印设置模型。
|
||||||
|
|
||||||
|
5. **日志记录**:所有关键操作(加载成功/失败、打印成功/失败)都会通过 `ILoggerService` 记录结构化日志。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 典型应用场景 | Typical Use Cases
|
||||||
|
|
||||||
|
### 场景 1:主窗口添加"用户手册"按钮
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// MainWindowViewModel.cs
|
||||||
|
private void ExecuteOpenUserManual()
|
||||||
|
{
|
||||||
|
var manualPath = ConfigurationManager.AppSettings["UserManual"];
|
||||||
|
var stream = File.OpenRead(manualPath);
|
||||||
|
var fileName = Path.GetFileName(manualPath);
|
||||||
|
_pdfViewerService.OpenViewer(stream, fileName);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:导出报告后自动打开 PDF 预览
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void ExportAndPreview()
|
||||||
|
{
|
||||||
|
// 生成 PDF 报告到临时文件
|
||||||
|
var tempPath = Path.Combine(Path.GetTempPath(), $"Report_{DateTime.Now:yyyyMMdd_HHmmss}.pdf");
|
||||||
|
GenerateReport(tempPath);
|
||||||
|
|
||||||
|
// 自动打开 PDF 预览
|
||||||
|
_pdfViewerService.OpenViewer(tempPath);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:批量打印检测报告
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public void BatchPrintReports(List<string> reportPaths, string printerName)
|
||||||
|
{
|
||||||
|
foreach (var path in reportPaths)
|
||||||
|
{
|
||||||
|
_printService.Print(
|
||||||
|
filePath: path,
|
||||||
|
printerName: printerName,
|
||||||
|
pageFrom: 1,
|
||||||
|
pageTo: null,
|
||||||
|
copies: 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# 实时日志查看器设计文档
|
||||||
|
|
||||||
|
#### 1. 核心目标
|
||||||
|
|
||||||
|
构建一个模态或非模态的 WPF 窗口,用于实时订阅 Serilog 事件,根据预定义的格式(如包含 `@l` 级别标记)自动渲染颜色,并提供过滤与自动滚动控制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. 功能模块规划
|
||||||
|
|
||||||
|
为了保证代码的可维护性和复用性,建议采用 MVVM 模式,参考 `XP.Common.GeneralForm` 的目录结构进行扩展:
|
||||||
|
|
||||||
|
text
|
||||||
|
|
||||||
|
编辑
|
||||||
|
|
||||||
|
```
|
||||||
|
XP.Common/
|
||||||
|
└── GeneralForm/
|
||||||
|
└── Views/
|
||||||
|
├── RealTimeLogViewer.xaml // 视图:RichTextBox + 控制栏
|
||||||
|
└── RealTimeLogViewer.xaml.cs
|
||||||
|
└── ViewModels/
|
||||||
|
└── RealTimeLogViewerViewModel.cs // 核心逻辑:处理颜色标记、过滤、滚动
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 详细功能规格
|
||||||
|
|
||||||
|
| 功能模块 | 详细描述 | 交互逻辑 |
|
||||||
|
| ------ | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| 颜色渲染引擎 | 基于 Serilog 标记<br>解析日志事件中的 Level 或 Message Template。 | * Error/Fatal: 红色/深红色<br>* Warning: 橙色/黄色<br>* Info: 默认色<br>* Debug: 浅灰色<br>利用 `RichTextBox` 的 `TextRange` 动态追加。 |
|
||||||
|
| 动态过滤器 | 关键词黑白名单 | * 包含: 仅显示包含关键字的行。<br>* 排除: 隐藏包含关键字的行。<br>* 重置: 恢复显示所有。 |
|
||||||
|
| 智能滚动 | 跟随开关 | * 开启: 新日志到达时,滚动条自动到底。<br>* 关闭: 用户可自由浏览历史,新日志仅在后台缓存/计数。 |
|
||||||
|
| 行数限制 | 内存保护机制 | * 配置: 默认 2000 行,可调。<br>* 清理: 达到上限后,自动移除最旧的文本段落(`Paragraph`)。 |
|
||||||
|
| 日志源接入 | 线程安全订阅 | 使用 `IObserver<LogEvent>` 或 `IObservable`,内部通过 `Dispatcher` 安全更新。 |
|
||||||
|
|
||||||
|
#### 4. 视觉交互 (UI/UX)
|
||||||
|
|
||||||
|
- **布局**:
|
||||||
|
- **顶部工具栏**:包含“自动滚动”开关(带图标)、“清空日志”按钮、过滤输入框。
|
||||||
|
- **中部显示区**:`RichTextBox` (只读),启用垂直滚动条。
|
||||||
|
- **底部状态栏**:显示当前总日志条数、过滤后的条数。
|
||||||
|
- **性能优化**:
|
||||||
|
- 由于 WPF `RichTextBox` 在大量文本下性能较差,建议增加**最大行数限制**(如默认保留 4000 行,超出自动删除顶部旧日志)。
|
||||||
|
|
||||||
|
#### 6. 开发建议
|
||||||
|
|
||||||
|
1. **颜色解析**:如果 Serilog 输出的是纯文本(如 `[INF] User logged in`),你需要编写正则表达式来匹配 `[INF]`、`[ERR]` 等前缀并着色。
|
||||||
|
2. **线程安全**:务必参考 `ProgressWindow` 中 `UpdateProgress` 的实现,使用 `Dispatcher.InvokeAsync` 或 `Dispatcher.CheckAccess` 来确保从后台线程(如 Serilog 的异步写入线程)更新 UI 时不会崩溃。
|
||||||
|
3. **资源占用**:考虑到这是一个“基础设施”类库,建议在窗口关闭时(`OnClosed` 事件)取消对 `IObservable` 的订阅,防止内存泄漏。
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
namespace XP.Common.Dump.Configs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 功能配置实体(从 App.config 读取)
|
||||||
|
/// Dump feature configuration entity (loaded from App.config)
|
||||||
|
/// </summary>
|
||||||
|
public class DumpConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 文件存储路径 | Dump file storage path
|
||||||
|
/// </summary>
|
||||||
|
public string StoragePath { get; set; } = @"D:\XplorePlane\Dump";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用定时触发 | Enable scheduled trigger
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableScheduledDump { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定时触发间隔(分钟)| Scheduled trigger interval (minutes)
|
||||||
|
/// </summary>
|
||||||
|
public int ScheduledIntervalMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mini Dump 文件大小上限(MB)| Mini Dump file size limit (MB)
|
||||||
|
/// </summary>
|
||||||
|
public long MiniDumpSizeLimitMB { get; set; } = 100;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 文件保留天数 | File retention days
|
||||||
|
/// </summary>
|
||||||
|
public int RetentionDays { get; set; } = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace XP.Common.Dump.Configs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 触发类型 | Dump trigger type
|
||||||
|
/// </summary>
|
||||||
|
public enum DumpTriggerType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 崩溃触发 | Crash trigger
|
||||||
|
/// </summary>
|
||||||
|
Crash,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定时触发 | Scheduled trigger
|
||||||
|
/// </summary>
|
||||||
|
Scheduled,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动触发 | Manual trigger
|
||||||
|
/// </summary>
|
||||||
|
Manual
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using XP.Common.Dump.Configs;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Dump.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 文件自动清理组件,按保留天数策略删除过期文件
|
||||||
|
/// Auto cleanup component for dump files, deletes expired files based on retention policy
|
||||||
|
/// </summary>
|
||||||
|
internal class DumpCleaner
|
||||||
|
{
|
||||||
|
private readonly DumpConfig _config;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public DumpCleaner(DumpConfig config, ILoggerService logger)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
_logger = logger?.ForModule<DumpCleaner>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行清理操作:删除超过保留天数的 .dmp 文件
|
||||||
|
/// Execute cleanup: delete .dmp files older than retention days
|
||||||
|
/// </summary>
|
||||||
|
public void CleanExpiredFiles()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_config.StoragePath))
|
||||||
|
{
|
||||||
|
_logger.Debug("存储目录不存在,跳过清理:{Path} | Storage directory does not exist, skipping cleanup: {Path}", _config.StoragePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dmpFiles = Directory.GetFiles(_config.StoragePath, "*.dmp");
|
||||||
|
var cutoffTime = DateTime.Now.AddDays(-_config.RetentionDays);
|
||||||
|
var deletedCount = 0;
|
||||||
|
|
||||||
|
foreach (var filePath in dmpFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var creationTime = File.GetCreationTime(filePath);
|
||||||
|
if (creationTime < cutoffTime)
|
||||||
|
{
|
||||||
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
File.Delete(filePath);
|
||||||
|
deletedCount++;
|
||||||
|
_logger.Info("已删除过期 Dump 文件:{FileName} | Deleted expired dump file: {FileName}", fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "删除 Dump 文件失败:{FilePath} | Failed to delete dump file: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Info("Dump 清理完成,共删除 {Count} 个文件 | Dump cleanup completed, {Count} files deleted", deletedCount);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Dump 清理过程发生异常:{Message} | Dump cleanup error: {Message}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using XP.Common.Dump.Configs;
|
||||||
|
using XP.Common.Dump.Interfaces;
|
||||||
|
using XP.Common.Dump.Native;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Dump.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 文件管理服务实现,负责 Dump 生成、崩溃事件订阅、定时调度和清理
|
||||||
|
/// Dump file management service implementation, handles dump generation, crash event subscription, scheduled tasks and cleanup
|
||||||
|
/// </summary>
|
||||||
|
public class DumpService : IDumpService
|
||||||
|
{
|
||||||
|
private readonly DumpConfig _config;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly DumpCleaner _cleaner;
|
||||||
|
private Timer? _scheduledTimer;
|
||||||
|
private Timer? _cleanupTimer;
|
||||||
|
private bool _isStarted;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认存储路径 | Default storage path
|
||||||
|
/// </summary>
|
||||||
|
private const string DefaultStoragePath = @"D:\XplorePlane\Dump";
|
||||||
|
|
||||||
|
public DumpService(DumpConfig config, ILoggerService logger)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
_logger = logger?.ForModule<DumpService>() ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_cleaner = new DumpCleaner(config, logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成 Dump 文件名(纯函数)| Generate dump file name (pure function)
|
||||||
|
/// 格式:XplorePlane_{yyyyMMdd_HHmm}_{TriggerType}.dmp
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="triggerType">触发类型 | Trigger type</param>
|
||||||
|
/// <param name="timestamp">时间戳 | Timestamp</param>
|
||||||
|
/// <returns>文件名 | File name</returns>
|
||||||
|
internal static string GenerateFileName(DumpTriggerType triggerType, DateTime timestamp)
|
||||||
|
{
|
||||||
|
return $"XplorePlane_{timestamp:yyyyMMdd_HHmm}_{triggerType}.dmp";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Mini Dump 文件大小是否在限制范围内 | Validate Mini Dump file size is within limit
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fileSizeBytes">文件大小(字节)| File size in bytes</param>
|
||||||
|
/// <param name="sizeLimitMB">大小限制(MB)| Size limit in MB</param>
|
||||||
|
/// <returns>true 表示在限制内 | true if within limit</returns>
|
||||||
|
internal static bool ValidateFileSize(long fileSizeBytes, long sizeLimitMB)
|
||||||
|
{
|
||||||
|
return fileSizeBytes <= sizeLimitMB * 1024 * 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Dump 文件的核心方法 | Core method to write dump file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="triggerType">触发类型 | Trigger type</param>
|
||||||
|
/// <param name="isMiniDump">是否为 Mini Dump | Whether it is a Mini Dump</param>
|
||||||
|
/// <returns>生成的文件完整路径,失败返回 null | Full path of generated file, null on failure</returns>
|
||||||
|
private string? WriteDump(DumpTriggerType triggerType, bool isMiniDump)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 非手动触发请求 Full Dump 时,降级为 Mini Dump | Downgrade to Mini Dump for non-manual Full Dump requests
|
||||||
|
if (!isMiniDump && triggerType != DumpTriggerType.Manual)
|
||||||
|
{
|
||||||
|
_logger.Warn("非手动触发不允许生成 Full Dump,已降级为 Mini Dump:{TriggerType} | Non-manual trigger cannot generate Full Dump, downgraded to Mini Dump: {TriggerType}", triggerType);
|
||||||
|
isMiniDump = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileName = GenerateFileName(triggerType, DateTime.Now);
|
||||||
|
var filePath = Path.Combine(_config.StoragePath, fileName);
|
||||||
|
|
||||||
|
// 确定 Dump 类型标志 | Determine dump type flags
|
||||||
|
uint dumpType = isMiniDump
|
||||||
|
? (uint)(MiniDumpType.MiniDumpWithDataSegs | MiniDumpType.MiniDumpWithHandleData | MiniDumpType.MiniDumpWithThreadInfo)
|
||||||
|
: (uint)MiniDumpType.MiniDumpWithFullMemory;
|
||||||
|
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
|
||||||
|
using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
|
||||||
|
{
|
||||||
|
bool success = NativeMethods.MiniDumpWriteDump(
|
||||||
|
process.Handle,
|
||||||
|
(uint)process.Id,
|
||||||
|
fileStream.SafeFileHandle,
|
||||||
|
dumpType,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero,
|
||||||
|
IntPtr.Zero);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
var errorCode = Marshal.GetLastWin32Error();
|
||||||
|
_logger.Error(null!, "MiniDumpWriteDump 调用失败,错误码:{ErrorCode} | MiniDumpWriteDump failed, error code: {ErrorCode}", errorCode);
|
||||||
|
// 清理失败的文件 | Clean up failed file
|
||||||
|
try { File.Delete(filePath); } catch { }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mini Dump 文件大小检查 | Mini Dump file size check
|
||||||
|
if (isMiniDump)
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(filePath);
|
||||||
|
if (!ValidateFileSize(fileInfo.Length, _config.MiniDumpSizeLimitMB))
|
||||||
|
{
|
||||||
|
_logger.Warn("Mini Dump 文件超过大小限制({SizeMB}MB),已删除:{FilePath} | Mini Dump file exceeds size limit ({SizeMB}MB), deleted: {FilePath}",
|
||||||
|
_config.MiniDumpSizeLimitMB, filePath);
|
||||||
|
File.Delete(filePath);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var dumpTypeStr = isMiniDump ? "Mini" : "Full";
|
||||||
|
_logger.Info("Dump 文件已生成:{FilePath},触发类型:{TriggerType},Dump 类型:{DumpType} | Dump file generated: {FilePath}, trigger: {TriggerType}, type: {DumpType}",
|
||||||
|
filePath, triggerType, dumpTypeStr);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Dump 文件写入失败:{Message} | Dump file write failed: {Message}", ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Dump 文件写入权限不足:{Message} | Dump file write permission denied: {Message}", ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Dump 文件生成异常:{Message} | Dump file generation error: {Message}", ex.Message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 手动触发方法 | Manual trigger methods
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? CreateMiniDump()
|
||||||
|
{
|
||||||
|
_logger.Info("手动触发 Mini Dump 生成 | Manually triggering Mini Dump generation");
|
||||||
|
var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: true);
|
||||||
|
if (filePath != null)
|
||||||
|
{
|
||||||
|
_logger.Info("手动 Mini Dump 已生成:{FilePath} | Manual Mini Dump generated: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? CreateFullDump()
|
||||||
|
{
|
||||||
|
_logger.Info("手动触发 Full Dump 生成 | Manually triggering Full Dump generation");
|
||||||
|
var filePath = WriteDump(DumpTriggerType.Manual, isMiniDump: false);
|
||||||
|
if (filePath != null)
|
||||||
|
{
|
||||||
|
_logger.Info("手动 Full Dump 已生成:{FilePath} | Manual Full Dump generated: {FilePath}", filePath);
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 生命周期管理 | Lifecycle management
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if (_isStarted) return;
|
||||||
|
|
||||||
|
_logger.Info("Dump 服务启动中 | Dump service starting");
|
||||||
|
|
||||||
|
// 确保存储目录存在 | Ensure storage directory exists
|
||||||
|
EnsureStorageDirectory();
|
||||||
|
|
||||||
|
// 订阅崩溃事件 | Subscribe to crash events
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
|
||||||
|
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
|
||||||
|
|
||||||
|
// 启动定时 Dump 计时器 | Start scheduled dump timer
|
||||||
|
if (_config.EnableScheduledDump)
|
||||||
|
{
|
||||||
|
var intervalMs = _config.ScheduledIntervalMinutes * 60 * 1000;
|
||||||
|
_scheduledTimer = new Timer(OnScheduledDumpCallback, null, intervalMs, intervalMs);
|
||||||
|
_logger.Info("定时 Dump 已启用,间隔:{Interval} 分钟 | Scheduled dump enabled, interval: {Interval} minutes", _config.ScheduledIntervalMinutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动每日清理计时器(每24小时执行一次)| Start daily cleanup timer (every 24 hours)
|
||||||
|
var dailyMs = 24 * 60 * 60 * 1000;
|
||||||
|
_cleanupTimer = new Timer(OnCleanupCallback, null, dailyMs, dailyMs);
|
||||||
|
|
||||||
|
// 立即执行一次清理 | Execute cleanup immediately
|
||||||
|
_cleaner.CleanExpiredFiles();
|
||||||
|
|
||||||
|
_isStarted = true;
|
||||||
|
_logger.Info("Dump 服务已启动 | Dump service started");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if (!_isStarted) return;
|
||||||
|
|
||||||
|
_logger.Info("Dump 服务停止中 | Dump service stopping");
|
||||||
|
|
||||||
|
// 取消事件订阅 | Unsubscribe from events
|
||||||
|
AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException;
|
||||||
|
TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
|
||||||
|
|
||||||
|
// 停止所有计时器 | Stop all timers
|
||||||
|
_scheduledTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
_cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
|
||||||
|
_isStarted = false;
|
||||||
|
_logger.Info("Dump 服务已停止 | Dump service stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
_scheduledTimer?.Dispose();
|
||||||
|
_scheduledTimer = null;
|
||||||
|
|
||||||
|
_cleanupTimer?.Dispose();
|
||||||
|
_cleanupTimer = null;
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 内部方法 | Internal methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确保存储目录存在,不存在则创建,创建失败回退默认路径
|
||||||
|
/// Ensure storage directory exists, create if not, fallback to default on failure
|
||||||
|
/// </summary>
|
||||||
|
private void EnsureStorageDirectory()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(_config.StoragePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(_config.StoragePath);
|
||||||
|
_logger.Info("已创建存储目录:{Path} | Created storage directory: {Path}", _config.StoragePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "存储目录创建失败:{Path},回退到默认路径 | Storage directory creation failed: {Path}, falling back to default", _config.StoragePath);
|
||||||
|
_config.StoragePath = DefaultStoragePath;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(DefaultStoragePath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(DefaultStoragePath);
|
||||||
|
_logger.Info("已创建默认存储目录:{Path} | Created default storage directory: {Path}", DefaultStoragePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception fallbackEx)
|
||||||
|
{
|
||||||
|
_logger.Fatal(fallbackEx, "默认存储目录创建也失败:{Path} | Default storage directory creation also failed: {Path}", DefaultStoragePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 未处理异常回调 | Unhandled exception callback
|
||||||
|
/// </summary>
|
||||||
|
private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
_logger.Info("检测到未处理异常,正在生成崩溃 Dump | Unhandled exception detected, generating crash dump");
|
||||||
|
WriteDump(DumpTriggerType.Crash, isMiniDump: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 未观察到的 Task 异常回调 | Unobserved task exception callback
|
||||||
|
/// </summary>
|
||||||
|
private void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
|
||||||
|
{
|
||||||
|
_logger.Info("检测到未观察的 Task 异常,正在生成崩溃 Dump | Unobserved task exception detected, generating crash dump");
|
||||||
|
WriteDump(DumpTriggerType.Crash, isMiniDump: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 定时 Dump 回调 | Scheduled dump callback
|
||||||
|
/// </summary>
|
||||||
|
private void OnScheduledDumpCallback(object? state)
|
||||||
|
{
|
||||||
|
_logger.Debug("定时 Dump 触发 | Scheduled dump triggered");
|
||||||
|
WriteDump(DumpTriggerType.Scheduled, isMiniDump: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清理回调 | Cleanup callback
|
||||||
|
/// </summary>
|
||||||
|
private void OnCleanupCallback(object? state)
|
||||||
|
{
|
||||||
|
_logger.Debug("定时清理触发 | Scheduled cleanup triggered");
|
||||||
|
_cleaner.CleanExpiredFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Dump.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dump 文件管理服务接口 | Dump file management service interface
|
||||||
|
/// </summary>
|
||||||
|
public interface IDumpService : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 手动触发 Mini Dump 生成 | Manually trigger Mini Dump generation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure</returns>
|
||||||
|
string? CreateMiniDump();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动触发 Full Dump 生成 | Manually trigger Full Dump generation
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>生成的 Dump 文件完整路径,失败返回 null | Full path of generated dump file, null on failure</returns>
|
||||||
|
string? CreateFullDump();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动服务(订阅崩溃事件、启动定时任务和清理任务)| Start service (subscribe crash events, start scheduled and cleanup tasks)
|
||||||
|
/// </summary>
|
||||||
|
void Start();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止服务(取消定时任务、取消事件订阅)| Stop service (cancel scheduled tasks, unsubscribe events)
|
||||||
|
/// </summary>
|
||||||
|
void Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Dump.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MiniDump 类型标志,用于 MiniDumpWriteDump 的 dumpType 参数
|
||||||
|
/// MiniDump type flags for MiniDumpWriteDump dumpType parameter
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
internal enum MiniDumpType : uint
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 仅包含基本信息 | Basic information only
|
||||||
|
/// </summary>
|
||||||
|
MiniDumpNormal = 0x00000000,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含数据段信息 | Include data segment information
|
||||||
|
/// </summary>
|
||||||
|
MiniDumpWithDataSegs = 0x00000001,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含完整内存信息(Full Dump 使用)| Include full memory (used for Full Dump)
|
||||||
|
/// </summary>
|
||||||
|
MiniDumpWithFullMemory = 0x00000002,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含句柄信息 | Include handle information
|
||||||
|
/// </summary>
|
||||||
|
MiniDumpWithHandleData = 0x00000004,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 包含线程信息 | Include thread information
|
||||||
|
/// </summary>
|
||||||
|
MiniDumpWithThreadInfo = 0x00001000,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
|
namespace XP.Common.Dump.Native
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Windows 原生方法 P/Invoke 声明 | Windows native method P/Invoke declarations
|
||||||
|
/// </summary>
|
||||||
|
internal static class NativeMethods
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 将进程的 Mini Dump 写入指定文件 | Write a Mini Dump of the process to the specified file
|
||||||
|
/// </summary>
|
||||||
|
[DllImport("dbghelp.dll", SetLastError = true)]
|
||||||
|
internal static extern bool MiniDumpWriteDump(
|
||||||
|
IntPtr hProcess,
|
||||||
|
uint processId,
|
||||||
|
SafeHandle hFile,
|
||||||
|
uint dumpType,
|
||||||
|
IntPtr exceptionParam,
|
||||||
|
IntPtr userStreamParam,
|
||||||
|
IntPtr callbackParam);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
using System;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using XP.Common.Localization;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 输入对话框 ViewModel | Input dialog ViewModel
|
||||||
|
/// </summary>
|
||||||
|
public class InputDialogViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly Func<string, string?>? _validate;
|
||||||
|
private string _inputText;
|
||||||
|
private string? _validationError;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口标题 | Window title
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示文本 | Prompt text
|
||||||
|
/// </summary>
|
||||||
|
public string Prompt { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确定按钮文本(多语言)| OK button text (localized)
|
||||||
|
/// </summary>
|
||||||
|
public string OkText { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消按钮文本(多语言)| Cancel button text (localized)
|
||||||
|
/// </summary>
|
||||||
|
public string CancelText { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户输入的文本 | User input text
|
||||||
|
/// </summary>
|
||||||
|
public string InputText
|
||||||
|
{
|
||||||
|
get => _inputText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _inputText, value))
|
||||||
|
{
|
||||||
|
// 输入变化时清除验证错误 | Clear validation error on input change
|
||||||
|
ValidationError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证错误信息,null 表示无错误 | Validation error message, null means no error
|
||||||
|
/// </summary>
|
||||||
|
public string? ValidationError
|
||||||
|
{
|
||||||
|
get => _validationError;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _validationError, value))
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(HasValidationError));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否存在验证错误 | Whether there is a validation error
|
||||||
|
/// </summary>
|
||||||
|
public bool HasValidationError => !string.IsNullOrEmpty(ValidationError);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||||
|
/// <param name="title">窗口标题 | Window title</param>
|
||||||
|
/// <param name="defaultValue">默认值 | Default value</param>
|
||||||
|
/// <param name="validate">验证委托,返回 null 表示通过,返回错误信息则显示 | Validation delegate</param>
|
||||||
|
public InputDialogViewModel(
|
||||||
|
string prompt,
|
||||||
|
string title,
|
||||||
|
string defaultValue = "",
|
||||||
|
Func<string, string?>? validate = null)
|
||||||
|
{
|
||||||
|
Prompt = prompt;
|
||||||
|
Title = title;
|
||||||
|
_inputText = defaultValue;
|
||||||
|
_validate = validate;
|
||||||
|
|
||||||
|
// 按钮文本使用多语言 | Button text uses localization
|
||||||
|
OkText = LocalizationHelper.Get("Button_OK");
|
||||||
|
CancelText = LocalizationHelper.Get("Button_Cancel");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行验证,返回是否通过 | Run validation, returns whether it passed
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true 表示验证通过 | true means validation passed</returns>
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
if (_validate == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var error = _validate(InputText);
|
||||||
|
ValidationError = error;
|
||||||
|
return error == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 进度条窗口 ViewModel,管理进度数据和窗口行为逻辑
|
||||||
|
/// </summary>
|
||||||
|
public class ProgressWindowViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ILoggerService? _logger;
|
||||||
|
private string _message;
|
||||||
|
private double _progress;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口标题(只读)
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示信息文本
|
||||||
|
/// </summary>
|
||||||
|
public string Message
|
||||||
|
{
|
||||||
|
get => _message;
|
||||||
|
set => SetProperty(ref _message, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进度值,范围 0-100
|
||||||
|
/// </summary>
|
||||||
|
public double Progress
|
||||||
|
{
|
||||||
|
get => _progress;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _progress, value))
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(ProgressText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 百分比显示文本,由 Progress 自动计算(只读)
|
||||||
|
/// </summary>
|
||||||
|
public string ProgressText => $"{Math.Floor(Progress)}%";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否允许用户关闭窗口(只读)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsCancelable { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">窗口标题</param>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <param name="progress">初始进度值</param>
|
||||||
|
/// <param name="isCancelable">是否允许取消</param>
|
||||||
|
/// <param name="logger">日志服务(可选)</param>
|
||||||
|
public ProgressWindowViewModel(
|
||||||
|
string title = "操作进行中",
|
||||||
|
string message = "请稍候...",
|
||||||
|
double progress = 0,
|
||||||
|
bool isCancelable = true,
|
||||||
|
ILoggerService? logger = null)
|
||||||
|
{
|
||||||
|
Title = title;
|
||||||
|
_message = message;
|
||||||
|
_progress = progress;
|
||||||
|
IsCancelable = isCancelable;
|
||||||
|
|
||||||
|
// 创建模块化日志实例(logger 为 null 时静默跳过)
|
||||||
|
_logger = logger?.ForModule<ProgressWindowViewModel>();
|
||||||
|
|
||||||
|
// 构造时记录 Info 级别日志
|
||||||
|
_logger?.Info("进度窗口已创建:Title={Title}, IsCancelable={IsCancelable}", Title, IsCancelable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新进度和提示信息(唯一的外部更新入口)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">提示信息文本</param>
|
||||||
|
/// <param name="progress">进度值,超出 [0, 100] 范围时自动修正</param>
|
||||||
|
public void UpdateProgress(string message, double progress)
|
||||||
|
{
|
||||||
|
// 检查 progress 是否超出有效范围,记录 Warn 日志
|
||||||
|
if (progress < 0 || progress > 100)
|
||||||
|
{
|
||||||
|
_logger?.Warn("进度值超出有效范围 [0, 100]:{Progress},将自动修正", progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 progress 值 Clamp 到 [0, 100] 范围
|
||||||
|
progress = Math.Clamp(progress, 0, 100);
|
||||||
|
|
||||||
|
// 更新属性,触发 PropertyChanged 通知
|
||||||
|
Message = message;
|
||||||
|
Progress = progress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<Window x:Class="XP.Common.GeneralForm.Views.InputDialog"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
Width="400" Height="180"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
Topmost="True"
|
||||||
|
WindowStyle="SingleBorderWindow">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid Margin="20,16,20,16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 提示文本 | Prompt text -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{Binding Prompt}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="13"
|
||||||
|
Foreground="#FF333333"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- 输入框 | Input text box -->
|
||||||
|
<telerik:RadWatermarkTextBox Grid.Row="1"
|
||||||
|
Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Height="28"
|
||||||
|
FontSize="13"
|
||||||
|
telerik:StyleManager.Theme="Crystal"
|
||||||
|
Margin="0,0,0,2"/>
|
||||||
|
|
||||||
|
<!-- 验证错误提示 | Validation error message -->
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="{Binding ValidationError}"
|
||||||
|
Foreground="#FFD32F2F"
|
||||||
|
FontSize="11"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Margin="0,7,0,0"
|
||||||
|
Visibility="{Binding HasValidationError, Converter={StaticResource BoolToVisibilityConverter}}"/>
|
||||||
|
|
||||||
|
<!-- 按钮区域 | Button area -->
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right">
|
||||||
|
<telerik:RadButton Content="{Binding OkText}"
|
||||||
|
Click="OnOkClick"
|
||||||
|
Width="80" Height="28"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
IsDefault="True"
|
||||||
|
telerik:StyleManager.Theme="Crystal"/>
|
||||||
|
<telerik:RadButton Content="{Binding CancelText}"
|
||||||
|
Click="OnCancelClick"
|
||||||
|
Width="80" Height="28"
|
||||||
|
IsCancel="True"
|
||||||
|
telerik:StyleManager.Theme="Crystal"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using XP.Common.GeneralForm.ViewModels;
|
||||||
|
using XP.Common.Localization;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用输入对话框,支持单行文本输入和可选的输入验证
|
||||||
|
/// General input dialog with single-line text input and optional validation
|
||||||
|
/// </summary>
|
||||||
|
public partial class InputDialog : Window
|
||||||
|
{
|
||||||
|
private readonly InputDialogViewModel _viewModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户输入的结果,取消时为 null | User input result, null if cancelled
|
||||||
|
/// </summary>
|
||||||
|
public string? Result { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||||
|
/// <param name="title">窗口标题 | Window title</param>
|
||||||
|
/// <param name="defaultValue">默认值 | Default value</param>
|
||||||
|
/// <param name="validate">可选的验证委托,返回 null 表示通过,返回错误信息则阻止确认 | Optional validation delegate</param>
|
||||||
|
public InputDialog(
|
||||||
|
string prompt,
|
||||||
|
string title,
|
||||||
|
string defaultValue = "",
|
||||||
|
Func<string, string?>? validate = null)
|
||||||
|
{
|
||||||
|
_viewModel = new InputDialogViewModel(prompt, title, defaultValue, validate);
|
||||||
|
DataContext = _viewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
// 继承调用方窗口图标 | Inherit caller window icon
|
||||||
|
if (Application.Current?.MainWindow != null)
|
||||||
|
{
|
||||||
|
Icon = Application.Current.MainWindow.Icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 确定按钮点击事件 | OK button click handler
|
||||||
|
/// </summary>
|
||||||
|
private void OnOkClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// 执行验证 | Run validation
|
||||||
|
if (!_viewModel.Validate())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Result = _viewModel.InputText;
|
||||||
|
DialogResult = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 取消按钮点击事件 | Cancel button click handler
|
||||||
|
/// </summary>
|
||||||
|
private void OnCancelClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
Result = null;
|
||||||
|
DialogResult = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 静态便捷方法,显示输入对话框并返回结果 | Static convenience method
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="prompt">提示文本 | Prompt text</param>
|
||||||
|
/// <param name="title">窗口标题 | Window title</param>
|
||||||
|
/// <param name="defaultValue">默认值 | Default value</param>
|
||||||
|
/// <param name="validate">可选的验证委托 | Optional validation delegate</param>
|
||||||
|
/// <param name="owner">父窗口(可选)| Owner window (optional)</param>
|
||||||
|
/// <returns>用户输入的值,取消时返回 null | User input, null if cancelled</returns>
|
||||||
|
public static string? Show(
|
||||||
|
string prompt,
|
||||||
|
string title,
|
||||||
|
string defaultValue = "",
|
||||||
|
Func<string, string?>? validate = null,
|
||||||
|
Window? owner = null)
|
||||||
|
{
|
||||||
|
var dialog = new InputDialog(prompt, title, defaultValue, validate);
|
||||||
|
// owner 未指定时自动回退到主窗口,确保对话框有父窗口约束不会被遮挡
|
||||||
|
// Fall back to MainWindow if owner not specified, ensuring dialog stays in front
|
||||||
|
dialog.Owner = owner ?? Application.Current?.MainWindow;
|
||||||
|
dialog.ShowDialog();
|
||||||
|
return dialog.Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<Window
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" x:Class="XP.Common.GeneralForm.Views.ProgressWindow"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ResizeMode="NoResize"
|
||||||
|
Width="400"
|
||||||
|
Height="150"
|
||||||
|
ShowInTaskbar="False"
|
||||||
|
WindowStyle="SingleBorderWindow">
|
||||||
|
|
||||||
|
<!-- 主布局:从上到下依次为提示信息、进度条、百分比文本 -->
|
||||||
|
<Grid Margin="24,20,24,20">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 提示信息文本 -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{Binding Message}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
FontSize="14"
|
||||||
|
Margin="0,0,0,12" />
|
||||||
|
|
||||||
|
<!-- 进度条 -->
|
||||||
|
<telerik:RadProgressBar Grid.Row="1"
|
||||||
|
Value="{Binding Progress, Mode=OneWay}"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="100"
|
||||||
|
Height="18"
|
||||||
|
Margin="0,0,0,8" telerik:StyleManager.Theme="Crystal" />
|
||||||
|
|
||||||
|
<!-- 百分比文本 -->
|
||||||
|
<TextBlock Grid.Row="2"
|
||||||
|
Text="{Binding ProgressText}"
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#666666" />
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using XP.Common.GeneralForm.ViewModels;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用进度条模态窗口,支持线程安全的进度更新和关闭操作
|
||||||
|
/// </summary>
|
||||||
|
public partial class ProgressWindow : Window
|
||||||
|
{
|
||||||
|
#region Win32 API
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable);
|
||||||
|
|
||||||
|
private const uint SC_CLOSE = 0xF060;
|
||||||
|
private const uint MF_BYCOMMAND = 0x00000000;
|
||||||
|
private const uint MF_GRAYED = 0x00000001;
|
||||||
|
private const uint MF_ENABLED = 0x00000000;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private readonly ProgressWindowViewModel _viewModel;
|
||||||
|
private readonly ILoggerService? _logger;
|
||||||
|
private bool _isClosingByCode;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="title">窗口标题</param>
|
||||||
|
/// <param name="message">提示信息</param>
|
||||||
|
/// <param name="isCancelable">是否允许用户关闭窗口</param>
|
||||||
|
/// <param name="logger">日志服务(可选)</param>
|
||||||
|
public ProgressWindow(
|
||||||
|
string title = "操作进行中",
|
||||||
|
string message = "请稍候...",
|
||||||
|
bool isCancelable = true,
|
||||||
|
ILoggerService? logger = null)
|
||||||
|
{
|
||||||
|
// 创建 ViewModel 并设置为 DataContext
|
||||||
|
_viewModel = new ProgressWindowViewModel(title, message, 0, isCancelable, logger);
|
||||||
|
DataContext = _viewModel;
|
||||||
|
|
||||||
|
// 保存日志服务引用,用于 View 层日志记录
|
||||||
|
_logger = logger?.ForModule<ProgressWindow>();
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
// 继承主窗口图标
|
||||||
|
if (Application.Current?.MainWindow != null)
|
||||||
|
{
|
||||||
|
Icon = Application.Current.MainWindow.Icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅 Closing 事件,拦截用户手动关闭
|
||||||
|
Closing += OnWindowClosing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新进度和提示信息(线程安全,可从任意线程调用)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">提示信息文本</param>
|
||||||
|
/// <param name="progress">进度值</param>
|
||||||
|
public void UpdateProgress(string message, double progress)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
_viewModel.UpdateProgress(message, progress);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() => _viewModel.UpdateProgress(message, progress));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// 其他调度异常静默处理,避免影响调用方
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 线程安全关闭窗口(使用 new 关键字隐藏基类 Close 方法,因为 Window.Close() 不是虚方法)
|
||||||
|
/// </summary>
|
||||||
|
public new void Close()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isClosingByCode = true;
|
||||||
|
|
||||||
|
if (Dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
_logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
|
||||||
|
base.Close();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_logger?.Info("进度窗口正在关闭:Title={Title}", _viewModel.Title);
|
||||||
|
base.Close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
// 窗口已关闭时 Dispatcher 调度可能抛出此异常,静默处理
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// 其他调度异常静默处理,避免影响调用方
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口源初始化时,根据 IsCancelable 控制关闭按钮状态
|
||||||
|
/// </summary>
|
||||||
|
protected override void OnSourceInitialized(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnSourceInitialized(e);
|
||||||
|
|
||||||
|
if (!_viewModel.IsCancelable)
|
||||||
|
{
|
||||||
|
DisableCloseButton();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 拦截窗口关闭事件
|
||||||
|
/// </summary>
|
||||||
|
private void OnWindowClosing(object? sender, CancelEventArgs e)
|
||||||
|
{
|
||||||
|
// 当不可取消且非程序主动关闭时,阻止关闭
|
||||||
|
if (!_viewModel.IsCancelable && !_isClosingByCode)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 Win32 API 禁用窗口关闭按钮(灰色不可点击)
|
||||||
|
/// </summary>
|
||||||
|
private void DisableCloseButton()
|
||||||
|
{
|
||||||
|
var hwnd = new WindowInteropHelper(this).Handle;
|
||||||
|
var hMenu = GetSystemMenu(hwnd, false);
|
||||||
|
if (hMenu != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_GRAYED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 Win32 API 启用窗口关闭按钮
|
||||||
|
/// </summary>
|
||||||
|
private void EnableCloseButton()
|
||||||
|
{
|
||||||
|
var hwnd = new WindowInteropHelper(this).Handle;
|
||||||
|
var hMenu = GetSystemMenu(hwnd, false);
|
||||||
|
if (hMenu != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
EnableMenuItem(hMenu, SC_CLOSE, MF_BYCOMMAND | MF_ENABLED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
using System.Configuration;
|
||||||
|
using XP.Common.Configs;
|
||||||
|
using XP.Common.Dump.Configs;
|
||||||
|
|
||||||
|
namespace XP.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用配置加载工具(读取App.config)
|
||||||
|
/// </summary>
|
||||||
|
public static class ConfigLoader
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 加载Serilog配置
|
||||||
|
/// </summary>
|
||||||
|
public static SerilogConfig LoadSerilogConfig()
|
||||||
|
{
|
||||||
|
var config = new SerilogConfig();
|
||||||
|
|
||||||
|
var logPath = ConfigurationManager.AppSettings["Serilog:LogPath"];
|
||||||
|
if (!string.IsNullOrEmpty(logPath)) config.LogPath = logPath;
|
||||||
|
|
||||||
|
var minLevel = ConfigurationManager.AppSettings["Serilog:MinimumLevel"];
|
||||||
|
if (!string.IsNullOrEmpty(minLevel)) config.MinimumLevel = minLevel;
|
||||||
|
|
||||||
|
var enableConsole = ConfigurationManager.AppSettings["Serilog:EnableConsole"];
|
||||||
|
if (bool.TryParse(enableConsole, out var console)) config.EnableConsole = console;
|
||||||
|
|
||||||
|
var rollingInterval = ConfigurationManager.AppSettings["Serilog:RollingInterval"];
|
||||||
|
if (!string.IsNullOrEmpty(rollingInterval)) config.RollingInterval = rollingInterval;
|
||||||
|
|
||||||
|
var fileSize = ConfigurationManager.AppSettings["Serilog:FileSizeLimitMB"];
|
||||||
|
if (long.TryParse(fileSize, out var size)) config.FileSizeLimitMB = size;
|
||||||
|
|
||||||
|
var retainCount = ConfigurationManager.AppSettings["Serilog:RetainedFileCountLimit"];
|
||||||
|
if (int.TryParse(retainCount, out var count)) config.RetainedFileCountLimit = count;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加载SQLite配置
|
||||||
|
/// </summary>
|
||||||
|
public static SqliteConfig LoadSqliteConfig()
|
||||||
|
{
|
||||||
|
var config = new SqliteConfig();
|
||||||
|
|
||||||
|
var dbPath = ConfigurationManager.AppSettings["Sqlite:DbFilePath"];
|
||||||
|
if (!string.IsNullOrEmpty(dbPath)) config.DbFilePath = dbPath;
|
||||||
|
|
||||||
|
var timeout = ConfigurationManager.AppSettings["Sqlite:ConnectionTimeout"];
|
||||||
|
if (int.TryParse(timeout, out var t)) config.ConnectionTimeout = t;
|
||||||
|
|
||||||
|
var createIfNotExists = ConfigurationManager.AppSettings["Sqlite:CreateIfNotExists"];
|
||||||
|
if (bool.TryParse(createIfNotExists, out var c)) config.CreateIfNotExists = c;
|
||||||
|
|
||||||
|
var enableWal = ConfigurationManager.AppSettings["Sqlite:EnableWalMode"];
|
||||||
|
if (bool.TryParse(enableWal, out var w)) config.EnableWalMode = w;
|
||||||
|
|
||||||
|
var enableSqlLog = ConfigurationManager.AppSettings["Sqlite:EnableSqlLogging"];
|
||||||
|
if (bool.TryParse(enableSqlLog, out var l)) config.EnableSqlLogging = l;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 加载 Dump 配置 | Load Dump configuration
|
||||||
|
/// </summary>
|
||||||
|
public static DumpConfig LoadDumpConfig()
|
||||||
|
{
|
||||||
|
var config = new DumpConfig();
|
||||||
|
|
||||||
|
var storagePath = ConfigurationManager.AppSettings["Dump:StoragePath"];
|
||||||
|
if (!string.IsNullOrEmpty(storagePath)) config.StoragePath = storagePath;
|
||||||
|
|
||||||
|
var enableScheduled = ConfigurationManager.AppSettings["Dump:EnableScheduledDump"];
|
||||||
|
if (bool.TryParse(enableScheduled, out var enabled)) config.EnableScheduledDump = enabled;
|
||||||
|
|
||||||
|
var interval = ConfigurationManager.AppSettings["Dump:ScheduledIntervalMinutes"];
|
||||||
|
if (int.TryParse(interval, out var min)) config.ScheduledIntervalMinutes = min;
|
||||||
|
|
||||||
|
var sizeLimit = ConfigurationManager.AppSettings["Dump:MiniDumpSizeLimitMB"];
|
||||||
|
if (long.TryParse(sizeLimit, out var size)) config.MiniDumpSizeLimitMB = size;
|
||||||
|
|
||||||
|
var retentionDays = ConfigurationManager.AppSettings["Dump:RetentionDays"];
|
||||||
|
if (int.TryParse(retentionDays, out var days)) config.RetentionDays = days;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace XP.Common.Helpers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 进程管理工具类 | Process Management Helper
|
||||||
|
/// 提供启动外部程序、窗口置前等通用功能 | Provides common functions like launching external programs and bringing windows to front
|
||||||
|
/// </summary>
|
||||||
|
public static class ProcessHelper
|
||||||
|
{
|
||||||
|
#region Win32 API
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool IsIconic(IntPtr hWnd);
|
||||||
|
|
||||||
|
private const int SW_RESTORE = 9;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动或激活外部程序 | Start or activate an external program
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exePath">可执行文件完整路径 | Full path to the executable</param>
|
||||||
|
/// <param name="singleInstance">是否单实例模式:true=已运行则置前,false=直接启动新实例 | Single instance mode: true=activate if running, false=always start new instance</param>
|
||||||
|
/// <returns>操作结果 | Operation result</returns>
|
||||||
|
public static ProcessResult StartOrActivate(string exePath, bool singleInstance = true)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(exePath))
|
||||||
|
{
|
||||||
|
return ProcessResult.Fail("程序路径未配置 | Program path not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(exePath))
|
||||||
|
{
|
||||||
|
return ProcessResult.Fail($"程序文件不存在: {exePath} | Program file not found: {exePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 获取进程名(不含扩展名)| Get process name (without extension)
|
||||||
|
var processName = Path.GetFileNameWithoutExtension(exePath);
|
||||||
|
|
||||||
|
if (singleInstance)
|
||||||
|
{
|
||||||
|
var processes = Process.GetProcessesByName(processName);
|
||||||
|
|
||||||
|
if (processes.Length > 0)
|
||||||
|
{
|
||||||
|
// 程序已运行,将窗口置前 | Program already running, bring window to front
|
||||||
|
var process = processes[0];
|
||||||
|
var hWnd = process.MainWindowHandle;
|
||||||
|
|
||||||
|
if (hWnd != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
BringToFront(hWnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessResult.Activated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动程序 | Start program
|
||||||
|
Process.Start(new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = exePath,
|
||||||
|
UseShellExecute = true
|
||||||
|
});
|
||||||
|
|
||||||
|
return ProcessResult.Started();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return ProcessResult.Fail($"操作失败: {ex.Message} | Operation failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断指定进程是否正在运行 | Check if a process is running
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="processName">进程名(不含扩展名)| Process name (without extension)</param>
|
||||||
|
/// <returns>是否运行中 | Whether the process is running</returns>
|
||||||
|
public static bool IsProcessRunning(string processName)
|
||||||
|
{
|
||||||
|
return Process.GetProcessesByName(processName).Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据可执行文件路径查找已运行的进程 | Find a running process by executable file path
|
||||||
|
/// 通过进程名匹配并验证完整路径,确保找到的是同一个程序 | Matches by process name and verifies full path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exePath">可执行文件完整路径 | Full path to the executable</param>
|
||||||
|
/// <returns>找到的进程对象,未找到返回 null | The found process, or null if not found</returns>
|
||||||
|
public static Process FindRunningProcess(string exePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(exePath))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var processName = Path.GetFileNameWithoutExtension(exePath);
|
||||||
|
var normalizedTarget = NormalizePath(exePath);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processes = Process.GetProcessesByName(processName);
|
||||||
|
foreach (var proc in processes)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 验证完整路径匹配,避免同名不同路径的进程误关联
|
||||||
|
var procPath = proc.MainModule?.FileName;
|
||||||
|
if (procPath != null && string.Equals(NormalizePath(procPath), normalizedTarget, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 无法访问进程信息(权限不足等),跳过
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// GetProcessesByName 异常,返回 null
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标准化文件路径,用于路径比较 | Normalize file path for comparison
|
||||||
|
/// </summary>
|
||||||
|
private static string NormalizePath(string path)
|
||||||
|
{
|
||||||
|
return Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将窗口置前显示 | Bring a window to the foreground
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hWnd">窗口句柄 | Window handle</param>
|
||||||
|
public static void BringToFront(IntPtr hWnd)
|
||||||
|
{
|
||||||
|
if (hWnd == IntPtr.Zero) return;
|
||||||
|
|
||||||
|
// 如果窗口被最小化,先恢复 | If window is minimized, restore it first
|
||||||
|
if (IsIconic(hWnd))
|
||||||
|
{
|
||||||
|
ShowWindow(hWnd, SW_RESTORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetForegroundWindow(hWnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 进程操作结果 | Process Operation Result
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否成功 | Whether the operation succeeded
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为新启动(false表示激活已有窗口)| Whether it was newly started (false means activated existing window)
|
||||||
|
/// </summary>
|
||||||
|
public bool IsNewlyStarted { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误信息 | Error message
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建启动成功结果 | Create a started result
|
||||||
|
/// </summary>
|
||||||
|
public static ProcessResult Started() => new() { Success = true, IsNewlyStarted = true };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建激活成功结果 | Create an activated result
|
||||||
|
/// </summary>
|
||||||
|
public static ProcessResult Activated() => new() { Success = true, IsNewlyStarted = false };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建失败结果 | Create a failed result
|
||||||
|
/// </summary>
|
||||||
|
public static ProcessResult Fail(string errorMessage) => new() { Success = false, ErrorMessage = errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
using System;
|
||||||
|
using System.Configuration;
|
||||||
|
using System.Globalization;
|
||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Configs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化配置实现 | Localization configuration implementation
|
||||||
|
/// 使用 App.config 存储语言设置 | Uses App.config to store language settings
|
||||||
|
/// </summary>
|
||||||
|
public class LocalizationConfig : ILocalizationConfig
|
||||||
|
{
|
||||||
|
private const string LanguageKey = "Language";
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">日志服务 | Logger service</param>
|
||||||
|
public LocalizationConfig(ILoggerService logger)
|
||||||
|
{
|
||||||
|
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<LocalizationConfig>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取保存的语言设置 | Get saved language setting
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>语言设置,如果未设置则返回 null | Language setting, or null if not set</returns>
|
||||||
|
public SupportedLanguage? GetSavedLanguage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var languageString = ConfigurationManager.AppSettings[LanguageKey];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(languageString))
|
||||||
|
{
|
||||||
|
_logger.Info("No saved language setting found in configuration");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.TryParse<SupportedLanguage>(languageString, true, out var language))
|
||||||
|
{
|
||||||
|
_logger.Info($"Loaded saved language setting: {language}");
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Warn($"Invalid language setting in configuration: '{languageString}'. Expected values: ZhCN, ZhTW, EnUS");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (ConfigurationErrorsException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Configuration error while reading language setting");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Unexpected error while reading saved language setting");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存语言设置 | Save language setting
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">要保存的语言 | Language to save</param>
|
||||||
|
public void SaveLanguage(SupportedLanguage language)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
|
||||||
|
var appSettings = config.AppSettings.Settings;
|
||||||
|
|
||||||
|
var languageValue = language.ToString();
|
||||||
|
|
||||||
|
if (appSettings[LanguageKey] == null)
|
||||||
|
{
|
||||||
|
appSettings.Add(LanguageKey, languageValue);
|
||||||
|
_logger.Info($"Added new language setting to configuration: {language}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appSettings[LanguageKey].Value = languageValue;
|
||||||
|
_logger.Info($"Updated language setting in configuration: {language}");
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Save(ConfigurationSaveMode.Modified);
|
||||||
|
ConfigurationManager.RefreshSection("appSettings");
|
||||||
|
|
||||||
|
_logger.Info($"Successfully saved language setting: {language}");
|
||||||
|
}
|
||||||
|
catch (ConfigurationErrorsException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Configuration error while saving language setting: {language}");
|
||||||
|
throw new InvalidOperationException($"Failed to save language setting to configuration file: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Access denied while saving language setting: {language}");
|
||||||
|
throw new InvalidOperationException("Access denied to configuration file. Please check file permissions.", ex);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Unexpected error while saving language setting: {language}");
|
||||||
|
throw new InvalidOperationException($"Failed to save language setting: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取系统默认语言 | Get system default language
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>系统默认语言 | System default language</returns>
|
||||||
|
public SupportedLanguage GetSystemDefaultLanguage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var systemCulture = CultureInfo.CurrentUICulture.Name;
|
||||||
|
_logger.Info($"Detecting system default language from culture: {systemCulture}");
|
||||||
|
|
||||||
|
var defaultLanguage = systemCulture switch
|
||||||
|
{
|
||||||
|
"zh-CN" => SupportedLanguage.ZhCN,
|
||||||
|
"zh-TW" or "zh-HK" or "zh-MO" => SupportedLanguage.ZhTW,
|
||||||
|
"en-US" or "en" => SupportedLanguage.EnUS,
|
||||||
|
_ when systemCulture.StartsWith("zh", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.ZhCN,
|
||||||
|
_ when systemCulture.StartsWith("en", StringComparison.OrdinalIgnoreCase) => SupportedLanguage.EnUS,
|
||||||
|
_ => SupportedLanguage.ZhCN // 默认简体中文 | Default to Simplified Chinese
|
||||||
|
};
|
||||||
|
|
||||||
|
_logger.Info($"System default language determined: {defaultLanguage} (from culture: {systemCulture})");
|
||||||
|
return defaultLanguage;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "Error while detecting system default language, falling back to ZhCN");
|
||||||
|
return SupportedLanguage.ZhCN; // 默认简体中文 | Default to Simplified Chinese
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 支持的语言枚举 | Supported language enumeration
|
||||||
|
/// </summary>
|
||||||
|
public enum SupportedLanguage
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 简体中文 | Simplified Chinese
|
||||||
|
/// </summary>
|
||||||
|
[Description("zh-CN")]
|
||||||
|
ZhCN,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 繁体中文 | Traditional Chinese
|
||||||
|
/// </summary>
|
||||||
|
[Description("zh-TW")]
|
||||||
|
ZhTW,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 美式英语 | American English
|
||||||
|
/// </summary>
|
||||||
|
[Description("en-US")]
|
||||||
|
EnUS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Prism 语言切换事件 | Prism language changed event
|
||||||
|
/// 用于跨模块通知语言切换 | Used for cross-module language change notification
|
||||||
|
/// </summary>
|
||||||
|
public class LanguageChangedEvent : PubSubEvent<LanguageChangedEventArgs>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 语言切换事件参数 | Language changed event arguments
|
||||||
|
/// </summary>
|
||||||
|
public class LanguageChangedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 旧语言 | Old language
|
||||||
|
/// </summary>
|
||||||
|
public SupportedLanguage OldLanguage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 新语言 | New language
|
||||||
|
/// </summary>
|
||||||
|
public SupportedLanguage NewLanguage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换时间 | Change timestamp
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="oldLanguage">旧语言 | Old language</param>
|
||||||
|
/// <param name="newLanguage">新语言 | New language</param>
|
||||||
|
public LanguageChangedEventArgs(SupportedLanguage oldLanguage, SupportedLanguage newLanguage)
|
||||||
|
{
|
||||||
|
OldLanguage = oldLanguage;
|
||||||
|
NewLanguage = newLanguage;
|
||||||
|
Timestamp = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化异常基类 | Localization exception base class
|
||||||
|
/// </summary>
|
||||||
|
public class LocalizationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息 | Exception message</param>
|
||||||
|
public LocalizationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息 | Exception message</param>
|
||||||
|
/// <param name="innerException">内部异常 | Inner exception</param>
|
||||||
|
public LocalizationException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化初始化异常 | Localization initialization exception
|
||||||
|
/// 当资源文件无法加载时抛出 | Thrown when resource files cannot be loaded
|
||||||
|
/// </summary>
|
||||||
|
public class LocalizationInitializationException : LocalizationException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息 | Exception message</param>
|
||||||
|
public LocalizationInitializationException(string message)
|
||||||
|
: base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">异常消息 | Exception message</param>
|
||||||
|
/// <param name="innerException">内部异常 | Inner exception</param>
|
||||||
|
public LocalizationInitializationException(string message, Exception innerException)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资源键未找到异常 | Resource key not found exception
|
||||||
|
/// </summary>
|
||||||
|
public class ResourceKeyNotFoundException : LocalizationException
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资源键 | Resource key
|
||||||
|
/// </summary>
|
||||||
|
public string ResourceKey { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resourceKey">资源键 | Resource key</param>
|
||||||
|
public ResourceKeyNotFoundException(string resourceKey)
|
||||||
|
: base($"Resource key not found: {resourceKey}")
|
||||||
|
{
|
||||||
|
ResourceKey = resourceKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows.Markup;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化标记扩展 | Localization markup extension
|
||||||
|
/// 用法: {loc:Localization Key=ResourceKey} | Usage: {loc:Localization Key=ResourceKey}
|
||||||
|
/// </summary>
|
||||||
|
[MarkupExtensionReturnType(typeof(string))]
|
||||||
|
public class LocalizationExtension : MarkupExtension
|
||||||
|
{
|
||||||
|
private static ILocalizationService? _localizationService;
|
||||||
|
private string _key = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 资源键 | Resource key
|
||||||
|
/// </summary>
|
||||||
|
[ConstructorArgument("key")]
|
||||||
|
public string Key
|
||||||
|
{
|
||||||
|
get => _key;
|
||||||
|
set => _key = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 默认构造函数 | Default constructor
|
||||||
|
/// </summary>
|
||||||
|
public LocalizationExtension()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 带资源键的构造函数 | Constructor with resource key
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
public LocalizationExtension(string key)
|
||||||
|
{
|
||||||
|
_key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化本地化服务(由 CommonModule 调用)| Initialize localization service (called by CommonModule)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizationService">本地化服务实例 | Localization service instance</param>
|
||||||
|
public static void Initialize(ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供本地化字符串值 | Provide localized string value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider">服务提供者 | Service provider</param>
|
||||||
|
/// <returns>本地化字符串 | Localized string</returns>
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_key))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_localizationService == null)
|
||||||
|
{
|
||||||
|
// 设计时回退 | Design-time fallback
|
||||||
|
return $"[{_key}]";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接返回当前语言的翻译字符串 | Return translated text for current language
|
||||||
|
return _localizationService.GetString(_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Resources;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
using XP.Common.Resources;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 静态本地化帮助类 | Static localization helper
|
||||||
|
/// 用法:Loc.Get("ResourceKey") 或 Loc.Get("ResourceKey", param1, param2)
|
||||||
|
/// Usage: Loc.Get("ResourceKey") or Loc.Get("ResourceKey", param1, param2)
|
||||||
|
/// </summary>
|
||||||
|
public static class LocalizationHelper
|
||||||
|
{
|
||||||
|
private static ILocalizationService? _localizationService;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化本地化帮助类(由 CommonModule 或 App 启动时调用)
|
||||||
|
/// Initialize the localization helper (called by CommonModule or App at startup)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizationService">本地化服务实例 | Localization service instance</param>
|
||||||
|
public static void Initialize(ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串(使用当前 UI 语言)| Get localized string (using current UI culture)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <returns>本地化字符串,找不到时返回键本身 | Localized string, returns key itself if not found</returns>
|
||||||
|
public static string Get(string key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (_localizationService != null)
|
||||||
|
return _localizationService.GetString(key);
|
||||||
|
|
||||||
|
// 兼容回退:未初始化时仍使用原始 ResourceManager
|
||||||
|
// Fallback: use original ResourceManager when not initialized
|
||||||
|
return Resources.Resources.ResourceManager.GetString(key, CultureInfo.CurrentUICulture) ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串并格式化 | Get localized string with formatting
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <param name="args">格式化参数 | Format arguments</param>
|
||||||
|
/// <returns>格式化后的本地化字符串 | Formatted localized string</returns>
|
||||||
|
public static string Get(string key, params object[] args)
|
||||||
|
{
|
||||||
|
var template = Get(key);
|
||||||
|
return args.Length > 0 ? string.Format(template, args) : template;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Resources;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资源源条目,封装名称与 ResourceManager 的映射
|
||||||
|
/// Resource source entry, encapsulating the mapping between name and ResourceManager
|
||||||
|
/// </summary>
|
||||||
|
internal class ResourceSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 资源源唯一标识 | Resource source unique identifier
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// .NET 资源管理器实例 | .NET ResourceManager instance
|
||||||
|
/// </summary>
|
||||||
|
public ResourceManager ResourceManager { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">资源源名称(如 "XP.Scan")| Resource source name (e.g. "XP.Scan")</param>
|
||||||
|
/// <param name="resourceManager">模块的 ResourceManager 实例 | Module's ResourceManager instance</param>
|
||||||
|
/// <exception cref="ArgumentNullException">当 name 或 resourceManager 为 null 时抛出 | Thrown when name or resourceManager is null</exception>
|
||||||
|
public ResourceSource(string name, ResourceManager resourceManager)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
ResourceManager = resourceManager ?? throw new ArgumentNullException(nameof(resourceManager));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Resources;
|
||||||
|
using System.Threading;
|
||||||
|
using Prism.Events;
|
||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
using XP.Common.Localization.Events;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Resx 本地化服务实现 | Resx localization service implementation
|
||||||
|
/// 使用 .NET ResourceManager 加载 Resx 资源文件 | Uses .NET ResourceManager to load Resx resource files
|
||||||
|
/// 支持多资源源 Fallback Chain 机制 | Supports multi-source Fallback Chain mechanism
|
||||||
|
/// </summary>
|
||||||
|
public class ResxLocalizationService : ILocalizationService
|
||||||
|
{
|
||||||
|
private readonly List<ResourceSource> _resourceSources = new List<ResourceSource>();
|
||||||
|
private readonly ReaderWriterLockSlim _rwLock = new ReaderWriterLockSlim();
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
private readonly ILocalizationConfig _config;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private SupportedLanguage _currentLanguage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前语言 | Get current language
|
||||||
|
/// </summary>
|
||||||
|
public SupportedLanguage CurrentLanguage => _currentLanguage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 语言切换事件 | Language changed event
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LanguageChangedEventArgs>? LanguageChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
|
||||||
|
/// <param name="config">本地化配置 | Localization configuration</param>
|
||||||
|
/// <param name="logger">日志服务 | Logger service</param>
|
||||||
|
public ResxLocalizationService(
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
|
ILocalizationConfig config,
|
||||||
|
ILoggerService logger)
|
||||||
|
{
|
||||||
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
_logger = (logger ?? throw new ArgumentNullException(nameof(logger))).ForModule<ResxLocalizationService>();
|
||||||
|
|
||||||
|
// 初始化默认资源源(XP.Common),注册到 Fallback Chain[0]
|
||||||
|
// Initialize default resource source (XP.Common), registered at Fallback Chain[0]
|
||||||
|
var defaultResourceManager = new ResourceManager(
|
||||||
|
"XP.Common.Resources.Resources",
|
||||||
|
Assembly.GetExecutingAssembly());
|
||||||
|
_resourceSources.Add(new ResourceSource("XP.Common", defaultResourceManager));
|
||||||
|
|
||||||
|
// 加载保存的语言或使用默认语言 | Load saved language or use default language
|
||||||
|
InitializeLanguage();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <returns>本地化字符串 | Localized string</returns>
|
||||||
|
public string GetString(string key)
|
||||||
|
{
|
||||||
|
return GetString(key, CultureInfo.CurrentUICulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串(指定语言)| Get localized string (specified language)
|
||||||
|
/// 从 Fallback Chain 末尾向前遍历,第一个返回非 null 值的 ResourceManager 即为结果
|
||||||
|
/// Traverses the Fallback Chain from end to beginning, first ResourceManager returning non-null wins
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <param name="culture">文化信息 | Culture info</param>
|
||||||
|
/// <returns>本地化字符串 | Localized string</returns>
|
||||||
|
public string GetString(string key, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rwLock.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从末尾向前遍历(后注册 = 高优先级)
|
||||||
|
// Traverse from end to beginning (last registered = highest priority)
|
||||||
|
for (int i = _resourceSources.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = _resourceSources[i].ResourceManager.GetString(key, culture);
|
||||||
|
if (value != null)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 单个 ResourceManager 抛出异常时捕获并继续遍历下一个
|
||||||
|
// Catch exception from individual ResourceManager and continue to next
|
||||||
|
_logger.Error(ex, $"资源源 '{_resourceSources[i].Name}' 查找键 '{key}' 时出错 | Error looking up key '{key}' in resource source '{_resourceSources[i].Name}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_rwLock.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部未找到则返回 key 本身并记录警告日志
|
||||||
|
// If all sources fail to find the key, return the key itself and log warning
|
||||||
|
_logger.Warn($"资源键未找到 | Resource key not found: '{key}' for culture '{culture?.Name ?? "null"}'");
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
|
||||||
|
/// Register a module resource source to the end of the Fallback Chain (highest priority)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">资源源名称 | Resource source name</param>
|
||||||
|
/// <param name="resourceManager">模块的 ResourceManager 实例 | Module's ResourceManager instance</param>
|
||||||
|
public void RegisterResourceSource(string name, ResourceManager resourceManager)
|
||||||
|
{
|
||||||
|
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||||
|
if (resourceManager == null) throw new ArgumentNullException(nameof(resourceManager));
|
||||||
|
|
||||||
|
_rwLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 检查重复名称 | Check for duplicate name
|
||||||
|
if (_resourceSources.Any(s => s.Name == name))
|
||||||
|
{
|
||||||
|
_logger.Warn($"资源源 '{name}' 已存在,注册被拒绝 | Resource source '{name}' already exists, registration rejected");
|
||||||
|
throw new InvalidOperationException($"Resource source '{name}' is already registered.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_resourceSources.Add(new ResourceSource(name, resourceManager));
|
||||||
|
_logger.Info($"资源源 '{name}' 已注册到 Fallback Chain | Resource source '{name}' registered to Fallback Chain");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_rwLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Fallback Chain 中注销指定资源源
|
||||||
|
/// Unregister the specified resource source from the Fallback Chain
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">资源源名称 | Resource source name</param>
|
||||||
|
public void UnregisterResourceSource(string name)
|
||||||
|
{
|
||||||
|
if (name == null) throw new ArgumentNullException(nameof(name));
|
||||||
|
|
||||||
|
// 禁止注销默认资源源 | Prevent unregistering default resource source
|
||||||
|
if (name == "XP.Common")
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Cannot unregister the default resource source 'XP.Common'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_rwLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var source = _resourceSources.FirstOrDefault(s => s.Name == name);
|
||||||
|
if (source != null)
|
||||||
|
{
|
||||||
|
_resourceSources.Remove(source);
|
||||||
|
_logger.Info($"资源源 '{name}' 已从 Fallback Chain 注销 | Resource source '{name}' unregistered from Fallback Chain");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 注销不存在的名称时静默忽略,记录警告日志
|
||||||
|
// Silently ignore non-existent name, log warning
|
||||||
|
_logger.Warn($"资源源 '{name}' 不存在,注销操作被忽略 | Resource source '{name}' does not exist, unregister ignored");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_rwLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置当前语言(仅保存配置,重启后生效)| Set current language (save config only, takes effect after restart)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">目标语言 | Target language</param>
|
||||||
|
public void SetLanguage(SupportedLanguage language)
|
||||||
|
{
|
||||||
|
_rwLock.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 如果语言相同,无需保存 | If language is the same, no need to save
|
||||||
|
if (_currentLanguage == language)
|
||||||
|
{
|
||||||
|
_logger.Info($"语言已经是 {language},无需切换 | Language is already {language}, no need to switch");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Info($"保存语言设置,重启后生效 | Saving language setting, takes effect after restart: {_currentLanguage} -> {language}");
|
||||||
|
|
||||||
|
// 仅保存到配置,不更新运行时状态 | Only save to config, do not update runtime state
|
||||||
|
_config.SaveLanguage(language);
|
||||||
|
|
||||||
|
_logger.Info($"语言设置已保存 | Language setting saved: {language}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"语言设置保存失败 | Failed to save language setting: {language}");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_rwLock.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有支持的语言 | Get all supported languages
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>支持的语言列表 | List of supported languages</returns>
|
||||||
|
public IEnumerable<SupportedLanguage> GetSupportedLanguages()
|
||||||
|
{
|
||||||
|
return Enum.GetValues(typeof(SupportedLanguage))
|
||||||
|
.Cast<SupportedLanguage>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化语言设置 | Initialize language settings
|
||||||
|
/// </summary>
|
||||||
|
private void InitializeLanguage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 尝试获取保存的语言 | Try to get saved language
|
||||||
|
var savedLanguage = _config.GetSavedLanguage();
|
||||||
|
var language = savedLanguage ?? _config.GetSystemDefaultLanguage();
|
||||||
|
|
||||||
|
_currentLanguage = language;
|
||||||
|
var culture = GetCultureInfo(language);
|
||||||
|
CultureInfo.CurrentUICulture = culture;
|
||||||
|
CultureInfo.CurrentCulture = culture;
|
||||||
|
|
||||||
|
_logger.Info($"语言初始化完成 | Language initialized: {language} (Culture: {culture.Name})");
|
||||||
|
|
||||||
|
// 验证资源文件是否可加载 | Verify resource file can be loaded
|
||||||
|
ValidateResourceFile(culture);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "语言初始化失败 | Language initialization failed");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取语言对应的 CultureInfo | Get CultureInfo for language
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">支持的语言 | Supported language</param>
|
||||||
|
/// <returns>文化信息 | Culture info</returns>
|
||||||
|
private CultureInfo GetCultureInfo(SupportedLanguage language)
|
||||||
|
{
|
||||||
|
return language switch
|
||||||
|
{
|
||||||
|
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
|
||||||
|
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
|
||||||
|
SupportedLanguage.EnUS => new CultureInfo("en-US"),
|
||||||
|
_ => new CultureInfo("zh-CN") // 默认简体中文 | Default to Simplified Chinese
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证资源文件是否可加载 | Validate resource file can be loaded
|
||||||
|
/// 使用默认资源源(XP.Common)进行验证 | Uses default resource source (XP.Common) for validation
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="culture">文化信息 | Culture info</param>
|
||||||
|
private void ValidateResourceFile(CultureInfo culture)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 使用第一个资源源(XP.Common)进行验证
|
||||||
|
// Use the first resource source (XP.Common) for validation
|
||||||
|
var testValue = _resourceSources[0].ResourceManager.GetString("App_Title", culture);
|
||||||
|
|
||||||
|
if (testValue != null)
|
||||||
|
{
|
||||||
|
_logger.Info($"资源文件验证成功 | Resource file validated for culture: {culture.Name}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Warn($"资源文件可能不完整 | Resource file may be incomplete for culture: {culture.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
_logger.Warn($"资源文件验证失败,将使用默认资源 | Resource file validation failed, will use default resources for culture: {culture.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化配置接口 | Localization configuration interface
|
||||||
|
/// 负责语言设置的加载和保存 | Responsible for loading and saving language settings
|
||||||
|
/// </summary>
|
||||||
|
public interface ILocalizationConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取保存的语言设置 | Get saved language setting
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>语言设置,如果未设置则返回 null | Language setting, or null if not set</returns>
|
||||||
|
SupportedLanguage? GetSavedLanguage();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存语言设置 | Save language setting
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">要保存的语言 | Language to save</param>
|
||||||
|
void SaveLanguage(SupportedLanguage language);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取系统默认语言 | Get system default language
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>系统默认语言 | System default language</returns>
|
||||||
|
SupportedLanguage GetSystemDefaultLanguage();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Resources;
|
||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
using XP.Common.Localization.Events;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 本地化服务接口 | Localization service interface
|
||||||
|
/// 提供多语言资源访问和语言切换功能 | Provides multilingual resource access and language switching
|
||||||
|
/// </summary>
|
||||||
|
public interface ILocalizationService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前语言 | Get current language
|
||||||
|
/// </summary>
|
||||||
|
SupportedLanguage CurrentLanguage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 语言切换事件 | Language changed event
|
||||||
|
/// </summary>
|
||||||
|
event EventHandler<LanguageChangedEventArgs> LanguageChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串(使用当前语言)| Get localized string (using current language)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <returns>本地化字符串 | Localized string</returns>
|
||||||
|
string GetString(string key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取本地化字符串(指定语言)| Get localized string (specified language)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">资源键 | Resource key</param>
|
||||||
|
/// <param name="culture">文化信息 | Culture info</param>
|
||||||
|
/// <returns>本地化字符串 | Localized string</returns>
|
||||||
|
string GetString(string key, CultureInfo culture);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置当前语言 | Set current language
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">目标语言 | Target language</param>
|
||||||
|
void SetLanguage(SupportedLanguage language);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取所有支持的语言 | Get all supported languages
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>支持的语言列表 | List of supported languages</returns>
|
||||||
|
IEnumerable<SupportedLanguage> GetSupportedLanguages();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册模块资源源到 Fallback Chain 末尾(最高优先级)
|
||||||
|
/// Register a module resource source to the end of the Fallback Chain (highest priority)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">资源源名称(如 "XP.Scan"),用于标识和注销 | Resource source name (e.g. "XP.Scan"), used for identification and unregistration</param>
|
||||||
|
/// <param name="resourceManager">模块的 ResourceManager 实例 | The module's ResourceManager instance</param>
|
||||||
|
void RegisterResourceSource(string name, ResourceManager resourceManager);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Fallback Chain 中注销指定资源源
|
||||||
|
/// Unregister the specified resource source from the Fallback Chain
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">资源源名称 | Resource source name</param>
|
||||||
|
void UnregisterResourceSource(string name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 语言选项数据模型 | Language option data model
|
||||||
|
/// 用于在 UI 中显示可选语言 | Used to display available languages in UI
|
||||||
|
/// </summary>
|
||||||
|
public class LanguageOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 语言枚举值 | Language enum value
|
||||||
|
/// </summary>
|
||||||
|
public SupportedLanguage Language { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 显示名称 | Display name
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 语言标志/图标 | Language flag/icon
|
||||||
|
/// </summary>
|
||||||
|
public string Flag { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="language">语言 | Language</param>
|
||||||
|
/// <param name="displayName">显示名称 | Display name</param>
|
||||||
|
/// <param name="flag">标志 | Flag</param>
|
||||||
|
public LanguageOption(SupportedLanguage language, string displayName, string flag = "")
|
||||||
|
{
|
||||||
|
Language = language;
|
||||||
|
DisplayName = displayName;
|
||||||
|
Flag = flag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Windows;
|
||||||
|
using Prism.Commands;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using XP.Common.Localization.Enums;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 语言切换器 ViewModel | Language switcher ViewModel
|
||||||
|
/// 提供语言选择和切换功能 | Provides language selection and switching functionality
|
||||||
|
/// </summary>
|
||||||
|
public class LanguageSwitcherViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
private readonly ILocalizationConfig _config;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private LanguageOption? _selectedLanguage;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 选中的语言选项 | Selected language option
|
||||||
|
/// </summary>
|
||||||
|
public LanguageOption? SelectedLanguage
|
||||||
|
{
|
||||||
|
get => _selectedLanguage;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _selectedLanguage, value))
|
||||||
|
{
|
||||||
|
ApplyCommand.RaiseCanExecuteChanged();
|
||||||
|
UpdatePreviewTexts();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 可用语言列表 | Available languages list
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LanguageOption> AvailableLanguages { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用按钮命令 | Apply button command
|
||||||
|
/// </summary>
|
||||||
|
public DelegateCommand ApplyCommand { get; }
|
||||||
|
|
||||||
|
private string _previewRestartNotice = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// 预览重启提示文本 | Preview restart notice text
|
||||||
|
/// </summary>
|
||||||
|
public string PreviewRestartNotice
|
||||||
|
{
|
||||||
|
get => _previewRestartNotice;
|
||||||
|
set => SetProperty(ref _previewRestartNotice, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _previewApplyButtonText = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// 预览应用按钮文本 | Preview apply button text
|
||||||
|
/// </summary>
|
||||||
|
public string PreviewApplyButtonText
|
||||||
|
{
|
||||||
|
get => _previewApplyButtonText;
|
||||||
|
set => SetProperty(ref _previewApplyButtonText, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="localizationService">本地化服务 | Localization service</param>
|
||||||
|
/// <param name="config">本地化配置 | Localization configuration</param>
|
||||||
|
/// <param name="logger">日志服务 | Logger service</param>
|
||||||
|
public LanguageSwitcherViewModel(
|
||||||
|
ILocalizationService localizationService,
|
||||||
|
ILocalizationConfig config,
|
||||||
|
ILoggerService logger)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
_logger = logger.ForModule<LanguageSwitcherViewModel>();
|
||||||
|
|
||||||
|
// 初始化可用语言列表 | Initialize available languages list
|
||||||
|
AvailableLanguages = new[]
|
||||||
|
{
|
||||||
|
new LanguageOption(SupportedLanguage.ZhCN, "简体中文", "🇨🇳"),
|
||||||
|
new LanguageOption(SupportedLanguage.ZhTW, "繁體中文", "🇹🇼"),
|
||||||
|
new LanguageOption(SupportedLanguage.EnUS, "English", "🇺🇸")
|
||||||
|
};
|
||||||
|
|
||||||
|
// 设置当前选中的语言 | Set currently selected language
|
||||||
|
var currentLang = localizationService.CurrentLanguage;
|
||||||
|
foreach (var lang in AvailableLanguages)
|
||||||
|
{
|
||||||
|
if (lang.Language == currentLang)
|
||||||
|
{
|
||||||
|
_selectedLanguage = lang;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化命令 | Initialize commands
|
||||||
|
ApplyCommand = new DelegateCommand(OnApply, CanApply);
|
||||||
|
|
||||||
|
// 初始化预览文本 | Initialize preview texts
|
||||||
|
UpdatePreviewTexts();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新预览文本 | Update preview texts
|
||||||
|
/// </summary>
|
||||||
|
private void UpdatePreviewTexts()
|
||||||
|
{
|
||||||
|
if (_selectedLanguage == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// 获取选中语言的 CultureInfo | Get CultureInfo for selected language
|
||||||
|
var previewCulture = GetCultureInfo(_selectedLanguage.Language);
|
||||||
|
|
||||||
|
// 使用选中的语言获取本地化文本 | Get localized text using selected language
|
||||||
|
PreviewRestartNotice = _localizationService.GetString("Settings_Language_RestartNotice", previewCulture);
|
||||||
|
PreviewApplyButtonText = _localizationService.GetString("Button_Apply", previewCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断是否可以应用 | Determine if can apply
|
||||||
|
/// 与配置文件中已保存的语言比较 | Compare with saved language in config
|
||||||
|
/// </summary>
|
||||||
|
private bool CanApply()
|
||||||
|
{
|
||||||
|
if (_selectedLanguage == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// 获取配置文件中已保存的语言 | Get saved language from config
|
||||||
|
var savedLanguage = _config.GetSavedLanguage() ?? _localizationService.CurrentLanguage;
|
||||||
|
return _selectedLanguage.Language != savedLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用语言切换 | Apply language change
|
||||||
|
/// </summary>
|
||||||
|
private void OnApply()
|
||||||
|
{
|
||||||
|
if (_selectedLanguage == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newLanguage = _selectedLanguage.Language;
|
||||||
|
_localizationService.SetLanguage(newLanguage);
|
||||||
|
|
||||||
|
_logger.Info($"Language switched to {newLanguage}");
|
||||||
|
|
||||||
|
// 显示重启提示对话框 | Show restart prompt dialog
|
||||||
|
MessageBox.Show(
|
||||||
|
_localizationService.GetString("Settings_Language_SavedRestartRequired"),
|
||||||
|
_localizationService.GetString("Dialog_Notice"),
|
||||||
|
MessageBoxButton.OK,
|
||||||
|
MessageBoxImage.Information);
|
||||||
|
|
||||||
|
// 更新命令状态 | Update command state
|
||||||
|
ApplyCommand.RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, $"Failed to switch language to {_selectedLanguage.Language}");
|
||||||
|
|
||||||
|
// 显示错误对话框 | Show error dialog
|
||||||
|
var errorMsg = string.Format(
|
||||||
|
_localizationService.GetString("Settings_Language_SwitchFailed"),
|
||||||
|
ex.Message);
|
||||||
|
MessageBox.Show(
|
||||||
|
errorMsg,
|
||||||
|
_localizationService.GetString("Dialog_Error"),
|
||||||
|
MessageBoxButton.OK,
|
||||||
|
MessageBoxImage.Error);
|
||||||
|
|
||||||
|
// 恢复到之前的语言 | Restore previous language
|
||||||
|
var currentLang = _localizationService.CurrentLanguage;
|
||||||
|
foreach (var lang in AvailableLanguages)
|
||||||
|
{
|
||||||
|
if (lang.Language == currentLang)
|
||||||
|
{
|
||||||
|
_selectedLanguage = lang;
|
||||||
|
RaisePropertyChanged(nameof(SelectedLanguage));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取语言对应的 CultureInfo | Get CultureInfo for language
|
||||||
|
/// </summary>
|
||||||
|
private CultureInfo GetCultureInfo(SupportedLanguage language)
|
||||||
|
{
|
||||||
|
return language switch
|
||||||
|
{
|
||||||
|
SupportedLanguage.ZhCN => new CultureInfo("zh-CN"),
|
||||||
|
SupportedLanguage.ZhTW => new CultureInfo("zh-TW"),
|
||||||
|
SupportedLanguage.EnUS => new CultureInfo("en-US"),
|
||||||
|
_ => new CultureInfo("zh-CN")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<Window x:Class="XP.Common.Localization.Views.LanguageSwitcherWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||||
|
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||||
|
xmlns:vm="clr-namespace:XP.Common.Localization.ViewModels"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:LanguageSwitcherViewModel}"
|
||||||
|
Title="{loc:Localization Key=Settings_Language}"
|
||||||
|
Height="315" Width="500"
|
||||||
|
MinHeight="300" MinWidth="450"
|
||||||
|
ResizeMode="CanResize"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="White">
|
||||||
|
|
||||||
|
<!-- 主容器,带边距 | Main container with margins -->
|
||||||
|
<Grid Margin="24,16,24,16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- 标题 | Title -->
|
||||||
|
<TextBlock Grid.Row="0"
|
||||||
|
Text="{loc:Localization Key=Settings_Language}"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Margin="0,0,0,8"/>
|
||||||
|
|
||||||
|
<!-- 副标题/描述 | Subtitle/Description -->
|
||||||
|
<TextBlock Grid.Row="1"
|
||||||
|
Text="{loc:Localization Key=Settings_Language_Description}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="#757575"
|
||||||
|
Margin="0,0,0,20"/>
|
||||||
|
|
||||||
|
<!-- 直接显示下拉选择框(无卡片)| Direct ComboBox display (no card) -->
|
||||||
|
<telerik:RadComboBox Grid.Row="2"
|
||||||
|
ItemsSource="{Binding AvailableLanguages}"
|
||||||
|
SelectedItem="{Binding SelectedLanguage, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Height="36"
|
||||||
|
Margin="0,0,0,16"
|
||||||
|
telerik:StyleManager.Theme="Crystal">
|
||||||
|
<telerik:RadComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="4,2">
|
||||||
|
<!-- 语言标志 | Language flag -->
|
||||||
|
<TextBlock Text="{Binding Flag}"
|
||||||
|
Margin="0,0,8,0"
|
||||||
|
FontSize="16"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<!-- 语言名称 | Language name -->
|
||||||
|
<TextBlock Text="{Binding DisplayName}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
FontSize="13"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</telerik:RadComboBox.ItemTemplate>
|
||||||
|
</telerik:RadComboBox>
|
||||||
|
|
||||||
|
<!-- 提示信息区域 | Notice information area -->
|
||||||
|
<Border Grid.Row="3"
|
||||||
|
Background="#E3F2FD"
|
||||||
|
BorderBrush="#2196F3"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="4"
|
||||||
|
Padding="12"
|
||||||
|
Margin="0,0,0,16">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 信息图标 | Information icon -->
|
||||||
|
<Path Grid.Column="0"
|
||||||
|
Data="M12,2C6.48,2 2,6.48 2,12C2,17.52 6.48,22 12,22C17.52,22 22,17.52 22,12C22,6.48 17.52,2 12,2M13,17H11V11H13V17M13,9H11V7H13V9Z"
|
||||||
|
Fill="#2196F3"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
Stretch="Uniform"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,2,12,0"/>
|
||||||
|
|
||||||
|
<!-- 提示文本 | Notice text -->
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding PreviewRestartNotice}"
|
||||||
|
FontSize="12"
|
||||||
|
FontWeight="Medium"
|
||||||
|
Foreground="#1976D2"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- 占位空间 | Spacer -->
|
||||||
|
<Grid Grid.Row="4"/>
|
||||||
|
|
||||||
|
<!-- 底部操作区 | Bottom action area -->
|
||||||
|
<Grid Grid.Row="5">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 应用按钮(绿色)| Apply button (green) -->
|
||||||
|
<telerik:RadButton Grid.Column="1"
|
||||||
|
Content="{Binding PreviewApplyButtonText}"
|
||||||
|
Command="{Binding ApplyCommand}"
|
||||||
|
Width="100"
|
||||||
|
Height="36"
|
||||||
|
telerik:StyleManager.Theme="Crystal">
|
||||||
|
<telerik:RadButton.Background>
|
||||||
|
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
|
||||||
|
<GradientStop Color="White"/>
|
||||||
|
<GradientStop Color="#FF77E062" Offset="1"/>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</telerik:RadButton.Background>
|
||||||
|
</telerik:RadButton>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using XP.Common.Localization.ViewModels;
|
||||||
|
|
||||||
|
namespace XP.Common.Localization.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// LanguageSwitcherWindow.xaml 的交互逻辑 | Interaction logic for LanguageSwitcherWindow.xaml
|
||||||
|
/// </summary>
|
||||||
|
public partial class LanguageSwitcherWindow : Window
|
||||||
|
{
|
||||||
|
public LanguageSwitcherWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数(带 ViewModel)| Constructor (with ViewModel)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="viewModel">语言切换器 ViewModel | Language switcher ViewModel</param>
|
||||||
|
public LanguageSwitcherWindow(LanguageSwitcherViewModel viewModel) : this()
|
||||||
|
{
|
||||||
|
DataContext = viewModel;
|
||||||
|
|
||||||
|
if (Application.Current?.MainWindow != null)
|
||||||
|
Icon = Application.Current.MainWindow.Icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System;
|
||||||
|
using Serilog;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Logging.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog日志服务实现(适配ILoggerService接口)| Serilog logger service implementation (adapts ILoggerService interface)
|
||||||
|
/// </summary>
|
||||||
|
public class SerilogLoggerService : ILoggerService
|
||||||
|
{
|
||||||
|
private ILogger _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数:初始化全局日志实例 | Constructor: initialize global logger instance
|
||||||
|
/// </summary>
|
||||||
|
public SerilogLoggerService()
|
||||||
|
{
|
||||||
|
_logger = Log.Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 私有构造函数:用于模块标记 | Private constructor: for module tagging
|
||||||
|
/// </summary>
|
||||||
|
private SerilogLoggerService(ILogger logger)
|
||||||
|
{
|
||||||
|
_logger = logger ?? Log.Logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Debug(string message, params object[] args)
|
||||||
|
{
|
||||||
|
_logger.Debug(message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Info(string message, params object[] args)
|
||||||
|
{
|
||||||
|
_logger.Information(message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Warn(string message, params object[] args)
|
||||||
|
{
|
||||||
|
_logger.Warning(message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Error(Exception ex, string message, params object[] args)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Fatal(Exception ex, string message, params object[] args)
|
||||||
|
{
|
||||||
|
_logger.Fatal(ex, message, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 手动指定模块名 | Manually specify module name
|
||||||
|
/// </summary>
|
||||||
|
public ILoggerService ForModule(string moduleName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(moduleName)) return this;
|
||||||
|
return new SerilogLoggerService(_logger.ForContext("SourceContext", moduleName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动使用类型全名作为模块名 | Automatically use type full name as module name
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">类型参数 | Type parameter</typeparam>
|
||||||
|
public ILoggerService ForModule<T>()
|
||||||
|
{
|
||||||
|
var typeName = typeof(T).FullName ?? typeof(T).Name;
|
||||||
|
return new SerilogLoggerService(_logger.ForContext("SourceContext", typeName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.Logging.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用日志服务接口(与具体日志框架解耦)| Generic logger service interface (decoupled from specific logging framework)
|
||||||
|
/// </summary>
|
||||||
|
public interface ILoggerService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 调试日志 | Debug log
|
||||||
|
/// </summary>
|
||||||
|
void Debug(string message, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 信息日志 | Information log
|
||||||
|
/// </summary>
|
||||||
|
void Info(string message, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 警告日志 | Warning log
|
||||||
|
/// </summary>
|
||||||
|
void Warn(string message, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误日志(带异常)| Error log (with exception)
|
||||||
|
/// </summary>
|
||||||
|
void Error(Exception ex, string message, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 致命错误日志(带异常)| Fatal error log (with exception)
|
||||||
|
/// </summary>
|
||||||
|
void Fatal(Exception ex, string message, params object[] args);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记日志所属模块(手动指定模块名)| Mark logger module (manually specify module name)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="moduleName">模块名称 | Module name</param>
|
||||||
|
ILoggerService ForModule(string moduleName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 标记日志所属模块(自动使用类型全名)| Mark logger module (automatically use type full name)
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">类型参数(自动推断命名空间+类名)| Type parameter (automatically infer namespace + class name)</typeparam>
|
||||||
|
ILoggerService ForModule<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using Serilog;
|
||||||
|
using Serilog.Events;
|
||||||
|
using XP.Common.Configs;
|
||||||
|
using XP.Common.Logging.ViewModels;
|
||||||
|
|
||||||
|
namespace XP.Common.Logging
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog全局初始化工具(应用启动时调用一次)
|
||||||
|
/// </summary>
|
||||||
|
public static class SerilogInitializer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化Serilog日志
|
||||||
|
/// </summary>
|
||||||
|
public static void Initialize(SerilogConfig config)
|
||||||
|
{
|
||||||
|
if (config == null) throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
|
// 确保日志目录存在
|
||||||
|
if (!System.IO.Directory.Exists(config.LogPath))
|
||||||
|
System.IO.Directory.CreateDirectory(config.LogPath);
|
||||||
|
|
||||||
|
// 解析日志级别
|
||||||
|
if (!Enum.TryParse<LogEventLevel>(config.MinimumLevel, true, out var minLevel))
|
||||||
|
minLevel = LogEventLevel.Information;
|
||||||
|
|
||||||
|
// 解析文件分割规则
|
||||||
|
if (!Enum.TryParse<RollingInterval>(config.RollingInterval, true, out var rollingInterval))
|
||||||
|
rollingInterval = RollingInterval.Day;
|
||||||
|
|
||||||
|
// 构建Serilog配置
|
||||||
|
var loggerConfig = new LoggerConfiguration()
|
||||||
|
.MinimumLevel.Is(minLevel)
|
||||||
|
.Enrich.FromLogContext() // 启用上下文(模块标记)
|
||||||
|
.WriteTo.File(
|
||||||
|
path: System.IO.Path.Combine(config.LogPath, "app_.log"), // 最终文件名:app_20260307.log
|
||||||
|
rollingInterval: rollingInterval,
|
||||||
|
fileSizeLimitBytes: config.FileSizeLimitMB * 1024 * 1024,
|
||||||
|
retainedFileCountLimit: config.RetainedFileCountLimit,
|
||||||
|
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 调试环境输出到控制台
|
||||||
|
if (config.EnableConsole)
|
||||||
|
loggerConfig.WriteTo.Console(
|
||||||
|
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}");
|
||||||
|
|
||||||
|
// 注册实时日志查看器 Sink(接收所有级别日志)| Register real-time log viewer sink (receive all levels)
|
||||||
|
loggerConfig.WriteTo.Sink(RealTimeLogSink.Instance, Serilog.Events.LogEventLevel.Verbose);
|
||||||
|
|
||||||
|
// 全局初始化
|
||||||
|
Log.Logger = loggerConfig.CreateLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
|
||||||
|
namespace XP.Common.Logging.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Serilog 自定义 Sink,将日志事件转发到 RealTimeLogViewerViewModel,并维护环形缓冲区
|
||||||
|
/// Custom Serilog Sink that forwards log events and maintains a ring buffer for history
|
||||||
|
/// </summary>
|
||||||
|
public class RealTimeLogSink : ILogEventSink
|
||||||
|
{
|
||||||
|
private const int BufferCapacity = 500;
|
||||||
|
private readonly ConcurrentQueue<LogEvent> _buffer = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局单例实例 | Global singleton instance
|
||||||
|
/// </summary>
|
||||||
|
public static RealTimeLogSink Instance { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志事件到达时触发 | Fired when a log event arrives
|
||||||
|
/// </summary>
|
||||||
|
public event Action<LogEvent>? LogEventReceived;
|
||||||
|
|
||||||
|
public void Emit(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
// 入队缓冲区 | Enqueue to buffer
|
||||||
|
_buffer.Enqueue(logEvent);
|
||||||
|
while (_buffer.Count > BufferCapacity)
|
||||||
|
{
|
||||||
|
_buffer.TryDequeue(out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogEventReceived?.Invoke(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取缓冲区中的历史日志快照 | Get a snapshot of buffered history logs
|
||||||
|
/// </summary>
|
||||||
|
public List<LogEvent> GetBufferedHistory()
|
||||||
|
{
|
||||||
|
return _buffer.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
using Prism.Commands;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using Serilog.Events;
|
||||||
|
using XP.Common.Localization;
|
||||||
|
|
||||||
|
namespace XP.Common.Logging.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实时日志查看器 ViewModel,管理日志条目、过滤、自动滚动等逻辑
|
||||||
|
/// Real-time log viewer ViewModel, manages log entries, filtering, auto-scroll, etc.
|
||||||
|
/// </summary>
|
||||||
|
public class RealTimeLogViewerViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
|
private readonly ObservableCollection<LogDisplayEntry> _allEntries = new();
|
||||||
|
private string _filterText = string.Empty;
|
||||||
|
private bool _isAutoScroll = true;
|
||||||
|
private int _maxLines = 2000;
|
||||||
|
private int _totalCount;
|
||||||
|
private int _filteredCount;
|
||||||
|
private bool _showDebug = true;
|
||||||
|
private bool _showInfo = true;
|
||||||
|
private bool _showWarning = true;
|
||||||
|
private bool _showError = true;
|
||||||
|
private bool _showFatal = true;
|
||||||
|
|
||||||
|
public RealTimeLogViewerViewModel()
|
||||||
|
{
|
||||||
|
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||||
|
|
||||||
|
FilteredEntries = new ObservableCollection<LogDisplayEntry>();
|
||||||
|
ClearCommand = new DelegateCommand(ClearAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过滤后的日志条目集合(绑定到 UI)| Filtered log entries (bound to UI)
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<LogDisplayEntry> FilteredEntries { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过滤关键词 | Filter keyword
|
||||||
|
/// </summary>
|
||||||
|
public string FilterText
|
||||||
|
{
|
||||||
|
get => _filterText;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _filterText, value))
|
||||||
|
{
|
||||||
|
ApplyFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否自动滚动到底部 | Whether to auto-scroll to bottom
|
||||||
|
/// </summary>
|
||||||
|
public bool IsAutoScroll
|
||||||
|
{
|
||||||
|
get => _isAutoScroll;
|
||||||
|
set => SetProperty(ref _isAutoScroll, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大行数限制 | Maximum line count limit
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLines
|
||||||
|
{
|
||||||
|
get => _maxLines;
|
||||||
|
set => SetProperty(ref _maxLines, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 总日志条数 | Total log entry count
|
||||||
|
/// </summary>
|
||||||
|
public int TotalCount
|
||||||
|
{
|
||||||
|
get => _totalCount;
|
||||||
|
private set => SetProperty(ref _totalCount, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过滤后的日志条数 | Filtered log entry count
|
||||||
|
/// </summary>
|
||||||
|
public int FilteredCount
|
||||||
|
{
|
||||||
|
get => _filteredCount;
|
||||||
|
private set => SetProperty(ref _filteredCount, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否显示 Debug 级别日志 | Whether to show Debug level logs
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowDebug
|
||||||
|
{
|
||||||
|
get => _showDebug;
|
||||||
|
set { if (SetProperty(ref _showDebug, value)) ApplyFilter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否显示 Info 级别日志 | Whether to show Information level logs
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowInfo
|
||||||
|
{
|
||||||
|
get => _showInfo;
|
||||||
|
set { if (SetProperty(ref _showInfo, value)) ApplyFilter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否显示 Warning 级别日志 | Whether to show Warning level logs
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowWarning
|
||||||
|
{
|
||||||
|
get => _showWarning;
|
||||||
|
set { if (SetProperty(ref _showWarning, value)) ApplyFilter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否显示 Error 级别日志 | Whether to show Error level logs
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowError
|
||||||
|
{
|
||||||
|
get => _showError;
|
||||||
|
set { if (SetProperty(ref _showError, value)) ApplyFilter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否显示 Fatal 级别日志 | Whether to show Fatal level logs
|
||||||
|
/// </summary>
|
||||||
|
public bool ShowFatal
|
||||||
|
{
|
||||||
|
get => _showFatal;
|
||||||
|
set { if (SetProperty(ref _showFatal, value)) ApplyFilter(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态栏文本 | Status bar text
|
||||||
|
/// </summary>
|
||||||
|
public string StatusText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
bool hasLevelFilter = !(_showDebug && _showInfo && _showWarning && _showError && _showFatal);
|
||||||
|
bool hasTextFilter = !string.IsNullOrEmpty(_filterText);
|
||||||
|
if (hasLevelFilter || hasTextFilter)
|
||||||
|
return LocalizationHelper.Get("LogViewer_StatusFiltered", TotalCount, FilteredCount);
|
||||||
|
return LocalizationHelper.Get("LogViewer_StatusTotal", TotalCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空日志命令 | Clear log command
|
||||||
|
/// </summary>
|
||||||
|
public DelegateCommand ClearCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动滚动请求事件,View 层订阅此事件执行滚动 | Auto-scroll request event
|
||||||
|
/// </summary>
|
||||||
|
public event Action? ScrollToBottomRequested;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 添加日志事件(线程安全,可从任意线程调用)| Add log event (thread-safe)
|
||||||
|
/// </summary>
|
||||||
|
public void AddLogEvent(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
if (logEvent == null) return;
|
||||||
|
|
||||||
|
var entry = new LogDisplayEntry(logEvent);
|
||||||
|
|
||||||
|
if (_dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
AddEntryInternal(entry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dispatcher.InvokeAsync(() => AddEntryInternal(entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内部添加日志条目 | Internal add log entry
|
||||||
|
/// </summary>
|
||||||
|
private void AddEntryInternal(LogDisplayEntry entry)
|
||||||
|
{
|
||||||
|
_allEntries.Add(entry);
|
||||||
|
|
||||||
|
// 超出最大行数时移除最旧的条目 | Remove oldest entries when exceeding max lines
|
||||||
|
while (_allEntries.Count > _maxLines)
|
||||||
|
{
|
||||||
|
var removed = _allEntries[0];
|
||||||
|
_allEntries.RemoveAt(0);
|
||||||
|
FilteredEntries.Remove(removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否匹配过滤条件 | Check if entry matches filter
|
||||||
|
if (MatchesFilter(entry))
|
||||||
|
{
|
||||||
|
FilteredEntries.Add(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
TotalCount = _allEntries.Count;
|
||||||
|
FilteredCount = FilteredEntries.Count;
|
||||||
|
RaisePropertyChanged(nameof(StatusText));
|
||||||
|
|
||||||
|
// 请求自动滚动 | Request auto-scroll
|
||||||
|
if (_isAutoScroll)
|
||||||
|
{
|
||||||
|
ScrollToBottomRequested?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用过滤器 | Apply filter
|
||||||
|
/// </summary>
|
||||||
|
private void ApplyFilter()
|
||||||
|
{
|
||||||
|
FilteredEntries.Clear();
|
||||||
|
|
||||||
|
foreach (var entry in _allEntries)
|
||||||
|
{
|
||||||
|
if (MatchesFilter(entry))
|
||||||
|
{
|
||||||
|
FilteredEntries.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FilteredCount = FilteredEntries.Count;
|
||||||
|
RaisePropertyChanged(nameof(StatusText));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断条目是否匹配过滤条件 | Check if entry matches filter
|
||||||
|
/// </summary>
|
||||||
|
private bool MatchesFilter(LogDisplayEntry entry)
|
||||||
|
{
|
||||||
|
// 级别过滤 | Level filter
|
||||||
|
var levelMatch = entry.LevelFull switch
|
||||||
|
{
|
||||||
|
"Debug" => _showDebug,
|
||||||
|
"Information" => _showInfo,
|
||||||
|
"Warning" => _showWarning,
|
||||||
|
"Error" => _showError,
|
||||||
|
"Fatal" => _showFatal,
|
||||||
|
_ => true
|
||||||
|
};
|
||||||
|
if (!levelMatch) return false;
|
||||||
|
|
||||||
|
// 关键词过滤 | Keyword filter
|
||||||
|
if (string.IsNullOrWhiteSpace(_filterText)) return true;
|
||||||
|
return entry.Message.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| entry.Level.Contains(_filterText, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| entry.Source.Contains(_filterText, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空所有日志 | Clear all logs
|
||||||
|
/// </summary>
|
||||||
|
private void ClearAll()
|
||||||
|
{
|
||||||
|
_allEntries.Clear();
|
||||||
|
FilteredEntries.Clear();
|
||||||
|
TotalCount = 0;
|
||||||
|
FilteredCount = 0;
|
||||||
|
RaisePropertyChanged(nameof(StatusText));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 日志显示条目模型 | Log display entry model
|
||||||
|
/// </summary>
|
||||||
|
public class LogDisplayEntry
|
||||||
|
{
|
||||||
|
public LogDisplayEntry(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
Timestamp = logEvent.Timestamp.LocalDateTime;
|
||||||
|
TimestampDisplay = Timestamp.ToString("HH:mm:ss.fff");
|
||||||
|
Level = logEvent.Level.ToString().Substring(0, 3).ToUpper();
|
||||||
|
LevelFull = logEvent.Level.ToString();
|
||||||
|
|
||||||
|
// 渲染消息文本 | Render message text
|
||||||
|
Message = logEvent.RenderMessage();
|
||||||
|
|
||||||
|
// 如果有异常,追加异常信息 | Append exception info if present
|
||||||
|
if (logEvent.Exception != null)
|
||||||
|
{
|
||||||
|
Message += Environment.NewLine + logEvent.Exception.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取 SourceContext | Extract SourceContext
|
||||||
|
if (logEvent.Properties.TryGetValue("SourceContext", out var sourceValue))
|
||||||
|
{
|
||||||
|
Source = sourceValue.ToString().Trim('"');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Source = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据日志级别设置颜色 | Set color based on log level
|
||||||
|
LevelColor = logEvent.Level switch
|
||||||
|
{
|
||||||
|
LogEventLevel.Fatal => new SolidColorBrush(Color.FromRgb(180, 0, 0)),
|
||||||
|
LogEventLevel.Error => new SolidColorBrush(Colors.Red),
|
||||||
|
LogEventLevel.Warning => new SolidColorBrush(Color.FromRgb(200, 130, 0)),
|
||||||
|
LogEventLevel.Debug => new SolidColorBrush(Colors.Gray),
|
||||||
|
LogEventLevel.Verbose => new SolidColorBrush(Colors.LightGray),
|
||||||
|
_ => new SolidColorBrush(Color.FromRgb(30, 30, 30))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Freeze 画刷,使其可跨线程访问(避免 DependencySource 线程异常)
|
||||||
|
// Freeze the brush so it can be accessed from any thread
|
||||||
|
LevelColor.Freeze();
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime Timestamp { get; }
|
||||||
|
public string TimestampDisplay { get; }
|
||||||
|
public string Level { get; }
|
||||||
|
public string LevelFull { get; }
|
||||||
|
public string Message { get; }
|
||||||
|
public string Source { get; }
|
||||||
|
public SolidColorBrush LevelColor { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<Window x:Class="XP.Common.GeneralForm.Views.RealTimeLogViewer"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||||
|
xmlns:loc="clr-namespace:XP.Common.Localization.Extensions"
|
||||||
|
Title="{loc:Localization LogViewer_Title}"
|
||||||
|
Width="960" Height="600"
|
||||||
|
MinWidth="700" MinHeight="400"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ShowInTaskbar="True"
|
||||||
|
WindowStyle="SingleBorderWindow">
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<!-- 顶部工具栏 | Top toolbar -->
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<!-- 级别筛选栏 | Level filter bar -->
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<!-- 日志显示区 | Log display area -->
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<!-- 底部状态栏 | Bottom status bar -->
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- === 顶部工具栏 | Top Toolbar === -->
|
||||||
|
<DockPanel Grid.Row="0" Margin="8,6" LastChildFill="True">
|
||||||
|
<!-- 左侧:自动滚动开关 + 清空按钮 | Left: Auto-scroll toggle + Clear button -->
|
||||||
|
<StackPanel DockPanel.Dock="Left" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<telerik:RadToggleButton x:Name="AutoScrollToggle"
|
||||||
|
telerik:StyleManager.Theme="Crystal"
|
||||||
|
IsChecked="{Binding IsAutoScroll, Mode=TwoWay}"
|
||||||
|
Content="{loc:Localization LogViewer_AutoScroll}"
|
||||||
|
Padding="10,4" Margin="0,0,6,0"/>
|
||||||
|
<telerik:RadButton telerik:StyleManager.Theme="Crystal"
|
||||||
|
Command="{Binding ClearCommand}"
|
||||||
|
Content="{loc:Localization LogViewer_ClearLog}"
|
||||||
|
Padding="10,4" Margin="0,0,12,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 右侧:过滤输入框 | Right: Filter input -->
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{loc:Localization LogViewer_Filter}" VerticalAlignment="Center" Margin="0,0,4,0"/>
|
||||||
|
<telerik:RadWatermarkTextBox telerik:StyleManager.Theme="Crystal"
|
||||||
|
Text="{Binding FilterText, UpdateSourceTrigger=PropertyChanged, Delay=300}"
|
||||||
|
WatermarkContent="{loc:Localization LogViewer_FilterWatermark}"
|
||||||
|
Width="260" Padding="4,3"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- === 级别筛选栏 | Level Filter Bar === -->
|
||||||
|
<Border Grid.Row="1" Background="#FFF8F8F8" BorderBrush="#FFDDDDDD" BorderThickness="0,0,0,1" Padding="8,4">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{loc:Localization LogViewer_LevelFilter}" VerticalAlignment="Center" Margin="0,0,8,0" Foreground="#FF666666" FontSize="12"/>
|
||||||
|
<CheckBox Content="Debug" IsChecked="{Binding ShowDebug}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Gray"/>
|
||||||
|
<CheckBox Content="Info" IsChecked="{Binding ShowInfo}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FF1E1E1E"/>
|
||||||
|
<CheckBox Content="Warning" IsChecked="{Binding ShowWarning}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFC88200"/>
|
||||||
|
<CheckBox Content="Error" IsChecked="{Binding ShowError}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="Red"/>
|
||||||
|
<CheckBox Content="Fatal" IsChecked="{Binding ShowFatal}" VerticalAlignment="Center" Margin="0,0,10,0" Foreground="#FFB40000"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- === 日志显示区(RadGridView)| Log Display Area === -->
|
||||||
|
<telerik:RadGridView Grid.Row="2"
|
||||||
|
x:Name="LogGridView"
|
||||||
|
telerik:StyleManager.Theme="Crystal"
|
||||||
|
ItemsSource="{Binding FilteredEntries}"
|
||||||
|
AutoGenerateColumns="False"
|
||||||
|
IsReadOnly="True"
|
||||||
|
RowIndicatorVisibility="Collapsed"
|
||||||
|
ShowGroupPanel="False"
|
||||||
|
ShowColumnHeaders="True"
|
||||||
|
CanUserFreezeColumns="False"
|
||||||
|
CanUserReorderColumns="False"
|
||||||
|
CanUserSortColumns="False"
|
||||||
|
CanUserResizeColumns="True"
|
||||||
|
IsFilteringAllowed="False"
|
||||||
|
SelectionMode="Extended"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
FontSize="12"
|
||||||
|
Margin="4,0,4,0">
|
||||||
|
<telerik:RadGridView.Columns>
|
||||||
|
<!-- 时间戳列 | Timestamp column -->
|
||||||
|
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColTime}"
|
||||||
|
DataMemberBinding="{Binding TimestampDisplay}"
|
||||||
|
Width="100"
|
||||||
|
IsReadOnly="True"/>
|
||||||
|
|
||||||
|
<!-- 级别列 | Level column -->
|
||||||
|
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColLevel}"
|
||||||
|
DataMemberBinding="{Binding Level}"
|
||||||
|
Width="60"
|
||||||
|
IsReadOnly="True">
|
||||||
|
<telerik:GridViewDataColumn.CellStyle>
|
||||||
|
<Style TargetType="telerik:GridViewCell">
|
||||||
|
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
</telerik:GridViewDataColumn.CellStyle>
|
||||||
|
</telerik:GridViewDataColumn>
|
||||||
|
|
||||||
|
<!-- 来源列 | Source column -->
|
||||||
|
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColSource}"
|
||||||
|
DataMemberBinding="{Binding Source}"
|
||||||
|
Width="200"
|
||||||
|
IsReadOnly="True">
|
||||||
|
<telerik:GridViewDataColumn.CellStyle>
|
||||||
|
<Style TargetType="telerik:GridViewCell">
|
||||||
|
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||||
|
</Style>
|
||||||
|
</telerik:GridViewDataColumn.CellStyle>
|
||||||
|
</telerik:GridViewDataColumn>
|
||||||
|
|
||||||
|
<!-- 消息列 | Message column -->
|
||||||
|
<telerik:GridViewDataColumn Header="{loc:Localization LogViewer_ColMessage}"
|
||||||
|
DataMemberBinding="{Binding Message}"
|
||||||
|
Width="*"
|
||||||
|
IsReadOnly="True">
|
||||||
|
<telerik:GridViewDataColumn.CellStyle>
|
||||||
|
<Style TargetType="telerik:GridViewCell">
|
||||||
|
<Setter Property="Foreground" Value="{Binding LevelColor}"/>
|
||||||
|
</Style>
|
||||||
|
</telerik:GridViewDataColumn.CellStyle>
|
||||||
|
</telerik:GridViewDataColumn>
|
||||||
|
</telerik:RadGridView.Columns>
|
||||||
|
</telerik:RadGridView>
|
||||||
|
|
||||||
|
<!-- === 底部状态栏 | Bottom Status Bar === -->
|
||||||
|
<StatusBar Grid.Row="3" Background="#FFF0F0F0" BorderBrush="#FFCCCCCC" BorderThickness="0,1,0,0">
|
||||||
|
<StatusBarItem>
|
||||||
|
<TextBlock Text="{Binding StatusText}" Foreground="#FF666666" FontSize="12"/>
|
||||||
|
</StatusBarItem>
|
||||||
|
<Separator/>
|
||||||
|
<StatusBarItem HorizontalAlignment="Right">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="{loc:Localization LogViewer_MaxLines}" Foreground="#FF999999" FontSize="11" VerticalAlignment="Center"/>
|
||||||
|
<telerik:RadNumericUpDown telerik:StyleManager.Theme="Crystal"
|
||||||
|
Value="{Binding MaxLines, Mode=TwoWay}"
|
||||||
|
Minimum="100" Maximum="10000"
|
||||||
|
NumberDecimalDigits="0"
|
||||||
|
SmallChange="500"
|
||||||
|
Width="90" Height="22"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StatusBarItem>
|
||||||
|
</StatusBar>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Windows;
|
||||||
|
using Serilog.Events;
|
||||||
|
using XP.Common.Logging.ViewModels;
|
||||||
|
|
||||||
|
namespace XP.Common.GeneralForm.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 实时日志查看器窗口,订阅 Serilog 事件并实时显示
|
||||||
|
/// Real-time log viewer window, subscribes to Serilog events and displays in real-time
|
||||||
|
/// </summary>
|
||||||
|
public partial class RealTimeLogViewer : Window
|
||||||
|
{
|
||||||
|
private readonly RealTimeLogViewerViewModel _viewModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLines">最大行数限制,默认 2000 | Max line count, default 2000</param>
|
||||||
|
public RealTimeLogViewer(int maxLines = 2000)
|
||||||
|
{
|
||||||
|
_viewModel = new RealTimeLogViewerViewModel { MaxLines = maxLines };
|
||||||
|
DataContext = _viewModel;
|
||||||
|
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
// 继承主窗口图标 | Inherit main window icon
|
||||||
|
if (Application.Current?.MainWindow != null)
|
||||||
|
{
|
||||||
|
Icon = Application.Current.MainWindow.Icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅自动滚动事件 | Subscribe to auto-scroll event
|
||||||
|
_viewModel.ScrollToBottomRequested += OnScrollToBottomRequested;
|
||||||
|
|
||||||
|
// 加载缓冲区中的历史日志 | Load buffered history logs
|
||||||
|
var history = RealTimeLogSink.Instance.GetBufferedHistory();
|
||||||
|
foreach (var logEvent in history)
|
||||||
|
{
|
||||||
|
_viewModel.AddLogEvent(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 订阅 Serilog Sink 事件(在历史加载之后,避免重复)| Subscribe after history load
|
||||||
|
RealTimeLogSink.Instance.LogEventReceived += OnLogEventReceived;
|
||||||
|
|
||||||
|
Closed += OnWindowClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 接收 Serilog 日志事件 | Receive Serilog log event
|
||||||
|
/// </summary>
|
||||||
|
private void OnLogEventReceived(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
_viewModel.AddLogEvent(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动滚动到底部 | Auto-scroll to bottom
|
||||||
|
/// </summary>
|
||||||
|
private void OnScrollToBottomRequested()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (LogGridView.Items.Count > 0)
|
||||||
|
{
|
||||||
|
var lastItem = LogGridView.Items[LogGridView.Items.Count - 1];
|
||||||
|
LogGridView.ScrollIntoView(lastItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// 滚动失败时静默处理 | Silently handle scroll failures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口关闭时取消订阅,防止内存泄漏 | Unsubscribe on close to prevent memory leaks
|
||||||
|
/// </summary>
|
||||||
|
private void OnWindowClosed(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
_viewModel.ScrollToBottomRequested -= OnScrollToBottomRequested;
|
||||||
|
RealTimeLogSink.Instance.LogEventReceived -= OnLogEventReceived;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using Prism.Ioc;
|
||||||
|
using Prism.Modularity;
|
||||||
|
using XP.Common.Dump.Configs;
|
||||||
|
using XP.Common.Dump.Implementations;
|
||||||
|
using XP.Common.Dump.Interfaces;
|
||||||
|
using XP.Common.Helpers;
|
||||||
|
using XP.Common.Localization.Configs;
|
||||||
|
using XP.Common.Localization.Extensions;
|
||||||
|
using XP.Common.Localization.Implementations;
|
||||||
|
using XP.Common.Localization.Interfaces;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Common.PdfViewer.Implementations;
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.Module
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通用模块 | Common module
|
||||||
|
/// 提供通用基础设施服务,包括本地化支持 | Provides common infrastructure services including localization support
|
||||||
|
/// </summary>
|
||||||
|
public class CommonModule : IModule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模块初始化 | Module initialization
|
||||||
|
/// 在所有类型注册完成后调用 | Called after all types are registered
|
||||||
|
/// </summary>
|
||||||
|
public void OnInitialized(IContainerProvider containerProvider)
|
||||||
|
{
|
||||||
|
// 获取日志服务 | Get logger service
|
||||||
|
var logger = containerProvider.Resolve<ILoggerService>().ForModule("XP.Common.Module.CommonModule");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 解析本地化服务 | Resolve localization service
|
||||||
|
var localizationService = containerProvider.Resolve<ILocalizationService>();
|
||||||
|
|
||||||
|
// 初始化 LocalizationExtension | Initialize LocalizationExtension
|
||||||
|
LocalizationExtension.Initialize(localizationService);
|
||||||
|
|
||||||
|
logger.Info("本地化系统初始化成功 | Localization system initialized successfully");
|
||||||
|
logger.Info($"当前语言 | Current language: {localizationService.CurrentLanguage}");
|
||||||
|
|
||||||
|
// 启动 Dump 服务 | Start Dump service
|
||||||
|
var dumpService = containerProvider.Resolve<IDumpService>();
|
||||||
|
dumpService.Start();
|
||||||
|
logger.Info("Dump 服务初始化成功 | Dump service initialized successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "本地化系统初始化失败 | Localization system initialization failed");
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册类型到 DI 容器 | Register types to DI container
|
||||||
|
/// </summary>
|
||||||
|
public void RegisterTypes(IContainerRegistry containerRegistry)
|
||||||
|
{
|
||||||
|
// 注册本地化配置服务为单例 | Register localization config service as singleton
|
||||||
|
containerRegistry.RegisterSingleton<ILocalizationConfig, LocalizationConfig>();
|
||||||
|
|
||||||
|
// 注册本地化服务为单例 | Register localization service as singleton
|
||||||
|
containerRegistry.RegisterSingleton<ILocalizationService, ResxLocalizationService>();
|
||||||
|
|
||||||
|
// 注册 Dump 配置为单例(通过工厂方法加载)| Register Dump config as singleton (via factory method)
|
||||||
|
containerRegistry.RegisterSingleton<DumpConfig>(() => ConfigLoader.LoadDumpConfig());
|
||||||
|
|
||||||
|
// 注册 Dump 服务为单例 | Register Dump service as singleton
|
||||||
|
containerRegistry.RegisterSingleton<IDumpService, DumpService>();
|
||||||
|
|
||||||
|
// 注册 PDF 打印服务为单例 | Register PDF print service as singleton
|
||||||
|
containerRegistry.RegisterSingleton<IPdfPrintService, PdfPrintService>();
|
||||||
|
|
||||||
|
// 注册 PDF 查看服务为单例 | Register PDF viewer service as singleton
|
||||||
|
containerRegistry.RegisterSingleton<IPdfViewerService, PdfViewerService>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 加载异常 | PDF load exception
|
||||||
|
/// 当 PDF 文件格式无效或加载失败时抛出 | Thrown when PDF format is invalid or loading fails
|
||||||
|
/// </summary>
|
||||||
|
public class PdfLoadException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 加载失败的文件路径 | File path that failed to load
|
||||||
|
/// </summary>
|
||||||
|
public string? FilePath { get; }
|
||||||
|
|
||||||
|
public PdfLoadException(string message, string? filePath = null, Exception? innerException = null)
|
||||||
|
: base(message, innerException)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 打印异常 | Print exception
|
||||||
|
/// 当打印过程中发生错误时抛出 | Thrown when an error occurs during printing
|
||||||
|
/// </summary>
|
||||||
|
public class PrintException : Exception
|
||||||
|
{
|
||||||
|
public PrintException(string message, Exception? innerException = null)
|
||||||
|
: base(message, innerException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 打印机未找到异常 | Printer not found exception
|
||||||
|
/// 当指定的打印机名称不存在或不可用时抛出 | Thrown when specified printer is not found or unavailable
|
||||||
|
/// </summary>
|
||||||
|
public class PrinterNotFoundException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 未找到的打印机名称 | Name of the printer that was not found
|
||||||
|
/// </summary>
|
||||||
|
public string PrinterName { get; }
|
||||||
|
|
||||||
|
public PrinterNotFoundException(string printerName)
|
||||||
|
: base($"打印机未找到 | Printer not found: {printerName}")
|
||||||
|
{
|
||||||
|
PrinterName = printerName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
using System;
|
||||||
|
using System.Drawing.Printing;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using Telerik.Windows.Controls;
|
||||||
|
using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
|
||||||
|
using Telerik.Windows.Documents.Fixed.Print;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Common.PdfViewer.Exceptions;
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 打印服务实现 | PDF print service implementation
|
||||||
|
/// 基于 Telerik RadPdfViewer.Print() 实现 | Based on Telerik RadPdfViewer.Print()
|
||||||
|
/// </summary>
|
||||||
|
public class PdfPrintService : IPdfPrintService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
public PdfPrintService(ILoggerService logger)
|
||||||
|
{
|
||||||
|
_logger = logger?.ForModule<PdfPrintService>()
|
||||||
|
?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
|
||||||
|
/// </summary>
|
||||||
|
public void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1)
|
||||||
|
{
|
||||||
|
// 1. 验证文件路径存在 | Validate file path exists
|
||||||
|
ValidateFilePath(filePath);
|
||||||
|
|
||||||
|
// 2. 验证打印机名称有效 | Validate printer name is valid
|
||||||
|
ValidatePrinterName(printerName);
|
||||||
|
|
||||||
|
RadPdfViewer? pdfViewer = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 3. 创建隐藏的 RadPdfViewer 并加载 PDF | Create hidden RadPdfViewer and load PDF
|
||||||
|
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||||
|
|
||||||
|
// 4. 创建 PrintDialog 并配置打印机名称 | Create PrintDialog and configure printer name
|
||||||
|
var printDialog = new PrintDialog();
|
||||||
|
printDialog.PrintQueue = new System.Printing.PrintQueue(
|
||||||
|
new System.Printing.LocalPrintServer(), printerName);
|
||||||
|
|
||||||
|
// 5. 配置页面范围 | Configure page range
|
||||||
|
if (pageFrom.HasValue || pageTo.HasValue)
|
||||||
|
{
|
||||||
|
printDialog.PageRangeSelection = PageRangeSelection.UserPages;
|
||||||
|
printDialog.PageRange = new PageRange(
|
||||||
|
pageFrom ?? 1,
|
||||||
|
pageTo ?? int.MaxValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 配置打印份数 | Configure copies
|
||||||
|
if (copies > 1)
|
||||||
|
{
|
||||||
|
printDialog.PrintTicket.CopyCount = copies;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 配置 Telerik PrintSettings | Configure Telerik PrintSettings
|
||||||
|
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||||
|
{
|
||||||
|
DocumentName = Path.GetFileName(filePath),
|
||||||
|
PageMargins = new Thickness(0),
|
||||||
|
UseDefaultPrinter = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 8. 调用 RadPdfViewer.Print() 静默打印 | Call RadPdfViewer.Print() for silent printing
|
||||||
|
pdfViewer.Print(printDialog, printSettings);
|
||||||
|
|
||||||
|
// 9. 记录 Info 日志 | Log info
|
||||||
|
var pageRange = FormatPageRange(pageFrom, pageTo);
|
||||||
|
_logger.Info("打印任务已提交 | Print job submitted: {FileName} → {PrinterName}, 页面范围 | Pages: {PageRange}, 份数 | Copies: {Copies}",
|
||||||
|
Path.GetFileName(filePath), printerName, pageRange, copies);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not FileNotFoundException
|
||||||
|
and not PrinterNotFoundException
|
||||||
|
and not PdfLoadException)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "打印失败 | Print failed: {FileName} → {PrinterName}", Path.GetFileName(filePath), printerName);
|
||||||
|
throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 10. 释放资源 | Release resources
|
||||||
|
DisposePdfViewer(pdfViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开打印设置对话框并打印 | Open print settings dialog and print
|
||||||
|
/// </summary>
|
||||||
|
public bool PrintWithDialog(string filePath)
|
||||||
|
{
|
||||||
|
// 1. 验证文件路径 | Validate file path
|
||||||
|
ValidateFilePath(filePath);
|
||||||
|
|
||||||
|
RadPdfViewer? pdfViewer = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
|
||||||
|
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||||
|
|
||||||
|
// 3. 配置 PrintSettings | Configure PrintSettings
|
||||||
|
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||||
|
{
|
||||||
|
DocumentName = Path.GetFileName(filePath),
|
||||||
|
PageMargins = new Thickness(0),
|
||||||
|
UseDefaultPrinter = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 调用 RadPdfViewer.Print() 弹出 PrintDialog | Call RadPdfViewer.Print() to show PrintDialog
|
||||||
|
pdfViewer.Print(printSettings);
|
||||||
|
|
||||||
|
_logger.Info("用户通过对话框打印 | User printed via dialog: {FileName}", Path.GetFileName(filePath));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "打印失败 | Print failed: {FileName}", Path.GetFileName(filePath));
|
||||||
|
throw new PrintException($"打印失败 | Print failed: {Path.GetFileName(filePath)}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 5. 释放资源 | Release resources
|
||||||
|
DisposePdfViewer(pdfViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开打印预览对话框 | Open print preview dialog
|
||||||
|
/// </summary>
|
||||||
|
public void PrintPreview(string filePath)
|
||||||
|
{
|
||||||
|
// 1. 验证文件路径 | Validate file path
|
||||||
|
ValidateFilePath(filePath);
|
||||||
|
|
||||||
|
RadPdfViewer? pdfViewer = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 加载 PDF 到隐藏的 RadPdfViewer | Load PDF into hidden RadPdfViewer
|
||||||
|
pdfViewer = CreatePdfViewerWithDocument(filePath);
|
||||||
|
|
||||||
|
// 3. 通过 PrintDialog 显示预览 | Show preview via PrintDialog
|
||||||
|
var printDialog = new PrintDialog();
|
||||||
|
|
||||||
|
var printSettings = new Telerik.Windows.Documents.Fixed.Print.PrintSettings
|
||||||
|
{
|
||||||
|
DocumentName = Path.GetFileName(filePath),
|
||||||
|
PageMargins = new Thickness(0),
|
||||||
|
UseDefaultPrinter = false
|
||||||
|
};
|
||||||
|
|
||||||
|
// 显示打印对话框(含预览功能)| Show print dialog (with preview capability)
|
||||||
|
pdfViewer.Print(printDialog, printSettings);
|
||||||
|
|
||||||
|
_logger.Info("打印预览已显示 | Print preview shown: {FileName}", Path.GetFileName(filePath));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not FileNotFoundException and not PdfLoadException)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "打印预览失败 | Print preview failed: {FileName}", Path.GetFileName(filePath));
|
||||||
|
throw new PrintException($"打印预览失败 | Print preview failed: {Path.GetFileName(filePath)}", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 4. 释放资源 | Release resources
|
||||||
|
DisposePdfViewer(pdfViewer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region 私有辅助方法 | Private Helper Methods
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证文件路径是否存在 | Validate file path exists
|
||||||
|
/// </summary>
|
||||||
|
private void ValidateFilePath(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath), "文件路径不能为空 | File path cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.Error(new FileNotFoundException(filePath), "文件不存在 | File not found: {FilePath}", filePath);
|
||||||
|
throw new FileNotFoundException($"文件不存在 | File not found: {filePath}", filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证打印机名称是否有效 | Validate printer name is valid
|
||||||
|
/// </summary>
|
||||||
|
private void ValidatePrinterName(string printerName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(printerName))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(printerName), "打印机名称不能为空 | Printer name cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通过 PrinterSettings.InstalledPrinters 查询系统已安装的打印机 | Query installed printers
|
||||||
|
bool found = false;
|
||||||
|
foreach (string installedPrinter in PrinterSettings.InstalledPrinters)
|
||||||
|
{
|
||||||
|
if (string.Equals(installedPrinter, printerName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found)
|
||||||
|
{
|
||||||
|
_logger.Error(new PrinterNotFoundException(printerName),
|
||||||
|
"打印机未找到 | Printer not found: {PrinterName}", printerName);
|
||||||
|
throw new PrinterNotFoundException(printerName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建隐藏的 RadPdfViewer 并加载 PDF 文档 | Create hidden RadPdfViewer and load PDF document
|
||||||
|
/// </summary>
|
||||||
|
private RadPdfViewer CreatePdfViewerWithDocument(string filePath)
|
||||||
|
{
|
||||||
|
var pdfViewer = new RadPdfViewer();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var provider = new PdfFormatProvider();
|
||||||
|
using var fileStream = File.OpenRead(filePath);
|
||||||
|
pdfViewer.Document = provider.Import(fileStream);
|
||||||
|
return pdfViewer;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not FileNotFoundException)
|
||||||
|
{
|
||||||
|
// 释放已创建的 viewer | Dispose created viewer
|
||||||
|
DisposePdfViewer(pdfViewer);
|
||||||
|
_logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
|
||||||
|
throw new PdfLoadException("PDF 文件加载失败 | PDF file load failed", filePath, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放 RadPdfViewer 资源 | Dispose RadPdfViewer resources
|
||||||
|
/// </summary>
|
||||||
|
private static void DisposePdfViewer(RadPdfViewer? pdfViewer)
|
||||||
|
{
|
||||||
|
if (pdfViewer != null)
|
||||||
|
{
|
||||||
|
pdfViewer.Document = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 格式化页面范围字符串 | Format page range string
|
||||||
|
/// </summary>
|
||||||
|
private static string FormatPageRange(int? pageFrom, int? pageTo)
|
||||||
|
{
|
||||||
|
if (!pageFrom.HasValue && !pageTo.HasValue)
|
||||||
|
{
|
||||||
|
return "全部 | All";
|
||||||
|
}
|
||||||
|
|
||||||
|
var from = pageFrom?.ToString() ?? "1";
|
||||||
|
var to = pageTo?.ToString() ?? "末页 | Last";
|
||||||
|
return $"{from}-{to}";
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using XP.Common.Localization;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Common.PdfViewer.Exceptions;
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
using XP.Common.PdfViewer.ViewModels;
|
||||||
|
using XP.Common.PdfViewer.Views;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 查看服务实现 | PDF viewer service implementation
|
||||||
|
/// 基于 Telerik RadPdfViewer 实现 | Based on Telerik RadPdfViewer
|
||||||
|
/// </summary>
|
||||||
|
public class PdfViewerService : IPdfViewerService
|
||||||
|
{
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly IPdfPrintService _printService;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public PdfViewerService(ILoggerService logger, IPdfPrintService printService)
|
||||||
|
{
|
||||||
|
_logger = logger?.ForModule<PdfViewerService>()
|
||||||
|
?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
_printService = printService
|
||||||
|
?? throw new ArgumentNullException(nameof(printService));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||||
|
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||||
|
public void OpenViewer(string filePath)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
// 1. 验证文件路径存在 | Validate file path exists
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(filePath),
|
||||||
|
"文件路径不能为空 | File path cannot be null or empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
_logger.Error(new FileNotFoundException(filePath),
|
||||||
|
"文件不存在 | File not found: {FilePath}", filePath);
|
||||||
|
throw new FileNotFoundException(
|
||||||
|
$"文件不存在 | File not found: {filePath}", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 创建 PdfViewerWindowViewModel | Create PdfViewerWindowViewModel
|
||||||
|
var viewModel = new PdfViewerWindowViewModel(filePath, _printService, _logger);
|
||||||
|
|
||||||
|
// 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
|
||||||
|
var window = new PdfViewerWindow
|
||||||
|
{
|
||||||
|
DataContext = viewModel
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 记录 Info 日志 | Log info
|
||||||
|
_logger.Info("打开 PDF 阅读器窗口 | Opening PDF viewer window: {FileName}",
|
||||||
|
Path.GetFileName(filePath));
|
||||||
|
|
||||||
|
// 5. 显示窗口(非模态)| Show window (non-modal)
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not FileNotFoundException
|
||||||
|
and not ArgumentNullException
|
||||||
|
and not PdfLoadException)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "PDF 文件加载失败 | PDF file load failed: {FilePath}", filePath);
|
||||||
|
throw new PdfLoadException(
|
||||||
|
"PDF 文件加载失败 | PDF file load failed", filePath, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||||
|
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||||
|
/// <exception cref="ArgumentNullException">流为 null | Stream is null</exception>
|
||||||
|
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||||
|
public void OpenViewer(Stream stream, string? title = null)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
|
||||||
|
// 1. 验证 stream 非 null | Validate stream is not null
|
||||||
|
ArgumentNullException.ThrowIfNull(stream, nameof(stream));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 2. 创建 ViewModel | Create ViewModel
|
||||||
|
var viewModel = new PdfViewerWindowViewModel(stream, title, _printService, _logger);
|
||||||
|
|
||||||
|
// 3. 创建 PdfViewerWindow 并设置 DataContext | Create PdfViewerWindow and set DataContext
|
||||||
|
var window = new PdfViewerWindow
|
||||||
|
{
|
||||||
|
DataContext = viewModel
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. 记录 Info 日志 | Log info
|
||||||
|
var displayTitle = title ?? LocalizationHelper.Get("PdfViewer_Title");
|
||||||
|
_logger.Info("打开 PDF 阅读器窗口(流模式)| Opening PDF viewer window (stream mode): {Title}",
|
||||||
|
displayTitle);
|
||||||
|
|
||||||
|
// 5. 显示窗口(非模态)| Show window (non-modal)
|
||||||
|
window.Show();
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not ArgumentNullException
|
||||||
|
and not PdfLoadException)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "PDF 文件加载失败(流模式)| PDF file load failed (stream mode)");
|
||||||
|
throw new PdfLoadException(
|
||||||
|
"PDF 文件加载失败 | PDF file load failed", null, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IDisposable 模式 | IDisposable Pattern
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放资源 | Dispose resources
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(disposing: true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放资源的核心方法 | Core dispose method
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">是否由 Dispose() 调用 | Whether called by Dispose()</param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// 释放托管资源 | Dispose managed resources
|
||||||
|
_logger.Info("PdfViewerService 已释放 | PdfViewerService disposed");
|
||||||
|
}
|
||||||
|
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 终结器安全网 | Finalizer safety net
|
||||||
|
/// 确保未显式释放时仍能清理非托管资源 | Ensures unmanaged resources are cleaned up if not explicitly disposed
|
||||||
|
/// </summary>
|
||||||
|
~PdfViewerService()
|
||||||
|
{
|
||||||
|
Dispose(disposing: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using XP.Common.PdfViewer.Exceptions;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 打印服务接口 | PDF print service interface
|
||||||
|
/// 提供 PDF 文件打印功能 | Provides PDF file printing functionality
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfPrintService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 使用指定打印机打印 PDF 文件 | Print PDF file with specified printer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
/// <param name="printerName">打印机名称 | Printer name</param>
|
||||||
|
/// <param name="pageFrom">起始页码(从 1 开始,null 表示从第一页)| Start page (1-based, null for first page)</param>
|
||||||
|
/// <param name="pageTo">结束页码(从 1 开始,null 表示到最后一页)| End page (1-based, null for last page)</param>
|
||||||
|
/// <param name="copies">打印份数(默认 1)| Number of copies (default 1)</param>
|
||||||
|
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||||
|
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||||
|
/// <exception cref="PrinterNotFoundException">打印机不存在 | Printer not found</exception>
|
||||||
|
/// <exception cref="PrintException">打印失败 | Print failed</exception>
|
||||||
|
void Print(string filePath, string printerName, int? pageFrom = null, int? pageTo = null, int copies = 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开打印设置对话框并打印 | Open print settings dialog and print
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
/// <returns>用户是否确认打印 | Whether user confirmed printing</returns>
|
||||||
|
bool PrintWithDialog(string filePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打开打印预览对话框 | Open print preview dialog
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
void PrintPreview(string filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using XP.Common.PdfViewer.Exceptions;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 查看服务接口 | PDF viewer service interface
|
||||||
|
/// 提供 PDF 文件加载和阅读器窗口管理功能 | Provides PDF file loading and viewer window management
|
||||||
|
/// </summary>
|
||||||
|
public interface IPdfViewerService : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件路径打开 PDF 阅读器窗口 | Open PDF viewer window by file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
/// <exception cref="FileNotFoundException">文件不存在 | File not found</exception>
|
||||||
|
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||||
|
void OpenViewer(string filePath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件流打开 PDF 阅读器窗口 | Open PDF viewer window by stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||||
|
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||||
|
/// <exception cref="ArgumentNullException">流为 null | Stream is null</exception>
|
||||||
|
/// <exception cref="PdfLoadException">PDF 格式无效 | Invalid PDF format</exception>
|
||||||
|
void OpenViewer(Stream stream, string? title = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Prism.Commands;
|
||||||
|
using Prism.Mvvm;
|
||||||
|
using XP.Common.Localization;
|
||||||
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Common.PdfViewer.Interfaces;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.ViewModels
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 阅读器窗口 ViewModel | PDF viewer window ViewModel
|
||||||
|
/// 轻量级 ViewModel,核心功能由 RadPdfViewer 控件内置处理 | Lightweight ViewModel, core features handled by RadPdfViewer
|
||||||
|
/// </summary>
|
||||||
|
public class PdfViewerWindowViewModel : BindableBase
|
||||||
|
{
|
||||||
|
private readonly IPdfPrintService _printService;
|
||||||
|
private readonly ILoggerService _logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口标题(含文件名)| Window title (with file name)
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 文件路径(用于打印)| PDF file path (for printing)
|
||||||
|
/// </summary>
|
||||||
|
public string? FilePath { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 文件流(用于流加载场景)| PDF file stream (for stream loading scenario)
|
||||||
|
/// </summary>
|
||||||
|
public Stream? PdfStream { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打印命令(打开打印设置对话框)| Print command (open print settings dialog)
|
||||||
|
/// </summary>
|
||||||
|
public DelegateCommand PrintCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 打印预览命令 | Print preview command
|
||||||
|
/// </summary>
|
||||||
|
public DelegateCommand PrintPreviewCommand { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件路径创建 ViewModel | Create ViewModel by file path
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filePath">PDF 文件路径 | PDF file path</param>
|
||||||
|
/// <param name="printService">打印服务 | Print service</param>
|
||||||
|
/// <param name="logger">日志服务 | Logger service</param>
|
||||||
|
public PdfViewerWindowViewModel(
|
||||||
|
string filePath,
|
||||||
|
IPdfPrintService printService,
|
||||||
|
ILoggerService logger)
|
||||||
|
{
|
||||||
|
_printService = printService ?? throw new ArgumentNullException(nameof(printService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
|
||||||
|
PdfStream = null;
|
||||||
|
|
||||||
|
// 使用多语言标题,包含文件名 | Use localized title with file name
|
||||||
|
var fileName = Path.GetFileName(filePath);
|
||||||
|
Title = LocalizationHelper.Get("PdfViewer_TitleWithFile", fileName);
|
||||||
|
|
||||||
|
// 初始化命令 | Initialize commands
|
||||||
|
PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
|
||||||
|
PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过文件流创建 ViewModel | Create ViewModel by stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stream">PDF 文件流 | PDF file stream</param>
|
||||||
|
/// <param name="title">窗口标题(可选)| Window title (optional)</param>
|
||||||
|
/// <param name="printService">打印服务 | Print service</param>
|
||||||
|
/// <param name="logger">日志服务 | Logger service</param>
|
||||||
|
public PdfViewerWindowViewModel(
|
||||||
|
Stream stream,
|
||||||
|
string? title,
|
||||||
|
IPdfPrintService printService,
|
||||||
|
ILoggerService logger)
|
||||||
|
{
|
||||||
|
_printService = printService ?? throw new ArgumentNullException(nameof(printService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
PdfStream = stream ?? throw new ArgumentNullException(nameof(stream));
|
||||||
|
FilePath = null;
|
||||||
|
|
||||||
|
// 使用自定义标题或默认多语言标题 | Use custom title or default localized title
|
||||||
|
Title = !string.IsNullOrWhiteSpace(title)
|
||||||
|
? title
|
||||||
|
: LocalizationHelper.Get("PdfViewer_Title");
|
||||||
|
|
||||||
|
// 初始化命令(流模式下无文件路径,命令不可用)| Initialize commands (no file path in stream mode, commands disabled)
|
||||||
|
PrintCommand = new DelegateCommand(ExecutePrint, CanExecutePrint);
|
||||||
|
PrintPreviewCommand = new DelegateCommand(ExecutePrintPreview, CanExecutePrint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行打印(打开打印设置对话框)| Execute print (open print settings dialog)
|
||||||
|
/// </summary>
|
||||||
|
private void ExecutePrint()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_printService.PrintWithDialog(FilePath!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "打印失败 | Print failed: {FilePath}", FilePath ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行打印预览 | Execute print preview
|
||||||
|
/// </summary>
|
||||||
|
private void ExecutePrintPreview()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_printService.PrintPreview(FilePath!);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Error(ex, "打印预览失败 | Print preview failed: {FilePath}", FilePath ?? string.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断打印命令是否可用(仅当 FilePath 有效时)| Check if print command is available (only when FilePath is valid)
|
||||||
|
/// </summary>
|
||||||
|
private bool CanExecutePrint()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(FilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<Window x:Class="XP.Common.PdfViewer.Views.PdfViewerWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation"
|
||||||
|
xmlns:converters="clr-namespace:Telerik.Windows.Documents.Converters;assembly=Telerik.Windows.Controls.FixedDocumentViewers"
|
||||||
|
Title="{Binding Title}"
|
||||||
|
Width="1024" Height="768"
|
||||||
|
MinWidth="800" MinHeight="600"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
|
ShowInTaskbar="True"
|
||||||
|
WindowStyle="SingleBorderWindow">
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- 缩略图转换器:将 RadFixedDocument 转换为缩略图集合 | Thumbnail converter: converts RadFixedDocument to thumbnails collection -->
|
||||||
|
<converters:ThumbnailsConverter x:Key="ThumbnailsConverter" ThumbnailsHeight="200"/>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<!-- 工具栏(RadPdfViewerToolbar 内置导航/缩放/旋转/打印)| Toolbar (built-in navigation/zoom/rotate/print) -->
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<!-- PDF 显示区域 | PDF display area -->
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Telerik 内置工具栏 | Built-in toolbar -->
|
||||||
|
<telerik:RadPdfViewerToolBar Grid.Row="0"
|
||||||
|
telerik:StyleManager.Theme="Crystal"
|
||||||
|
RadPdfViewer="{Binding ElementName=pdfViewer, Mode=OneTime}"/>
|
||||||
|
|
||||||
|
<!-- 主内容区域:左侧缩略图面板 + 右侧 PDF 显示 | Main content: left thumbnails + right PDF display -->
|
||||||
|
<Grid Grid.Row="1">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<!-- 左侧缩略图面板 | Left thumbnails panel -->
|
||||||
|
<ColumnDefinition Width="180"/>
|
||||||
|
<!-- PDF 显示区域 | PDF display area -->
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- 页面缩略图面板 | Page thumbnails panel -->
|
||||||
|
<Border Grid.Column="0" BorderBrush="#DDDDDD" BorderThickness="0,0,1,0" Background="#F5F5F5">
|
||||||
|
<ListBox x:Name="thumbnailListBox"
|
||||||
|
ItemsSource="{Binding ElementName=pdfViewer, Path=Document, Converter={StaticResource ThumbnailsConverter}}"
|
||||||
|
SelectionChanged="ThumbnailListBox_SelectionChanged"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||||
|
Background="Transparent"
|
||||||
|
BorderThickness="0"
|
||||||
|
Padding="4">
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Margin="2,4" HorizontalAlignment="Center">
|
||||||
|
<!-- 缩略图图片 | Thumbnail image -->
|
||||||
|
<Border BorderBrush="#CCCCCC" BorderThickness="1" Background="White"
|
||||||
|
HorizontalAlignment="Center">
|
||||||
|
<Image Source="{Binding ImageSource}" Stretch="Uniform"
|
||||||
|
MaxWidth="150" MaxHeight="200"/>
|
||||||
|
</Border>
|
||||||
|
<!-- 页码 | Page number -->
|
||||||
|
<TextBlock Text="{Binding PageNumber}"
|
||||||
|
HorizontalAlignment="Center" Margin="0,2,0,0"
|
||||||
|
FontSize="11" Foreground="#666666"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- PDF 渲染区域 | PDF rendering area -->
|
||||||
|
<telerik:RadPdfViewer Grid.Column="1"
|
||||||
|
x:Name="pdfViewer"
|
||||||
|
telerik:StyleManager.Theme="Crystal"
|
||||||
|
DataContext="{Binding ElementName=pdfViewer, Path=CommandDescriptors}"/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using Telerik.Windows.Documents.Fixed.FormatProviders.Pdf;
|
||||||
|
using XP.Common.PdfViewer.Exceptions;
|
||||||
|
using XP.Common.PdfViewer.ViewModels;
|
||||||
|
|
||||||
|
namespace XP.Common.PdfViewer.Views
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 阅读器窗口 code-behind | PDF viewer window code-behind
|
||||||
|
/// 负责在 Loaded 事件中加载 PDF 文档,在 Closed 事件中释放资源
|
||||||
|
/// Responsible for loading PDF document on Loaded event and releasing resources on Closed event
|
||||||
|
/// </summary>
|
||||||
|
public partial class PdfViewerWindow : Window
|
||||||
|
{
|
||||||
|
// 防止缩略图选择和页面跳转之间的循环触发 | Prevent circular trigger between thumbnail selection and page navigation
|
||||||
|
private bool _isSyncingSelection;
|
||||||
|
|
||||||
|
public PdfViewerWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += OnLoaded;
|
||||||
|
Closed += OnClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口加载事件:通过 PdfFormatProvider 加载 PDF 文档到 RadPdfViewer
|
||||||
|
/// Window loaded event: load PDF document into RadPdfViewer via PdfFormatProvider
|
||||||
|
/// </summary>
|
||||||
|
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var vm = DataContext as PdfViewerWindowViewModel;
|
||||||
|
if (vm == null) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var provider = new PdfFormatProvider();
|
||||||
|
|
||||||
|
if (vm.PdfStream != null)
|
||||||
|
{
|
||||||
|
// 从文件流加载 PDF | Load PDF from stream
|
||||||
|
pdfViewer.Document = provider.Import(vm.PdfStream);
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(vm.FilePath))
|
||||||
|
{
|
||||||
|
// 从文件路径加载 PDF | Load PDF from file path
|
||||||
|
using var stream = File.OpenRead(vm.FilePath);
|
||||||
|
pdfViewer.Document = provider.Import(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文档加载后,监听页面变化以同步缩略图选中状态 | After document loaded, listen for page changes to sync thumbnail selection
|
||||||
|
pdfViewer.CurrentPageChanged += PdfViewer_CurrentPageChanged;
|
||||||
|
|
||||||
|
// 初始选中第一页缩略图 | Initially select first page thumbnail
|
||||||
|
if (thumbnailListBox.Items.Count > 0)
|
||||||
|
{
|
||||||
|
thumbnailListBox.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is not PdfLoadException)
|
||||||
|
{
|
||||||
|
// 捕获加载异常并包装为 PdfLoadException | Catch loading exceptions and wrap as PdfLoadException
|
||||||
|
throw new PdfLoadException(
|
||||||
|
"PDF 文件加载失败 | PDF file load failed",
|
||||||
|
vm.FilePath,
|
||||||
|
ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 缩略图列表选择变化:跳转到对应页面 | Thumbnail selection changed: navigate to corresponding page
|
||||||
|
/// ListBox.SelectedIndex 从 0 开始,RadPdfViewer.CurrentPageNumber 从 1 开始
|
||||||
|
/// ListBox.SelectedIndex is 0-based, RadPdfViewer.CurrentPageNumber is 1-based
|
||||||
|
/// </summary>
|
||||||
|
private void ThumbnailListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_isSyncingSelection) return;
|
||||||
|
if (thumbnailListBox.SelectedIndex < 0) return;
|
||||||
|
|
||||||
|
_isSyncingSelection = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// SelectedIndex(0-based) + 1 = CurrentPageNumber(1-based)
|
||||||
|
pdfViewer.CurrentPageNumber = thumbnailListBox.SelectedIndex + 1;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSyncingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PDF 阅读器页面变化:同步缩略图选中状态 | PDF viewer page changed: sync thumbnail selection
|
||||||
|
/// </summary>
|
||||||
|
private void PdfViewer_CurrentPageChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_isSyncingSelection) return;
|
||||||
|
|
||||||
|
_isSyncingSelection = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// CurrentPageNumber(1-based) - 1 = SelectedIndex(0-based)
|
||||||
|
var index = pdfViewer.CurrentPageNumber - 1;
|
||||||
|
if (index >= 0 && index < thumbnailListBox.Items.Count)
|
||||||
|
{
|
||||||
|
thumbnailListBox.SelectedIndex = index;
|
||||||
|
thumbnailListBox.ScrollIntoView(thumbnailListBox.SelectedItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isSyncingSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 窗口关闭事件:释放 RadPdfViewer 文档资源
|
||||||
|
/// Window closed event: release RadPdfViewer document resources
|
||||||
|
/// </summary>
|
||||||
|
private void OnClosed(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// 取消事件订阅 | Unsubscribe events
|
||||||
|
pdfViewer.CurrentPageChanged -= PdfViewer_CurrentPageChanged;
|
||||||
|
|
||||||
|
// RadPdfViewer 内部管理文档资源,设置 Document = null 触发释放
|
||||||
|
// RadPdfViewer internally manages document resources, setting Document = null triggers release
|
||||||
|
pdfViewer.Document = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Telerik.Windows.Controls.RadProgressBar, Telerik.Windows.Controls, Version=2024.1.408.310, Culture=neutral, PublicKeyToken=5803cfa389c90ce7
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1497
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,378 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="App_Title" xml:space="preserve">
|
||||||
|
<value>XplorePlane X-Ray Inspection System</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_File" xml:space="preserve">
|
||||||
|
<value>File</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Settings" xml:space="preserve">
|
||||||
|
<value>Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_OK" xml:space="preserve">
|
||||||
|
<value>OK</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Cancel" xml:space="preserve">
|
||||||
|
<value>Cancel</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhCN" xml:space="preserve">
|
||||||
|
<value>Simplified Chinese</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhTW" xml:space="preserve">
|
||||||
|
<value>Traditional Chinese</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_EnUS" xml:space="preserve">
|
||||||
|
<value>English</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language" xml:space="preserve">
|
||||||
|
<value>Language Settings</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_Description" xml:space="preserve">
|
||||||
|
<value>Select your preferred display language</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||||
|
<value>Language changes will take effect on the next application startup.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Apply" xml:space="preserve">
|
||||||
|
<value>Apply</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Close" xml:space="preserve">
|
||||||
|
<value>Close</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Save" xml:space="preserve">
|
||||||
|
<value>Save</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Delete" xml:space="preserve">
|
||||||
|
<value>Delete</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Edit" xml:space="preserve">
|
||||||
|
<value>Edit</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Add" xml:space="preserve">
|
||||||
|
<value>Add</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Refresh" xml:space="preserve">
|
||||||
|
<value>Refresh</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Search" xml:space="preserve">
|
||||||
|
<value>Search</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Reset" xml:space="preserve">
|
||||||
|
<value>Reset</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Ready" xml:space="preserve">
|
||||||
|
<value>Ready</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Loading" xml:space="preserve">
|
||||||
|
<value>Loading...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Saving" xml:space="preserve">
|
||||||
|
<value>Saving...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Processing" xml:space="preserve">
|
||||||
|
<value>Processing...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Success" xml:space="preserve">
|
||||||
|
<value>Success</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Failed" xml:space="preserve">
|
||||||
|
<value>Failed</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Error" xml:space="preserve">
|
||||||
|
<value>Error</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Warning" xml:space="preserve">
|
||||||
|
<value>Warning</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Connected" xml:space="preserve">
|
||||||
|
<value>Connected</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Disconnected" xml:space="preserve">
|
||||||
|
<value>Disconnected</value>
|
||||||
|
</data>
|
||||||
|
<data name="Welcome_Message" xml:space="preserve">
|
||||||
|
<value>Welcome to XplorePlane X-Ray Inspection System</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||||
|
<value>Operation completed successfully</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationFailed" xml:space="preserve">
|
||||||
|
<value>Operation failed</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||||
|
<value>Are you sure you want to delete?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||||
|
<value>There are unsaved changes. Do you want to save?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_InvalidInput" xml:space="preserve">
|
||||||
|
<value>Invalid input. Please check and try again</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||||
|
<value>Connection lost</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_RestartRequired" xml:space="preserve">
|
||||||
|
<value>Language setting saved. Please restart the application to apply the new language.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_View" xml:space="preserve">
|
||||||
|
<value>View</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Tools" xml:space="preserve">
|
||||||
|
<value>Tools</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Help" xml:space="preserve">
|
||||||
|
<value>Help</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Exit" xml:space="preserve">
|
||||||
|
<value>Exit</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_About" xml:space="preserve">
|
||||||
|
<value>About</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||||
|
<value>Confirmation</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Information" xml:space="preserve">
|
||||||
|
<value>Information</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Warning" xml:space="preserve">
|
||||||
|
<value>Warning</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Error" xml:space="preserve">
|
||||||
|
<value>Error</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Notice" xml:space="preserve">
|
||||||
|
<value>Notice</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||||
|
<value>Language setting saved. Please restart the application to apply the new language.</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||||
|
<value>Failed to switch language: {0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||||
|
<value>Scan Mode:</value>
|
||||||
|
<comment>Scan - Scan mode label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||||
|
<value>Frame Merge:</value>
|
||||||
|
<comment>Scan - Frame merge label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||||
|
<value>Acquisition Count:</value>
|
||||||
|
<comment>Scan - Acquisition count label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||||
|
<value>Rotation Angle:</value>
|
||||||
|
<comment>Scan - Rotation angle label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||||
|
<value>Progress:</value>
|
||||||
|
<comment>Scan - Acquisition progress label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Start" xml:space="preserve">
|
||||||
|
<value>Start</value>
|
||||||
|
<comment>Scan - Start acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||||
|
<value>Stop</value>
|
||||||
|
<comment>Scan - Stop acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||||
|
<value>Total {0} logs</value>
|
||||||
|
<comment>LogViewer - Status bar total count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||||
|
<value>Total {0} logs, {1} after filtering</value>
|
||||||
|
<comment>LogViewer - Status bar filtered count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Title" xml:space="preserve">
|
||||||
|
<value>Real-Time Log Viewer</value>
|
||||||
|
<comment>LogViewer - Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||||
|
<value>Auto Scroll</value>
|
||||||
|
<comment>LogViewer - Auto-scroll button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||||
|
<value>Clear Log</value>
|
||||||
|
<comment>LogViewer - Clear log button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Filter" xml:space="preserve">
|
||||||
|
<value>Filter:</value>
|
||||||
|
<comment>LogViewer - Filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||||
|
<value>Enter keyword to filter logs...</value>
|
||||||
|
<comment>LogViewer - Filter watermark</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||||
|
<value>Level Filter:</value>
|
||||||
|
<comment>LogViewer - Level filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||||
|
<value>Time</value>
|
||||||
|
<comment>LogViewer - Time column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||||
|
<value>Level</value>
|
||||||
|
<comment>LogViewer - Level column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||||
|
<value>Source</value>
|
||||||
|
<comment>LogViewer - Source column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||||
|
<value>Message</value>
|
||||||
|
<comment>LogViewer - Message column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||||
|
<value>Max Lines:</value>
|
||||||
|
<comment>LogViewer - Max lines label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_Title" xml:space="preserve">
|
||||||
|
<value>PDF Viewer</value>
|
||||||
|
<comment>PdfViewer - Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||||
|
<value>PDF Viewer - {0}</value>
|
||||||
|
<comment>PdfViewer - Window title with file name</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||||
|
<value>PDF loaded: {0} ({1} pages)</value>
|
||||||
|
<comment>PdfViewer - Load success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||||
|
<value>PDF file load failed</value>
|
||||||
|
<comment>PdfViewer - Load failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||||
|
<value>Print job submitted: {0} → {1}</value>
|
||||||
|
<comment>PdfViewer - Print success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||||
|
<value>Print failed</value>
|
||||||
|
<comment>PdfViewer - Print failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||||
|
<value>Printer not found: {0}</value>
|
||||||
|
<comment>PdfViewer - Printer not found</comment>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="App_Title" xml:space="preserve">
|
||||||
|
<value>XplorePlane X射线检测系统</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_File" xml:space="preserve">
|
||||||
|
<value>文件</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Settings" xml:space="preserve">
|
||||||
|
<value>设置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_OK" xml:space="preserve">
|
||||||
|
<value>确定</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Cancel" xml:space="preserve">
|
||||||
|
<value>取消</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhCN" xml:space="preserve">
|
||||||
|
<value>简体中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhTW" xml:space="preserve">
|
||||||
|
<value>繁體中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_EnUS" xml:space="preserve">
|
||||||
|
<value>English</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language" xml:space="preserve">
|
||||||
|
<value>语言设置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_Description" xml:space="preserve">
|
||||||
|
<value>选择您偏好的显示语言</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||||
|
<value>语言切换将在下次启动应用程序时生效。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Apply" xml:space="preserve">
|
||||||
|
<value>应用</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Close" xml:space="preserve">
|
||||||
|
<value>关闭</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Save" xml:space="preserve">
|
||||||
|
<value>保存</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Delete" xml:space="preserve">
|
||||||
|
<value>删除</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Edit" xml:space="preserve">
|
||||||
|
<value>编辑</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Add" xml:space="preserve">
|
||||||
|
<value>添加</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Refresh" xml:space="preserve">
|
||||||
|
<value>刷新</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Search" xml:space="preserve">
|
||||||
|
<value>搜索</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Reset" xml:space="preserve">
|
||||||
|
<value>重置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Ready" xml:space="preserve">
|
||||||
|
<value>就绪</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Loading" xml:space="preserve">
|
||||||
|
<value>加载中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Saving" xml:space="preserve">
|
||||||
|
<value>保存中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Processing" xml:space="preserve">
|
||||||
|
<value>处理中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Success" xml:space="preserve">
|
||||||
|
<value>成功</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Failed" xml:space="preserve">
|
||||||
|
<value>失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Error" xml:space="preserve">
|
||||||
|
<value>错误</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Connected" xml:space="preserve">
|
||||||
|
<value>已连接</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Disconnected" xml:space="preserve">
|
||||||
|
<value>已断开</value>
|
||||||
|
</data>
|
||||||
|
<data name="Welcome_Message" xml:space="preserve">
|
||||||
|
<value>欢迎使用 XplorePlane X射线检测系统</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||||
|
<value>操作成功完成</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationFailed" xml:space="preserve">
|
||||||
|
<value>操作失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||||
|
<value>确定要删除吗?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||||
|
<value>有未保存的更改,是否保存?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_InvalidInput" xml:space="preserve">
|
||||||
|
<value>输入无效,请检查后重试</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||||
|
<value>连接已断开</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_RestartRequired" xml:space="preserve">
|
||||||
|
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_View" xml:space="preserve">
|
||||||
|
<value>视图</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Tools" xml:space="preserve">
|
||||||
|
<value>工具</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Help" xml:space="preserve">
|
||||||
|
<value>帮助</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Exit" xml:space="preserve">
|
||||||
|
<value>退出</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_About" xml:space="preserve">
|
||||||
|
<value>关于</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||||
|
<value>确认</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Information" xml:space="preserve">
|
||||||
|
<value>信息</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Error" xml:space="preserve">
|
||||||
|
<value>错误</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Notice" xml:space="preserve">
|
||||||
|
<value>提示</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||||
|
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||||
|
<value>语言切换失败:{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||||
|
<value>采集模式:</value>
|
||||||
|
<comment>Scan - 采集模式标签 | Scan mode label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||||
|
<value>帧合并:</value>
|
||||||
|
<comment>Scan - 帧合并标签 | Frame merge label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||||
|
<value>采集张数:</value>
|
||||||
|
<comment>Scan - 采集张数标签 | Acquisition count label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||||
|
<value>旋转角度:</value>
|
||||||
|
<comment>Scan - 旋转角度标签 | Rotation angle label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||||
|
<value>采集进度:</value>
|
||||||
|
<comment>Scan - 采集进度标签 | Acquisition progress label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Start" xml:space="preserve">
|
||||||
|
<value>开始采集</value>
|
||||||
|
<comment>Scan - 开始采集按钮 | Start acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||||
|
<value>停止采集</value>
|
||||||
|
<comment>Scan - 停止采集按钮 | Stop acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||||
|
<value>共 {0} 条日志</value>
|
||||||
|
<comment>LogViewer - 状态栏总数 | Status bar total count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||||
|
<value>共 {0} 条日志,过滤后 {1} 条</value>
|
||||||
|
<comment>LogViewer - 状态栏过滤后 | Status bar filtered count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Title" xml:space="preserve">
|
||||||
|
<value>实时日志查看器</value>
|
||||||
|
<comment>LogViewer - 窗口标题 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||||
|
<value>自动滚动</value>
|
||||||
|
<comment>LogViewer - 自动滚动按钮 | Auto-scroll button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||||
|
<value>清空日志</value>
|
||||||
|
<comment>LogViewer - 清空日志按钮 | Clear log button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Filter" xml:space="preserve">
|
||||||
|
<value>过滤:</value>
|
||||||
|
<comment>LogViewer - 过滤标签 | Filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||||
|
<value>输入关键词过滤日志...</value>
|
||||||
|
<comment>LogViewer - 过滤水印 | Filter watermark</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||||
|
<value>级别筛选:</value>
|
||||||
|
<comment>LogViewer - 级别筛选标签 | Level filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||||
|
<value>时间</value>
|
||||||
|
<comment>LogViewer - 时间列头 | Time column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||||
|
<value>级别</value>
|
||||||
|
<comment>LogViewer - 级别列头 | Level column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||||
|
<value>来源</value>
|
||||||
|
<comment>LogViewer - 来源列头 | Source column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||||
|
<value>消息</value>
|
||||||
|
<comment>LogViewer - 消息列头 | Message column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||||
|
<value>最大行数:</value>
|
||||||
|
<comment>LogViewer - 最大行数标签 | Max lines label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_Title" xml:space="preserve">
|
||||||
|
<value>PDF 阅读器</value>
|
||||||
|
<comment>PdfViewer - 窗口标题 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||||
|
<value>PDF 阅读器 - {0}</value>
|
||||||
|
<comment>PdfViewer - 带文件名的窗口标题 | Window title with file name</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||||
|
<value>PDF 文件加载成功:{0}({1} 页)</value>
|
||||||
|
<comment>PdfViewer - 加载成功日志 | Load success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||||
|
<value>PDF 文件加载失败</value>
|
||||||
|
<comment>PdfViewer - 加载失败日志 | Load failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||||
|
<value>打印任务已提交:{0} → {1}</value>
|
||||||
|
<comment>PdfViewer - 打印成功日志 | Print success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||||
|
<value>打印失败</value>
|
||||||
|
<comment>PdfViewer - 打印失败日志 | Print failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||||
|
<value>打印机未找到:{0}</value>
|
||||||
|
<comment>PdfViewer - 打印机未找到 | Printer not found</comment>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="App_Title" xml:space="preserve">
|
||||||
|
<value>XplorePlane X射线检测系统</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_File" xml:space="preserve">
|
||||||
|
<value>文件</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Settings" xml:space="preserve">
|
||||||
|
<value>设置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_OK" xml:space="preserve">
|
||||||
|
<value>确定</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Cancel" xml:space="preserve">
|
||||||
|
<value>取消</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhCN" xml:space="preserve">
|
||||||
|
<value>简体中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhTW" xml:space="preserve">
|
||||||
|
<value>繁體中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_EnUS" xml:space="preserve">
|
||||||
|
<value>English</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language" xml:space="preserve">
|
||||||
|
<value>语言设置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_Description" xml:space="preserve">
|
||||||
|
<value>选择您偏好的显示语言</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||||
|
<value>语言切换将在下次启动应用程序时生效。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Apply" xml:space="preserve">
|
||||||
|
<value>应用</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Close" xml:space="preserve">
|
||||||
|
<value>关闭</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Save" xml:space="preserve">
|
||||||
|
<value>保存</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Delete" xml:space="preserve">
|
||||||
|
<value>删除</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Edit" xml:space="preserve">
|
||||||
|
<value>编辑</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Add" xml:space="preserve">
|
||||||
|
<value>添加</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Refresh" xml:space="preserve">
|
||||||
|
<value>刷新</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Search" xml:space="preserve">
|
||||||
|
<value>搜索</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Reset" xml:space="preserve">
|
||||||
|
<value>重置</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Ready" xml:space="preserve">
|
||||||
|
<value>就绪</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Loading" xml:space="preserve">
|
||||||
|
<value>加载中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Saving" xml:space="preserve">
|
||||||
|
<value>保存中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Processing" xml:space="preserve">
|
||||||
|
<value>处理中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Success" xml:space="preserve">
|
||||||
|
<value>成功</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Failed" xml:space="preserve">
|
||||||
|
<value>失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Error" xml:space="preserve">
|
||||||
|
<value>错误</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Connected" xml:space="preserve">
|
||||||
|
<value>已连接</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Disconnected" xml:space="preserve">
|
||||||
|
<value>已断开</value>
|
||||||
|
</data>
|
||||||
|
<data name="Welcome_Message" xml:space="preserve">
|
||||||
|
<value>欢迎使用 XplorePlane X射线检测系统</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||||
|
<value>操作成功完成</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationFailed" xml:space="preserve">
|
||||||
|
<value>操作失败</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||||
|
<value>确定要删除吗?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||||
|
<value>有未保存的更改,是否保存?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_InvalidInput" xml:space="preserve">
|
||||||
|
<value>输入无效,请检查后重试</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||||
|
<value>连接已断开</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_RestartRequired" xml:space="preserve">
|
||||||
|
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_View" xml:space="preserve">
|
||||||
|
<value>视图</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Tools" xml:space="preserve">
|
||||||
|
<value>工具</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Help" xml:space="preserve">
|
||||||
|
<value>帮助</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Exit" xml:space="preserve">
|
||||||
|
<value>退出</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_About" xml:space="preserve">
|
||||||
|
<value>关于</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||||
|
<value>确认</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Information" xml:space="preserve">
|
||||||
|
<value>信息</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Error" xml:space="preserve">
|
||||||
|
<value>错误</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Notice" xml:space="preserve">
|
||||||
|
<value>提示</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||||
|
<value>语言设置已保存,请重启应用程序以应用新语言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||||
|
<value>语言切换失败:{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||||
|
<value>采集模式:</value>
|
||||||
|
<comment>Scan - 采集模式标签 | Scan mode label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||||
|
<value>帧合并:</value>
|
||||||
|
<comment>Scan - 帧合并标签 | Frame merge label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||||
|
<value>采集张数:</value>
|
||||||
|
<comment>Scan - 采集张数标签 | Acquisition count label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||||
|
<value>旋转角度:</value>
|
||||||
|
<comment>Scan - 旋转角度标签 | Rotation angle label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||||
|
<value>采集进度:</value>
|
||||||
|
<comment>Scan - 采集进度标签 | Acquisition progress label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Start" xml:space="preserve">
|
||||||
|
<value>开始采集</value>
|
||||||
|
<comment>Scan - 开始采集按钮 | Start acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||||
|
<value>停止采集</value>
|
||||||
|
<comment>Scan - 停止采集按钮 | Stop acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||||
|
<value>共 {0} 条日志</value>
|
||||||
|
<comment>LogViewer - 状态栏总数 | Status bar total count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||||
|
<value>共 {0} 条日志,过滤后 {1} 条</value>
|
||||||
|
<comment>LogViewer - 状态栏过滤后 | Status bar filtered count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Title" xml:space="preserve">
|
||||||
|
<value>实时日志查看器</value>
|
||||||
|
<comment>LogViewer - 窗口标题 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||||
|
<value>自动滚动</value>
|
||||||
|
<comment>LogViewer - 自动滚动按钮 | Auto-scroll button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||||
|
<value>清空日志</value>
|
||||||
|
<comment>LogViewer - 清空日志按钮 | Clear log button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Filter" xml:space="preserve">
|
||||||
|
<value>过滤:</value>
|
||||||
|
<comment>LogViewer - 过滤标签 | Filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||||
|
<value>输入关键词过滤日志...</value>
|
||||||
|
<comment>LogViewer - 过滤水印 | Filter watermark</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||||
|
<value>级别筛选:</value>
|
||||||
|
<comment>LogViewer - 级别筛选标签 | Level filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||||
|
<value>时间</value>
|
||||||
|
<comment>LogViewer - 时间列头 | Time column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||||
|
<value>级别</value>
|
||||||
|
<comment>LogViewer - 级别列头 | Level column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||||
|
<value>来源</value>
|
||||||
|
<comment>LogViewer - 来源列头 | Source column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||||
|
<value>消息</value>
|
||||||
|
<comment>LogViewer - 消息列头 | Message column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||||
|
<value>最大行数:</value>
|
||||||
|
<comment>LogViewer - 最大行数标签 | Max lines label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_Title" xml:space="preserve">
|
||||||
|
<value>PDF 阅读器</value>
|
||||||
|
<comment>PdfViewer - 窗口标题 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||||
|
<value>PDF 阅读器 - {0}</value>
|
||||||
|
<comment>PdfViewer - 带文件名的窗口标题 | Window title with file name</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||||
|
<value>PDF 文件加载成功:{0}({1} 页)</value>
|
||||||
|
<comment>PdfViewer - 加载成功日志 | Load success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||||
|
<value>PDF 文件加载失败</value>
|
||||||
|
<comment>PdfViewer - 加载失败日志 | Load failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||||
|
<value>打印任务已提交:{0} → {1}</value>
|
||||||
|
<comment>PdfViewer - 打印成功日志 | Print success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||||
|
<value>打印失败</value>
|
||||||
|
<comment>PdfViewer - 打印失败日志 | Print failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||||
|
<value>打印机未找到:{0}</value>
|
||||||
|
<comment>PdfViewer - 打印机未找到 | Printer not found</comment>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<data name="App_Title" xml:space="preserve">
|
||||||
|
<value>XplorePlane X射線檢測系統</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_File" xml:space="preserve">
|
||||||
|
<value>檔案</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Settings" xml:space="preserve">
|
||||||
|
<value>設定</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_OK" xml:space="preserve">
|
||||||
|
<value>確定</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Cancel" xml:space="preserve">
|
||||||
|
<value>取消</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhCN" xml:space="preserve">
|
||||||
|
<value>簡體中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_ZhTW" xml:space="preserve">
|
||||||
|
<value>繁體中文</value>
|
||||||
|
</data>
|
||||||
|
<data name="Language_EnUS" xml:space="preserve">
|
||||||
|
<value>English</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language" xml:space="preserve">
|
||||||
|
<value>語言設定</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_Description" xml:space="preserve">
|
||||||
|
<value>選擇您偏好的顯示語言</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_RestartNotice" xml:space="preserve">
|
||||||
|
<value>語言切換將在下次啟動應用程式時生效。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Apply" xml:space="preserve">
|
||||||
|
<value>套用</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Close" xml:space="preserve">
|
||||||
|
<value>關閉</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Save" xml:space="preserve">
|
||||||
|
<value>儲存</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Delete" xml:space="preserve">
|
||||||
|
<value>刪除</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Edit" xml:space="preserve">
|
||||||
|
<value>編輯</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Add" xml:space="preserve">
|
||||||
|
<value>新增</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Refresh" xml:space="preserve">
|
||||||
|
<value>重新整理</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Search" xml:space="preserve">
|
||||||
|
<value>搜尋</value>
|
||||||
|
</data>
|
||||||
|
<data name="Button_Reset" xml:space="preserve">
|
||||||
|
<value>重設</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Ready" xml:space="preserve">
|
||||||
|
<value>就緒</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Loading" xml:space="preserve">
|
||||||
|
<value>載入中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Saving" xml:space="preserve">
|
||||||
|
<value>儲存中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Processing" xml:space="preserve">
|
||||||
|
<value>處理中...</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Success" xml:space="preserve">
|
||||||
|
<value>成功</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Failed" xml:space="preserve">
|
||||||
|
<value>失敗</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Error" xml:space="preserve">
|
||||||
|
<value>錯誤</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Connected" xml:space="preserve">
|
||||||
|
<value>已連線</value>
|
||||||
|
</data>
|
||||||
|
<data name="Status_Disconnected" xml:space="preserve">
|
||||||
|
<value>已中斷連線</value>
|
||||||
|
</data>
|
||||||
|
<data name="Welcome_Message" xml:space="preserve">
|
||||||
|
<value>歡迎使用 XplorePlane X射線檢測系統</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationSuccess" xml:space="preserve">
|
||||||
|
<value>操作成功完成</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_OperationFailed" xml:space="preserve">
|
||||||
|
<value>操作失敗</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConfirmDelete" xml:space="preserve">
|
||||||
|
<value>確定要刪除嗎?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_UnsavedChanges" xml:space="preserve">
|
||||||
|
<value>有未儲存的變更,是否儲存?</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_InvalidInput" xml:space="preserve">
|
||||||
|
<value>輸入無效,請檢查後重試</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_ConnectionLost" xml:space="preserve">
|
||||||
|
<value>連線已中斷</value>
|
||||||
|
</data>
|
||||||
|
<data name="Message_RestartRequired" xml:space="preserve">
|
||||||
|
<value>語言設定已儲存,請重新啟動應用程式以套用新語言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_View" xml:space="preserve">
|
||||||
|
<value>檢視</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Tools" xml:space="preserve">
|
||||||
|
<value>工具</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Help" xml:space="preserve">
|
||||||
|
<value>說明</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_Exit" xml:space="preserve">
|
||||||
|
<value>結束</value>
|
||||||
|
</data>
|
||||||
|
<data name="Menu_About" xml:space="preserve">
|
||||||
|
<value>關於</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Confirmation" xml:space="preserve">
|
||||||
|
<value>確認</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Information" xml:space="preserve">
|
||||||
|
<value>資訊</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Warning" xml:space="preserve">
|
||||||
|
<value>警告</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Error" xml:space="preserve">
|
||||||
|
<value>錯誤</value>
|
||||||
|
</data>
|
||||||
|
<data name="Dialog_Notice" xml:space="preserve">
|
||||||
|
<value>提示</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SavedRestartRequired" xml:space="preserve">
|
||||||
|
<value>語言設定已儲存,請重新啟動應用程式以套用新語言。</value>
|
||||||
|
</data>
|
||||||
|
<data name="Settings_Language_SwitchFailed" xml:space="preserve">
|
||||||
|
<value>語言切換失敗:{0}</value>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_ScanMode" xml:space="preserve">
|
||||||
|
<value>採集模式:</value>
|
||||||
|
<comment>Scan - 採集模式標籤 | Scan mode label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_FrameMerge" xml:space="preserve">
|
||||||
|
<value>幀合併:</value>
|
||||||
|
<comment>Scan - 幀合併標籤 | Frame merge label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Nums" xml:space="preserve">
|
||||||
|
<value>採集張數:</value>
|
||||||
|
<comment>Scan - 採集張數標籤 | Acquisition count label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Angles" xml:space="preserve">
|
||||||
|
<value>旋轉角度:</value>
|
||||||
|
<comment>Scan - 旋轉角度標籤 | Rotation angle label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Text_Progress" xml:space="preserve">
|
||||||
|
<value>採集進度:</value>
|
||||||
|
<comment>Scan - 採集進度標籤 | Acquisition progress label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Start" xml:space="preserve">
|
||||||
|
<value>開始採集</value>
|
||||||
|
<comment>Scan - 開始採集按鈕 | Start acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="Scan_Button_Stop" xml:space="preserve">
|
||||||
|
<value>停止採集</value>
|
||||||
|
<comment>Scan - 停止採集按鈕 | Stop acquisition button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusTotal" xml:space="preserve">
|
||||||
|
<value>共 {0} 筆日誌</value>
|
||||||
|
<comment>LogViewer - 狀態列總數 | Status bar total count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_StatusFiltered" xml:space="preserve">
|
||||||
|
<value>共 {0} 筆日誌,篩選後 {1} 筆</value>
|
||||||
|
<comment>LogViewer - 狀態列篩選後 | Status bar filtered count</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Title" xml:space="preserve">
|
||||||
|
<value>即時日誌檢視器</value>
|
||||||
|
<comment>LogViewer - 視窗標題 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_AutoScroll" xml:space="preserve">
|
||||||
|
<value>自動捲動</value>
|
||||||
|
<comment>LogViewer - 自動捲動按鈕 | Auto-scroll button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ClearLog" xml:space="preserve">
|
||||||
|
<value>清空日誌</value>
|
||||||
|
<comment>LogViewer - 清空日誌按鈕 | Clear log button</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_Filter" xml:space="preserve">
|
||||||
|
<value>篩選:</value>
|
||||||
|
<comment>LogViewer - 篩選標籤 | Filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_FilterWatermark" xml:space="preserve">
|
||||||
|
<value>輸入關鍵詞篩選日誌...</value>
|
||||||
|
<comment>LogViewer - 篩選浮水印 | Filter watermark</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_LevelFilter" xml:space="preserve">
|
||||||
|
<value>級別篩選:</value>
|
||||||
|
<comment>LogViewer - 級別篩選標籤 | Level filter label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColTime" xml:space="preserve">
|
||||||
|
<value>時間</value>
|
||||||
|
<comment>LogViewer - 時間欄頭 | Time column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColLevel" xml:space="preserve">
|
||||||
|
<value>級別</value>
|
||||||
|
<comment>LogViewer - 級別欄頭 | Level column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColSource" xml:space="preserve">
|
||||||
|
<value>來源</value>
|
||||||
|
<comment>LogViewer - 來源欄頭 | Source column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_ColMessage" xml:space="preserve">
|
||||||
|
<value>訊息</value>
|
||||||
|
<comment>LogViewer - 訊息欄頭 | Message column header</comment>
|
||||||
|
</data>
|
||||||
|
<data name="LogViewer_MaxLines" xml:space="preserve">
|
||||||
|
<value>最大行數:</value>
|
||||||
|
<comment>LogViewer - 最大行數標籤 | Max lines label</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_Title" xml:space="preserve">
|
||||||
|
<value>PDF 閱讀器</value>
|
||||||
|
<comment>PdfViewer - 視窗標題 | Window title</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_TitleWithFile" xml:space="preserve">
|
||||||
|
<value>PDF 閱讀器 - {0}</value>
|
||||||
|
<comment>PdfViewer - 帶檔案名稱的視窗標題 | Window title with file name</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadSuccess" xml:space="preserve">
|
||||||
|
<value>PDF 檔案載入成功:{0}({1} 頁)</value>
|
||||||
|
<comment>PdfViewer - 載入成功日誌 | Load success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_LoadFailed" xml:space="preserve">
|
||||||
|
<value>PDF 檔案載入失敗</value>
|
||||||
|
<comment>PdfViewer - 載入失敗日誌 | Load failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintSuccess" xml:space="preserve">
|
||||||
|
<value>列印任務已提交:{0} → {1}</value>
|
||||||
|
<comment>PdfViewer - 列印成功日誌 | Print success log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrintFailed" xml:space="preserve">
|
||||||
|
<value>列印失敗</value>
|
||||||
|
<comment>PdfViewer - 列印失敗日誌 | Print failed log</comment>
|
||||||
|
</data>
|
||||||
|
<data name="PdfViewer_PrinterNotFound" xml:space="preserve">
|
||||||
|
<value>印表機未找到:{0}</value>
|
||||||
|
<comment>PdfViewer - 印表機未找到 | Printer not found</comment>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<!-- NuGet 包版本 (用于包管理) -->
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<!-- 程序集版本 (用于代码引用) -->
|
||||||
|
<AssemblyVersion>1.4.16.1</AssemblyVersion>
|
||||||
|
<!-- 文件版本 (用于 Windows 文件属性显示) -->
|
||||||
|
<FileVersion>1.4.16.1</FileVersion>
|
||||||
|
<!-- 版权声明 -->
|
||||||
|
<Copyright>Copyright © 2026 Hexagon. All rights reserved.</Copyright>
|
||||||
|
<!-- 公司名称 -->
|
||||||
|
<Company>Hexagon Manufacturing Intelligence (Qingdao) Co., Ltd.</Company>
|
||||||
|
<!-- 产品名称 -->
|
||||||
|
<Product>XplorePlane XP.Common Infrastructure</Product>
|
||||||
|
<!-- 标题 -->
|
||||||
|
<AssemblyTitle>XP.Common Core Library</AssemblyTitle>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.3" />
|
||||||
|
<PackageReference Include="Emgu.CV" Version="4.10.0.5680" />
|
||||||
|
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.10.0.5680" />
|
||||||
|
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.1" />
|
||||||
|
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
|
||||||
|
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Telerik.UI.for.Wpf.NetCore.Xaml" Version="2024.1.408" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Controls\ViewModels\" />
|
||||||
|
<Folder Include="Controls\Views\" />
|
||||||
|
<Folder Include="Extensions\" />
|
||||||
|
<Folder Include="Serialization\" />
|
||||||
|
<Folder Include="Converters\" />
|
||||||
|
<Folder Include="PdfViewer\Exceptions\" />
|
||||||
|
<Folder Include="PdfViewer\Interfaces\" />
|
||||||
|
<Folder Include="PdfViewer\Implementations\" />
|
||||||
|
<Folder Include="PdfViewer\ViewModels\" />
|
||||||
|
<Folder Include="PdfViewer\Views\" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Resources\Resources.resx">
|
||||||
|
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Resources\Resources.zh-CN.resx">
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Resources\Resources.zh-TW.resx">
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
<EmbeddedResource Update="Resources\Resources.en-US.resx">
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Resources\Resources.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,533 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Prism.Events;
|
||||||
|
using XP.Hardware.Detector.Abstractions.Enums;
|
||||||
|
using XP.Hardware.Detector.Abstractions.Events;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 面阵探测器抽象基类 | Area detector abstract base class
|
||||||
|
/// 封装通用逻辑:参数校验、状态更新、事件发布
|
||||||
|
/// 使用模板方法模式,子类实现具体的硬件操作
|
||||||
|
/// </summary>
|
||||||
|
public abstract class AreaDetectorBase : IAreaDetector
|
||||||
|
{
|
||||||
|
protected readonly IEventAggregator _eventAggregator;
|
||||||
|
protected readonly object _statusLock = new object();
|
||||||
|
protected DetectorStatus _status = DetectorStatus.Uninitialized;
|
||||||
|
protected bool _disposed = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器状态 | Detector status
|
||||||
|
/// </summary>
|
||||||
|
public DetectorStatus Status
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_statusLock)
|
||||||
|
{
|
||||||
|
return _status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器类型 | Detector type
|
||||||
|
/// </summary>
|
||||||
|
public abstract DetectorType Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="eventAggregator">事件聚合器 | Event aggregator</param>
|
||||||
|
protected AreaDetectorBase(IEventAggregator eventAggregator)
|
||||||
|
{
|
||||||
|
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化探测器 | Initialize detector
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> InitializeAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Uninitialized)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure("探测器已初始化 | Detector already initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Initializing);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await InitializeInternalAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"初始化异常 | Initialization exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 启动连续采集 | Start continuous acquisition
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> StartAcquisitionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Acquiring);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await StartAcquisitionInternalAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"启动采集异常 | Start acquisition exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 停止采集 | Stop acquisition
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> StopAcquisitionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
// 未初始化时不允许停止采集操作 | Stop acquisition not allowed when uninitialized
|
||||||
|
if (Status == DetectorStatus.Uninitialized)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure("探测器未初始化,无法执行停止采集操作 | Detector not initialized, cannot stop acquisition");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await StopAcquisitionInternalAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"停止采集异常 | Stop acquisition exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧采集 | Single frame acquisition
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> AcquireSingleFrameAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Acquiring);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await AcquireSingleFrameInternalAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 恢复状态 | Restore status
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"单帧采集异常 | Single frame acquisition exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 暗场校正 | Dark field correction
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> DarkCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameCount <= 0)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Correcting);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await DarkCorrectionInternalAsync(frameCount, cancellationToken);
|
||||||
|
|
||||||
|
// 恢复状态 | Restore status
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
PublishCorrectionCompleted(CorrectionType.Dark, result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"暗场校正异常 | Dark correction exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 增益校正 | Gain correction
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> GainCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameCount <= 0)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Correcting);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await GainCorrectionInternalAsync(frameCount, cancellationToken);
|
||||||
|
|
||||||
|
// 恢复状态 | Restore status
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
PublishCorrectionCompleted(CorrectionType.Gain, result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"增益校正异常 | Gain correction exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动校正 | Auto correction
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> AutoCorrectionAsync(int frameCount, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameCount <= 0)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure("帧数必须大于 0 | Frame count must be greater than 0");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Correcting);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await AutoCorrectionInternalAsync(frameCount, cancellationToken);
|
||||||
|
|
||||||
|
// 恢复状态 | Restore status
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
PublishCorrectionCompleted(CorrectionType.Auto, result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"自动校正异常 | Auto correction exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 坏像素校正 | Bad pixel correction
|
||||||
|
/// </summary>
|
||||||
|
public async Task<DetectorResult> BadPixelCorrectionAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// 参数校验 | Parameter validation
|
||||||
|
if (Status != DetectorStatus.Ready)
|
||||||
|
{
|
||||||
|
return DetectorResult.Failure($"探测器状态不正确,当前状态:{Status} | Detector status incorrect, current status: {Status}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 更新状态 | Update status
|
||||||
|
UpdateStatus(DetectorStatus.Correcting);
|
||||||
|
|
||||||
|
// 调用子类实现 | Call derived class implementation
|
||||||
|
var result = await BadPixelCorrectionInternalAsync(cancellationToken);
|
||||||
|
|
||||||
|
// 恢复状态 | Restore status
|
||||||
|
UpdateStatus(DetectorStatus.Ready);
|
||||||
|
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
PublishCorrectionCompleted(CorrectionType.BadPixel, result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PublishError(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateStatus(DetectorStatus.Error);
|
||||||
|
var errorResult = DetectorResult.Failure($"坏像素校正异常 | Bad pixel correction exception: {ex.Message}", ex);
|
||||||
|
PublishError(errorResult);
|
||||||
|
return errorResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取探测器信息 | Get detector information
|
||||||
|
/// </summary>
|
||||||
|
public abstract DetectorInfo GetInfo();
|
||||||
|
|
||||||
|
// 模板方法,由子类实现 | Template methods, implemented by derived classes
|
||||||
|
protected abstract Task<DetectorResult> InitializeInternalAsync(CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> StartAcquisitionInternalAsync(CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> StopAcquisitionInternalAsync(CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> AcquireSingleFrameInternalAsync(CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> DarkCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> GainCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> AutoCorrectionInternalAsync(int frameCount, CancellationToken cancellationToken);
|
||||||
|
protected abstract Task<DetectorResult> BadPixelCorrectionInternalAsync(CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新状态并发布事件 | Update status and publish event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newStatus">新状态 | New status</param>
|
||||||
|
/// <summary>
|
||||||
|
/// 更新状态并发布事件 | Update status and publish event
|
||||||
|
/// 验证状态转换的合法性 | Validate state transition legality
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="newStatus">新状态 | New status</param>
|
||||||
|
protected void UpdateStatus(DetectorStatus newStatus)
|
||||||
|
{
|
||||||
|
lock (_statusLock)
|
||||||
|
{
|
||||||
|
// 验证状态转换的合法性 | Validate state transition legality
|
||||||
|
if (!IsValidStateTransition(_status, newStatus))
|
||||||
|
{
|
||||||
|
var errorMsg = $"非法的状态转换:{_status} -> {newStatus} | Invalid state transition: {_status} -> {newStatus}";
|
||||||
|
PublishError(DetectorResult.Failure(errorMsg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_status != newStatus)
|
||||||
|
{
|
||||||
|
var oldStatus = _status;
|
||||||
|
_status = newStatus;
|
||||||
|
_eventAggregator.GetEvent<StatusChangedEvent>().Publish(newStatus);
|
||||||
|
|
||||||
|
// 记录状态转换日志(如果需要)| Log state transition (if needed)
|
||||||
|
System.Diagnostics.Debug.WriteLine($"状态转换 | State transition: {oldStatus} -> {newStatus}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证状态转换是否合法 | Validate if state transition is legal
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentStatus">当前状态 | Current status</param>
|
||||||
|
/// <param name="newStatus">新状态 | New status</param>
|
||||||
|
/// <returns>是否合法 | Whether it's legal</returns>
|
||||||
|
private bool IsValidStateTransition(DetectorStatus currentStatus, DetectorStatus newStatus)
|
||||||
|
{
|
||||||
|
// 相同状态总是允许的 | Same state is always allowed
|
||||||
|
if (currentStatus == newStatus)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义合法的状态转换规则 | Define legal state transition rules
|
||||||
|
return (currentStatus, newStatus) switch
|
||||||
|
{
|
||||||
|
// 从未初始化状态只能转到初始化中 | From Uninitialized can only go to Initializing
|
||||||
|
(DetectorStatus.Uninitialized, DetectorStatus.Initializing) => true,
|
||||||
|
|
||||||
|
// 从初始化中可以转到就绪或错误 | From Initializing can go to Ready or Error
|
||||||
|
(DetectorStatus.Initializing, DetectorStatus.Ready) => true,
|
||||||
|
(DetectorStatus.Initializing, DetectorStatus.Error) => true,
|
||||||
|
|
||||||
|
// 从就绪状态可以转到采集中、校正中 | From Ready can go to Acquiring or Correcting
|
||||||
|
(DetectorStatus.Ready, DetectorStatus.Acquiring) => true,
|
||||||
|
(DetectorStatus.Ready, DetectorStatus.Correcting) => true,
|
||||||
|
|
||||||
|
// 从采集中可以转到就绪或错误 | From Acquiring can go to Ready or Error
|
||||||
|
(DetectorStatus.Acquiring, DetectorStatus.Ready) => true,
|
||||||
|
(DetectorStatus.Acquiring, DetectorStatus.Error) => true,
|
||||||
|
|
||||||
|
// 从校正中可以转到就绪或错误 | From Correcting can go to Ready or Error
|
||||||
|
(DetectorStatus.Correcting, DetectorStatus.Ready) => true,
|
||||||
|
(DetectorStatus.Correcting, DetectorStatus.Error) => true,
|
||||||
|
|
||||||
|
// 从错误状态可以转到初始化中(重新初始化)| From Error can go to Initializing (re-initialize)
|
||||||
|
(DetectorStatus.Error, DetectorStatus.Initializing) => true,
|
||||||
|
|
||||||
|
// 任何状态都可以转到错误状态 | Any state can go to Error
|
||||||
|
(_, DetectorStatus.Error) => true,
|
||||||
|
|
||||||
|
// 其他转换都是非法的 | All other transitions are illegal
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布错误事件 | Publish error event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">错误结果 | Error result</param>
|
||||||
|
protected void PublishError(DetectorResult result)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<ErrorOccurredEvent>().Publish(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布图像采集事件 | Publish image captured event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">事件参数 | Event arguments</param>
|
||||||
|
protected void PublishImageCaptured(ImageCapturedEventArgs args)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<ImageCapturedEvent>().Publish(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布校正完成事件 | Publish correction completed event
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">校正类型 | Correction type</param>
|
||||||
|
/// <param name="result">校正结果 | Correction result</param>
|
||||||
|
protected void PublishCorrectionCompleted(CorrectionType type, DetectorResult result)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<CorrectionCompletedEvent>().Publish(
|
||||||
|
new CorrectionCompletedEventArgs(type, result));
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDisposable 实现 | IDisposable implementation
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Dispose(true);
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放资源 | Dispose resources
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposing">是否释放托管资源 | Whether to dispose managed resources</param>
|
||||||
|
protected virtual void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
// 释放托管资源 | Release managed resources
|
||||||
|
}
|
||||||
|
// 释放非托管资源 | Release unmanaged resources
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using XP.Hardware.Detector.Abstractions.Enums;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器信息 | Detector information
|
||||||
|
/// 包含探测器的硬件参数和配置信息
|
||||||
|
/// </summary>
|
||||||
|
public class DetectorInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器类型 | Detector type
|
||||||
|
/// </summary>
|
||||||
|
public DetectorType Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器型号 | Detector model
|
||||||
|
/// </summary>
|
||||||
|
public string Model { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 序列号 | Serial number
|
||||||
|
/// </summary>
|
||||||
|
public string SerialNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 固件版本 | Firmware version
|
||||||
|
/// </summary>
|
||||||
|
public string FirmwareVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大分辨率宽度 | Maximum resolution width
|
||||||
|
/// </summary>
|
||||||
|
public uint MaxWidth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 最大分辨率高度 | Maximum resolution height
|
||||||
|
/// </summary>
|
||||||
|
public uint MaxHeight { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 像素尺寸(微米)| Pixel size (micrometers)
|
||||||
|
/// </summary>
|
||||||
|
public double PixelSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 位深度 | Bit depth
|
||||||
|
/// </summary>
|
||||||
|
public int BitDepth { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器操作结果封装 | Detector operation result wrapper
|
||||||
|
/// 统一封装成功/失败状态、数据和错误信息
|
||||||
|
/// </summary>
|
||||||
|
public class DetectorResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 操作是否成功 | Whether the operation succeeded
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误消息 | Error message
|
||||||
|
/// </summary>
|
||||||
|
public string ErrorMessage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异常对象 | Exception object
|
||||||
|
/// </summary>
|
||||||
|
public Exception Exception { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误码 | Error code
|
||||||
|
/// </summary>
|
||||||
|
public int ErrorCode { get; }
|
||||||
|
|
||||||
|
protected DetectorResult(bool isSuccess, string errorMessage = null, Exception exception = null, int errorCode = 0)
|
||||||
|
{
|
||||||
|
IsSuccess = isSuccess;
|
||||||
|
ErrorMessage = errorMessage;
|
||||||
|
Exception = exception;
|
||||||
|
ErrorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建成功结果 | Create success result
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">成功消息 | Success message</param>
|
||||||
|
/// <returns>成功结果 | Success result</returns>
|
||||||
|
public static DetectorResult Success(string message = null)
|
||||||
|
{
|
||||||
|
return new DetectorResult(true, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建失败结果 | Create failure result
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorMessage">错误消息 | Error message</param>
|
||||||
|
/// <param name="exception">异常对象 | Exception object</param>
|
||||||
|
/// <param name="errorCode">错误码 | Error code</param>
|
||||||
|
/// <returns>失败结果 | Failure result</returns>
|
||||||
|
public static DetectorResult Failure(string errorMessage, Exception exception = null, int errorCode = -1)
|
||||||
|
{
|
||||||
|
return new DetectorResult(false, errorMessage, exception, errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 带数据的探测器操作结果 | Detector operation result with data
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据类型 | Data type</typeparam>
|
||||||
|
public class DetectorResult<T> : DetectorResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 结果数据 | Result data
|
||||||
|
/// </summary>
|
||||||
|
public T Data { get; }
|
||||||
|
|
||||||
|
private DetectorResult(bool isSuccess, T data, string errorMessage = null, Exception exception = null, int errorCode = 0)
|
||||||
|
: base(isSuccess, errorMessage, exception, errorCode)
|
||||||
|
{
|
||||||
|
Data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建成功结果 | Create success result
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">结果数据 | Result data</param>
|
||||||
|
/// <param name="message">成功消息 | Success message</param>
|
||||||
|
/// <returns>成功结果 | Success result</returns>
|
||||||
|
public static DetectorResult<T> Success(T data, string message = null)
|
||||||
|
{
|
||||||
|
return new DetectorResult<T>(true, data, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建失败结果 | Create failure result
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errorMessage">错误消息 | Error message</param>
|
||||||
|
/// <param name="exception">异常对象 | Exception object</param>
|
||||||
|
/// <param name="errorCode">错误码 | Error code</param>
|
||||||
|
/// <returns>失败结果 | Failure result</returns>
|
||||||
|
public new static DetectorResult<T> Failure(string errorMessage, Exception exception = null, int errorCode = -1)
|
||||||
|
{
|
||||||
|
return new DetectorResult<T>(false, default(T), errorMessage, exception, errorCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 采集模式枚举 | Acquisition mode enumeration
|
||||||
|
/// 定义图像采集的工作模式
|
||||||
|
/// </summary>
|
||||||
|
public enum AcquisitionMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 连续采集模式 | Continuous acquisition mode
|
||||||
|
/// </summary>
|
||||||
|
Continuous = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 单帧采集模式 | Single frame acquisition mode
|
||||||
|
/// </summary>
|
||||||
|
SingleFrame = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Binning 模式枚举 | Binning mode enumeration
|
||||||
|
/// 定义像素合并模式,用于提高信噪比或采集速度
|
||||||
|
/// </summary>
|
||||||
|
public enum BinningMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 1x1 模式(无合并)| 1x1 mode (no binning)
|
||||||
|
/// </summary>
|
||||||
|
Bin1x1 = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 2x2 模式(2x2 像素合并)| 2x2 mode (2x2 pixel binning)
|
||||||
|
/// </summary>
|
||||||
|
Bin2x2 = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 4x4 模式(4x4 像素合并)| 4x4 mode (4x4 pixel binning)
|
||||||
|
/// </summary>
|
||||||
|
Bin4x4 = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校正类型枚举 | Correction type enumeration
|
||||||
|
/// 定义探测器支持的校正类型
|
||||||
|
/// </summary>
|
||||||
|
public enum CorrectionType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 暗场校正 | Dark field correction
|
||||||
|
/// </summary>
|
||||||
|
Dark = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 增益校正(亮场校正)| Gain correction (bright field correction)
|
||||||
|
/// </summary>
|
||||||
|
Gain = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动校正(暗场+增益+坏像素)| Auto correction (dark + gain + bad pixel)
|
||||||
|
/// </summary>
|
||||||
|
Auto = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 坏像素校正 | Bad pixel correction
|
||||||
|
/// </summary>
|
||||||
|
BadPixel = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器状态枚举 | Detector status enumeration
|
||||||
|
/// 定义探测器的运行状态
|
||||||
|
/// </summary>
|
||||||
|
public enum DetectorStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 未初始化 | Uninitialized
|
||||||
|
/// </summary>
|
||||||
|
Uninitialized = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化中 | Initializing
|
||||||
|
/// </summary>
|
||||||
|
Initializing = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 就绪 | Ready
|
||||||
|
/// </summary>
|
||||||
|
Ready = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 采集中 | Acquiring
|
||||||
|
/// </summary>
|
||||||
|
Acquiring = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校正中 | Correcting
|
||||||
|
/// </summary>
|
||||||
|
Correcting = 4,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误 | Error
|
||||||
|
/// </summary>
|
||||||
|
Error = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 探测器类型枚举 | Detector type enumeration
|
||||||
|
/// 定义支持的探测器厂商类型
|
||||||
|
/// </summary>
|
||||||
|
public enum DetectorType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Varex 探测器 | Varex detector
|
||||||
|
/// </summary>
|
||||||
|
Varex = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// iRay 探测器 | iRay detector (预留)
|
||||||
|
/// </summary>
|
||||||
|
IRay = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hamamatsu 探测器 | Hamamatsu detector (预留)
|
||||||
|
/// </summary>
|
||||||
|
Hamamatsu = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace XP.Hardware.Detector.Abstractions.Enums
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 增益模式枚举 | Gain mode enumeration
|
||||||
|
/// 定义探测器的信号放大倍数
|
||||||
|
/// </summary>
|
||||||
|
public enum GainMode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 低增益模式 | Low gain mode
|
||||||
|
/// </summary>
|
||||||
|
Low = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 高增益模式 | High gain mode
|
||||||
|
/// </summary>
|
||||||
|
High = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
using XP.Hardware.Detector.Abstractions;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校正完成事件 | Correction completed event
|
||||||
|
/// 当探测器校正操作完成时触发
|
||||||
|
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||||
|
/// </summary>
|
||||||
|
public class CorrectionCompletedEvent : PubSubEvent<CorrectionCompletedEventArgs>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System;
|
||||||
|
using XP.Hardware.Detector.Abstractions.Enums;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校正完成事件参数 | Correction completed event arguments
|
||||||
|
/// 携带校正类型和结果信息
|
||||||
|
/// </summary>
|
||||||
|
public class CorrectionCompletedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 校正类型 | Correction type
|
||||||
|
/// </summary>
|
||||||
|
public CorrectionType Type { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校正结果 | Correction result
|
||||||
|
/// </summary>
|
||||||
|
public DetectorResult Result { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数 | Constructor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">校正类型 | Correction type</param>
|
||||||
|
/// <param name="result">校正结果 | Correction result</param>
|
||||||
|
public CorrectionCompletedEventArgs(CorrectionType type, DetectorResult result)
|
||||||
|
{
|
||||||
|
Type = type;
|
||||||
|
Result = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
using XP.Hardware.Detector.Abstractions;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 错误发生事件 | Error occurred event
|
||||||
|
/// 当探测器操作发生错误时触发
|
||||||
|
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorOccurredEvent : PubSubEvent<DetectorResult>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
using XP.Hardware.Detector.Abstractions;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 图像采集事件 | Image captured event
|
||||||
|
/// 当探测器采集到新图像时触发
|
||||||
|
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||||
|
/// </summary>
|
||||||
|
public class ImageCapturedEvent : PubSubEvent<ImageCapturedEventArgs>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 图像采集事件参数 | Image captured event arguments
|
||||||
|
/// 携带图像数据和元信息
|
||||||
|
/// </summary>
|
||||||
|
public class ImageCapturedEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 16 位图像原始数据(无符号)| 16-bit raw image data (unsigned)
|
||||||
|
/// </summary>
|
||||||
|
public ushort[] ImageData { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图像宽度 | Image width
|
||||||
|
/// </summary>
|
||||||
|
public uint Width { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 图像高度 | Image height
|
||||||
|
/// </summary>
|
||||||
|
public uint Height { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 帧号 | Frame number
|
||||||
|
/// </summary>
|
||||||
|
public int FrameNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 存储路径 | Save path
|
||||||
|
/// </summary>
|
||||||
|
public string SavePath { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 曝光时间(毫秒)| Exposure time (milliseconds)
|
||||||
|
/// </summary>
|
||||||
|
public uint ExposureTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 采集时间戳 | Capture timestamp
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CaptureTime { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
using XP.Hardware.Detector.Abstractions.Enums;
|
||||||
|
|
||||||
|
namespace XP.Hardware.Detector.Abstractions.Events
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 状态变更事件 | Status changed event
|
||||||
|
/// 当探测器状态发生变化时触发
|
||||||
|
/// 线程安全:使用 Prism EventAggregator 确保线程安全的发布和订阅
|
||||||
|
/// </summary>
|
||||||
|
public class StatusChangedEvent : PubSubEvent<DetectorStatus>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user