将网页漂亮的打印到纸上的CSS
简介 (§)
在工作中,我经常做的一件事就是用 HTML 编写打印生成器,以重新创建和替换公司传统上在纸上或 Excel 中手写的表单。这样,公司就可以使用新的网络工具,通过数据库中的 URL 参数自动填写表格,同时获得大家熟悉的物理输出。
本文介绍了控制网页打印效果的 CSS 基础知识,以及我学到的一些技巧和窍门,或许对你有所帮助。
几个示例 (§)
下面是一些页面示例,包括一些背景,还有一些 logo。
我会第一个承认这些页面有点难看,还需要进一步打磨。但它们能完成工作,而且我还在改进。
@page (§)
CSS 有一个名为 @page 的规则,它可以将网站的打印偏好告知浏览器。通常,我使用
@page
{
size: Letter portrait;
margin: 0;
}
我将在后面有关页边距的章节中解释为什么选择 margin: 0
。根据您与公制系统的关系,您应该使用 Letter
或 A4
。
设置 @page
的大小和边距与设置 <html>
或 <body>
元素的宽度、高度和边距不同。@page
超越了 DOM
- 它包含了 DOM
。在网页上,
<html>
元素的边界是屏幕的边缘,但在打印时,它的边界是 @page
。
由 @page
控制的设置或多或少与按下 Ctrl+P
时在浏览器打印对话框中获得的设置一致。
下面是我用来做一些实验的示例文件:
<!DOCTYPE html>
<html>
<style>
@page
{
/* see below for each experiment */
}
html
{
width: 100%;
height: 100%;
background-color: lightblue;
/* grid by shunryu111 https://stackoverflow.com/a/32861765/5430534 */
background-size: 0.25in 0.25in;
background-image:
linear-gradient(to right, gray 1px, transparent 1px),
linear-gradient(to bottom, gray 1px, transparent 1px);
}
</style>
<body>
<h1>Sample text</h1>
<p>sample text</p>
</body>
</html>
下面是浏览器中的显示效果:
下面是不同 @page
值的结果:
@page { size: Letter portrait; margin: 1in; }
:
@page { size: Letter landscape; margin: 1in; }
:
@page { size: Letter landscape; margin: 0; }
:
设置 @page
尺寸并不能将该尺寸的纸张放入打印机的进纸托盘。这部分需要您自己完成。
请注意,当我将尺寸设置为 A5 时,我的打印机保持在 Letter 尺寸,而 A5 尺寸完全符合 Letter 尺寸,这就给人一种页边距的感觉,尽管它不是来自页margin
设置。
@page { size: A5 portrait; margin: 0; }
:
但是,如果我告诉打印机我装的是真正的 A5 纸张,那么它看起来就和预期的一样。
根据我的实验,Chrome 浏览器只有在边距设置为默认的情况下才会遵循 @page
规则。一旦你在打印对话框中更改了页边距,你的输出结果就会变成物理纸张尺寸和所选页边距。
@page { size: A5 portrait; margin: 0; }
:
即使你选择的 @page 大小完全适合你的实体纸张,页边距仍然很重要。在这里,我制作了一个不带边距的 5x5 正方形和一个带边距的 5x5 正方形。<html>
元素的大小受 @page
大小和页边距margin的限制。
@page { size: 5in 5in; margin: 0; }
:
@page { size: 5in 5in; margin: 1in; }
:
我做这些测试并不是因为我希望在 A5 或 5x5 纸张上打印,而是因为我花了一段时间才弄明白 @page
到底是什么。现在,我非常有信心始终使用页边距为 0 的 Letter 纸张。
@media print (§)
有一个名为 print
的 media query ,在这里你可以编写只在打印时应用的样式。我的生成器页面通常包含一个页眉、一些选项和一些帮助用户的文本,这些显然不应该在打印时显示出来,所以这里就需要在这些元素上添加 display:none
。
/* Normal styles that appear while you are preparing the document */
header
{
display: block;
}
@media print
{
/* Disappear when you are printing the document */
header
{
display: none;
}
}
宽度, 高度, margin, 和 padding (§)
你需要对box model有一定的了解,这样才能获得你想要的边距,而不会让电脑太费力。
我之所以总是设置 @page margin: 0
,是因为我更愿意在 DOM 元素上处理页边距。当我尝试使用 @page margin: 0.5in
时,经常会不小心出现双边框,把内容挤压得比我预期的要小,而且我的单页设计会扩展到第二页。
如果我想使用 @page margin,那么实际的页面内容就需要一直靠着 DOM 的边缘布局,这对我来说更难考虑,也更难在打印前预览。对我来说,记住 <html>
占据了整个物理纸张,我的页边距在 DOM 内而不是 DOM 外会更容易一些。
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
width: 8.5in;
height: 11in;
}
当涉及到多页打印生成器时,您需要一个单独的 DOM 元素来代表每一页。由于不能使用多个 <html>
或 <body>
,因此需要另一个元素。我喜欢 <article>
。即使是单页生成器,你也可以始终使用文章。
由于每个 <article>
代表一页,因此我不希望在 <html>
或 <body>
上有任何边距或填充。我们将逻辑推进一步--让文章占据整个物理页面并在其中设置页边距对我来说更容易。
@page
{
size: Letter portrait;
margin: 0;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
}
当我说要在文章中添加 margin
时,我使用的不是 margin
属性,而是 padding
。这是因为在 box model中,margin
位于元素的外部和周围。如果使用 0.5 英寸的边距,就必须将文章设置为 7.5×10,这样文章加上 2×margin 就等于 8.5×11。
相反,padding
位于元素的内侧,因此我可以将文章定义为 8.5×11 并加上 0.5 英寸的 padding
,这样文章内的所有元素都会留在页面上。
如果设置了 box-sizing: border-box
,很多关于元素尺寸的直觉就会变得简单。这样,当你调整内部填充时,文章的外部尺寸就会被锁定。这是我的代码段:
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
让我们把这一切合起来:
@page
{
size: Letter portrait;
margin: 0;
}
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html,
body
{
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
padding: 0.5in;
}
元素位置 (§)
设置好文章和页边距后,文章内的空间就可以随意使用了。使用你认为适合项目的 HTML/CSS 设计文档。有时,这意味着使用柔性或网格来布局元素,因为你在输出时有一定的回旋余地。有时,这意味着要创建特定大小的正方形,以适合特定品牌的贴纸。有时,这意味着要将所有内容都精确到毫米,因为用户需要将一张特殊的预标签纸送入打印机,才能将你的数据放在上面,而你却无法控制那张特殊的纸。
我在这里并不是要教你如何编写 HTML,所以你需要具备编写 HTML 的能力。我只能说,你所面对的是一张纸的有限空间,而不像浏览器窗口可以任意滚动和缩放。如果您的文档将包含任意数量的项目,请准备好通过创建更多 <article>
来进行分页。
带有重复元素的多页文档 (§)
我编写的很多打印生成器都包含表格数据,比如一张列满细列项目的发票。如果您的 <table>
足够大,可以放到第二页,浏览器会自动在每页顶部复制 <thead>
。
<table>
<thead>
<tr>
<th>Sample text</th>
<th>Sample text</th>
</tr>
</thead>
<tbody>
<tr><td>0</td><td>0</td></tr>
<tr><td>1</td><td>1</td></tr>
<tr><td>2</td><td>4</td></tr>
...
</tbody>
</table>
如果只是打印一个没有任何装饰的 <table> 就很好,但在很多实际场景中并没有那么简单。我正在创建的文档通常在每页的顶部有信头,底部有页脚,还有其他需要在每页明确重复的自定义元素。如果只是跨页打印一个长表格,就没有什么能力在中间页上将其他元素放在上面、下面或周围。
因此,我使用 javascript 生成页面,将表格分割成几个较小的表格。一般的做法是这样的:
- 将
<article>
元素视为一次性元素,并随时准备从内存中的对象重新生成它们。所有用户输入和配置都应在文章之外的单独页眉/选项框中进行。 - 编写一个名为
new_page
的函数,用于创建一个新的文章元素,并在其中包含必要的重复页眉、页脚等。 - 编写一个名为
render_pages
的函数,从基础数据中创建文章,每次填满上一页时调用new_page
。我通常使用offsetTop
来查看内容在页面上的位置,当然你也可以使用更智能的技术来使每一页都完美贴合。 - 当基础数据发生变化时,调用
render_pages
。
function delete_articles()
{
for (const article of Array.from(document.getElementsByTagName("article")))
{
document.body.removeChild(article);
}
}
function new_page()
{
const article = document.createElement("article");
article.innerHTML = `
<header>...</header>
<table>...</table>
<footer>...</footer>
`;
document.body.append(article);
return article;
}
function render_pages()
{
delete_articles();
let page = new_page();
let tbody = page.query("table tbody");
for (const line_item of line_items)
{
// I usually pick this threshold by experimentation but you can probably
// do something more rigorously correct.
if (tbody.offsetTop + tbody.offsetParent.offsetTop > 900)
{
page = new_page();
tbody = page.query("table tbody");
}
const tr = document.createElement("tr");
tbody.append(tr);
// ...
}
}
当基础数据发生变化时,调用 render_pages
:
function renumber_pages()
{
let pagenumber = 1;
const pages = document.getElementsByTagName("article");
for (const page of pages)
{
page.querySelector(".pagenumber").innerText = pagenumber;
page.querySelector(".totalpages").innerText = pages.length;
pagenumber += 1;
}
}
纵向/横向模式 (§)
我已经说明 @page
规则有助于告知浏览器的默认打印设置,但用户可以根据自己的需要覆盖它。如果你将 @page
设置为纵向模式,而用户将其覆盖为横向模式,那么你的布局和分页可能会看起来不对,特别是如果你硬编码了任何页面阈值的话。
您可以为纵向和横向创建单独的 <style>
元素,并使用 javascript 在两者之间进行切换。也许有更好的方法,但 @page
等规则的行为与普通 CSS 属性不同,所以我不确定。你还应该保存一些变量,帮助你的 render_pages
函数做正确的事情。
你也可以停止硬编码阈值,但这样我就不得不听从自己的建议了。
<select onchange="return page_orientation_onchange(event);">
<option selected>Portrait</option>
<option>Landscape</option>
</select>
<style id="style_portrait" media="all">
@page
{
size: Letter portrait;
margin: 0;
}
article
{
width: 8.5in;
height: 11in;
}
</style>
<style id="style_landscape" media="not all">
@page
{
size: Letter landscape;
margin: 0;
}
article
{
width: 11in;
height: 8.5in;
}
</style>
let print_orientation = "portrait";
function page_orientation_onchange(event)
{
print_orientation = event.target.value.toLocaleLowerCase();
if (print_orientation == "portrait")
{
document.getElementById("style_portrait").setAttribute("media", "all");
document.getElementById("style_landscape").setAttribute("media", "not all");
}
if (print_orientation == "landscape")
{
document.getElementById("style_landscape").setAttribute("media", "all");
document.getElementById("style_portrait").setAttribute("media", "not all");
}
render_printpages();
}
function render_printpages()
{
if (print_orientation == "portrait")
{
// ...
}
else
{
// ...
}
}
数据源 (§)
有几种方法可以将数据导入页面。有时,我会将所有数据打包到 URL 参数中,因此 javascript 只需执行 const url_params = new URLSearchParams(window.location.search); 然后再执行 url_params.get("title")。这样做有一些好处:
- 页面加载速度非常快。
- 通过更改 URL 可以方便地进行调试和实验。
- 生成器可以离线工作。
这也有一些缺点:
- URL会变得很长,而且不灵活,人们无法轻松地通过电子邮件发送给对方。请参阅本文顶部的示例链接。
- 如果 URL 通过电子邮件发送,即使数据库中的源记录稍后发生变化,数据也会被 "锁定"。
- 浏览器对 URL 长度有限制。这些限制很高,但不是无限的,而且可能因客户而异。
有时,我会使用 javascript 通过 API 获取数据库记录,因此 URL 参数只包含记录的主键和模式设置。
这样做有一些好处:
- URL 更短。
- 数据总是新鲜的。
也有缺点:
- 用户在获取数据时需要等待一秒钟。
- 必须编写更多代码。
有时,我会在文章上设置 contenteditable
,这样用户就可以在打印前做一些小的修改。我还喜欢使用真实的复选框输入,用户可以在打印前点击。这些功能会带来一些便利,但在大多数情况下,让用户先更改数据库中的源记录会更明智。此外,这些功能还限制了将文章元素视为一次性元素的能力。
速查表 (§)
<!DOCTYPE html>
<html>
<style>
@page
{
size: Letter portrait;
margin: 0;
}
html
{
box-sizing: border-box;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html,
body
{
margin: 0;
background-color: lightblue;
}
header
{
background-color: white;
max-width: 8.5in;
margin: 8px auto;
padding: 8px;
}
article
{
background-color: white;
padding: 0.5in;
width: 8.5in;
height: 11in;
/* For centering the page on the screen during preparation */
margin: 8px auto;
}
@media print
{
html,
body
{
background-color: white !important;
}
body > header
{
display: none;
}
article
{
margin: 0 !important;
}
}
</style>
<body>
<header>
<p>Some help text to explain the purpose of this generator.</p>
<p><button onclick="return window.print();">Print</button></p>
</header>
<article>
<h1>Sample page 1</h1>
<p>sample text</p>
</article>
<article>
<h1>Sample page 2</h1>
<p>sample text</p>
</article>
</body>
</html>