如何在 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:246 的 c.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 的 12 列网格如何工作? — 网格本身的详细说明
- 50 行以内用 Go 生成发票 PDF — 用扁平网格组一份完整文档
- Layout guide — Row / Col / Box 的完整参考
试用 gpdf
gpdf 是一个 Go PDF 生成库。MIT 许可、零外部依赖、原生 CJK 支持。
go get github.com/gpdf-dev/gpdf