8

将网页漂亮的打印到纸上的CSS

 6 months ago
source link: https://www.techug.com/post/css-for-printing/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

将网页漂亮的打印到纸上的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>

下面是浏览器中的显示效果:

sample_in_browser.png

下面是不同 @page 值的结果:

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

letter_portrait_1in.png

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

letter_landscape_in1.png

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

letter_landscape_0.png

设置 @page尺寸并不能将该尺寸的纸张放入打印机的进纸托盘。这部分需要您自己完成。

请注意,当我将尺寸设置为 A5 时,我的打印机保持在 Letter 尺寸,而 A5 尺寸完全符合 Letter 尺寸,这就给人一种页边距的感觉,尽管它不是来自页margin设置。

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

a5_portrait_0.png

但是,如果我告诉打印机我装的是真正的 A5 纸张,那么它看起来就和预期的一样。

a5_portrait_0_a5paper.png

根据我的实验,Chrome 浏览器只有在边距设置为默认的情况下才会遵循 @page 规则。一旦你在打印对话框中更改了页边距,你的输出结果就会变成物理纸张尺寸和所选页边距。

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

a5_portrait_0_default.png
a5_portrait_0_none.png

即使你选择的 @page 大小完全适合你的实体纸张,页边距仍然很重要。在这里,我制作了一个不带边距的 5x5 正方形和一个带边距的 5x5 正方形。<html> 元素的大小受 @page 大小和页边距margin的限制。

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

5in_5in_0.png

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

5in_5in_1in.png

我做这些测试并不是因为我希望在 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;
    }
}
mediaprint_1.png
mediaprint_2.png

宽度, 高度, margin, 和 padding (§)

你需要对box model有一定的了解,这样才能获得你想要的边距,而不会让电脑太费力。

box_model.png

我之所以总是设置 @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 生成页面,将表格分割成几个较小的表格。一般的做法是这样的:

  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);
        // ...
    }
}

当基础数据发生变化时,调用 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,这样用户就可以在打印前做一些小的修改。我还喜欢使用真实的复选框输入,用户可以在打印前点击。这些功能会带来一些便利,但在大多数情况下,让用户先更改数据库中的源记录会更明智。此外,这些功能还限制了将文章元素视为一次性元素的能力。

速查表 (§)

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>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>

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK