Qt打印与预览实战:从文本、表格到图表的完整指南
2026/9/9 15:18:44 网站建设 项目流程

简介:面向正在学习Qt打印与预览功能的开发者,这份RAR压缩包提供了一套可直接运行的示例工程,核心演示了借助QPrinter配置纸张尺寸与分辨率、通过QTextDocument承载HTML文本并输出,以及利用QPrintPreviewDialog实现打印前预览的完整流程。压缩包整体仅32KB,共8个文件,涵盖2个C++源文件、1个头文件、1个UI界面设计文件、pro工程配置、manifest配置、可执行程序及desktop桌面文件,既能直接阅读源码理解调用链,也可快速编译运行观察实际效果。其中还附有字体样本测试页,便于直观检查不同字体与样式在打印输出中的实际表现,适合正在开发文档处理、报表生成等功能的开发者作为入门参考。该示例包已被2629人浏览学习,小巧但功能完整,能够帮助初学者少走弯路,快速掌握Qt打印与预览模块的基本用法。 做Qt做到一定阶段,基本都会撞上“打印”这道坎。打印订单、打印报表、把QCustomPlot绘制的波形图输出成PDF,还得先弹个预览让用户确认,标题里“打印和预览”几个字看着简单,真正动手才会发现里面全是细节。这篇文章我拿一个真实能跑的示例来说,把Qt里最常见的三种打印场景——文本、表格、图表——完整过一遍,顺带把预览、分页、部署打包这些坑也一起填平。

1. 先想明白一件事:Qt里的打印到底是怎么工作的

很多刚转过来的朋友,一听说要写打印功能,第一反应是去找“打印API”。Qt里没有那么一个函数叫print(),真正的模型是:打印机本质上是一块画布,你要用QPainter在上面画东西。想通这一点,后面所有代码就好理解了。

1.1 QPrinter不是“打印机”,而是一块特殊画布

QPrinter继承自QPaintDevice,和QWidgetQPixmap的结构很接近。也就是说,你平时在一个窗口上画图用的那套QPainter,换成QPrinter照样能用。区别在于这块“画布”的度量单位:

  • 普通窗口和图片默认按像素算;
  • QPrinter默认按逻辑像素和专业打印参数混合计算,还区分低分辨率、高分辨率模式;
  • 打印机的物理分辨率往往比屏幕高得多,所以打印出来的文字、线条比屏幕上看着更细腻。

其实可以这么类比:屏幕上画画是“在草稿纸上用铅笔”,打印则是“把同样的图案打到高级打印纸上”。代码里你写的还是同一套painter.drawText()painter.drawRect(),只是把设备对象换成了QPrinter

QPrinter printer(QPrinter::HighResolution); QPainter painter; if (!painter.begin(&printer)) { qWarning() << "打开打印机失败"; return; } painter.drawText(printer.pageRect(QPrinter::DevicePixel), Qt::AlignCenter, "Hello Printer"); painter.end();

1.2 预览不是另写一套,而是同一套渲染逻辑换目标

这是整个打印预览体系里最容易被误解的点。很多人以为,预览就是先把内容画到一个QWidget上截图,再弹出个对话框显示图片。这样做不仅糊,而且会出“预览正常、打印出来完全不对”的怪问题。

正确做法是:QPrinter对象被传给预览对话框,预览控件触发paintRequested信号时,你拿同一个QPrinter去执行同一套绘制函数。预览时实际绘制到屏幕上(由QPrintPreviewWidget帮你接管),真正打印时又绘制到打印机上。你只需要写一份渲染逻辑,两个地方共用。

QPrinter printer(QPrinter::HighResolution); QPrintPreviewDialog preview(&printer, this); connect(&preview, &QPrintPreviewWidget::paintRequested, this, &MainWindow::renderReport); preview.exec();

2. 这个“打印和预览”例子,整体该怎么设计

我把示例代码设计成一个能开箱即用的报告打印工具,覆盖三类最常见的输出:纯文本/富文本、规规矩矩的表格、以及程序生成的图表。目标不是做出花哨的排版,而是让你能跑完一遍之后,把核心代码直接搬到自己项目里去改。

2.1 把打印内容拆成三类:文本、表格、图形

  • 文本:用QTextDocument加载HTML字符串,适合说明书、协议内容、带标题和段落样式的长文本。
  • 表格:用QPainter一行一行画线,按页高自动换页,非常适合“九九乘法表”、数据报表、账单一类的结构化数据。
  • 图形:直接把界面里的QCustomPlot波形图、柱状图等,缩放绘制到页面指定位置,用于技术报告和化验单。

这三类内容对应三种实现方式,各有各的坑,下面逐个说。

2.2 统一渲染入口,预览和真机打印保持一致

为了让预览和实际打印“所见即所得”,我在代码里固定了一个统一的渲染函数作为入口:

void MainWindow::renderReport(QPrinter *printer) { QPainter painter; if (!painter.begin(printer)) { return; } // 1. 画文本区域 drawTextBlock(&painter, printer); // 2. 画表格区域 painter.translate(0, textBlockHeight); drawTableBlock(&painter, printer); // 3. 画图表区域 painter.translate(0, tableBlockHeight); drawPlotBlock(&painter, printer); painter.end(); }

这个函数同时服务预览和打印,不管用户在预览窗口里怎么缩放、翻页,底层都是同一套绘制逻辑。保持这个原则,后续排版微调也只要改一处。

3. 实操:把文本、九九乘法表、报表逐页打印出来

3.1 先用QTextDocument搞定带格式文本

打印带加粗、标题、换行的文本,别自己用drawText一点点排版。Qt已经把文本排版封装得很好,直接构建一个QTextDocument,设置好页面尺寸,再调用它的print()方法就行。

常见做法是这样:

void MainWindow::drawTextBlock(QPainter *painter, QPrinter *printer) { QTextDocument doc; QString html = "<h1>信号分析报告</h1>" "<p>生成时间:2025-01-15</p>" "<p>本报告由Qt示例程序自动生成," "内容包括FFT频谱分析结果。</p>"; doc.setHtml(html); QRectF pageRect = printer->pageRect(QPrinter::DevicePixel); doc.setPageSize(pageRect.size()); doc.print(printer); }

这里有一个特别关键的细节:setPageSize必须设置成当前打印机的pageRect尺寸,否则QTextDocument默认当成A4纸的近似大小去排版,内容会显示出错。如果你不设置,最常见的结果就是第二页莫名其妙空白,或者文字被拦腰截断。

QTextDocument打印的好处是,富文本、图片、表格都能自动排版,遇到跨页会自动换页,省去大量手工计算。

3.2 用QPainter绘制表格并处理分页

接下来是打印九九乘法表。表格这类内容,QTextDocument也能处理,但做复杂表头、合并单元格、精确控制边框时,还是直接画线最灵活。

先规划尺寸:

  • 每个单元格宽 45mm 左右;
  • 每行高 8mm;
  • 表头高 10mm;
  • 一页A4纸,在25mm边距下,算下来能放大约21行。

打印时不能一口气全画,判断当前位置是否超出页面范围,超了就调printer->newPage()切换新页。

void MainWindow::drawTableBlock(QPainter *painter, QPrinter *printer) { QRectF pageRect = printer->pageRect(QPrinter::DevicePixel); const int rows = 9; const int cols = 9; const qreal cellW = 45; const qreal cellH = 25; const qreal headerH = 30; qreal startX = pageRect.left() + 10; qreal startY = pageRect.top() + 20; qreal y = startY; painter->setFont(QFont("Microsoft YaHei", 9)); // 表头 for (int j = 1; j <= cols; ++j) { QRectF headerCell(startX + (j - 1) * cellW, y, cellW, headerH); painter->drawRect(headerCell); painter->drawText(headerCell, Qt::AlignCenter, QString("第%1列").arg(j)); } y += headerH; // 九九乘法表内容 for (int i = 1; i <= rows; ++i) { if (y + cellH > pageRect.bottom() - 10) { printer->newPage(); y = pageRect.top() + 20; } for (int j = 1; j <= cols; ++j) { QRectF cell(startX + (j - 1) * cellW, y, cellW, cellH); painter->drawRect(cell); QString text = QString("%1×%2=%3").arg(j).arg(i).arg(i * j); painter->drawText(cell, Qt::AlignCenter, text); } y += cellH; } }

需要注意,printer->pageRect()返回的是整页中可打印区域的尺寸,不同品牌打印机给出的上下左右边距不一样。你要是用固定的A4像素尺寸去排版,在佳能打印机上正常的页面,换到惠普就可能右边裁掉一截。用pageRect()动态计算才是稳妥的。

3.3 接入QPrintPreviewDialog,让用户先看再打

预览对话框接起来很快,但是有一个使用习惯上的坑:预览对话框内部的事件循环会阻塞你的代码,用户点“打印”按钮后,系统会直接用同一个QPrinter走系统打印流程,不再触发paintRequested

也就是说,你不用再手动弹QPrintDialog,预览界面右上角自带的“打印”按钮已经把这条链路打通了。

void MainWindow::onPreviewButtonClicked() { QPrinter printer(QPrinter::HighResolution); QPrintPreviewDialog preview(&printer, this); connect(&preview, &QPrintPreviewWidget::paintRequested, this, [&printer, this]() { renderReport(&printer); }); preview.exec(); }

如果你还需要用户手动选择打印机、份数、双面等,可以在预览窗口打开前先弹一次标准打印设置对话框:

QPrintDialog printDialog(&printer, this); if (printDialog.exec() != QDialog::Accepted) { return; }

4. 进阶:用QCustomPlot把时域信号变频域图再打印

信号分析是Qt里很常见的场景。我们在界面上用QCustomPlot显示时域波形,再用kissfft快速傅里叶变换转成频域图。图表刷在屏幕上没问题,但把它塞进打印报告,就需要点特殊手法。

4.1 频域转换数据的准备与显示

先说数据准备,不展开完整FFT代码,核心流程是:

  1. 采集一段时域信号,长度N取1024或2048;
  2. 用kissfft库做快速傅里叶变换,得到频域复数结果;
  3. 对每个频点计算幅值sqrt(re*re + im*im),取一半频谱绘制;
  4. 横轴换算成实际频率(需要知道采样率),纵轴用分贝值更直观。

QCustomPlot里大致这样设置:

QVector<double> freqX, magY; computeFFT(timeDomain, sampleRate, freqX, magY); ui->plot->clearGraphs(); ui->plot->addGraph(); ui->plot->graph(0)->setData(freqX, magY); ui->plot->xAxis->setLabel("频率 (Hz)"); ui->plot->yAxis->setLabel("幅值 (dB)"); ui->plot->rescaleAxes(); ui->plot->replot();

4.2 两种把QCustomPlot图形送到打印机的方法

第一种,直接把图绘制到打印机Painter上。QCustomPlot提供了toPainter接口,绘图数据不变,只是把目标设备从屏幕控件换成QPrinter

void MainWindow::drawPlotBlock(QPainter *painter, QPrinter *printer) { QRectF pageRect = printer->pageRect(QPrinter::DevicePixel); double plotW = pageRect.width() - 40; double plotH = 220; ui->plot->toPainter(painter, plotW, plotH); }

这种做法的好处是矢量输出,打印出来的曲线边缘锐利,放大看也不毛糙。

第二种,先把图渲染成高分辨率图片,再用drawPixmap贴到打印页面上。适合你不想让QCustomPlot依赖出现在打印模块里的情况。

QPixmap pix = ui->plot->toPixmap(1600, 400); painter->drawPixmap(pageRect.left() + 20, pageRect.top() + 20, pix);

这里有一点要提醒:QCustomPlottoPixmap默认按屏幕像素密度生成,在4K屏幕上看着是清楚的,但打印机的物理分辨率远高于屏幕。要想打印出来不糊,至少按打印区域尺寸的2~3倍生成位图。

4.3 打印清晰度为什么要单独调分辨率

QPrinter的构造参数决定了逻辑分辨率:

  • QPrinter::ScreenResolution:约96dpi,屏幕预览流畅,打印出来细节不够;
  • QPrinter::HighResolution:通常是1200dpi或更高,适合打印带文字的报表;
  • 自己setResolution(300):适合图文混排,文字和图形都有不错的效果。

实际项目中,我一般这么选:纯文字合同选高分辨率模式;带大量图片的富文本选300dpi;纯图表输出选150dpi就够。分辨率高了,绘制数据量大,预览时刷帧变慢,没必要无脑拉满。

5. 常见问题与排查技巧实录

这部分是我实际踩过坑后总结的,几乎每个问题都曾经让同事卡过半天以上。

5.1 打印相关典型问题速查表

现象可能原因处理方法
预览正常,打印偏位打印代码里用了固定像素值改用pageRect()动态计算坐标
中文变成方块/乱码系统缺少对应中文字体在绘制前设置QFont("Microsoft YaHei")等已安装字体
HTML打印多出空白页QTextDocument页面尺寸未设置调用setPageSize(pageRect.size())
打印出来文字发虚没设分辨率或分辨率过低printer.setResolution(300)或更高
QCustomPlot打印只有上半截页面高度超出可打印区域,未分页对绘图区域做分页处理
程序打包到别的电脑打印按钮灰掉缺少Qt打印支持插件部署时拷贝printsupport插件
输出PDF后过大会卡顿图元数量过多转为位图后压缩打印,或降低曲线点密度

5.2 分页错位与内容被裁切的排查思路

如果你见过“表格第三行被切掉一半”这种事故,大概率是两次计算可打印区域用了不同的单位。

QPrinter::pageRect()有两种取法:

  • pageRect()(Qt 5.15之前常用),返回逻辑单位下的页面矩形,单位和当前分辨率挂钩;
  • pageRect(QPrinter::DevicePixel),返回设备像素,不受逻辑DPI影响。

我自己习惯在渲染入口统一转成设备像素,后面所有坐标都按设备像素算,省得在不同分辨率、不同驱动之间来回换算。

另外,考虑一下打印机驱动里的页边距设置。如果你在程序里又设置了一圈自定义setPageMargins,而驱动本身有最小可打印边距,程序设置的值小于驱动最小值时,系统会悄悄帮你改写边距,表现就是“预览和实际打印不对称”。

5.3 中文字体与HTML打印的细节坑

QTextDocument加载HTML后,默认字体会跟随QApplication::font()。如果你的程序没设置中文字体,在纯英文Windows系统上容易退化成宋体,部分加粗效果会失效。最稳的办法是在HTML里直接写好字体栈:

<body style="font-family:'Microsoft YaHei','PingFang SC','Noto Sans CJK SC',sans-serif;">

另外,HTML里引用的本地图片路径,在预览时能显示,但打成PDF后可能在别的电脑上找不到。原因是图片没有嵌入到文档里。如果要输出PDF再分发,尽量把图片转成base64内嵌,或者直接用QTextDocumentaddResource接口注册图片资源。

6. 部署打包时别把打印插件漏了

很多人在自己电脑上开发得好好的,一发布到客户机器,要么找不到打印机,要么点打印直接崩溃。十有八九是打包插件没带全。

6.1 Windows下windeployqt之后的目录检查

windeployqt打包Qt程序后,程序目录下会多出platformsimageformats等一系列依赖目录。打印功能依赖的是printsupport目录,里面必须至少要有:

  • windowsprintersupport.dll

如果这个目录和dll不存在,程序在运行时会静默失去打印能力,QPrinter::isValid()可能返回false,预览对话框里枚举不到任何打印机,但程序本身不崩溃,排查起来非常隐蔽。

建议在发布包自检逻辑里加一句:

if (!printer.isValid()) { QMessageBox::warning(this, "提示", "当前系统未检测到可用打印机,或打印模块部署不完整。"); }

6.2 Linux环境下系统打印服务的依赖

Linux下Qt打印底层依赖CUPS(Common UNIX Printing System)。如果目标机器上CUPS服务没启动,或者安装Qt时没有安装libqt5printsupport5,打印功能就只是个空壳。

这时候你可以在程序里输出PDF,因为Qt内置了PDF打印支持,不依赖任何打印机驱动;但真要找准一个具体打印机打印,CUPS服务必须正常。

printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName("report.pdf");

在Linux无头服务器上,这个技巧特别实用,能绕过一堆图形界面和驱动问题,先把文件生成出来给用户下载。

7. 我在项目里喜欢保留的几个习惯

最后分享几个我自己长期用着的经验,不一定都在教科书里。

第一,打印代码一定放在独立模块里,不要在界面类里写一大坨绘制逻辑。因为打印需求一定会经历“先能打出来,再调样式,再适配不同纸张”的过程,独立模块让你改样式时不至于把界面按钮刷新也带崩。

第二,第一次接入预览,先别急着调样式,先验证“预览和打印是否走同一套渲染函数”。如果发现打印结果和预览不一样,就说明有某个环节用了不同代码分支,尽早发现这个,后面所有样式调整才有意义。

第三,打印输出加一个“导出PDF”的隐藏入口。很多客户嘴上说打印,实际要的是“能不能给我个电子版”。我在打印预览界面里放了一个导出PDF按钮,直接复用renderReport,用QPrinter::PdfFormat生成文件。这个功能投入极小,但客户满意度提升不是一点半点。

第四,表格类打印把单元格坐标统一用“毫米”换算,不要直接填像素。我一般会先获取当前打印机的物理分辨率,再转换成毫米刻度,这样换打印机型号时表格比例不会崩掉。

上面这套方案,基本能覆盖日常业务系统里九成以上的打印和预览需求。如果你想在这个例子上继续扩展,可以试试把QTextDocument换成更复杂的报表模板引擎,或者把QPrintPreviewDialog换成自己写的左侧缩略图、右侧页面的预览控件,工程量可控,核心思路不变。

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

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

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

立即咨询