Qt打印与预览实战:从文本、表格到图表的完整指南
简介面向正在学习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和QWidget、QPixmap的结构很接近。也就是说你平时在一个窗口上画图用的那套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代码核心流程是采集一段时域信号长度N取1024或2048用kissfft库做快速傅里叶变换得到频域复数结果对每个频点计算幅值sqrt(re*re im*im)取一半频谱绘制横轴换算成实际频率需要知道采样率纵轴用分贝值更直观。QCustomPlot里大致这样设置QVectordouble 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);这里有一点要提醒QCustomPlot的toPixmap默认按屏幕像素密度生成在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 stylefont-family:Microsoft YaHei,PingFang SC,Noto Sans CJK SC,sans-serif;另外HTML里引用的本地图片路径在预览时能显示但打成PDF后可能在别的电脑上找不到。原因是图片没有嵌入到文档里。如果要输出PDF再分发尽量把图片转成base64内嵌或者直接用QTextDocument的addResource接口注册图片资源。6. 部署打包时别把打印插件漏了很多人在自己电脑上开发得好好的一发布到客户机器要么找不到打印机要么点打印直接崩溃。十有八九是打包插件没带全。6.1 Windows下windeployqt之后的目录检查用windeployqt打包Qt程序后程序目录下会多出platforms、imageformats等一系列依赖目录。打印功能依赖的是printsupport目录里面必须至少要有windowsprintersupport.dll如果这个目录和dll不存在程序在运行时会静默失去打印能力QPrinter::isValid()可能返回false预览对话框里枚举不到任何打印机但程序本身不崩溃排查起来非常隐蔽。建议在发布包自检逻辑里加一句if (!printer.isValid()) { QMessageBox::warning(this, 提示, 当前系统未检测到可用打印机或打印模块部署不完整。); }6.2 Linux环境下系统打印服务的依赖Linux下Qt打印底层依赖CUPSCommon 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换成自己写的左侧缩略图、右侧页面的预览控件工程量可控核心思路不变。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →