简介:面向 Delphi 与 C++ Builder 开发者的 PDF 处理组件套件,基于 PDFium 开源渲染引擎,可在 Delphi/C++ Builder 5 至 10.3 及 Lazarus 2.0.6 环境中实现 PDF 查看、页面导航、文本提取与内容编辑等功能,适合需要在桌面应用中集成 PDF 能力的初中级开发者直接复用。压缩包共 1010 个文件,约 23.75MB,以 671 个 dcu 编译单元、35 个 pas 源码、31 个 dpk 包工程及 44 个 hpp 头文件为主体,另含 res/dcr 资源、dfm/lfm 窗体、dll 与 pdfium 运行库及多份 license、readme 说明,源码与编译产物齐备,便于按需编译与二次开发。已有 623 人学习下载,可据此获取完整源码工程、示例窗体与运行库配置,快速验证渲染与编辑流程并排查集成问题。
1. 拿到一份 Delphi 10.3 的 PDFium 组件源码,先别急着编译
如果你正在用 Delphi 10.3 做桌面端项目,客户又要求「打开 PDF、翻页、缩放、填表单、加批注、导出」,那大概率绕不开 PDFium 这套渲染内核。这份 Winsoft PDFium Component Suite 5.4 的完整源码包,干的就是把 PDFium 的 C 接口封装成 Delphi 原生组件,让你在 VCL 或 FMX 窗体上拖一个控件就能显示 PDF,而不是自己写一堆 PAnsiChar 和回调函数去啃 C++ 头文件。它适合两类人:一是手上已有 Delphi 10.3 工程、需要快速补上 PDF 预览与编辑能力的中小型团队;二是想研究「C 库如何被 Pascal 优雅包一层」的组件开发者。源码包意味着你能改封装层、能跟调试器进到 Pascal 侧,而不是对着一个黑盒 DLL 猜参数。
2. 组件套件拆解:从 PDFium 内核到 Delphi 控件的分层
2.1 三层结构:原生 DLL、Pascal 绑定层、可视组件层
这套东西的骨架其实很清晰,我一般把它理解成三层。最底下是 PDFium 的原生动态库,负责真正的 PDF 解析、页面光栅化、字体加载、表单域读取,这一层是 C/C++ 写的,Delphi 侧碰不到它的内部,只能通过导出函数调用。中间是 Pascal 绑定层,把FPDF_LoadDocument、FPDF_RenderPageBitmap、FPDF_GetFormType这类 C 函数声明成external的 Pascal 例程,同时把句柄、结构体、枚举翻译成 Delphi 类型。最上面是可视组件层,把「加载文档 → 取页 → 渲染到位图 → 画到 Canvas」这条链路包成一个带LoadFromFile、PageNumber、Zoom属性的控件,设计期就能摆到窗体上。
分层带来的直接好处是:渲染出问题你先判断是 DLL 版本不对还是绑定层参数传错;界面刷新慢你先看是组件层重复渲染还是 PDFium 本身光栅化耗时。源码在手,这三层你都能下断点。
2.2 目录与单元职责:拿到源码先认文件
完整源码包解压后,通常按「运行库 / 绑定单元 / 组件单元 / 示例工程 / 文档」分区。不同版本命名会有出入,但职责划分基本一致,认文件比背路径重要:
| 区域 | 典型内容 | 你该关注什么 |
|---|---|---|
| 原生库目录 | PDFium 的 DLL(32/64 位各一份) | 位数必须和你的目标平台一致 |
| 绑定单元 | 声明 C 函数、句柄类型、结构体的 Pascal 单元 | 函数签名、调用约定、字符编码 |
| 组件单元 | 可视控件、非可视文档对象的实现 | 属性、事件、渲染生命周期 |
| 示例工程 | 打开、翻页、缩放、表单的演示 | 最快的跑通入口 |
| 文档 | 安装说明、API 概览 | 版本对应的编译选项 |
我拿到任何组件源码的第一动作都是先打开示例工程,而不是先读绑定单元。示例能编译通过,说明 DLL 位数、搜索路径、单元引用这条链是通的,后面再改才有基准。
2.3 渲染链路:一次翻页到底发生了什么
组件层最核心的一条链路是「翻页重绘」。理解它,你才知道卡顿该往哪优化。典型实现是这样:
// 组件内部翻页重绘的简化逻辑(示意,非逐字源码) procedure TPDFViewer.RenderCurrentPage; var Page: FPDF_PAGE; Bitmap: FPDF_BITMAP; W, H: Integer; begin if FDocument = nil then Exit; // 1. 按当前页码取页面句柄 Page := FPDF_LoadPage(FDocument, FPageIndex); if Page = nil then Exit; try // 2. 按缩放比例算出像素宽高 W := Round(FPDF_GetPageWidth(Page) * FZoom); H := Round(FPDF_GetPageHeight(Page) * FZoom); // 3. 建一个 BGRA 位图承接光栅化结果 Bitmap := FPDFBitmap_Create(W, H, 1); try // 4. 白底填充,避免透明区域发黑 FPDFBitmap_FillRect(Bitmap, 0, 0, W, H, $FFFFFFFF); // 5. 真正渲染,0 表示不旋转 FPDF_RenderPageBitmap(Bitmap, Page, 0, 0, W, H, 0, 0); // 6. 把位图缓冲画到控件 Canvas DrawBitmapToCanvas(Bitmap); finally FPDFBitmap_Destroy(Bitmap); end; finally FPDF_ClosePage(Page); end; end;这段逻辑里几个参数值得盯:FPDFBitmap_Create第三个参数传 1 表示带 Alpha 通道,如果你的显示区域不需要透明,传 0 能省一点内存;FPDF_RenderPageBitmap最后两个参数是旋转角度和渲染标志,标志位里可以控制是否走 LCD 文本优化、是否反锯齿,缩放很大时关掉反锯齿能明显提速。FPDF_ClosePage必须配对调用,页面句柄不释放,翻几十页内存就上去了,这是血泪经验。
2.4 文档对象与可视控件分离
套件里通常有一个非可视的文档对象负责「打开、关闭、取页数、取元数据」,可视控件只负责「显示当前页」。这种分离在二次开发时很关键:你要做缩略图栏、要做后台预渲染、要做文本搜索,都应该挂在文档对象上,而不是去戳控件。控件只保留一个「当前页索引」和「缩放」状态,重绘时向文档对象要页面句柄。这样多视图共享同一份文档(比如左边缩略图、右边正文)才不会重复加载整个 PDF。
3. 跑起来:Delphi 10.3 下的编译与首个 PDF 预览
3.1 环境准备与位数对齐
Delphi 10.3 的工程默认可能是 32 位 Win32,而 PDFium 原生库往往同时提供 32/64 位。第一件事是确认目标平台,然后把对应位数的 DLL 放到可执行文件同目录,或者加进系统搜索路径。位数不匹配的典型症状是「编译通过、一运行就报找不到入口点或模块加载失败」,这个坑几乎每个第一次用的人都会踩。
步骤上我一般这么走:先在 IDE 里把目标平台切成 Win32,编译示例工程;确认能出 EXE 后,把 32 位 DLL 拷到输出目录;运行,看能否打开一个测试 PDF。通了再切 Win64 重复一遍。不要一上来就 64 位,示例工程里有些老代码对 64 位指针处理未必干净,先用 32 位建立基准。
3.2 最小可运行代码:打开并显示第一页
示例工程之外,自己新建一个 VCL 窗体做最小验证,能最快暴露配置问题。核心代码大致是:
// 窗体上放一个 TPDFViewer(组件名以实际为准)和一个按钮 procedure TForm1.btnOpenClick(Sender: TObject); begin if OpenDialog1.Execute then begin // 加载文档;失败时组件内部一般会抛异常或返回 False PDFViewer1.LoadFromFile(OpenDialog1.FileName); // 定位到第一页并触发重绘 PDFViewer1.PageNumber := 1; // 初始缩放设为适应宽度,避免大页面超出窗体 PDFViewer1.Zoom := 1.0; end; end;LoadFromFile内部会调FPDF_LoadDocument,传的是文件路径;如果 PDF 有密码,多数封装会提供带密码参数的重载。PageNumber是从 1 开始还是从 0 开始,不同封装不一样,源码里看一眼属性 setter 就知道,别凭感觉。Zoom是浮点倍率,1.0 表示 100%,设成 0 有时表示「适应页面」,具体看实现。
3.3 表单与批注能力的调用入口
套件既然叫 Component Suite,通常不止「看」,还带表单域读写和批注。表单这块的调用路径一般是:先判断页面有没有表单(FPDF_GetFormType之类),再按字段名取句柄,读值或写值。批注则是创建注释对象、设类型和矩形、挂到页面上。这部分源码里封装程度差异较大,有的只包了读取,写入要自己补绑定。我的建议是先把「显示 + 翻页 + 缩放」跑稳,再逐个开表单和批注,一次只验证一个能力,出问题好定位。
提示:测试 PDF 不要只用自己生成的那种干净文件,找一份带表单域、带中文字体、带扫描图的真实文档,字体缺失和表单编码问题往往在这类文件上才暴露。
4. 避坑与排查:五个真实会卡住你的点
4.1 现象:一加载就报模块找不到或入口点错误
原因基本是位数不匹配,或者 DLL 放错目录。Delphi 调试时的工作目录和 EXE 输出目录可能不是同一个,IDE 里运行找的是调试工作目录,双击 EXE 找的是 EXE 所在目录。解决方式是把 DLL 同时放到这两个位置,或者在工程选项里把输出目录和工作目录统一。排查时用依赖查看工具确认 EXE 实际加载的是哪个路径的 DLL,别靠猜。
4.2 现象:中文显示成方块或乱码
PDFium 渲染中文依赖字体回退,如果系统缺对应字体,或者封装层传路径时用了 ANSI 字符串导致路径截断,就会出方块。先确认文件路径不含中文(含中文时确认封装用的是宽字符接口),再确认目标机器装了常用中文字体。源码里如果绑定层把FPDF_LoadDocument声明成PAnsiChar版本,中文路径就会翻车,这时要么改绑定用宽字符版本,要么运行时把文件拷到纯英文临时路径再加载。
4.3 现象:翻页越多内存越高,最后卡死
页面句柄或位图没释放。FPDF_LoadPage和FPDF_ClosePage必须配对,FPDFBitmap_Create和FPDFBitmap_Destroy也必须配对。封装层如果在异常路径上提前 return,就容易漏掉 finally 里的释放。排查方法是连续翻 100 页看任务管理器内存曲线,单调上升就是泄漏。修的时候把所有资源获取放进 try/finally,别图省事。
4.4 现象:大页面高缩放时界面卡顿
每次滚动或缩放都全量重渲染,且没做缓存。合格做法是缓存当前页的位图,缩放变化时才重建,平移时只做 Canvas 偏移绘制。另外渲染标志里开着一堆优化项在高倍缩放下反而拖慢,可以按缩放级别动态调整。源码里如果组件每次Repaint都走完整渲染链路,这就是你要改的第一处。
4.5 现象:表单填写后保存,值丢了
多数封装只做了「读表单」,写回需要调FPDF_FORMFILL_*系列并触发保存流程,有的还要先初始化表单环境。如果源码里没有完整的保存链路,你得自己补绑定。验证方法是填一个字段后立刻读回内存中的值,再保存到新文件、用别的阅读器打开确认。别只在自己组件里看,容易自欺欺人。
5. 二次开发:改封装层而不是改调用方
5.1 优先扩展绑定层,保持组件层稳定
二次开发最容易犯的错是直接在窗体代码里绕开组件、自己声明 C 函数。这样短期能跑,长期没人维护。正确姿势是把缺的 C 函数补进绑定单元,保持调用约定、字符编码、句柄类型和现有代码一致,然后在组件层加一个方法暴露出去。这样你的改动对上层是透明的,别人接手也能顺着分层找到位置。补绑定时特别注意stdcall和cdecl的区别,PDFium 导出多为cdecl,声明错了在 32 位下栈会乱,表现为「调用完程序莫名退出」,这种玄学问题查起来很费时间。
5.2 用预渲染队列解决翻页体验
一个具体技巧:做一个后台预渲染。用户停在第 N 页时,后台线程提前把 N+1、N-1 页渲染成位图缓存起来,翻页时直接贴图。注意 PDFium 的文档对象不是线程安全的,常见做法是每个线程各自FPDF_LoadDocument打开同一文件,或者用锁串行化对同一文档的访问。我一般选前者,内存换简单,避免锁竞争把 UI 拖死。缓存要设上限,比如只留前后各两页,否则大文档照样吃内存。
5.3 验证改动是否真的生效
改完封装层,别只看「能显示」。我的验证清单是:换三份不同来源的 PDF(文本型、扫描型、带表单型)各跑一遍;连续翻页 100 次看内存;缩放从 50% 拉到 400% 看清晰度和耗时;填一个表单字段保存后用外部阅读器复核。这套走完,改动才算落地。
5.4 一个我踩过的教训
早些年我图快,直接在组件源码里硬编码了一个临时文件路径做缓存,本地跑得好好的,到客户机器上因为权限问题写不进去,整个预览功能静默失效,日志里什么都没有。从那以后我每次动这类底层组件,都强制走一遍「换机器、换路径、换权限」的验证,缓存目录一律走系统临时目录 API 而不是写死。希望帮到你。
本文还有配套的精品资源,点击获取