最近开发的一个项目要求通过软件生成pdf报告,并要求为每页添加页眉页脚及水印。 pdf内容可以使用QTextDocument,QTextDocument支持文档结构,同时支持富文本。使用文档结构和富文本完全可以满足段落文本、图片、表格排版。最后通过QTextDocument转为pdf。
实现的效果:
一、遇到的问题
QPdfWriter本身是不支持设置页眉页脚和水印的,需要通过QPainter绘制。网上提供的方法大都存在下述问题:
绘制页眉页脚、水印,无法自动换页,需要通过
n
e
w
P
a
g
e
(
)
,然后继续绘制
\color{#FF0000}{绘制页眉页脚、水印,无法自动换页,需要通过newPage(),然后继续绘制}
绘制页眉页脚、水印,无法自动换页,需要通过newPage(),然后继续绘制
二、解决思路
QPdfWriter pdfWriter(fileName);
QTextDocument textDocument;
// 通过QTextDocument对pdf文档进行排版
textDocument.print(&pdfWriter); // 将textDocument内容输出到pdfWriter,生成pdf完成
QTextDocument 通过pint方法将内容输出到QPdfWriter,那么pint方法内部是如何将内容绘制到QPdfWriter的呢?接下来我们通过源码看看内部是如何实现的。
void QTextDocument::print(QPagedPaintDevice *printer) const
{
Q_D(const QTextDocument);
if (!printer)
return;
bool documentPaginated = d->pageSize.isValid() && !d->pageSize.isNull()
&& d->pageSize.height() != INT_MAX;
QPagedPaintDevicePrivate *pd = QPagedPaintDevicePrivate::get(printer);
// ### set page size to paginated size?
QMarginsF m = printer->pageLayout().margins(QPageLayout::Millimeter);
if (!documentPaginated && m.left() == 0 && m.right() == 0 && m.top() == 0 && m.bottom() == 0) {
m.setLeft(2.);
m.setRight(2.);
m.setTop(2.);
m.setBottom(2.);
printer->setPageMargins(m, QPageLayout::Millimeter); // 设置页面边距
}
// ### use the margins correctly
QPainter p(printer); // 通过QPdfWriter构造QPainter
// Check that there is a valid device to print to.
if (!p.isActive())
return;
const QTextDocument *doc = this;
QScopedPointer<QTextDocument> clonedDoc;
(void)doc->documentLayout(); // make sure that there is a layout
QRectF body = QRectF(QPointF(0, 0), d->pageSize);
QPointF pageNumberPos;
// 获取DPI,DPI是什么具体我也不清楚
qreal sourceDpiX = qt_defaultDpiX();
qreal sourceDpiY = qt_defaultDpiY();
const qreal dpiScaleX = qreal(printer->logicalDpiX()) / sourceDpiX;
const qreal dpiScaleY = qreal(printer->logicalDpiY()) / sourceDpiY;
// 通过断点调试documentPaginated的值为false,我们直接看else的逻辑
if (documentPaginated) {
QPaintDevice *dev = doc->documentLayout()->paintDevice();
if (dev) {
sourceDpiX = dev->logicalDpiX();
sourceDpiY = dev->logicalDpiY();
}
// scale to dpi
p.scale(dpiScaleX, dpiScaleY);
QSizeF scaledPageSize = d->pageSize;
scaledPageSize.rwidth() *= dpiScaleX;
scaledPageSize.rheight() *= dpiScaleY;
const QSizeF printerPageSize(printer->width(), printer->height());
// scale to page
p.scale(printerPageSize.width() / scaledPageSize.width(),
printerPageSize.height() / scaledPageSize.height());
} else {
doc = clone(const_cast<QTextDocument *>(this));
clonedDoc.reset(const_cast<QTextDocument *>(doc));
for (QTextBlock srcBlock = firstBlock(), dstBlock = clonedDoc->firstBlock();
srcBlock.isValid() && dstBlock.isValid();
srcBlock = srcBlock.next(), dstBlock = dstBlock.next()) {
dstBlock.layout()->setFormats(srcBlock.layout()->formats());
}
QAbstractTextDocumentLayout *layout = doc->documentLayout();
layout->setPaintDevice(p.device());
// copy the custom object handlers
layout->d_func()->handlers = documentLayout()->d_func()->handlers;
// 2 cm margins, scaled to device in QTextDocumentLayoutPrivate::layoutFrame
const int horizontalMargin = int((2/2.54)*sourceDpiX);
const int verticalMargin = int((2/2.54)*sourceDpiY);
QTextFrameFormat fmt = doc->rootFrame()->frameFormat();
fmt.setLeftMargin(horizontalMargin);
fmt.setRightMargin(horizontalMargin);
fmt.setTopMargin(verticalMargin);
fmt.setBottomMargin(verticalMargin);
doc->rootFrame()->setFrameFormat(fmt);
// pageNumberPos must be in device coordinates, so scale to device here
const int dpiy = p.device()->logicalDpiY();
body = QRectF(0, 0, printer->width(), printer->height()); // body很重要,是页面内容区域的大小
pageNumberPos = QPointF(body.width() - horizontalMargin * dpiScaleX,
body.height() - verticalMargin * dpiScaleY
+ QFontMetrics(doc->defaultFont(), p.device()).ascent()
+ 5 * dpiy / 72.0); // pageNumberPos: 绘制页码的坐标
// fromPage和toPage可以理解为从第几页到第几页
int fromPage = pd->fromPage;
int toPage = pd->toPage;
if (fromPage == 0 && toPage == 0) {
fromPage = 1;
toPage = doc->pageCount();
}
// paranoia check
fromPage = qMax(1, fromPage);
toPage = qMin(doc->pageCount(), toPage);
if (toPage < fromPage) {
// if the user entered a page range outside the actual number
// of printable pages, just return
return;
}
int page = fromPage;
// 通过while循环绘制完所有页
while (true) {
printPage(page, &p, doc, body, pageNumberPos); // 绘制单独的一整页,各个参数前面已经介绍到了
if (page == toPage)
break;
++page;
if (!printer->newPage()) // 绘制完一页后,通过newPage创建一个新页
return;
}
}
通过对QTextDocument::print方法的分析,发现最后使用printPage完成pdf的整个绘制,那么我们岂不是重写print方法或printPage方法就可以完成页面内容、页眉页脚、水印的绘制了。但又发现print方法或printPage方法不是虚方法无法重写。
QTextDocument内部其实已经自动完成了分页,通过pageCount方法就可以获取到总页数,页码坐标和页面内容区域源码也提供了计算方法。既然如此,那我们可以自己实现print和printPage方法来完成绘制页面内容、页眉页脚、水印的功能。
三、解决方法
先把print和printPage整个源码搬过来,然后删掉和自己无关的,再添加上所要实现的功能代码
print方法的实现:
void print(QTextDocument* doc, QPdfWriter* printer)
{
printer->setPageMargins(QMarginsF(20, 20, 20, 20), QPageLayout::Millimeter); // 页面边距设置
QPainter p(printer);
(void)doc->documentLayout(); // make sure that there is a layout
QRectF body = QRectF(QPointF(0, 0), doc->pageSize()); // 页面内容区域矩形大小
QPointF pageNumberPos;
// 通过源码断点看到DPI的默认值,此处先写死
qreal sourceDpiX = 2.0833333333333335;
qreal sourceDpiY = 2.0833333333333335;
const qreal dpiScaleX = qreal(printer->logicalDpiX()) / sourceDpiX;
const qreal dpiScaleY = qreal(printer->logicalDpiY()) / sourceDpiY;
QAbstractTextDocumentLayout* layout = doc->documentLayout();
layout->setPaintDevice(p.device());
const int horizontalMargin = int((2 / 2.54) * sourceDpiX);
const int verticalMargin = int((2 / 2.54) * sourceDpiY);
const int dpiy = p.device()->logicalDpiY();
body = QRectF(0, 0, printer->width(), printer->height());
pageNumberPos = QPointF(body.width() - horizontalMargin * dpiScaleX,
body.height() - verticalMargin * dpiScaleY
+ QFontMetrics(doc->defaultFont(), p.device()).ascent()
+ 5 * dpiy / 72.0);
doc->setPageSize(body.size());
int fromPage = 1; // 开始页面直接给定为1,我希望从第一页开始绘制
int toPage = doc->pageCount(); // 结束页面为页面总数
if (fromPage == 0 && toPage == 0)
{
fromPage = 1;
toPage = doc->pageCount();
}
// paranoia check
fromPage = qMax(1, fromPage);
toPage = qMin(doc->pageCount(), toPage);
int page = fromPage;
while (true)
{
printPage(page, &p, doc, body, pageNumberPos);
if (page == toPage)
break;
++page;
if (!printer->newPage())
return;
}
}
printPage方法的实现:
static void printPage(int index, QPainter* painter, const QTextDocument* doc, const QRectF &body, const QPointF &pageNumberPos)
{
QRectF view(0, (index - 1) * body.height(), body.width(), body.height());
painter->save();
painter->translate(-(view.topLeft() + QPoint(0, 70)));
QPen pen1;
pen1.setColor(Qt::black);
pen1.setWidth(5);
painter->setPen(pen1);
// 页眉
QRect pageTopRect(0, view.y(), view.width(), 50);
painter->drawText(pageTopRect, Qt::AlignVCenter | Qt::AlignRight, "报告编号:20240315155503");
pageTopRect.setWidth(300);
painter->drawPixmap(pageTopRect, QPixmap(":/Snipaste_2024-04-21_18-54-22.png"));
painter->drawLine(0, view.y() + 52, view.width(), view.y() + 52);
// 页脚
QRect pageBottomRect(0, view.y() + view.height() + 70, view.width(), 50);
auto avgWidth = pageBottomRect.width() / 3;
auto leftBottomRect = pageBottomRect;
leftBottomRect.setWidth(avgWidth);
auto centerBottomRect = pageBottomRect;
centerBottomRect.setX(avgWidth);
centerBottomRect.setWidth(avgWidth);
auto rightBottomRect = pageBottomRect;
auto rightBottomRect = pageBottomRect;
rightBottomRect.setX(avgWidth * 2);
rightBottomRect.setWidth(avgWidth);
painter->drawText(leftBottomRect, Qt::AlignVCenter | Qt::AlignLeft, "检测日期:2024/3/15");
painter->drawText(centerBottomRect, Qt::AlignCenter, "检测人员:");
painter->drawText(rightBottomRect, Qt::AlignVCenter | Qt::AlignRight, "审核人员");
painter->restore();
painter->save();
// 绘制水印
painter->translate(-view.topLeft());
QPen pen;
pen.setColor(Qt::red);
pen.setWidth(10);
painter->setPen(pen);
painter->translate(view.center());
painter->rotate(-30);
painter->translate(-view.center());
for (int x = view.x() - 1000; x < view.width() + 1000; x += 500)
{
for (int y = view.y() - 1000; y < view.y() + view.height() + 1000; y += 300)
{
painter->drawText(x, y, "2024/04/21");
}
}
painter->restore();
painter->save();
painter->translate(body.left(), body.top() - (index - 1) * body.height());
QAbstractTextDocumentLayout* layout = doc->documentLayout();
QAbstractTextDocumentLayout::PaintContext ctx;
painter->setClipRect(view);
ctx.clip = view;
// don't use the system palette text as default text color, on HP/UX
// for example that's white, and white text on white paper doesn't
// look that nice
ctx.palette.setColor(QPalette::Text, Qt::black);
layout->draw(painter, ctx);
if (!pageNumberPos.isNull())
{
painter->setClipping(false);
painter->setFont(QFont(doc->defaultFont()));
const QString pageString = QString::number(index);
// 绘制页码
painter->drawText(view.width() / 2, view.y() + view.height() + 70, pageString);
}
painter->restore();
}
主方法修改:
QPdfWriter pdfWriter(fileName);
QTextDocument textDocument;
// 通过QTextDocument对pdf文档进行排版
//textDocument.print(&pdfWriter); // 将textDocument内容输出到pdfWriter,生成pdf完成
print(&textDocument,&pdfWriter); // 使用新方法代替Qt提供的方法