用于打印纸张的CSS

介绍

在工作中,我经常做的一件事情是在HTML中编写打印生成器,以重建和替换公司传统上手写在纸张上或在Excel中完成的表单。这使得公司能够转向新的基于Web的工具,在这些工具中,表单由来自我们数据库的URL参数自动填充,同时获得每个人都熟悉的相同物理输出。

本文解释了一些控制网页在打印时外观的CSS基础知识,以及我学到的一些技巧和窍门,希望能对你有所帮助。

示例文件

以下是一些示例页面生成器,以建立一些上下文,并可能增加一些可信度。

我将首先承认这些页面有点丑陋,可能需要更多的精雕细琢。但它们能够完成工作,而且我仍然受雇于公司。

发票生成器

带侧边栏输入的封面页

带有contenteditable的封面页

QR码生成器

@page

CSS有一个叫做@page的规则,它通知浏览器您网站的打印首选项。通常,我使用以下方式:

@page {
  size: Letter portrait;
  margin: 0;
}

我将在稍后关于边距的部分解释为什么选择 margin: 0。您应根据您与度量制度的关系适当使用Letter或A4。

设置@page的大小和边距与设置

或元素的宽度、高度和边距不同。@page超出了DOM —— 它包含了DOM。在Web上,您的元素受到屏幕边缘的限制,但在打印时,它受到@page的限制。

由@page控制的设置基本上对应于在按下Ctrl+P时浏览器打印对话框中获得的设置。

以下是我用来进行一些实验的示例文件:

<!DOCTYPE html>
<html>
<style>
@page {
  /* 请参阅下面的每个实验 */
}
html {
  width: 100%;
  height: 100%;
  background-color: lightblue;

  /* 由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>示例文本</h1>
  <p>示例文本</p>
</body>
</html>

在浏览器中的效果如下图所示:

以下是一些不同@page值的结果:

@page { size: Letter portrait; margin: 1in; }:

@page { size: Letter landscape; margin: 1in; }:

@page { size: Letter landscape; margin: 0; }:

设置@page大小实际上不会将该大小的纸张放入打印机的进纸盒中。您需要自己完成这部分工作

请注意,当我将 size 设置为A5 时,我的打印机仍然保持在Letter尺寸,而A5尺寸完全适合于Letter尺寸,这使得出现了边距的外观,尽管不是来自 margin 设置。

@page { size: A5 portrait; margin: 0; }:

但是,如果我告诉打印机我已加载了实际的A5纸张,那么它的外观就如预期的那样。

根据我的试验,Chrome仅在将Margin设置为默认值时才遵循@page规则。一旦您在打印对话框中更改了Margin,您的输出将成为您的物理纸张大小和所选Margin的产物。

@page { size: A5 portrait; margin: 0; }:

即使您选择的@page大小完全适合您的物理纸张,margin 仍然很重要。在这里,我使用没有边距的5x5正方形和带有边距的5x5正方形。 <html>元素的大小受限于@page大小和边距的组合。

@page { size: 5in 5in; margin: 1in; }:

我进行了所有这些测试,不是因为我期望在A5或5x5纸张上打印,而是因为我花了一段时间才弄清楚@page到底是什么。现在我对始终使用Letter并设置边距为0非常自信。

@media print

有一个名为print的媒体查询,您可以在其中编写仅在打印时应用的样式。我的生成器页面通常包含标题、一些选项和一些用户帮助文本,显然这些内容在打印时不应该显示,这就是您在这里在这些元素上添加display:none的地方。

/* 准备文档时显示的正常样式 */
header {
  display: block;
}

@media print {
  /* 打印文档时消失的样式 */
  header {
    display: none;
  }
}

宽度、高度、边距和填充

要想在不太折磨电脑的情况下获得您想要的边距,您需要了解一些关于盒模型的知识。

我始终将@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 属性,而是使用 padding。这是因为 margin 在盒模型中将元素外部周围。如果您使用0.5in的 margin,您将不得不将文章设置为7.5×10,以便文章加上2×margin等于8.5×11。如果您想调整边距,您将不得不调整其他尺寸。

相反, padding 放在元素的内部,因此我可以将文章定义为8.5×11,并在其中添加0.5in的 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设计文档。有时这意味着使用flex或grid来布局元素,因为您对输出有一定的灵活性。有时这意味着创建特定大小的正方形以适应某种品牌的贴纸纸张。有时这意味着绝对定位几乎所有内容,因为用户需要将一个特殊的预先标记的纸张通过打印机,然后将您的数据放在其上,而您无法控制该特殊纸张。

这里为了给您提供一般的HTML编写教程,因此您需要自己能够做到这一点。我只能说,要注意您正在处理的是一张纸的有限空间,而不是可以滚动和缩放到任何长度或比例的浏览器窗口。如果您的文档将包含任意数量的项目,请准备通过创建更多的 <article> 进行分页。

带有重复元素的多页文档

我编写的许多打印生成器包含表格数据,比如一张包含许多行项目的发票。如果您的 <table> 足够大以至于需要跨页,浏览器将自动在每页顶部重复 <thead>

<table>
  <thead>
    <tr>
      <th>示例文本</th>
      <th>示例文本</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将页面生成,将表格拆分为几个较小的表格。这里的一般步骤是:

  1. <article> 元素视为可丢弃的,并随时准备从内存中的对象重新生成它们。所有用户输入和配置都应在单独的标题/选项框中进行,位于文章之外。
  2. 编写一个名为 new_page 的函数,它创建一个带有必要重复头/页脚等的新文章元素。
  3. 编写一个名为 render_pages 的函数,它从基本数据创建文章,每次填满上一页时都调用 new_page。我通常使用 offsetTop 来查看内容何时到达页面的末尾,尽管您可以使用更智能的技术来精确地确定每页的适合情况。
  4. 每当基本数据更改时,调用 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);
    // ...
  }
}

通常很好在页面上包括一个 "第X页,共Y页" 的计数器。由于直到生成所有页面后才知道页面数,因此无法在循环中完成此操作。我在最后调用类似这样的函数:

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 规则如何帮助指定浏览器的默认打印设置,但是如果用户想要覆盖它们,您的布局和分页可能会出现问题,特别是如果您在硬编码任何页面阈值。

您可以通过为纵向和横向分别创建独立的 <style> 元素,并使用JavaScript在它们之间切换来适应用户的偏好。也许有更好的方法来做到这一点,但是像 @page 这样的at-rule的行为与普通CSS属性不同,所以我不确定。您还应该保存一些变量,可以帮助您的 render_pages 函数做正确的事情。

您还可以停止硬编码阈值,但那我就得听我的建议了。

<select onchange="return page_orientation_onchange(event);">
  <option selected>纵向</option>
  <option>横向</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,以便用户在打印之前可以做出小的更改。我还喜欢使用真正的、活动的复选框输入,用户可以在打印之前单击。这些功能增加了一些方便性,但在大多数情况下,让用户先更改数据库中的源记录会更明智。此外,它们会限制您将文章元素视为可丢弃的能力。

基本要点速查表

sample_cheatsheet.html

<!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>一些帮助文本,用于解释生成器的目的。</p>
    <p><button onclick="return window.print();">打印</button></p>
  </header>

  <article>
    <h1>示例页面 1</h1>
    <p>示例文本</p>
  </article>

  <article>
    <h1>示例页面 2</h1>
    <p>示例文本</p>
  </article>
</body>
</html>
2024-03-21

访问量 16

扫码关注公众号“前端微志”

第一时间获取新周刊

预览图片