从平台抽象层到PostScript生成,从PDF导出到高DPI打印------揭秘Qt如何在Windows GDI、macOS CUPS、Linux Print Dialog之间构建统一的打印基础设施
前言
在Qt应用开发中,打印功能往往被低估。大多数开发者只知道QPrinter配合QPrintDialog就能"打印",但当你需要在Windows GDI、macOS CUPS、Linux三种完全不同的打印子系统上实现一致的打印输出时,当你需要在300 DPI的激光打印机上精确渲染复杂矢量图形时,当你需要生成符合PDF/A标准的归档文档时------你才会发现Qt Print Support框架的精妙之处。
本文将深入Qt Print Support框架的内核设计,从平台抽象层到PostScript生成,从PDF导出到高DPI打印,为你揭示Qt如何在异构打印子系统之上构建统一输出管道的完整架构。
一、Print Support框架架构概览
1.1 核心类层次结构
Qt Print Support框架采用了分层架构设计,核心类层次如下:
QPrinter (核心设备抽象)
├── QPrinterInfo (打印机信息管理)
├── QPrintDialog (打印对话框)
├── QPageSetupDialog (页面设置对话框)
├── QPrintPreviewDialog (打印预览对话框)
└── QPrintPreviewWidget (打印预览组件)
平台抽象层 (QPlatformPrinterSupport)
├── Windows (QWin32PrintEngine)
├── macOS (QCocoaPrinterSupport)
└── Linux (QCupsPrinterSupport)
源码路径:src/printsupport/kernel/
1.2 打印管道的三层架构
Qt的打印系统采用三层架构:
- 应用层 :
QPrinter+QPaintDevice接口 - 引擎层 :
QPaintEngine的打印专用实现 - 平台层:操作系统原生打印API封装
cpp
// 核心抽象:QPrinter继承自QPaintDevice
class Q_PRINTSUPPORT_EXPORT QPrinter : public QPaintDevice
{
Q_DECLARE_PRIVATE(QPrinter)
public:
enum PrinterMode {
ScreenResolution,
PrinterResolution,
HighResolution
};
explicit QPrinter(PrinterMode mode = ScreenResolution);
~QPrinter();
// 核心输出接口
void setOutputFormat(OutputFormat format);
OutputFormat outputFormat() const;
// 打印引擎获取
QPaintEngine *paintEngine() const override;
protected:
QPrinter(QPrinterPrivate *ptr);
};
二、QPrinter核心实现原理
2.1 QPrinter的构造与初始化
QPrinter的构造函数接受PrinterMode参数,这个参数决定了打印输出的分辨率策略:
cpp
QPrinter::QPrinter(PrinterMode mode)
: QPaintDevice()
{
d_ptr = new QPrinterPrivate(this);
// 根据模式设置分辨率
switch (mode) {
case ScreenResolution:
d_ptr->resolution = qt_defaultDpi();
break;
case PrinterResolution:
d_ptr->resolution = 72; // PostScript标准DPI
break;
case HighResolution:
d_ptr->resolution = 300; // 默认高精度
break;
}
// 创建平台相关的打印引擎
d_ptr->initEngine();
}
源码路径:src/printsupport/kernel/qprinter.cpp
2.2 打印引擎的创建与选择
QPrinterPrivate::initEngine()根据输出格式选择合适的打印引擎:
cpp
void QPrinterPrivate::initEngine()
{
delete engine;
engine = nullptr;
if (outputFormat == QPrinter::PdfFormat) {
// PDF引擎:跨平台矢量输出
engine = new QPdfPrintEngine(this);
} else {
// 原生打印引擎:平台相关
QPlatformPrinterSupport *ps = QPlatformPrinterSupportPlugin::get();
if (ps) {
engine = ps->createPaintEngine(printerMode);
}
}
if (engine) {
// 设置引擎的分辨率
engine->setProperty(QPrintEngine::PPK_Resolution, resolution);
}
}
2.3 PDF打印引擎深度解析
QPdfPrintEngine是Qt最重要的打印引擎之一,它不依赖操作系统打印API,直接生成PDF文件:
cpp
// src/printsupport/pdf/qpdfprintengine.cpp
bool QPdfPrintEngine::begin(QPaintDevice *pdev)
{
// 1. 初始化PDF文档结构
pdfEngine = new QPdfEngine();
pdfEngine->setOutputFileName(outputFileName);
// 2. 设置页面尺寸
QSizeF pageSize = pageLayout.pageSize().size(QPageSize::Point);
pdfEngine->setPageSize(pageSize);
// 3. 设置元数据
pdfEngine->setCreator(QStringLiteral("Qt %1").arg(QT_VERSION_STR));
pdfEngine->setTitle(docName);
// 4. 开始PDF生成
return pdfEngine->begin(pdev);
}
PDF引擎的关键特性:
- 矢量图形保留 :所有
QPainter绘图命令都转换为PDF矢量指令 - 字体嵌入:确保跨平台字体一致性
- 图像压缩:自动选择合适的JPEG/Flate压缩
- 色彩管理:支持CMYK色彩空间(需要平台支持)
三、平台打印引擎实现
3.1 Windows GDI打印引擎
Windows平台使用GDI(Graphics Device Interface)进行打印:
cpp
// src/plugins/printsupport/windows/qwin32printengine.cpp
class QWin32PrintEngine : public QPaintEngine
{
public:
bool begin(QPaintDevice *pdev) override;
bool end() override;
void drawPixmap(const QRectF &r, const QPixmap &pm, const QRectF &sr) override;
void drawPath(const QPainterPath &path) override;
private:
HDC m_hdc; // Windows设备上下文
DOCINFO m_docInfo;
};
Windows打印流程:
cpp
bool QWin32PrintEngine::begin(QPaintDevice *pdev)
{
// 1. 打开打印机
OpenPrinterW(printerName, &hPrinter, nullptr);
// 2. 获取设备模式
DEVMODEW *devMode = getDevmode();
// 3. 创建设备上下文
m_hdc = CreateDCW(L"WINSPOOL", printerName, nullptr, devMode);
// 4. 开始打印文档
memset(&m_docInfo, 0, sizeof(DOCINFO));
m_docInfo.cbSize = sizeof(DOCINFO);
m_docInfo.lpszDocName = docName.utf16();
StartDocW(m_hdc, &m_docInfo);
// 5. 开始第一页
StartPage(m_hdc);
return true;
}
3.2 macOS CUPS打印引擎
macOS使用CUPS(Common Unix Printing System)和Apple的打印框架:
objc
// src/plugins/printsupport/cocoa/qcocoaprinterengine.mm
@implementation QCocoaPrintEngine
- (BOOL)beginWithPaintDevice:(QPaintDevice *)pdev
{
// 1. 获取Printing Dialog
NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
// 2. 设置页面格式
NSPageLayout *pageLayout = [NSPageLayout pageLayout];
[pageLayout beginSheetWithPrintInfo:printInfo
modalForWindow:qt_mac_window_for_view(qt_view)
delegate:nil
didEndSelector:nil
contextInfo:nil];
// 3. 创建打印操作
NSPrintOperation *op = [NSPrintOperation
printOperationWithView:qtPrintView
printInfo:printInfo];
// 4. 运行打印对话框
[op runOperationModalForWindow:qtWindow
delegate:nil
didRunSelector:nil
contextInfo:nil];
return YES;
}
@end
3.3 Linux CUPS打印引擎
Linux通过CUPS的IPP(Internet Printing Protocol)进行打印:
cpp
// src/plugins/printsupport/cups/qcupsprinterengine.cpp
bool QCupsPrintEngine::begin(QPaintDevice *pdev)
{
// 1. 连接到CUPS服务器
cups_dest_t *dests;
int numDests = cupsGetDests(&dests);
// 2. 查找目标打印机
cups_dest_t *dest = cupsGetDest(printerName, nullptr, numDests, dests);
// 3. 创建打印任务
int jobId = cupsPrintFile(dest->name,
tempFilePath, // PostScript或PDF临时文件
docName,
0, nullptr);
// 4. 监控打印任务状态
cups_job_t *jobs;
int numJobs = cupsGetJobs(&jobs, dest->name, 1, -1);
return jobId > 0;
}
四、高级打印技术与实战
4.1 高DPI打印支持
现代打印机支持600 DPI甚至1200 DPI输出,Qt提供了完整的高DPI打印支持:
cpp
class HighDpiPrintWidget : public QWidget
{
protected:
void paintEvent(QPaintEvent *event) override
{
QPainter painter(this);
// 1. 设置视口和窗口的映射关系
painter.setViewport(0, 0, width(), height());
painter.setWindow(0, 0,
logicalDpiX() * width() / 72,
logicalDpiY() * height() / 72);
// 2. 使用打印机实际DPI进行绘制
QPrinter *printer = dynamic_cast<QPrinter*>(painter.device());
if (printer) {
int dpiX = printer->resolution();
int dpiY = printer->resolution();
// 3. 根据DPI缩放字体和图形
QFont font = painter.font();
font.setPointSizeF(font.pointSizeF() * dpiX / 72.0);
painter.setFont(font);
// 4. 绘制高分辨率图形
drawHighDpiContent(painter, dpiX, dpiY);
}
}
void drawHighDpiContent(QPainter &painter, int dpiX, int dpiY)
{
// 矢量图形:自动适应高DPI
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
// 绘制1像素宽的线在高DPI下仍然清晰
QPen pen(Qt::black);
pen.setWidthF(1.0 / dpiX * 72.0); // 转换为逻辑坐标
painter.setPen(pen);
painter.drawLine(QLineF(0, 0, 100, 100));
}
};
4.2 多页打印与分页控制
实现复杂的多页打印需要精确的分页控制:
cpp
class MultiPageReport : public QObject
{
Q_OBJECT
public:
void printReport(QPrinter *printer)
{
QPainter painter(printer);
// 1. 计算可打印区域
QRectF printableArea = printer->pageRect(QPrinter::Point);
qreal margin = 36.0; // 0.5英寸边距
QRectF contentRect = printableArea.adjusted(margin, margin,
-margin, -margin);
// 2. 准备打印内容
QList<ReportPage> pages = preparePages();
// 3. 逐页打印
for (int i = 0; i < pages.size(); ++i) {
if (i > 0) {
// 换页
if (!printer->newPage()) {
qWarning() << "Failed to create new page";
break;
}
painter.begin(printer); // 重新初始化painter
}
// 4. 绘制当前页
drawPage(painter, pages[i], contentRect);
// 5. 绘制页眉页脚
drawHeaderFooter(painter, printableArea, i + 1, pages.size());
}
}
void drawPage(QPainter &painter, const ReportPage &page,
const QRectF &contentRect)
{
// 使用QTextDocument渲染富文本
QTextDocument document;
document.setPageSize(contentRect.size());
document.setDocumentMargin(0);
document.setHtml(page.htmlContent());
// 绘制到打印机
document.drawContents(&painter, contentRect.toRect());
}
void drawHeaderFooter(QPainter &painter, const QRectF &pageRect,
int currentPage, int totalPages)
{
QFont oldFont = painter.font();
QFont headerFont = oldFont;
headerFont.setPointSize(8);
painter.setFont(headerFont);
// 页眉
painter.drawText(pageRect.left(), pageRect.top() - 10,
"Qt Print Support Report");
// 页脚(页码)
QString pageStr = QString("Page %1 of %2")
.arg(currentPage).arg(totalPages);
painter.drawText(pageRect.right() - 100, pageRect.bottom() + 20,
pageStr);
painter.setFont(oldFont);
}
};
4.3 打印预览实现原理
QPrintPreviewWidget通过虚拟打印到PDF实现预览:
cpp
// src/printsupport/widgets/qprintpreviewwidget.cpp
void QPrintPreviewWidgetPrivate::generatePreview()
{
// 1. 创建临时PDF引擎
QPdfPrintEngine *previewEngine = new QPdfPrintEngine();
previewEngine->setOutputFormat(QPrinter::PdfFormat);
previewEngine->setOutputFileName(tempPdfPath);
// 2. 渲染所有页面到PDF
QPainter painter;
for (int i = 0; i < totalPages; ++i) {
if (i == 0) {
painter.begin(previewEngine);
} else {
previewEngine->newPage();
}
// 调用用户的paint函数
emit q->paintRequested(previewEngine);
}
painter.end();
// 3. 将PDF转换为图像用于显示
Poppler::Document *pdfDoc = Poppler::Document::load(tempPdfPath);
for (int i = 0; i < pdfDoc->numPages(); ++i) {
QImage pageImage = pdfDoc->page(i)->renderToImage(96, 96);
previewPages.append(pageImage);
}
// 4. 在QGraphicsView中显示
updatePreviewScene();
}
五、性能优化与最佳实践
5.1 图像打印优化
打印高分辨率图像时的内存和性能优化:
cpp
void optimizeImagePrinting(QPrinter *printer, const QImage &image)
{
QPainter painter(printer);
// 1. 检查图像尺寸与打印机DPI的匹配度
int printerDpi = printer->resolution();
QSize imageSize = image.size();
// 2. 如果图像分辨率低于打印机DPI,进行高质量缩放
if (imageSize.width() < printer->width()) {
QImage scaledImage = image.scaled(printer->width(),
printer->height(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
// 3. 使用QImageWriter进行优化的图像输出
QByteArray imageData;
QBuffer buffer(&imageData);
buffer.open(QIODevice::WriteOnly);
QImageWriter writer(&buffer, "JPEG");
writer.setQuality(95); // 高质量JPEG压缩
writer.write(scaledImage);
// 4. 直接绘制优化后的图像数据
QImage optimizedImage = QImage::fromData(imageData);
painter.drawImage(0, 0, optimizedImage);
} else {
// 图像分辨率足够,直接绘制
painter.drawImage(0, 0, image);
}
}
5.2 大型表格打印优化
打印包含成千上万行数据的表格时的优化策略:
cpp
class LargeTablePrinter : public QObject
{
Q_OBJECT
public:
void printLargeTable(QPrinter *printer, const QSqlQuery &query)
{
QPainter painter(printer);
painter.setRenderHint(QPainter::Antialiasing);
// 1. 计算每页可打印的行数
QFontMetrics fm = painter.fontMetrics();
int lineHeight = fm.height() + 4;
int availableHeight = printer->pageRect().height();
int rowsPerPage = availableHeight / lineHeight - 5; // 减去页眉页脚
// 2. 分批获取数据并打印
int currentRow = 0;
int currentPage = 0;
while (query.next()) {
// 3. 检查是否需要换页
if (currentRow % rowsPerPage == 0 && currentRow > 0) {
printer->newPage();
currentPage++;
drawTableHeader(painter, printer);
}
// 4. 绘制当前行
drawTableRow(painter, query, currentRow % rowsPerPage);
currentRow++;
}
qDebug() << "Printed" << currentRow << "rows on"
<< currentPage + 1 << "pages";
}
void drawTableRow(QPainter &painter, const QSqlQuery &query, int row)
{
int y = headerHeight + row * lineHeight;
// 使用QTextLayout进行高效的文本布局
QTextLayout textLayout;
textLayout.setFont(painter.font());
// 只绘制可见列
for (int col = 0; col < visibleColumns; ++col) {
QString text = query.value(col).toString();
int x = columnPositions[col];
// 文本截断优化
QRect textRect(x, y, columnWidths[col], lineHeight);
QString elidedText = painter.fontMetrics().elidedText(
text, Qt::ElideRight, columnWidths[col]);
painter.drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter,
elidedText);
}
}
};
5.3 异步打印与进度反馈
对于耗时的打印任务,应该提供异步接口和进度反馈:
cpp
class AsyncPrinter : public QObject
{
Q_OBJECT
signals:
void printProgress(int percentage);
void printFinished(bool success);
public slots:
void printAsync(QPrinter *printer, const ReportData &data)
{
// 使用Qt Concurrent进行异步打印
QtConcurrent::run([this, printer, data]() {
QTime startTime = QTime::currentTime();
// 1. 准备打印
QPainter painter(printer);
int totalPages = calculateTotalPages(data);
// 2. 逐页打印并报告进度
for (int i = 0; i < totalPages; ++i) {
if (i > 0) {
printer->newPage();
}
// 绘制页面
drawPage(painter, data, i);
// 计算并发送进度
int progress = (i + 1) * 100 / totalPages;
emit printProgress(progress);
// 检查是否取消
if (m_cancelRequested) {
emit printFinished(false);
return;
}
}
painter.end();
QTime endTime = QTime::currentTime();
qDebug() << "Print completed in"
<< startTime.msecsTo(endTime) << "ms";
emit printFinished(true);
});
}
void cancelPrint()
{
m_cancelRequested = true;
}
private:
bool m_cancelRequested = false;
};
六、实战案例:发票打印系统
6.1 发票模板设计
cpp
class InvoicePrinter : public QObject
{
Q_OBJECT
public:
bool printInvoice(QPrinter *printer, const Invoice &invoice)
{
QPainter painter(printer);
painter.setRenderHint(QPainter::Antialiasing);
painter.setRenderHint(QPainter::TextAntialiasing);
// 1. 设置发票专用字体
QFont invoiceFont("SimSun", 10); // 宋体,适合发票打印
invoiceFont.setStyleStrategy(QFont::PreferDevice);
painter.setFont(invoiceFont);
// 2. 绘制发票头部
drawInvoiceHeader(painter, invoice);
// 3. 绘制发票主体(表格)
drawInvoiceTable(painter, invoice);
// 4. 绘制发票底部(合计、签名等)
drawInvoiceFooter(painter, invoice);
// 5. 绘制二维码/条形码
drawInvoiceBarcode(painter, invoice);
return true;
}
void drawInvoiceHeader(QPainter &painter, const Invoice &invoice)
{
// 发票标题
QFont titleFont = painter.font();
titleFont.setPointSize(16);
titleFont.setBold(true);
painter.setFont(titleFont);
painter.drawText(100, 80, "增值税专用发票");
// 发票代码和号码
QFont codeFont = painter.font();
codeFont.setPointSize(9);
painter.setFont(codeFont);
painter.drawText(500, 80,
QString("发票代码:%1").arg(invoice.code()));
painter.drawText(500, 100,
QString("发票号码:%1").arg(invoice.number()));
// 开票日期
painter.drawText(100, 120,
QString("开票日期:%1").arg(invoice.date().toString("yyyy年MM月dd日")));
}
void drawInvoiceTable(QPainter &painter, const Invoice &invoice)
{
// 表格位置和尺寸
int startX = 50;
int startY = 150;
int rowHeight = 30;
// 绘制表格线
painter.setPen(QPen(Qt::black, 1));
// 表头
QStringList headers = {"货物名称", "规格型号", "单位", "数量", "单价", "金额", "税率", "税额"};
QList<int> columnWidths = {150, 80, 60, 60, 80, 100, 60, 80};
int currentX = startX;
for (int i = 0; i < headers.size(); ++i) {
QRect headerRect(currentX, startY, columnWidths[i], rowHeight);
painter.drawRect(headerRect);
painter.drawText(headerRect, Qt::AlignCenter, headers[i]);
currentX += columnWidths[i];
}
// 数据行
int currentY = startY + rowHeight;
for (const InvoiceItem &item : invoice.items()) {
currentX = startX;
QStringList rowData = {
item.name(),
item.specification(),
item.unit(),
QString::number(item.quantity()),
QString::number(item.unitPrice(), 'f', 2),
QString::number(item.amount(), 'f', 2),
QString("%1%").arg(item.taxRate() * 100),
QString::number(item.taxAmount(), 'f', 2)
};
for (int i = 0; i < rowData.size(); ++i) {
QRect cellRect(currentX, currentY, columnWidths[i], rowHeight);
painter.drawRect(cellRect);
painter.drawText(cellRect, Qt::AlignCenter, rowData[i]);
currentX += columnWidths[i];
}
currentY += rowHeight;
// 检查是否需要换页
if (currentY > painter.device()->height() - 100) {
QPrinter *printer = dynamic_cast<QPrinter*>(painter.device());
if (printer) {
printer->newPage();
currentY = 100;
}
}
}
}
};
七、常见问题与解决方案
7.1 打印模糊问题
问题:打印出来的文字或图形模糊不清。
原因:
- 使用了
ScreenResolution模式,导致DPI不匹配 - 图像分辨率低于打印机DPI
- 没有启用抗锯齿
解决方案:
cpp
void fixBlurryPrinting(QPrinter *printer)
{
// 1. 使用高分辨率模式
printer->setResolution(QPrinter::HighResolution);
QPainter painter(printer);
// 2. 启用抗锯齿
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// 3. 使用打印机实际DPI进行字体设置
int dpi = printer->resolution();
QFont font = painter.font();
font.setPointSizeF(font.pointSizeF() * dpi / 72.0);
painter.setFont(font);
// 4. 图像打印前进行高质量缩放
QImage image("input.png");
if (image.width() < printer->width()) {
image = image.scaled(printer->width(), printer->height(),
Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
painter.drawImage(0, 0, image);
}
7.2 打印速度慢问题
问题:打印复杂文档时速度非常慢。
原因:
- 每一页都重新查询数据库
- 没有使用双缓冲
- 图像没有进行压缩优化
解决方案:
cpp
void optimizePrintSpeed(QPrinter *printer)
{
// 1. 预加载所有数据
QList<PageData> allPages = preloadAllData();
// 2. 使用双缓冲
QPixmap buffer(printer->width(), printer->height());
QPainter painter;
for (int i = 0; i < allPages.size(); ++i) {
buffer.fill(Qt::white);
QPainter bufferPainter(&buffer);
drawPage(bufferPainter, allPages[i]);
bufferPainter.end();
if (i == 0) {
painter.begin(printer);
} else {
printer->newPage();
}
painter.drawPixmap(0, 0, buffer);
}
painter.end();
// 3. 图像压缩
QImage image = getLargeImage();
QByteArray compressedData;
QBuffer buffer2(&compressedData);
buffer2.open(QIODevice::WriteOnly);
image.save(&buffer2, "JPEG", 85); // 85%质量,减小文件大小
}
八、总结
Qt Print Support框架通过精巧的分层架构,在异构操作系统打印子系统之上构建了统一的打印API。其核心设计理念包括:
- 平台抽象 :通过
QPlatformPrinterSupport实现跨平台打印 - 引擎架构 :
QPaintEngine的打印专用实现(PDF/GDI/CUPS) - 设备无关 :
QPaintDevice接口让打印和绘图使用同一套API - 高DPI支持:完整的分辨率管理和缩放策略
在实际开发中,应该根据需求选择合适的打印模式(PDF输出 vs 原生打印),并注意性能优化(异步打印、图像压缩、分批处理)。
随着无纸化办公的普及,PDF输出(Qt PDF模块)正逐渐成为主流,但掌握原生打印API仍然是Qt开发者的必备技能。
注:若有发现问题欢迎大家提出来纠正