
简介PdfiumViewer是一款基于C#开发的PDF查看控件面向需要在桌面应用中嵌入文档浏览功能的.NET开发者可应用于合同预览、电子签章、报告阅读等场景。它基于Google开源的Pdfium渲染引擎支持跨平台具备缩放、平移、书签导航、文本搜索、加密文档处理等能力并能通过丰富API定制交互行为充分满足不同界面布局的展示需求。压缩包共228个文件大小48.5MB以C#源码78个cs、程序集19个dll、工程配置csproj/config/sln等为主同时包含示例PDF、界面XAML、图标资源、NuGet打包脚本及许可证便于二次开发与学习。包内除完整Demo外还提供WinForms与WPF两种界面框架的示例项目并通过测试工程展示如何加载文档、设置页面及处理事件读者可从中获得从加载PDF到界面集成的完整代码思路也能学习控件打包与分发方法。目前已有1567人浏览学习适合希望快速为.NET应用增加PDF展示功能的初中级开发者参考。1. 接手老系统的第一件事PDF 查看不做插件改用 PdfiumViewer 这个 pdf查看控件做过档案管理系统的人应该都遇过这种场面老 OA 里预览合同 PDF 靠 IE 的 ActiveX 插件安全整改一纸文件下来强制换内核ActiveX 直接失效业务方又接受不了整个预览流程改成网页版 PDF.js。我当时在 WinForms 客户端里接入 PdfiumViewer一个基于 Chromium PDFium 渲染引擎的 .NET pdf查看控件一个迭代就把替换做完了没有给用户装任何 Adobe 软件。PdfiumViewer 解决的是桌面端一个很具体的问题不依赖 Adobe Reader、不碰 WebBrowser 控件就能在原生窗口里渲染 PDF。它交付的东西很直接——一个 PdfViewer 控件加一个 PdfDocument 文档对象渲染由原生 PDFium 完成托管代码只负责调度。适合四类场景WinForms/WPF 里嵌 PDF 预览、内网环境不允许联网装插件的客户端、批量解析 PDF 页面的内部工具、以及要把 PDF 渲染和现有 C# 业务系统整合的开发者。2. PdfiumViewer 的选型逻辑与最小渲染链路2.1 选型理由为什么不用 WebBrowser、不用 Adobe 控件桌面程序加 PDF 预览最本能的方案是拖一个 WebBrowser 控件进去让它打开本地 PDF 文件。但 WebBrowser 本质是 IE 内核系统升级后渲染稳定性差甚至在部分精简系统里连空白页都打不开。Adobe Acrobat 的 ActiveX 在老机器上能凑合可它要求每台工控机都装 Acrobat 客户端许可证、安装策略、静默安装参数全要运维去铺几十台机器跑下来就是一次大型翻车现场。PdfiumViewer 走的是另一条路渲染在原生库 PDFium 里完成。PDFium 原本是 Chromium 内置的 PDF 解析引擎支持加密 PDF、表单、注释、打印等日常需求它在 C 层直接解析页面树和字体不依赖操作系统的字体服务。控件部分用 WinForms 的 UserControl 实现工具栏、事件、属性都齐省掉从零封装的时间。我一般把方案定在这里还有个现实原因它的渲染结果是 Bitmap可以直接画到任意 GDI 表面上和业务系统里已有的自定义绘图模块共存。托管层接口也很简单核心就三个类型PdfDocument 负责装载文档PdfRenderer 负责把某一页渲染成位图PdfViewer 是现成的交互控件。没有复杂的对象树任何一层出问题都能很快定位到具体类型这对维护期很友好。2.2 最小示例从安装包到第一页显示用 NuGet 包管理器搜索 PdfiumViewer 安装即可同时需要配套的原生库支持包常见的是 PdfiumViewer.Native.x86 和 PdfiumViewer.Native.x64按最终发布位数二选一。这两个包负责把 pdfium.dll 按平台目录拷到输出目录这一步经常被漏掉后面避坑章专门讲。最小代码using System; using System.IO; using System.Windows.Forms; using PdfiumViewer; public class PdfPreviewForm : Form { private PdfViewer _viewer; private PdfDocument _document; private Stream _stream; public PdfPreviewForm(string pdfPath) { _viewer new PdfViewer(); _viewer.Dock DockStyle.Fill; Controls.Add(_viewer); // 用只读流加载FileShare.Read 允许其他程序同时读这个文件 _stream new FileStream(pdfPath, FileMode.Open, FileAccess.Read, FileShare.Read); _document PdfDocument.Load(_stream); _viewer.Document _document; _viewer.ZoomMode PdfViewerZoomMode.FitWidth; } protected override void OnFormClosed(FormClosedEventArgs e) { // 释放顺序先让控件脱离文档再释放文档和流 _viewer.Document null; _document?.Dispose(); _stream?.Dispose(); base.OnFormClosed(e); } }这段代码的要点PdfDocument.Load 接收的是 Stream 而不是路径字符串这意味着你可以从内存流、网络缓冲、临时解密的文件流里加载 PDF不必真的落盘。PdfViewer.Document赋值后控件接管绘制ZoomMode设为 FitWidth 后第一页会按控件宽度铺开。这里有一个很隐蔽的坑stream 如果被局部变量持有并在构造方法里释放PDFium 加载文档时不会把整个文件读进内存而是按偏移量随机读取这个流。流的生命周期必须覆盖控件的整个使用过程所以代码里把它提升到了字段级。后面避坑章里会再提一次因为这是白屏问题最常见的原因。常用属性可以先记一张表后面章节会逐个用到属性作用说明Document当前显示的文档切换文档前先置 nullZoomMode缩放模式FitWidth 按宽度自适应Zoom当前缩放比例1.0 表示 100%CurrentPageIndex当前页码从 0 开始PageCount总页数只读Rotation旋转角度0/90/180/270ShowToolbar是否显示默认工具栏业务自定义导航时设为 false3. 从“能看”到“能用”翻页、缩放、旋转与打印3.1 翻页与页码状态业务系统最常用的三个动作实际业务里最常见的交互是目录树点一下右侧跳到对应页。PdfViewer 暴露了CurrentPageIndex和PageCount直接赋值就会触发重绘不需要额外调用 Refresh。// 跳转到目录指定的页目录里通常从 1 开始控件从 0 开始 public void JumpToPage(int oneBasedPageNumber) { int target Math.Max(0, Math.Min(_viewer.PageCount - 1, oneBasedPageNumber - 1)); _viewer.CurrentPageIndex target; } // 上一页 / 下一页按钮的通用逻辑 private void NavigateRelative(int offset) { int next Math.Max(0, Math.Min(_viewer.PageCount - 1, _viewer.CurrentPageIndex offset)); _viewer.CurrentPageIndex next; }页码属性从 0 开始是新手最容易别扭的地方目录清单里的“第 3 页”对应控件里的CurrentPageIndex 2如果直接把业务页码传进去永远差一页。另一个细节是状态同步。PdfViewer 的页码变化不像 TextBox 的 TextChanged 那样高频广播业务状态栏里的“3 / 28”建议在翻页按钮或 JumpToPage 的逻辑里同步更新而不是依赖事件。原因是控件内部有多种翻页入口滚轮翻页、键盘方向键、工具栏按钮都会改页码监听单一事件反而容易漏状态。我一般会把翻页逻辑封到一个 PageNavigator 类里按钮点击和目录跳转都走同一个入口顺带更新按钮的可用状态和页码标签。private void SyncNavigationState() { int current _viewer.CurrentPageIndex; int total _viewer.PageCount; lblPageInfo.Text ${current 1} / {total}; btnPrev.Enabled current 0; btnNext.Enabled current total - 1; }3.2 缩放三件套哪些模式适合什么场景PdfiumViewer 的缩放体系有两个入口ZoomMode 和 Zoom容易搞混。ZoomMode 是自动模式由控件根据容器尺寸计算缩放系数Zoom 是手动缩放比例。两者互斥手动设置 Zoom 后自动模式就不再生效。// 窗口尺寸变化时让页面始终适合宽度这是阅读最舒服的模式 _viewer.ZoomMode PdfViewerZoomMode.FitWidth; // 用户手动放大到 125% _viewer.Zoom 1.25; // 切换到整页模式适合看 PPT 式的版式 _viewer.ZoomMode PdfViewerZoomMode.FitHeight;选模式的判断标准很简单合同、技术文档这类流式排版用 FitWidth因为行宽和屏幕宽度匹配时阅读效率最高PPT 转出的 PDF、整页海报用 FitHeight保证整版轮廓完整。不要提供太多模式给用户界面上一组“适合宽度 / 适合整页 / 固定比例”就够模式多了用户自己都混乱。缩放还有一个直接影响体验的点放大到 150% 以上时如果控件仍用 96 DPI 的底图做软件放大文字边缘就会发虚。这个问题的根因不在 Zoom 属性而在渲染时的 DPI 参数第 4 章专门展开。3.3 旋转与打印两个不高频但必须稳妥的功能旋转在某些场景躲不掉扫描仪出来的 PDF 方向不对用户希望能转 90 度再看。PdfViewer 的旋转操作最终落在 Rotation 属性上四个有效值0、90、180、270。旋转 90 度或 270 度后页面的宽高关系互换原来 FitWidth 的模式会导致页面高度超出可视区所以我一般会在旋转后重新设置一次 ZoomMode。// 顺时针旋转超过 360 度回绕 _viewer.Rotation (_viewer.Rotation 90) % 360; // 横向页面旋转后自动切换到适合高度 if (_viewer.Rotation 90 || _viewer.Rotation 270) { _viewer.ZoomMode PdfViewerZoomMode.FitHeight; } else { _viewer.ZoomMode PdfViewerZoomMode.FitWidth; }打印走的是 PdfDocument 的 CreatePrintDocument 方法它返回一个标准的 System.Drawing.Printing.PrintDocument可以直接挂在打印预览或直接调 Print。这样打印不经过屏幕渲染PDFium 内部会按打印模式重新计算解析参数。// 用默认打印机打印当前文档 using (var printDocument _document.CreatePrintDocument()) { if (printDocument.PrinterSettings.IsValid) { printDocument.Print(); } }有两点要提前确认一是打印前核对打印机默认纸张尺寸和 PDF 页面尺寸A4 文档被 B5 纸张打印时经常出现边缘裁切这属于打印机驱动问题不是 PdfiumViewer 的 bug二是如果文档里有表单填充内容和批注打印结果和屏幕显示不一致是正常的PDF 标准里显示和打印本就允许不同渲染参数业务上要统一就以实际打印件为准。4. 渲染质量与内存控制PdfRenderer 的参数边界4.1 DPI 与显示分辨率放大到 200% 就糊的真正原因PdfiumViewer 的 PdfRenderer 渲染页面时需要四个关键参数目标位图宽高、DPI 和渲染标志。如果只是把 PdfViewer 当黑盒用放大发糊的锅通常会甩给控件其实问题出在渲染时的 DPI 上。// 以屏幕 96 DPI 渲染一页宽度按显示区域像素给 using (var image _document.Renderer.Render( pageIndex: 0, width: clientWidth, height: clientHeight, dpiX: 96, dpiY: 96, flags: PdfRenderFlags.None)) { // image 就是这一页的位图交给 PictureBox 或画到自定义控件 }当显示区域宽度是 2000 像素时如果用 96 DPI 渲染位图原始宽度可能只有 1300 像素左右控件把它拉宽到 2000 像素多余部分全靠插值补齐文字自然发虚。常见做法是让渲染 DPI 跟控件当前的实际显示比例挂钩放大到 200% 时用 192 DPI 渲染再缩放到显示尺寸清晰度立刻不同。PdfRenderFlags 的常用取值也值得记一下取值作用适用场景None默认屏幕渲染日常预览ForPrinting按打印需求渲染解析精度更高打印、导出高清图片Annotations页面上的注释、批注一起渲染审批场景必须开LcdText使用 LCD 亚像素字体平滑白底文字多的标准文档我踩过的坑是 ForPrinting 这个标志它不只是改变 DPI还会改变 PDFium 对字体和透明度的处理路径个别 PDF 在 None 模式下某个图形元素渲染不全切到 ForPrinting 反而好了。所以遇到渲染异常时把 flags 换一换再试很多时候不是数据坏了是渲染参数没配对。4.2 大文件与长文档内存回收不是 GC 玄学PdfiumViewer 打开大 PDF 时内存上涨是正常的因为 PDFium 内部有页面缓存渲染过的 Bitmap 也占内存。真正的问题通常出在误用文档流不释放、切换文档时旧引用还在、渲染线程堆积位图。首先是文件流的选择。打开大文件时建议用只读共享流避免文件被其他程序占用导致打不开var stream new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);其次是位图的生命周期。如果你用 PdfRenderer 手动渲染页面并赋给 PictureBox每次翻页都要把旧图 Dispose 掉否则 PictureBox 会一直持有上一页的图内存只涨不降。// 翻页时渲染并替换图片的正确姿势 private void UpdatePageImage(int pageIndex) { if (_currentPageImage ! null) { _currentPageImage.Dispose(); // 先释放旧图 _currentPageImage null; } _currentPageImage _document.Renderer.Render(pageIndex, width, height, 96, 96, PdfRenderFlags.None); pictureBox.Image _currentPageImage; }最后是文档切换时的释放顺序。如果打开第二份 PDF 前不把旧引用切断GC 要经过好几轮代际回收才能把旧文档清掉表现就是连续打开多个文件后内存峰值持续走高。标准切换顺序是先让控件脱离文档再释放文档再释放流// 正确的文档切换姿势 _viewer.Document null; _oldDocument?.Dispose(); _oldStream?.Dispose(); var newStream new FileStream(newPath, FileMode.Open, FileAccess.Read, FileShare.Read); var newDoc PdfDocument.Load(newStream); _viewer.Document newDoc;有人习惯不置 null 直接调用新文档赋值旧文档就被挂在控件的内部引用链上释放函数等了也白等。这是内存问题的头号来源不是 GC 玄学是引用没断。5. 避坑PdfiumViewer 的 6 个典型踩坑记录5.1 BadImageFormatException原生库位数不一致一加载就崩现象64 位系统上项目编译成 AnyCPU运行到PdfDocument.Load时抛 BadImageFormatException提示“试图加载格式不正确的程序”。原因PdfiumViewer 托管部分是 AnyCPU但原生 pdfium.dll 分 x86 和 x64 两个版本。NuGet 装的是 Native.x86程序却跑在 64 位进程里CLR 加载原生库时位数不匹配。解决把项目平台目标固定为 x64并安装 PdfiumViewer.Native.x64 包不要用 AnyCPU 赌运行环境。排查时在启动处打印进程位数和程序集位置Debug.WriteLine($进程位数: {(Environment.Is64BitProcess ? x64 : x86)}); Debug.WriteLine($PdfiumViewer 所在目录: {typeof(PdfViewer).Assembly.Location});5.2 白屏但进程还活着文档流被提前释放现象窗体正常打开控件区域一片空白程序不报错任务管理器里进程还活着。翻页或缩放时偶尔又正常显示。原因最常见是传入 PdfDocument.Load 的 Stream 被 using 释放了。PDFium 按需从流里读取页面数据流关闭后渲染静默失败画出空白页不抛异常。其次是 Document 属性没赋值或赋了 null控件没有可绘制的内容。解决把 Stream 提升为窗体字段生命周期和 PdfDocument 保持一致构造时先创建控件、再加载文档、最后赋值 Document。如果白屏同时伴随 PDFium 原生 DLL 的报错优先检查 5.1。// 反例用完即释放必然白屏或渲染异常 using (var s File.OpenRead(path)) { _viewer.Document PdfDocument.Load(s); } // s 在这里被释放控件还指着它5.3 滚轮不生效焦点落在父容器上现象鼠标悬停在 PDF 区域滚动滚轮外层窗体或面板滚动PDF 页面不动点一下 PDF 区域后滚轮才生效。原因PdfViewer 继承自 UserControl默认没有获得焦点滚轮消息被父窗体捕获。窗口刚打开时焦点通常在第一个按钮或容器上控件内部收不到滚轮事件。解决窗体显示时主动把焦点交给 PdfViewer。在 Load 或 Activated 事件里调用_viewer.Focus()。如果 PDF 嵌在 Panel、SplitContainer 等容器里还要检查容器是否截获了 MouseWheel 消息必要时重写容器的 OnMouseEnter 让焦点转移。private void PdfPreviewForm_Shown(object sender, EventArgs e) { _viewer.Focus(); // 让滚轮直接作用于 PDF 区域 }5.4 中文和生僻字渲染成方块现象PDF 页面里中文全部变成豆腐块英文和数字正常或仅部分页的某些汉字乱码。用 Adobe Reader 打开同一份文件没有这个问题。原因分两种情况。第一种是 PDF 没有嵌入中文字体依赖系统字体库精简版 Windows 或未装中文字体的系统就缺字形。第二种是 PDFium 解析嵌入子集字体时对某些 CID 编码处理不完整特定字形失败。解决先确认 PDF 是否内嵌字体。如果 Adobe Reader 和 PdfiumViewer 都异常说明文件本身没嵌字体给目标机器安装中文字体是根本方案如果只有 PdfiumViewer 异常把渲染 flags 从 None 换成 ForPrinting 再渲染一次部分版本对嵌入字体的解析路径不同这个问题会自己消失。5.5 打开第二份文档后第一份的句柄不释放现象连续打开三个 PDF任务管理器里这个进程的句柄数和内存只涨不降关闭窗体后仍有一段时间居高不下。原因切换文档时只调用了PdfDocument.Dispose()但没有断开PdfViewer.Document的引用。控件内部还保留着旧文档的页面信息和渲染状态Dispose 无法触发清理。解决严格按“先解绑、再释放”的顺序操作。通过第 4 章给出的切换函数处理确认_viewer.Document null执行后再释放旧对象。程序退出时也要在窗体 OnFormClosed 里走同一套流程避免进程收尾阶段被系统判定为异常退出。5.6 打印输出和屏幕显示不一致现象用 CreatePrintDocument 打印的 PDF行距比屏幕预览更紧或某些图形元素消失打印预览正常但纸质输出不对。原因打印走了 PDFium 的独立渲染路径打印模式下字体替换、颜色转换、透明合成的策略都不同于屏幕渲染。特别是嵌入了大字体集合的 PDF打印驱动和 PDFium 对字体子集的解算结果可能不同。解决打印前确认打印机纸张尺寸与 PDF 页面一致并在打印文档上关闭边距重排。如果团队里没有明确的打印验收标准我一般会先打一页 A4 样张和原文件对比确认规则后再批量打印。担心打印差异时也可以手动用 Renderer 以 ForPrinting 模式渲染成位图再交给 GDI 打印这条路径完全由代码控制行为可预期。6. 进阶自绘高亮与后台渲染把 PdfiumViewer 打磨成阅读器6.1 文本查找后高亮PDF 坐标和屏幕坐标的换算PdfiumViewer 自带的工具栏没有搜索高亮要在 PDF 上做关键词高亮得先把文本位置找出来再在渲染后的位图上叠加绘制。文本定位坐标的单位是 PDF 点即 1/72 英寸渲染成位图时要乘缩放系数同时 PDF 坐标原点在页面左下角GDI 原点在左上角Y 轴方向相反转换时要用screenY pageHeight - pdfY - rectHeight换算漏掉这一步高亮框会跑到完全错误的位置。// 渲染页面后在目标区域叠加半透明高亮 using (var pageImage renderer.Render(0, width, height, dpiX, dpiY, PdfRenderFlags.None)) using (var canvas Graphics.FromImage(pageImage)) using (var brush new SolidBrush(Color.FromArgb(48, Color.Orange))) { // highlightRect 已从 PDF 坐标换算到屏幕坐标Y 轴方向已反转 canvas.FillRectangle(brush, highlightRect); pictureBox.Image pageImage; }这个方案只适合单页局部高亮整篇文档几十个关键词同时高亮时位图绘制会变成性能瓶颈。实际项目中我一般把高亮区域存成一个矩形列表翻页时只绘制当前页命中的区域避免一次画几百个矩形。6.2 后台渲染300 页以上文档拖页卡顿的出路PdfiumViewer 的 Render 方法不是轻量操作300 页以上的扫描档 PDF 每页动辄几 MB 位图主线程直接渲染时界面会明显掉帧。常见做法是把渲染放到独立线程UI 线程只接收最终位图。// 后台线程渲染UI 线程只负责换图前提是同一时间只有一个渲染任务 Task.Run(() { using (var bmp _document.Renderer.Render( pageIndex, width, height, 144, 144, PdfRenderFlags.None)) { BeginInvoke(new Action(() { pictureBox.Image?.Dispose(); pictureBox.Image bmp; })); } });注意 P/Invoke 层的 PdfDocument 和 PdfRenderer 不是线程安全的同一个文档不能被两个线程同时渲染。线程池多开几个任务看似提升速度实际会造成 PDFium 内部状态错乱。正确姿势是串行化用一个 TaskScheduler 或队列保证任何时候只有一个渲染任务在跑当前任务完成前不提交下一个。这样虽然单页速度没变但翻页不再卡死 UI长文档的体感提升非常明显。做 PDF 控件这些年最大的教训是不要急着画 UI先把 PDF 的坐标体系、DPI 和页面模型搞明白。坐标算错了高亮画在错误位置DPI 设低了放大模糊页面模型理解错了一切交互都是隔靴搔痒。把这些基础钉死后再做功能PdfiumViewer 就是个非常可靠的地基希望帮到你。本文还有配套的精品资源点击获取