将Feature/XP.Common和Feature/XP.Hardware分支合并至Develop/XP.forHardwareAndCommon,完善XPapp注册和相关硬件类库通用类库功能。

This commit is contained in:
QI Mingxuan
2026-04-16 17:31:13 +08:00
parent 6ec4c3ddaa
commit 2bd6e566c3
581 changed files with 74600 additions and 222 deletions
@@ -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;
}
}
}