整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

利用CSS样式实现分页打印

利用CSS样式实现分页打印

用CSS样式实现分页打印,其主要应用thead标记、tfoot标记和page-break-after属性。

(1)thead标记

thead用于设置表格的表头。

(2)tfoot标记

tfoot用于设置表格的表尾。

(3)page-break-after属性

page-break-after属性在打印文档时发生作用,用于进行分页打印。但是对于<br>和<hr>对象不起作用。其语法格式如下:

page-break-after:auto | always | avoid | left | right | null

参数说明:

page-break:打印时在样式控制的对象前后换页。

after:设置对象后出现页分隔符。设置为always时,始终在对象之后插入页分隔符。

auto:需要在对象之后插入页分隔符时插入。

always:始终在对象之后插入页分隔符。

avoid:未支持。避免在对象后面插入分隔符。

left:未支持。在对象后面插入页分隔符,直到它到达一个空白的左页边。

right:未支持。在对象后面插入页分隔符,直到它到达一个空白的右页边。

null:空白字符串。取消了分隔符设置。

WebBrowser.ExecWB的完整说明

<OBJECT classid=CLSID:8856F961-340A-11D0-A96B-00C04FD705A2 height=0 id=WebBrowser width=0></OBJECT>
<input name=Button .Click=document.all.WebBrowser.ExecWB(1,1) type=button value=打开>
<input name=Button .Click=document.all.WebBrowser.ExecWB(2,1) type=button value=关闭所有>
<input name=Button .Click=document.all.WebBrowser.ExecWB(4,1) type=button value=另存为>
<input name=Button .Click=document.all.WebBrowser.ExecWB(6,1) type=button value=打印>
<input name=Button .Click=document.all.WebBrowser.ExecWB(6,6) type=button value=直接打印>
<input name=Button .Click=document.all.WebBrowser.ExecWB(7,1) type=button value=打印预览>
<input name=Button .Click=document.all.WebBrowser.ExecWB(8,1) type=button value=页面设置>
<input name=Button .Click=document.all.WebBrowser.ExecWB(10,1) type=button value=属性>
<input name=Button .Click=document.all.WebBrowser.ExecWB(17,1) type=button value=全选>
<input name=Button .Click=document.all.WebBrowser.ExecWB(22,1) type=button value=刷新>
<input name=Button .Click=document.all.WebBrowser.ExecWB(45,1) type=button value=关闭>

运用CSS样式实现分页打印。其具体步骤如下:

(1)编写用于控制指定内容不打印的CSS样式,代码如下。

@media print{
    .bgnoprint{
        background:display:none;
    }
    .noprint{
        display:none
    }
}

(2)应用include命令连接数据源文件,并应用do…while循环语句输出图书信息到浏览器,并设置好表头、表尾及打印分页,关键代码如下:

<?php include "conn/conn.php"; ?>
<table width="99%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td height="27" align="center" style=" font-size:14px;"><b>图书信息查询</b></td>
</tr>
</table>
<table width="98%" border="0" cellpadding="0" cellspacing="1" bgcolor="#000000" bordercolor="#FFFFFF" bordercolordark="#000000" bordercolorlight="#FFFFFF" >
<thead style="display:table-header-group;"> <!--设置表头-->
<tr bgcolor="#EFEFEF">
<td width="6%" height="20" align="center">编号</td>
<td width="27%" align="center">图书名称</td>
<td width="23%" align="center">内容简介</td>
<td width="8%" align="center">定价</td>
<td width="10%" align="center">作者</td>
<td width="15%" align="center">出版社</td>
<td width="11%" align="center">发行时间</td>
</tr>
</thead>
<!--控制分页-->
<?php
$sql=mysql_query("select * from tb_book");
$info=mysql_fetch_array($sql);
$row=1;
do{
?>
<tr align="center" <?php if($row==2){ ?>style="page-break-after:always"<?php } ?>>
<td bgcolor="#FFFFFF"><?php echo $info[id];?></td>
<td height="25" align="left" bgcolor="#FFFFFF"> <?php echo $info[bookname];?></td>
<td align="left" bgcolor="#FFFFFF"> <?php echo $info[synopsis];?></td>
<td bgcolor="#FFFFFF"><?php echo $info[price];?></td>
<td bgcolor="#FFFFFF"><?php echo $info[maker];?></td>
<td bgcolor="#FFFFFF"><?php echo $info[publisher];?></td>
<td bgcolor="#FFFFFF"><?php echo $info[issuDate];?></td>
</tr>
<?php
$row++;
}while($info=mysql_fetch_array($sql))
?>
<!--设置表尾-->
<tfoot style="display:table-footer-group; border:none;"><tr><td></td></tr></tfoot>
</table>

(3)建立HTML的object标签,调用WebBrowser控件,代码如下:

<object id="Wb" classid="ClSID:8856F961-340A-11D0-A96B-00C04Fd705A2" width="0" height="0">
</object>

(4)建立相关的打印超级链接,并调用WebBrowser控件的相应参数实现打印预览及打印功能,代码如下:

最近在业务上遇到了一个问题是要将页面打印输出成pdf文件,通过点击一个按钮,就能够将页面写在一个pdf上,并下载下来,需要保证pdf的内容具有很好的可读性。

经评估要实现这个需求,一种可行的方案是将HTML页面转为PDF,并实现下载。通过技术调研,最终的方案确定为通过html2canvas + jspdf这两个库来实现,通过使用html2canvas提供的方法,将页面元素转为base64图片流,然后将其插入jspdf插件中,实现保存并下载pdf。

html2canvas + jspdf方案是前端实现页面打印的一种常用方案,但是在实践过程中,遇到的最大问题就是分页截断的问题:当页面元素超过一页A4纸的时候,连续的页面就会因为分页而导致内容被截断,进而影响了pdf的可读性。

由于网上关于分页截断的解决思路比较少,所以特意将此次的解决方案记录下来。

使用 JSPDF 和 html2canvas 创建简单的 PDF文件

首先,我们开始使用 JSPDF 和 html2canvas 生成一个简单的 PDF文件。

创建一个 JSPDF 实例

创建一个 JSPDF 实例,设置页面的大小、方向和其他参数。参考官网可以写一个很简单的实例

var doc=new jsPDF({
  orientation: 'landscape',
  unit: 'in',
  format: [4, 2]
}

doc.text('Hello world!', 1, 1)
doc.save('two-by-four.pdf')

生成一个pdf文件,并且在文件中写入一定内容,其实JSPDF这个库就能做到。

但是很多业务场景下,我们的目标内容会更复杂,而且还要考虑样式,所以最好的方式是引入html2canvas这个库,将页面元素转换成base64数据,然后贴在pdf中(使用addImage方法),这样就能保证页面的内容。

引入了html2canvas库后,我们更多关注是利用现成组件库、框架或者原生html和css实现更复杂的页面内容

引入 html2canvas

使用 html2canvas 捕捉 HTML 内容或特定的 HTML 元素,并将其转换为 Canvas。其中,html2canvas 函数的主要用法是:

html2canvas(element, options);
  • element 要渲染为 canvas 的 HTML 元素。这可以是一个 DOM 元素,也可以是一个选择器字符串,表示需要渲染的元素。
  • options(可选): 一个包含配置选项的对象,用于定制 html2canvas 的行为。

以下是一些常见的配置选项:

  • allowTaint(默认值: false): 是否允许加载跨域的图片,默认为 **false**。如果设为 truehtml2canvas 将尝试加载跨域的图片,但在某些情况下可能会受到浏览器的限制。
  • backgroundColor(默认值: #ffffff): canvas 的背景颜色。
  • useCORS(默认值: false): 是否使用 CORS(Cross-Origin Resource Sharing)来加载图片。如果设置为 **true**,则 html2canvas 将尝试使用 CORS 来加载图片。
  • logging(默认值: false): 是否输出日志信息到控制台。
  • widthheight canvas 的宽度和高度。如果未指定,则默认为目标元素的宽度和高度。
  • scale(默认值: window.devicePixelRatio): 缩放因子,决定 canvas 的分辨率。

下面是一个简单的demo,可以看到html2canvas能够将dom元素转化为一张base64图片,将鼠标选中元素,可以感受到图片和文字的不同。

<div id="capture" style="padding: 10px; background: #f5da55">
    <h4 style="color: #000; ">Hello world!</h4>
</div>
html2canvas(document.querySelector("#capture")).then(canvas=> {
    document.body.appendChild(canvas)
});

Untitled.png

将html2canvas转化的图片放到pdf中

这一步我们需要使用JSPDF 的addImage方法,其语法如下:

addImage(imageData, format, x, y, width, height, alias, compression)
  • imageData - 要添加的图像数据。可以是图像的 URL、图像的 base64 编码字符串或图像的二进制数据
  • format - 图像的格式。可以是 "JPEG"、"PNG" 或 "TIFF"。
  • x - 图像在 PDF 文档中的 x 坐标。
  • y - 图像在 PDF 文档中的 y 坐标。
  • width - 图像的宽度。
  • height - 图像的高度。
  • alias - 图像的别名。此别名可用于在 PDF 文档中引用图像。
  • compression - 图像的压缩级别。可以是 "NONE"、"FAST" 或 "SLOW"。

下面是一串示例代码:

import jsPDF from 'jspdf';

export default function addImageUsage() {
  const doc=new jsPDF();
  const imageData=【替换成base64数据流】;
  doc.addImage(imageData, 'png', 0, 0, 10, 10);
  doc.addImage(imageData, 'png', 100, 100, 10, 10);
  doc.addImage(imageData, 'png', 200, 200, 10, 10);

  drawNet(doc);

  doc.save('output.pdf');
}

const drawNet=(doc)=> {
  const gap=10;
  const start=[0, 0];
  const end=[595.28, 841.89];

  // 所有横线
  for (let i=start[0]; i < end[0]; i=i + gap) {
    doc.line(i, 0, i, end[0]);
  }
  // 所有纵线
  for (let j=start[1]; j < end[1]; j=j + gap) {
    doc.line(0, j, end[1], j);
  }
};

此示例将在 PDF 文档(默认是A4纸大小,宽高为[595.28, 841.89]像素)的 (10, 10)(100, 100)(200, 200) 坐标处,添加一张png 图像。图像的宽度和高度将分别为 10 和 10 像素,为了了解pdf中的坐标系统,此示例还在pdf文档中生成了间距为10px的网格系统。

JSPDF 和 html2canvas结合起来用

了解了上面的三个关键点,接下来我们将这三个步骤串联起来,实现一个基本的html→pdf的方案。大致步骤如下:

  1. 写一个基本html页面
  2. 创建jspdf实例
  3. 获取页面的dom节点,使用html2canvas将其转化为base64数据流
  4. base64数据流装载到jspdf提供的addImage方法中
  5. 保存pdf

基于这5个步骤,可以实现基本的页面打印。

import html2canvas from 'html2canvas';
import jsPDF, { RGBAData } from 'jspdf';

// 将元素转化为canvas元素
// 通过 放大 提高清晰度
// width为内容宽度
async function toCanvas(element: HTMLElement) {
  if (!element) return { width: 0, height: 0 };

  // canvas元素
  const canvas=await html2canvas(element, {
    scale: window.devicePixelRatio * 2, // 增加清晰度
    useCORS: true // 允许跨域
  });

  // 获取canvas转化后的宽高
  const { width: canvasWidth, height: canvasHeight }=canvas;

  // 转化成图片Data
  const canvasData=canvas.toDataURL('image/jpeg', 1.0);

  return { width: canvasWidth, height: canvasHeight, data: canvasData };
}

/**
 * 生成pdf(A4多页pdf截断问题, 包括页眉、页脚 和 上下左右留空的护理)
 */
export async function generatePDF({
  /** pdf内容的dom元素 */
  element,

  /** pdf文件名 */
  filename
}) {
  if (!(element instanceof HTMLElement)) {
    return;
  }

  const pdf=new jsPDF();

  // 一页的高度, 转换宽度为一页元素的宽度
  const {
    width: imageWidth,
    height: imageHeight,
    data
  }=await toCanvas(element);

  // 添加图片
  function addImage(
    _x: number,
    _y: number,
    pdfInstance: jsPDF,
    base_data:
      | string
      | HTMLImageElement
      | HTMLCanvasElement
      | Uint8Array
      | RGBAData,
    _width: number,
    _height: number
  ) {
    pdfInstance.addImage(base_data, 'JPEG', _x, _y, _width, _height);
  }

  addImage(0, 0, pdf, data!, imageWidth, imageHeight);

  return pdf.save(filename);
}

多页:比例缩放+循环移位

通常,在我们的实践中,会发现2个问题:

  • 生成的pdf内容与实际的页面元素比例不一致
  • 页面内容超出一页pdf的高度,但是生成的pdf只有一页,没有展示全部的页面信息

这两个问题的解决方案是等比例缩放+循环移位

  • 等比例缩放

通过比例缩放,实现页面内容等比例展示在pdf文档中

令页面元素的宽高为x, y(转化成canvas图片的宽高),pdf文档的宽高为w, h。因为高度可以通过加页延伸,所以可以按照宽度进行缩放,缩放后的图片高度可以通过下列公式计算

  • 循环移位

如果页面的高度超出了pdf文档的高度,即y > h,使用addPage方法添加一页即可。但是在新的一页中,我们的图片内容的高度需要调整。

假设y=2 * h,这意味我们需要两页才能完整得展示页面内容。在一页pdf中,图片在起始位置插入即可,即

 PDF.addImage(pageData, 'JPEG', 0, 0, x, y)// 注意x,y 是缩放后的大小

在第二页pdf中,图片的纵向位置需要调整一页pdf的高度,即

 PDF.addImage(pageData, 'JPEG', 0, -h, x, y)// 注意x,y 是缩放后的大小

通过循环计算剩余高度,然后不停调整纵向位置移动base64的图片位置,可以解决多页的问题。

分页截断的挑战

尽管 JSPDFhtml2canvas 是功能强大的工具,但是他们也有很多槽点,比如得手动分页,手动处理分页截断的问题。等你实践到这一步,就开始面临分页截断的问题,类似的问题也有网友在Github上提出,但是底下依然没有很好的解决思路。

好在掘金上有人分享了一个不错的方法:

jsPDF + html2canvas A4分页截断 完美解决方案(含代码 + 案例) - 掘金

概括一下,其处理分页截断的原理就是在使用addImage之前,将html进行分页,通过维护一个高度位置数据,来记录每次循环迭代addImage的位置。

从高到低遍历维护一个分页数组pages,该数组记录每一页的起始位置,如:pages[0] 对应 第一页起始位置,pages[1] 对应第二页起始位置

Untitled2.png

接下来我们重点讨论如何将页面进行切割,然后生成pages这个数组。

假设页面的高度是1500,pdf宽高是[500, 900],如果不用处理分页截断的问题,我们可以想到第一页(0-900)是用来承载页面从高度为0到900的信息;

第二页(900-1800)是用来承载页面从高度900到1500的,所以pages数组为[0, 900]。

如果要处理分页截断呢,这时候就需要计算页面元素的距离pdf文档起始位置的高度h1,以及该元素的内部高度h2,通过这两个高度来判断这个元素要不要放在下一页,防止截断,示意图如下:

Untitled4.png

如果h1 + h2 > 页面高度, 这时候说明这个元素不处理的就会被分页截断,所以应该要把这个元素放到第二页去渲染,这就意味着pages记录的数据要变化,示意图如下,可以看到pages[1]我们往上调整了,比第二页pdf的起始位置更高。

Untitled5.png

说明渲染第二页pdf的时候,要从h1开始渲染,pages数组为[0, h1],解释为第一页pdf渲染页面高度区域为0-900, 第二页pdf渲染html高度区域为h1-1500。注意到第一页渲染的时候到尾部的时候,会有部分内容和第二页头部内容重合。因为h1900这部分的内容肯定会渲染,这部分内容一直都是页面元素,我们改变pages[1]的值的原因只是创建一个副本,让页面看起来内容没有被截断。

为了解决这个问题(为了美观),我们用填充一块白色区域遮掉它!此处使用jspdfrectsetFillColor方法,把重合的区域遮白处理。

pdf.setFillColor(255, 255, 255);
pdf.rect(x, y, Math.ceil(_width), Math.ceil(_height), 'F');

如何获得h1和h2

上面我们谈到了h1h2,其中h1是元素盒子的上边距到打印区域的高度(比例缩放后的高度),h2是元素盒子的内部高度。

计算h1: getBoundingClientRect方法

const rect=contentElement.getBoundingClientRect() || {};
const topDistance=rect.top;
return topDistance;

Untitled6.png

计算h2:offsetHeight方法

Untitled7.png

值得注意的是,因为打印区域的html元素不一定是从窗口顶部开始,所以为了计算实际的h1(元素到打印区域的顶部距离),可以采用这样的方法:

  • getBoundingClientRect方法计算元素到窗口顶部的距离
  • 循环打印之前将pages信息针对第一个元素进行一个高度校准。
// 对pages进行一个值的修正,因为pages生成是根据根元素来的,根元素并不是我们实际要打印的元素,而是element,
  // 所以要把它修正,让其值是以真实的打印元素顶部节点为准
  const newPages=pages.map((item)=> item - pages[0]);

在线demo演示和源代码

上述即是在实现前端页面生成pdf的过程中遇到的问题,以及解决思路。

为了更直观得感受效果,本文也给出了不同场景(单页、多页、多页截断、自定义页眉页脚、横向)下的pdf生成效果,可以通过此链接体验:https://pdf-demo-phi.vercel.app/

此demo的源代码如下:pdf-demo

与现有文章不同的是,本仓库的代码特点在于:

  • 支持设置pdf打印的方向,比如横向
  • 修正了高度计算问题,解决了多出一个空白页问题。掘金那篇文章计算元素高度时候没有减去容器距离顶部高度,所以导致很多新手使用那份代码的时候,会发现自己的页面顶部被裁剪到了,原因就是这个
  • 支持自定义页眉页脚
  • 支持扩展自定义分页方法,如果遇到复杂的组件,可以自定扩展逻辑计算高度

作者:燕平

来源:微信公众号:Goodme前端团队

出处:https://mp.weixin.qq.com/s/-1nA-VI6kmgqHRcYs_NZFA

# 一、前言

数据能够打印到pdf文件,当然可以打印到纸张,而且使用qprinter默认就是打印到纸张的,上一篇文章写得功能是打印到pdf,其实还要单独特殊设置打印到文件,并指定格式为pdf。不指定输出文件和格式默认就是打印到纸张,关于Qt打印内容到纸张,网上的办法非常多,比如有些直接用painter绘制,逐步控制分页打印,个人还是喜欢html格式的内容传入,因为html格式相当灵活,可控范围相当大,而且整齐,甚至可以先直接输出到网页预览下效果,根据需求调整,后面的图文混排就是用的html+table实现的,如果不需要边框可以设置边框粗细为0,其实还是表格,但是看起来像文档一样。

数据打印基本步骤:

1. 准备好要打印的数据。

2. 实例化QPrinter对象。

3. 设置输出格式setOutputFormat(QPrinter::NativeFormat)。

4. 弹出打印预览对话框QPrintPreviewDialog。

5. 关联信号槽在槽函数中绘制内容。

6. 实例化文档对象QTextDocument。

7. 将内容作为html设置到文档对象。

8. 调用文档对象的print方法传入QPrinter对象打印。

结构体支持的参数:

- 文件名称

- 表名

- 主标题

- 副标题

- 字段名称集合

- 字段宽度集合

- 内容集合

- 行内容分隔符

- 子内容分隔符

- 边框宽度

- 校验列

- 校验类型

- 校验值

- 检验颜色

- 最后列拉伸填充

- 横向排版

- 纸张边距

## 二、功能特点

1. 组件同时集成了导出数据到csv、xls、pdf和打印数据。

2. 所有操作全部提供静态方法无需new,数据和属性等各种参数设置采用结构体数据,极为方便。

3. 同时支持QTableView、QTableWidget、QStandardItemModel、QSqlTableModel等数据源。

4. 提供静态方法直接传入QTableView、QTableWidget控件,自动识别列名、列宽和数据内容。

5. 每组功能都提供单独的完整的示例,注释详细,非常适合各阶段Qter程序员。

6. 原创导出数据机制,不依赖任何office组件或者操作系统等第三方库,支持嵌入式linux。

7. 速度超快,9个字段10万行数据只需要2秒钟完成。

8. 只需要四个步骤即可开始急速导出海量数据比如100W条记录到Excel。

9. 同时提供直接写入数据接口和多线程写入数据接口,不卡主界面。

10. 可设置标题、副标题、表名。

11. 可设置导出数据的字段名、列名、列宽。

12. 可设置末尾列自动拉伸填充,默认拉伸更美观。

13. 可设置是否启用校验过滤数据,启用后符合规则的数据特殊颜色显示。

14. 可指定校验的列、校验规则、校验值、校验值数据类型。

15. 校验规则支持 精确等于==、大于>、大于等于>=、小于<、小于等于<=、不等于!=、包含contains。

16. 校验值数据类型支持 整型int、浮点型float、双精度型double,默认文本字符串类型。

17. 可设置随机背景颜色及需要随机背景色的列集合。

18. 支持分组输出数据,比如按照设备分组输出数据,方便查看。

19. 可设置csv分隔符、行内容分隔符、子内容分隔符。

20. 可设置边框宽度、自动填数据类型,默认自动数据类型开启。

21. 可设置是否开启数据单元格样式,默认不开启,不开启可以节约大概30%的文件体积。

22. 可设置横向排版、纸张边距等,比如导出到pdf以及打印数据。

23. 支持图文混排导出数据到pdf以及打印数据,自动分页。

24. 灵活性超高,可自由更改源码设置对齐方式、文字颜色、背景颜色等。

25. 支持任意excel表格软件,包括但不限于excel2003-2021、wps、openoffice等。

26. 纯Qt编写,支持任意Qt版本+任意编译器+任意系统。

## 三、体验地址

1. 体验地址:[https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A](https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A) 提取码:o05q 文件名:bin_dataout.zip

2. 国内站点:[https://gitee.com/feiyangqingyun](https://gitee.com/feiyangqingyun)

3. 国际站点:[https://github.com/feiyangqingyun](https://github.com/feiyangqingyun)

4. 个人主页:[https://blog.csdn.net/feiyangqingyun](https://blog.csdn.net/feiyangqingyun)

5. 知乎主页:[https://www.zhihu.com/people/feiyangqingyun/](https://www.zhihu.com/people/feiyangqingyun/)

## 四、效果图

## 五、相关代码