gpdf 的 12 列网格是怎么工作的?
gpdf 的 12 列网格用 r.Col(span, fn) 接收 1–12 的整数。列宽为 span/12 的比例,没有断点、没有槽宽间距,为 PDF 固定宽度设计。
换个说法的问题
你用过 gpdf 的 API——页面构建器、行构建器、列构建器——列的构造函数接收一个数字:r.Col(4, fn)、r.Col(8, fn)。这个数字是什么?总和不到 12 会怎样?和你已经熟悉的 CSS 网格有什么区别?
短答
r.Col(span, fn) 接收 1 到 12 的整数。这个整数代表此列在行里的占比——span / 12 的可用宽度。小于 1 会被夹到 1,大于 12 会被夹到 12,每行总和是否为 12 由你决定,库本身不强制。网格被固定为 12 个分格,剩下的就是你如何切行的问题。
完整示例
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(12, func(c *template.ColBuilder) {
c.Text("发票 #2026-0416", template.FontSize(18), template.Bold())
})
})
// 两列表头 (6 + 6)
page.AutoRow(func(r *template.RowBuilder) {
r.Col(6, func(c *template.ColBuilder) {
c.Text("收票方")
c.Text("Acme 有限公司")
})
r.Col(6, func(c *template.ColBuilder) {
c.Text("开票日期")
c.Text("2026-04-16")
})
})
// 三列摘要 (4 + 4 + 4)
page.AutoRow(func(r *template.RowBuilder) {
r.Col(4, func(c *template.ColBuilder) {
c.Text("小计")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("税额")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("合计")
})
})
// 非对称 (8 + 4) — 正文 + 侧栏
page.AutoRow(func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("明细在这里列出")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("备注")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("layout.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
运行 go run main.go 即得一页 PDF,四行各用不同方式切分。
为什么是 12
12 可以干净地被 2、3、4、6 整除。一半 (6+6)、三等分 (4+4+4)、四等分 (3+3+3+3)、带侧栏 (3+9 或 4+8)、正文加边栏 (8+4)——真实世界的版式几乎全都落在这些组合里。如果选因子少的数字,这些组合里会有一种直接失效。Bootstrap 在 2011 年就定下了 12 列,到今天"12 列网格"已经是设计师和前端工程师共享的行话。gpdf 特意沿用这个习语——PDF 布局和网页布局在思维模型上并无本质差别,哪怕输出的是固定宽度的纸张。
把数学写清楚
A4 纵向、四边各 15 mm 的话,可用宽度是 180 mm。行内的 Col(4) 占 4/12,也就是 60 mm。Col(8) 占 120 mm。列与列之间默认没有槽宽 (gutter)。如果想要留白,就在较短的列里放一个 c.Spacer,或把一个 Col(1) 留空。
宽度在构建期按百分比计算(相关实现在 gpdf/template/grid.go),布局引擎再用"当前页宽减去边距"换算为实际的点值。也就是说同样的 r.Col(6, fn),在 A4 和 Letter 上物理宽度不同,但占行的比例始终不变。
总和不是 12 会怎样
gpdf 并不校验 span 的合计。这是有意的。
- 合计 < 12:行的右侧留空。想让元素贴在左边、其余留白时用得上。
- 合计 > 12:最后一列溢出右边距。通常是 bug。PDF 看起来会错位,但不会 crash。
多数布局正好是每行 12,因为那样能填满页面。但你想"在行中间只放一块 6 宽的内容"时,最直接的写法就是 Col(3) 空、Col(6) 内容、Col(3) 空——这个网格本来就是为这种简写设计的。
AutoRow 与 Row 的区别
page.AutoRow(fn) 的高度随最高的列伸展。大多数行都应该用它。
page.Row(height, fn) 固定高度,超出的内容会被裁剪。用在"发票表头必须刚好 30 mm 以便后续装订对齐"之类的场景——视觉一致性优先于内容自由度。
page.Row(document.Mm(30), func(r *template.RowBuilder) {
r.Col(8, func(c *template.ColBuilder) {
c.Text("Logo")
})
r.Col(4, func(c *template.ColBuilder) {
c.Text("发票号")
})
})
网格没做的事
不支持嵌套。ColBuilder 接收内容元素 (Text / Image / Table / List / Spacer),但不能塞一个子行进去。看似需要嵌套的结构,通常用页面层面的两个兄弟行就能更清爽地表达。
没有偏移列。Bootstrap 的 .offset-2 在 gpdf 里不存在。要把内容推到右边,就在左边放一个空的 Col(n)。
没有断点。PDF 页面不会自适应。在任何设备上打开都是同一版式——因为输出是定坐标的光栅,而不是会重新流式布局的 DOM。
这些"没有"正是设计的核心。网格少一个特性,读 PDF 结果时就少一类需要推理的歧义。
延伸阅读
- 如何在 gpdf 中嵌入日文字体? — 在网格列里处理 CJK
- Go PDF 库横评 2026 — Builder API 与 gofpdf / gopdf / Maroto 的对比
- 布局指南 — 行、列、间距的完整参考
试用 gpdf
gpdf 是一个 Go 语言的 PDF 生成库。MIT 许可、零外部依赖、原生 CJK 支持。
go get github.com/gpdf-dev/gpdf