WPF .NET Core环境下实现PDF预览的两种技术方案
方案一:集成 WebView2 控件
该方法依托微软官方提供的 WebView2 组件,通过嵌入 Chromium 内核实现对 PDF 文件的原生渲染。此方案优势在于渲染质量高且支持现代 PDF 特性,但依赖终端环境安装 WebView2 运行时。
首先在项目依赖管理中检索并安装 Microsoft.Web.WebView2 程序包。安装完成后,在视图层注册控件命名空间并构建基础布局:
<UserControl x:Class="AppCore.Views.DocumentPreviewPanel"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf">
<Grid Background="#F8F9FA">
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Button x:Name="NavigationBackBtn" Grid.Row="0"
Content="返回" HorizontalAlignment="Left" Margin="12"
Click="ExecuteNavigationBack"/>
<wv2:WebView2 x:Name="EmbeddedBrowserControl" Grid.Row="1"
Loaded="OnBrowserEngineInitialized"/>
</Grid>
</UserControl>
后端逻辑主要负责异步初始化浏览器核心进程,并通过文件协议加载本地文档路径。为避免阻塞 UI 线程,初始化操作应严格采用异步模式:
using Microsoft.Web.WebView2.Wpf;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
namespace AppCore.Views
{
public partial class DocumentPreviewPanel : UserControl
{
public DocumentPreviewPanel() => InitializeComponent();
private void ExecuteNavigationBack(object sender, RoutedEventArgs e)
{
var navService = NavigationService.GetNavigationService(this);
if (navService?.CanGoBack == true) navService.GoBack();
}
private async void OnBrowserEngineInitialized(object sender, RoutedEventArgs e)
{
if (sender is not WebView2 browser) return;
await browser.EnsureCoreWebView2Async(null);
var targetDocument = new Uri(@"file:///C:/ProjectAssets/TechnicalManual.pdf", UriKind.Absolute);
browser.CoreWebView2?.Navigate(targetDocument.ToString());
}
}
}
方案二:基于 Pdfium 渲染引擎的定制化实现
若需要脱离系统浏览器环境并实现精细的交互控制(如缩放、分页导航、滚动条隐藏),可采用基于 PDFium 的第三方渲染库。通过 NuGet 引入 PdfiumViewer 及对应的原生架构包(如 PdfiumViewer.Native.x86_64)后,可构建具备完整操控面板的视图。
XAML 布局将渲染区域与操作侧边栏分离,并通过数据绑定实时反馈当前页码与缩放比例:
<UserControl x:Class="AppCore.Views.PdfReaderWorkspace"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:pdf="clr-namespace:PdfViewer;assembly=PdfViewer">
<Grid Background="#FFFFFF">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="50"/>
</Grid.ColumnDefinitions>
<pdf:PdfRenderer x:Name="MainDocumentRenderer" Grid.Column="0"/>
<StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="0,8" Background="#F0F0F0">
<Button Content="◀" Click="NavigatePrev" Margin="4" ToolTip="上一页"/>
<TextBlock Text="{Binding DisplayedPage}" TextAlignment="Center" Margin="4,0" FontSize="11"/>
<TextBlock Text="/" TextAlignment="Center" FontSize="11"/>
<TextBlock Text="{Binding ElementName=MainDocumentRenderer, Path=PageCount}" TextAlignment="Center" FontSize="11" Margin="0,0,0,4"/>
<Button Content="▶" Click="NavigateNext" Margin="4" ToolTip="下一页"/>
<Separator Margin="0,4"/>
<Button Content="➕" Click="AdjustScale" Tag="up" Margin="4" ToolTip="放大"/>
<TextBlock Text="{Binding ScalePercentage}" TextAlignment="Center" FontSize="11" Margin="4,0"/>
<Button Content="➖" Click="AdjustScale" Tag="down" Margin="4" ToolTip="缩小"/>
<Separator Margin="0,4"/>
<Button Content="⬌" Click="SetFitMode" Tag="width" Margin="4" ToolTip="适应宽度"/>
<Button Content="⬍" Click="SetFitMode" Tag="height" Margin="4" ToolTip="适应高度"/>
</StackPanel>
</Grid>
</UserControl>
视图模型层实现 INotifyPropertyChanged 接口,拦截渲染引擎的属性变更事件以驱动 UI 更新。文档加载采用内存流处理,避免直接锁定资源文件:
using PdfViewer;
using PdfViewer.Enums;
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Resources;
namespace AppCore.Views
{
public partial class PdfReaderWorkspace : UserControl, INotifyPropertyChanged
{
public PdfReaderWorkspace()
{
InitializeComponent();
DataContext = this;
MainDocumentRenderer.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
MainDocumentRenderer.HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden;
MainDocumentRenderer.ZoomMode = PdfViewerZoomMode.FitWidth;
MainDocumentRenderer.PropertyChanged += (_, _) =>
{
OnPropertyChanged(nameof(DisplayedPage));
OnPropertyChanged(nameof(ScalePercentage));
};
Loaded += OnWorkspaceReady;
}
public int DisplayedPage => MainDocumentRenderer.PageNumber + 1;
public string ScalePercentage => $"{(int)(MainDocumentRenderer.ZoomFactor * 100)}%";
private async void OnWorkspaceReady(object sender, RoutedEventArgs e) => await LoadEmbeddedDocument();
private async Task LoadEmbeddedDocument()
{
var resUri = new Uri("Assets/Documentation.pdf", UriKind.Relative);
var info = Application.GetResourceStream(resUri);
if (info is null) return;
using var buffer = new MemoryStream();
await info.Stream.CopyToAsync(buffer);
buffer.Position = 0;
MainDocumentRenderer.Open(buffer);
}
private void NavigatePrev(object s, RoutedEventArgs e) => MainDocumentRenderer.GoToPreviousPage();
private void NavigateNext(object s, RoutedEventArgs e) => MainDocumentRenderer.GoToNextPage();
private void AdjustScale(object s, RoutedEventArgs e)
{
if (s is Button btn && btn.Tag?.ToString() is string direction)
{
if (direction == "up") MainDocumentRenderer.ZoomIn();
else if (direction == "down" && MainDocumentRenderer.ZoomFactor > 0.5) MainDocumentRenderer.ZoomOut();
}
}
private void SetFitMode(object s, RoutedEventArgs e)
{
if (s is Button btn && btn.Tag?.ToString() is string mode)
{
MainDocumentRenderer.ZoomMode = mode == "width" ? PdfViewerZoomMode.FitWidth : PdfViewerZoomMode.FitHeight;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string prop = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
}
