现代Qt开发教程(新手篇)5.17--QtPdf PDF 渲染基础
1. 前言:别再用 QProcess 调外部阅读器了
说到在 Qt 应用里显示 PDF,我见过最多的做法是 QProcess 启动系统自带的 PDF 阅读器——Windows 下调 AcroRd32.exe,Linux 下调 evince 或者 okular,macOS 下调 open 命令。这做法能用是能用,但问题一堆:用户体验割裂(PDF 突然在一个完全独立的窗口里弹出来)、无法在应用内集成导航和批注功能、跨平台行为不一致(不同系统装的阅读器不一样,有的甚至没装)。更不用说你完全无法控制 PDF 的渲染效果——缩放、翻页、高亮、叠加注释,统统做不到。
QtPdf 模块就是来解决这个问题的。它提供了 PDF 文档加载、页面渲染、内嵌显示的完整能力,而且是纯 Qt 实现——不依赖外部阅读器,不依赖 Poppler 或 MuPDF 之类的第三方库(底层用的是 Qt 自有的 PDF 渲染引擎),编译出来就能跑,跨平台行为一致。对于需要嵌入 PDF 查看功能的应用来说——帮助文档阅读器、电子书阅读器、报表预览组件——QtPdf 是最直接的方案。
这篇我们要做的是用 QPdfDocument 加载 PDF 文件,用 QPdfPageRenderer 把指定页面渲染为 QImage,用 QPdfView 控件直接在窗口中显示 PDF 并支持页面导航和缩放。整个流程从底层渲染到高层控件都会覆盖到。
2. 环境说明
本篇基于 Qt 6.9.1,需要引入 Pdf 和 Widgets 模块。CMake 配置如下:
find_package(Qt6 REQUIRED COMPONENTS Pdf Widgets)QtPdf 在 Qt Installer 中属于标准安装包。它的渲染引擎是 Qt 自有的——不依赖 Poppler、MuPDF 等第三方库,这意味着你不需要在目标系统上额外安装任何 PDF 相关的依赖。QtPdf 支持的 PDF 特性覆盖了绝大多数常见文档:文字渲染、矢量图形、图片嵌入、书签、链接、表单字段。不过它不支持 JavaScript 交互和多媒体嵌入(音频视频),这些属于 PDF 规范的扩展特性,大多数应用场景也不需要。
工具链:MSVC 2019+、GCC 11+、Clang 14+,C++17 标准,CMake 3.26+。QtPdf 是纯 CPU 渲染,对 GPU 没有要求。
3. 核心概念讲解
3.1 QPdfDocument--PDF 文档加载与元信息
QPdfDocument 是 QtPdf 的核心类,负责加载 PDF 文件并提供文档级的元信息和页面索引。它的用法很直接——构造时传入 parent,调用 load() 加载文件路径,加载完成后就可以查询页数、获取页面尺寸、渲染指定页面。
QPdfDocument doc(this);
doc.load(QStringLiteral("/path/to/document.pdf"));
if (doc.status() != QPdfDocument::Ready) {
qWarning() << "PDF 加载失败,状态:" << doc.status();
return;
}
qDebug() << "总页数:" << doc.pageCount();
qDebug() << "第 0 页尺寸:" << doc.pagePointSize(0); // 单位:磅(point,1/72 英寸)QPdfDocument::load() 是异步的——调用后立即返回,文档在后台加载。加载状态通过 status() 属性获取,有三个主要值:Loading(正在加载)、Ready(加载完成,可以使用)、Error(加载失败)。如果你想等待加载完成再做操作,可以监听 statusChanged 信号。
pagePointSize() 返回的是指定页面的尺寸,单位是磅(point)——在 PDF 规范中 1 磅等于 1/72 英寸。A4 纸的尺寸是 595 x 842 磅。这个尺寸是"逻辑尺寸",实际渲染到 QImage 时需要乘以你期望的 DPI 缩放因子——如果想在 150 DPI 下渲染,就把页面尺寸乘以 150/72 的系数。
QPdfDocument 还提供了一些元信息查询:metaData() 可以获取标题、作者、主题、创建日期等文档属性;getAllText() 和 getTextInPage() 可以提取文本内容(不过这些属于 QPdfDocument 的进阶功能,本篇聚焦在渲染和显示上)。
3.2 QPdfPageRenderer--页面渲染为 QImage
QPdfPageRenderer 是负责把 PDF 页面光栅化为 QImage 的类。它和 QPdfDocument 配合使用——先创建 QPdfDocument 加载文件,再把 QPdfDocument 传给 QPdfPageRenderer,然后调用 renderPage() 获取指定页面的 QImage。
QPdfDocument *doc = new QPdfDocument(this);
doc->load(QStringLiteral(":/sample.pdf"));
QPdfPageRenderer renderer(this);
renderer.setDocument(doc);
// 渲染第 0 页,尺寸为页面原始大小的 2 倍(约 144 DPI)
QImage image = renderer.renderPage(0,
doc->pagePointSize(0) * 2.0); // imageSize 参数是 QSizeF
if (!image.isNull()) {
image.save(QStringLiteral("page_0.png"));
qDebug() << "页面渲染成功,尺寸:" << image.size();
}renderPage() 的第一个参数是页面索引(从 0 开始),第二个参数是期望的图像尺寸(QSizeF,单位是磅的缩放值)。这个尺寸决定了渲染的分辨率——传入的值越大,输出的 QImage 像素越多,细节越清晰,但渲染时间也越长。屏幕显示用页面原始尺寸的 1.5 到 2 倍就够了(相当于 108 到 144 DPI),打印输出用 4 到 6 倍(相当于 288 到 432 DPI)。
QPdfPageRenderer 的渲染是同步的——renderPage() 会阻塞直到渲染完成。对于单页渲染这通常只需要几毫秒到几十毫秒(取决于页面复杂度和分辨率),但如果要批量渲染大量页面或者高分辨率渲染,最好放在后台线程里执行,避免阻塞 UI 线程。
3.3 QPdfView--开箱即用的 PDF 显示控件
如果你只需要在窗口里显示 PDF、支持翻页和缩放,不需要自己管理渲染流程,QPdfView 就是直接拿来用的控件。它是一个 QWidget 子类(不是 QML 组件),集成了文档加载、页面渲染、滚动浏览、页面导航的全部功能。
QPdfView *pdfView = new QPdfView(parent);
QPdfDocument *doc = new QPdfDocument(pdfView);
doc->load(QStringLiteral(":/sample.pdf"));
pdfView->setDocument(doc);
pdfView->setPageMode(QPdfView::PageMode::MultiPage); // 多页连续滚动QPdfView 的 setPageMode 控制显示模式:PageMode::SinglePage 一次只显示一页(需要手动翻页),PageMode::MultiPage 是多页连续排列、滚动浏览(类似大多数 PDF 阅读器的默认行为)。对于文档阅读来说 MultiPage 更自然,SinglePage 更适合演示文稿那种逐页翻阅的场景。
QPdfView 还内置了缩放能力——通过 setZoomFactor() 设置缩放比例,1.0 是原始大小。不过 QPdfView 没有内置鼠标滚轮缩放——如果你需要用户通过 Ctrl+滚轮缩放,需要自己重写 wheelEvent 或者在 QPdfView 外面包一层事件过滤器。
QPdfView 的 ZoomMode 也是一个值得了解的属性:ZoomMode::Custom 使用你手动设置的 zoomFactor,ZoomMode::FitToWidth 让页面宽度自动适配控件宽度,ZoomMode::FitInView 让整页适配控件的可见区域。FitToWidth 和 FitInView 在窗口大小变化时会自动重新计算缩放比例——对于窗口大小不固定的应用来说很实用。
3.4 页面导航与缩放
页面导航的核心是"当前页码"这个状态。QPdfView 本身提供了 document() 指向内部的 QPdfDocument,你可以通过 QPdfDocument::pageCount() 获取总页数,然后配合 QPdfView 的 pageMode 和滚动位置来推算当前页码。不过 QtPdf 在页面导航方面的 API 设计得比较"薄"——QPdfView 没有直接提供 currentPage() 方法。如果你的应用需要精确的当前页码指示(比如显示"第 3 / 10 页"),需要自己监听 QPdfView 的滚动位置变化来计算当前可见的页面索引。
缩放方面,QPdfView 的 setZoomFactor() 接受一个浮点数值,1.0 表示 100%(每磅对应 1 个逻辑像素)。常见的缩放范围是 0.25 到 5.0,低于 0.25 文字几乎看不清,高于 5.0 单页占用的渲染内存会非常大。
4. 综合示例:PDF 查看器小程序
把前面讲的内容整合起来,我们写一个基于 Widgets 的 PDF 查看器。核心功能:QPdfView 居中显示 PDF 文档,顶部工具栏包含"打开文件"按钮、"上一页/下一页"按钮、"页码显示"标签、"放大/缩小"按钮以及"缩放比例"显示。用 QPdfDocument 加载文件,QPdfView 负责渲染和显示,工具栏提供导航和缩放交互。
这个示例是纯 C++ Widgets 项目,不需要 QML。CMake 配置:
find_package(Qt6 REQUIRED COMPONENTS Pdf Widgets)
qt_add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME}
PRIVATE Qt6::Pdf Qt6::Widgets)main.cpp 的完整代码:
#include <QApplication>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QPdfDocument>
#include <QPdfView>
#include <QPushButton>
#include <QToolBar>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
class PdfViewer : public QWidget
{
Q_OBJECT
public:
explicit PdfViewer(QWidget *parent = nullptr) : QWidget(parent)
{
setWindowTitle(QStringLiteral("QtPdf PDF 查看器"));
resize(900, 700);
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// 工具栏
auto *toolbar = new QToolBar();
toolbar->setMovable(false);
auto *btnOpen = new QPushButton(QStringLiteral("打开文件"));
toolbar->addWidget(btnOpen);
toolbar->addSeparator();
auto *btnPrev = new QPushButton(QStringLiteral("上一页"));
toolbar->addWidget(btnPrev);
m_pageLabel = new QLabel(QStringLiteral("0 / 0"));
m_pageLabel->setMinimumWidth(80);
m_pageLabel->setAlignment(Qt::AlignCenter);
toolbar->addWidget(m_pageLabel);
auto *btnNext = new QPushButton(QStringLiteral("下一页"));
toolbar->addWidget(btnNext);
toolbar->addSeparator();
auto *btnZoomOut = new QPushButton(QStringLiteral("缩小"));
toolbar->addWidget(btnZoomOut);
m_zoomLabel = new QLabel(QStringLiteral("100%"));
m_zoomLabel->setMinimumWidth(60);
m_zoomLabel->setAlignment(Qt::AlignCenter);
toolbar->addWidget(m_zoomLabel);
auto *btnZoomIn = new QPushButton(QStringLiteral("放大"));
toolbar->addWidget(btnZoomIn);
mainLayout->addWidget(toolbar);
// PDF 视图
m_pdfView = new QPdfView();
m_pdfView->setPageMode(QPdfView::PageMode::MultiPage);
m_pdfView->setZoomMode(QPdfView::ZoomMode::Custom);
m_pdfDoc = new QPdfDocument(this);
mainLayout->addWidget(m_pdfView);
// 连接信号
connect(btnOpen, &QPushButton::clicked, this, &PdfViewer::openFile);
connect(btnPrev, &QPushButton::clicked, this, &PdfViewer::prevPage);
connect(btnNext, &QPushButton::clicked, this, &PdfViewer::nextPage);
connect(btnZoomIn, &QPushButton::clicked, this, &PdfViewer::zoomIn);
connect(btnZoomOut, &QPushButton::clicked, this, &PdfViewer::zoomOut);
connect(m_pdfDoc, &QPdfDocument::statusChanged, this, [this]() {
if (m_pdfDoc->status() == QPdfDocument::Ready) {
m_pageLabel->setText(
QStringLiteral("1 / %1").arg(m_pdfDoc->pageCount()));
updateZoomLabel();
}
});
// 默认打开一个示例提示
m_pageLabel->setText(QStringLiteral("请打开 PDF 文件"));
}
private slots:
void openFile()
{
QString path = QFileDialog::getOpenFileName(
this, QStringLiteral("选择 PDF 文件"), QString(),
QStringLiteral("PDF 文件 (*.pdf);;所有文件 (*)"));
if (path.isEmpty()) {
return;
}
if (m_pdfDoc->status() == QPdfDocument::Ready) {
m_pdfDoc->close();
}
m_pdfDoc->load(path);
if (m_pdfDoc->status() == QPdfDocument::Error) {
qWarning() << "PDF 加载失败:" << path;
return;
}
m_pdfView->setDocument(m_pdfDoc);
m_currentPage = 0;
m_zoomFactor = 1.0;
m_pdfView->setZoomFactor(m_zoomFactor);
}
void prevPage()
{
if (m_currentPage > 0) {
m_currentPage--;
updatePageLabel();
}
}
void nextPage()
{
if (m_currentPage < m_pdfDoc->pageCount() - 1) {
m_currentPage++;
updatePageLabel();
}
}
void zoomIn()
{
m_zoomFactor = qMin(m_zoomFactor * 1.25, 5.0);
m_pdfView->setZoomFactor(m_zoomFactor);
updateZoomLabel();
}
void zoomOut()
{
m_zoomFactor = qMax(m_zoomFactor / 1.25, 0.25);
m_pdfView->setZoomFactor(m_zoomFactor);
updateZoomLabel();
}
private:
void updatePageLabel()
{
m_pageLabel->setText(
QStringLiteral("%1 / %2")
.arg(m_currentPage + 1)
.arg(m_pdfDoc->pageCount()));
}
void updateZoomLabel()
{
m_zoomLabel->setText(
QStringLiteral("%1%").arg(static_cast<int>(m_zoomFactor * 100)));
}
QPdfView *m_pdfView = nullptr;
QPdfDocument *m_pdfDoc = nullptr;
QLabel *m_pageLabel = nullptr;
QLabel *m_zoomLabel = nullptr;
int m_currentPage = 0;
double m_zoomFactor = 1.0;
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qDebug() << "QtPdf PDF 渲染基础示例";
qDebug() << "本示例演示 QPdfDocument + QPdfPageRenderer + QPdfView";
PdfViewer viewer;
viewer.show();
return app.exec();
}
#include "main.moc"运行程序后你会看到一个带工具栏的窗口。点击"打开文件"选择一个 PDF,文档内容会立即在中央区域以多页连续滚动的模式显示出来。"放大"和"缩小"按钮每次调整 25% 的缩放比例,标签实时显示当前百分比。"上一页/下一页"按钮控制页码指示器。
几个实现细节值得说明。QPdfView 的 setDocument() 调用后,控件内部会自动开始渲染页面——不需要手动调用 QPdfPageRenderer。实际上 QPdfView 内部就持有一个 QPdfPageRenderer 实例,你在使用 QPdfView 时不需要再自己创建渲染器。只有当你需要把 PDF 页面渲染到 QImage 上做进一步处理(比如叠加水印、OCR 识别)时,才需要直接用 QPdfPageRenderer。
关于页码导航的问题:示例中 m_currentPage 变量维护的是页码索引,配合翻页按钮使用。但 QPdfView 在 MultiPage 模式下是滚动浏览的,用户滚动时当前可见页码会变化。如果你需要精确追踪滚动位置对应的页码,需要继承 QPdfView 重写 paintEvent 或者用 QScrollBar 的 valueChanged 信号来推算。示例中的翻页按钮只是更新了页码标签,没有联动 QPdfView 的滚动位置——这个联动需要根据 QPdfView 内部的布局来计算,实现起来稍复杂,留给练习项目。
5. 练习项目
练习项目:带搜索功能的 PDF 阅读器。
在基础查看器上增加文本搜索功能:工具栏增加一个 QLineEdit 搜索框和"搜索"按钮,输入关键词后调用 QPdfDocument::search() 方法搜索所有页面中包含该关键词的位置,搜索结果高亮显示在 QPdfView 上。QPdfView 支持通过 setHighlight() 或自定义 QPdfPageRenderer 的方式叠加搜索结果标记。
完成标准是这样的:搜索框输入关键词后点击搜索,在当前页面中找到的所有匹配项用黄色矩形框高亮标记出来,状态栏显示"找到 N 处匹配"。"上一个"/"下一个"按钮在高亮结果之间跳转,跳转时自动滚动到对应位置。
几个实现提示:QPdfDocument::search() 返回的是 QVector<QPdfSearchModelResult>(或者在较新版本中通过 QPdfSearchModel),每个结果包含页面索引和在页面上的矩形区域。高亮叠加可以通过 QPdfView 的 overlay 机制或者自己用 QPdfPageRenderer 渲染页面后用 QPainter 画矩形来实现。
6. 官方文档参考
Qt 文档 · QtPdf 模块 -- QtPdf 模块总览
Qt 文档 · QPdfDocument -- PDF 文档加载与管理
Qt 文档 · QPdfPageRenderer -- 页面渲染器
Qt 文档 · QPdfView -- PDF 显示控件
(链接已验证,2026-04-23 可访问)
到这里就大功告成了。QtPdf 提供了从底层渲染到高层控件的三层能力——QPdfDocument 负责文档加载和元信息管理,QPdfPageRenderer 把页面光栅化为 QImage 供自定义处理,QPdfView 作为开箱即用的控件直接嵌入 Widgets 界面。整个过程不依赖任何外部 PDF 库,纯 Qt 实现,编译出来就能跑。如果你的应用需要嵌入 PDF 查看、报表预览、文档阅读功能,QtPdf 是最轻量的选择。下一篇我们转向服务端方向,看 QtHttpServer 如何在 Qt 应用内嵌入一个 HTTP 服务器。