☰
Qt打印与预览实战:QPrinter、QPrintPreviewWidget与PDF导出
2026/10/15 16:21:28 网站建设 项目流程

简介:这份资源是面向Qt初学者与桌面应用开发者的打印与预览功能示例,围绕在窗口中输入文本并完成打印、预览的完整流程展开,帮助解决Qt应用中打印模块集成与预览界面搭建的实际问题。压缩包共8个文件,约32KB,包含2个cpp源文件、1个h头文件、1个ui界面文件,以及pro工程文件、manifest清单、desktop启动项和可执行程序,覆盖从源码到运行环境的完整结构。资源以QPrinter配置纸张与分辨率、QTextDocument承载待打印内容为主线,并演示QPrintPreviewWidget与QPrintPreviewDialog两种预览方式,示例中的fontsampler文本样本可用于测试不同字体与样式的打印表现。目前已有2629人学习下载,适合需要快速理解Qt打印工作流、对照源码调试预览效果的开发者参考。

1. 打印和预览这件事,为什么在 Qt 里总有人翻车

很多做桌面端的朋友第一次接到「加个打印功能」的需求时,心里想的是:不就是调个QPrinter然后QPainter画一下吗?结果真上手才发现,屏幕上一套坐标、打印机上一套 DPI,预览窗口里又是另一套缩放逻辑,三套东西对不上,打出来要么被裁掉半页,要么字体糊成一团。Qt 的打印和预览这套东西,本质上是把「渲染」和「输出设备」解耦:QPrinter代表物理或虚拟输出设备,QPrintPreviewWidget/QPrintPreviewDialog负责把同一份绘制逻辑渲染到屏幕上给你看。它适合谁?适合所有需要「所见即所得」导出 PDF 或走纸打印的桌面工具开发者,尤其是报表、标签、票据、图表导出这类场景。这一篇就按我实际落地的顺序,把最小可跑、参数怎么调、坑在哪讲清楚。

2. 从 QPrinter 到 QPrintPreviewWidget:先把渲染管线搭对

2.1 为什么不能直接往 QPrinter 上画

新手最容易犯的错,是拿到QPrinter就直接QPainter painter(&printer)开始画,画完发现预览没法做,因为一旦begin了,这一页的内容就定死了。正确的思路是把「画什么」抽成一个独立函数,参数是QPainter*和页面信息,打印和预览都调它。这样预览窗口每次刷新、打印每次走纸,走的都是同一份代码,不会出现「预览对、打印错」的玄学问题。

// 统一的绘制入口:谁调用都行,painter 由外部传入 void renderPage(QPainter *painter, const QRectF &pageRect, int pageIndex) { // pageRect 是当前页可绘制区域,已经扣掉了页边距 painter->save(); painter->setRenderHint(QPainter::Antialiasing, true); // 标题:用相对坐标,别写死像素 QFont titleFont = painter->font(); titleFont.setPointSizeF(14); painter->setFont(titleFont); painter->drawText(pageRect.topLeft() + QPointF(0, 20), QString("第 %1 页").arg(pageIndex + 1)); // 正文区域:从标题下方开始 QRectF body = pageRect.adjusted(0, 40, 0, 0); painter->drawRect(body); // 调试用边框,上线可去掉 painter->drawText(body, Qt::TextWordWrap, "这里是正文内容……"); painter->restore(); }

这段代码的关键点有三个。第一,pageRect由调用方算好传进来,绘制函数不关心是 A4 还是热敏纸。第二,所有坐标基于pageRect做相对偏移,不写死100, 200这种绝对值。第三,save()/restore()成对出现,避免画笔、字体状态污染下一页。参数上,setPointSizeF用浮点而不是整数,是因为不同 DPI 下整数点号缩放后容易出现半像素抖动。

2.2 用 QPrintPreviewWidget 做实时预览

QPrintPreviewWidget比QPrintPreviewDialog更灵活,因为它是个普通 widget,可以嵌进你自己的窗口布局里,工具栏、缩放条都能自定义。核心是连它的paintRequested信号。

// 构造预览控件,ZoomMode 决定初始缩放策略 QPrintPreviewWidget *preview = new QPrintPreviewWidget(&printer, this); preview->setZoomMode(QPrintPreviewWidget::FitInView); // 关键:每次需要重绘时,Qt 会发这个信号 connect(preview, &QPrintPreviewWidget::paintRequested, this, [this](QPrinter *printer) { QPainter painter(printer); // 计算可绘制区域:页面尺寸减去页边距 QRectF pageRect = printer->pageRect(QPrinter::DevicePixel); const int pageCount = 3; // 实际项目里由数据量决定 for (int i = 0; i < pageCount; ++i) { if (i > 0) printer->newPage(); // 第一页不用 newPage renderPage(&painter, pageRect, i); } }); preview->updatePreview(); // 主动触发一次渲染

逻辑说明:paintRequested会在缩放、翻页、窗口大小变化时反复触发,所以绘制函数必须是无副作用的纯渲染。pageRect(QPrinter::DevicePixel)拿到的是设备像素单位,和QPainter的坐标系一致,别用QPrinter::Millimeter混着算,否则预览和打印会差一个缩放系数。newPage()的调用时机是「画完上一页、画下一页之前」,第一页之前调用会多出一张空白页,这是血泪经验。

2.3 页面尺寸和边距到底怎么设

QPrinter的页面设置分两层:纸张尺寸(setPageSize)和页边距(setPageMargins)。很多人只设了纸张,忘了边距,结果内容贴着纸边,打印机物理不可打印区域直接把它吃掉。

参数常用取值说明
setPageSizeQPageSize::A4票据场景用QPageSize::Custom自定义
setPageMarginsQMarginsF(15, 15, 15, 15)单位由第二参数决定,一般用QPageLayout::Millimeter
setOrientationQPageLayout::Portrait横向报表改Landscape
setResolution300影响 DevicePixel 换算,预览可低、打印要高

设置边距时注意,setPageMargins在部分平台需要配合setPageLayout一起用才生效,单独调可能被系统默认值覆盖。稳妥做法是构造一个QPageLayout对象,把尺寸、方向、边距一次性设进去,再printer.setPageLayout(layout)。

3. 把预览、打印、导出 PDF 串成一条线

3.1 一个 QPrinter 同时服务三种输出

预览、走纸打印、导出 PDF,这三件事在 Qt 里其实是同一个QPrinter换不同的OutputFormat。预览时QPrinter只是个「假设备」,真正渲染到屏幕的是预览控件;打印时OutputFormat是NativeFormat;导出 PDF 时切成PdfFormat并指定文件名。

// 导出 PDF:复用同一份 renderPage void exportToPdf(const QString &filePath) { QPrinter printer(QPrinter::HighResolution); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(filePath); printer.setPageSize(QPageSize(QPageSize::A4)); printer.setPageMargins(QMarginsF(15, 15, 15, 15), QPageLayout::Millimeter); QPainter painter; if (!painter.begin(&printer)) { // 必须检查 begin 返回值 qWarning("PDF 设备打开失败:%s", qPrintable(filePath)); return; } QRectF pageRect = printer.pageRect(QPrinter::DevicePixel); for (int i = 0; i < 3; ++i) { if (i > 0) printer.newPage(); renderPage(&painter, pageRect, i); } painter.end(); // 不 end 文件不完整 }

逻辑说明:HighResolution模式让 PDF 内部按高 DPI 记录,放大不糊。painter.begin返回 false 通常意味着路径不可写或设备被占用,必须处理,否则后面所有绘制都是空操作。painter.end()一定要调,否则 PDF 文件尾部信息写不进去,打开会报损坏。参数上,导出 PDF 时页边距和预览保持一致,才能做到「预览什么样、导出什么样」。

3.2 多页数据的自动分页思路

真实项目里页数不是写死的 3,而是根据数据量算出来的。常见做法是先测量一页能放多少行,再按行数切分。

// 简化版分页:按固定行高估算每页容量 int rowsPerPage(const QRectF &bodyRect, qreal rowHeight) { // 向下取整,留一行余量防止最后一行被裁 return qMax(1, static_cast<int>(bodyRect.height() / rowHeight) - 1); } void renderAll(QPainter *painter, QPrinter *printer, const QVector<QString> &rows) { QRectF pageRect = printer->pageRect(QPrinter::DevicePixel); QRectF body = pageRect.adjusted(0, 40, 0, 0); const qreal rowH = 20.0; const int perPage = rowsPerPage(body, rowH); const int totalPages = (rows.size() + perPage - 1) / perPage; for (int p = 0; p < totalPages; ++p) { if (p > 0) printer->newPage(); painter->save(); int start = p * perPage; int end = qMin(start + perPage, rows.size()); for (int i = start; i < end; ++i) { QRectF cell(body.left(), body.top() + (i - start) * rowH, body.width(), rowH); painter->drawText(cell, rows[i]); } painter->restore(); } }

逻辑说明:rowsPerPage里减 1 是留余量,因为字体实际渲染高度可能比rowH略大,不减的话最后一行容易被页脚裁掉。totalPages用向上取整公式(n + per - 1) / per,避免整数除法丢页。参数rowH要和字体点号匹配,14pt 字体配 20 像素行高比较稳,太小会重叠。

3.3 预览控件的缩放与翻页交互

QPrintPreviewWidget自带setZoomFactor、setCurrentPage、fitInView等方法,做工具栏时直接调。

// 工具栏按钮:放大、缩小、适应宽度、翻页 connect(ui->btnZoomIn, &QPushButton::clicked, preview, [preview]{ preview->setZoomFactor(preview->zoomFactor() * 1.2); }); connect(ui->btnZoomOut, &QPushButton::clicked, preview, [preview]{ preview->setZoomFactor(preview->zoomFactor() / 1.2); }); connect(ui->btnFit, &QPushButton::clicked, preview, [preview]{ preview->fitInView(); }); connect(ui->btnNext, &QPushButton::clicked, preview, [preview]{ preview->setCurrentPage(preview->currentPage() + 1); });

逻辑说明:zoomFactor是乘性缩放,每次乘 1.2 比加固定值更符合视觉习惯。fitInView会自动算缩放比,适合「适应窗口」按钮。翻页时currentPage从 1 开始计数,越界时控件内部会忽略,但最好自己加边界判断,避免日志里刷警告。

4. 避坑与排查:打印预览里最容易踩的五个坑

4.1 预览正常,打印出来内容偏移或缩放

现象:预览窗口里排版完美,走纸打印后整体右移或缩小一圈。原因:预览用的是逻辑坐标,打印时QPrinter的pageRect受物理不可打印区域影响,两者DevicePixel换算基准不同。解决:统一用printer.pageRect(QPrinter::DevicePixel)取区域,不要自己用毫米换算像素;打印前调printer.setFullPage(false)让 Qt 帮你扣掉硬件边距。

4.2 中文乱码或方框

现象:英文数字正常,中文变成方块。原因:QPainter默认字体在某些平台不含中文字形,或者 PDF 导出时没嵌入字体。解决:显式设置一个含中文的字体族,如painter->setFont(QFont("SimSun", 12));导出 PDF 时确认QPrinter的字体嵌入选项开启,否则换台机器打开就缺字。

4.3 多页打印只有第一页有内容

现象:数据明明有多页,打印出来只有第一页。原因:忘了在循环里调printer->newPage(),或者newPage()调在了绘制之后。解决:确认newPage()在「上一页画完、下一页画之前」调用,第一页之前不要调。用qDebug()打印每页开始时的painter状态辅助定位。

4.4 预览控件空白,paintRequested 不触发

现象:预览窗口一片白,信号没进来。原因:QPrinter对象生命周期问题,或者updatePreview()没调。解决:把QPrinter作为成员变量而不是局部变量,确保预览控件存活期间它一直有效;构造完控件后主动调一次updatePreview()。

4.5 导出 PDF 文件损坏打不开

现象:PDF 生成了但打开报错。原因:painter.end()没调,或者begin失败后继续绘制。解决:begin返回值必须判断,失败直接返回;所有绘制路径都要保证end()被调用,可以用 RAII 封装一个小的 painter guard。

5. 进阶:把打印逻辑做成可测试的纯函数

做到这一步,打印功能基本能用了。但如果你想让这块代码长期可维护,我建议再往前一步:把renderPage做成不依赖QPrinter的纯函数,只接收QPainter*和一个描述页面几何的结构体。这样你可以用QImage当画布做单元测试,断言某个坐标上像素颜色对不对,而不用真的连打印机。

// 可测试的渲染核心:不碰 QPrinter,只认 QPainter 和几何信息 struct PageGeometry { QRectF contentRect; // 已扣边距的可绘制区 int pageIndex; int totalPages; }; void renderPagePure(QPainter *painter, const PageGeometry &geo, const QVector<QString> &rows) { painter->save(); painter->setFont(QFont("SimSun", 12)); // 页眉 painter->drawText(geo.contentRect.topLeft(), QString("第 %1/%2 页") .arg(geo.pageIndex + 1).arg(geo.totalPages)); // 正文 QRectF body = geo.contentRect.adjusted(0, 30, 0, 0); qreal y = body.top(); for (const QString &row : rows) { painter->drawText(QPointF(body.left(), y), row); y += 20; } painter->restore(); }

测试时这样用:

// 用 QImage 当假设备,验证渲染不崩、像素有内容 QImage canvas(595, 842, QImage::Format_ARGB32); // A4 @72dpi canvas.fill(Qt::white); QPainter p(&canvas); PageGeometry geo{QRectF(20, 20, 555, 802), 0, 1}; renderPagePure(&p, geo, {"测试行一", "测试行二"}); p.end(); // 断言:标题区域不应还是纯白 QColor px = canvas.pixelColor(25, 25); QVERIFY(px != QColor(Qt::white));

这套做法的价值在于:打印相关的 bug 往往在真机上才暴露,调试成本高;把渲染逻辑抽成纯函数后,CI 里跑一遍就能拦住大部分坐标和分页错误。参数上,QImage的尺寸按目标纸张在 72 DPI 下的像素数给,A4 约 595×842,这样坐标换算和真实打印一致。

我自己的习惯是,任何涉及「输出设备」的代码,第一版就把它和业务逻辑切开,宁可多写一个结构体,也不让QPrinter渗透到绘制函数里。这样后面换 PDF 库、加水印、做批量导出,都只是换外层调用,核心渲染一行不用动。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询