全部文章

如何在 gpdf 的 Col 中嵌套一个 Row?

不能。ColBuilder 上没有 Row 方法,gpdf 的 12 列网格刻意保持扁平。这里给出替代嵌套 Row 的三种惯用写法。

换种问法

你习惯了 Bootstrap 或 Tailwind,.row 里面套 .col,再套 .row,网格可以一直级联。坐下来用 gpdf,看到同样的 r.Col(span, fn) 语法,就在列回调里去找 c.Row(...)。补全没出来。这是漏写吗?

一句话回答

不是。gpdf 的 12 列网格是有意保持扁平的。 ColBuilder 只接受内容 — Text / Image / Table / Box / List / Spacer — 而 Row / AutoRow 只在 PageBuilder 上。如果你来这里找语法,答案是没有。下面给你三种替代。

API 的实际样子

ColBuilder 的方法集(来自 gpdf/template/grid.go):

func (c *ColBuilder) Text(text string, opts ...TextOption)
func (c *ColBuilder) Image(src []byte, opts ...ImageOption)
func (c *ColBuilder) Box(fn func(c *ColBuilder), opts ...BoxOption)
func (c *ColBuilder) Table(header []string, rows [][]string, opts ...TableOption)
func (c *ColBuilder) Line(opts ...LineOption)
func (c *ColBuilder) List(items []string, opts ...ListOption)
func (c *ColBuilder) Spacer(height document.Value)
// …PageNumber, TotalPages, RichText, QRCode, Barcode

没有 Row,没有 AutoRow,也没有 Col。Col → Row 这条路径作为方法不存在,最接近的是 c.Box(fn, ...),但它接收的还是一个 *ColBuilder,不是 Row。你可以把列套进列里(通过 Box 半模拟),但你不能在一列里打开一行新的横向布局。这就是约束。

惯用写法 1 — 页面层的兄弟 Row

写下"嵌套 Row"的场合,90% 实际上要的是这个。

package main

import (
    "log"
    "os"

    "github.com/gpdf-dev/gpdf"
    "github.com/gpdf-dev/gpdf/document"
    "github.com/gpdf-dev/gpdf/template"
)

func main() {
    doc := gpdf.NewDocument(
        gpdf.WithPageSize(document.A4),
        gpdf.WithMargins(document.UniformEdges(document.Mm(15))),
    )
    page := doc.AddPage()

    // 想写但写不了:
    //
    //   page.AutoRow(func(r *template.RowBuilder) {
    //       r.Col(8, func(c *template.ColBuilder) {
    //           c.Row(...) ❌ 不存在
    //       })
    //   })

    // 实际这么写:
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("文章标题", template.FontSize(18), template.Bold())
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("2026-05-16")
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(8, func(c *template.ColBuilder) {
            c.Text("导语段落占据同样 8 宽的列。")
        })
        r.Col(4, func(c *template.ColBuilder) {
            c.Text("作者:Taiki Noda")
        })
    })

    data, err := doc.Generate()
    if err != nil {
        log.Fatal(err)
    }
    _ = os.WriteFile("flat.pdf", data, 0o644)
}

两个 AutoRow 共享同样的 8+4 跨度,所以视觉上列是对齐的。这不是子网格,而是两行扁平 Row 用了同样的分割。和 CSS 里 .col-8 内嵌 .row 渲染出来的输出是一样的 — 因为嵌套原本买到的只是语法上的局部性,gpdf 把这份预算用在了"列宽一致性"上。

惯用写法 2 — 视觉分组用 c.Box

如果真实动机是"在这列里画一个带边框的卡片,里面叠两行内容",那你要的是 Box,不是子 Row:

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("账单地址", template.Bold())
            c.Text("艾克姆有限公司")
            c.Text("上海市浦东新区")
        },
            template.WithBoxBorder(template.Border(
                template.BorderWidth(document.Pt(1)),
                template.BorderColor(pdf.RGBHex(0xBDBDBD)),
            )),
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Box(func(c *template.ColBuilder) {
            c.Text("收货地址", template.Bold())
            c.Text("同账单地址")
        },
            template.WithBoxPadding(document.UniformEdges(document.Mm(4))),
        )
    })
})

Box 接收的 *ColBuilder 内部是纵向堆叠。Box 不能水平拆分 — 要水平拆,回到惯用写法 1。但对"卡片"模式来说,这就是正确工具。grid.go:246c.Box 是网格允许的唯一一种嵌套,而且它是有意的一维。

惯用写法 3 — 把子网格直接展平到 12 列

有时你真的想在"页面左半"里做 2 列:缩略图 + 说明放左半,正文放右半。直觉是 Col(6) > Row > Col(6) + Col(6)。展平等价就是 Col(3) + Col(3) + Col(6)

page.AutoRow(func(r *template.RowBuilder) {
    r.Col(3, func(c *template.ColBuilder) {
        c.Image(thumbBytes)
    })
    r.Col(3, func(c *template.ColBuilder) {
        c.Text("Photo by Ansel Adams", template.Italic())
        c.Text("1942")
    })
    r.Col(6, func(c *template.ColBuilder) {
        c.Text("正文段落占据页面的右半。")
    })
})

3 + 3 加起来等于 6,缩略图 + 说明刚好占左半。12 能整除 2、3、4、6,所以嵌套网格几乎总能干净展平。如果你的嵌套是 Col(8) > Row > Col(7) + Col(5),那确实展不平 — 但这些数在真实文档里也没什么意义。选能展平的版本。

为什么不让嵌套

扁平网格一次解析宽度。Row 是 (页宽 − 边距) 的百分比,每个 Col(span) 是它的 span / 12。就这样。没有递归,没有宽中宽中宽,不用把父上下文穿过整个布局引擎。grid.go 里计算列宽的那行字面上只有一行:

Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),

加上嵌套,这行就变成树遍历。Col(12) 里的 Col(8) 里的 Col(6),那个 6 是父列的 50%、Row 的 50%、还是页面的 50%?Bootstrap 选了"父的 50%",然后为了能用,加了断点和槽宽 (gutter)。PDF 没有断点。PDF 也没有流式容器。借嵌套语法等于把三个我们本来不需要解决的问题引进来,换的只是用不上的语法糖。

"我就是想要语法局部性"

合理。展平的副作用是:两个概念上属于一起的 AutoRow,改着改着会在源码里漂开。一个小辅助函数就能补上:

func card(page *template.PageBuilder, title, body string) {
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(title, template.Bold())
        })
    })
    page.AutoRow(func(r *template.RowBuilder) {
        r.Col(12, func(c *template.ColBuilder) {
            c.Text(body)
        })
    })
}

局部性在你的函数里,不在 API 里。gpdf 不内置 card,因为它只有三行,而你自己写的版本一定比我们准备的更贴合你的文档。

相关菜谱

试用 gpdf

gpdf 是一个 Go PDF 生成库。MIT 许可、零外部依赖、原生 CJK 支持。

go get github.com/gpdf-dev/gpdf

⭐ 在 GitHub 上 Star · 阅读文档