如何让表格跨多页输出?
什么都不用做。给 gpdf 传一个行数超过一页的表格,它会自动把表体分页,并在每一页顶部重复表头。
换个说法
我有一份报表 —— 发票明细、交易日志、300 行的导出 —— 显然装不进一张 A4。在 Go 的 PDF 库里,要让表格流到第 2 页、第 3 页,并且每页顶部重新出现表头,我得做什么? 在 gpdf 里,答案很短。
结论
什么都不用做。写一次 Table 调用,把所有行都给它,gpdf 就会分页:
c.Table(header, rows) // rows 有 300 条 —— gpdf 会把它分到多页
表体会逐行分到所需的页数上。header 切片会自动在每个续页顶部重新渲染 —— 列宽、样式都一样。没有 PageBreak() 方法,没有 MaxRowsPerPage 选项,没有数行的循环。处理溢出是布局引擎的活,不是你的活。
可运行代码
一个输出跨页表格的完整程序。存为 main.go,运行 go run .,得到 report.pdf。
package main
import (
"fmt"
"log"
"os"
"github.com/gpdf-dev/gpdf"
"github.com/gpdf-dev/gpdf/document"
"github.com/gpdf-dev/gpdf/pdf"
"github.com/gpdf-dev/gpdf/template"
)
func main() {
doc := gpdf.NewDocument(
gpdf.WithPageSize(gpdf.A4),
gpdf.WithMargins(document.UniformEdges(document.Mm(20))),
)
brand := pdf.RGBHex(0x1A237E)
header := []string{"Date", "Invoice #", "Customer", "Amount"}
rows := make([][]string, 0, 200)
for i := 1; i <= 200; i++ {
rows = append(rows, []string{
fmt.Sprintf("2026-%02d-%02d", (i%6)+1, (i%28)+1),
fmt.Sprintf("INV-%05d", 10000+i),
fmt.Sprintf("Customer #%d", i),
fmt.Sprintf("$%d.00", 100+i*7),
})
}
page := doc.AddPage()
page.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Text("2026 Invoice Ledger", template.FontSize(18), template.Bold())
c.Spacer(document.Mm(4))
c.Table(header, rows,
template.ColumnWidths(20, 20, 40, 20),
template.TableHeaderStyle(
template.TextColor(pdf.White),
template.BgColor(brand),
),
)
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile("report.pdf", data, 0o644); err != nil {
log.Fatal(err)
}
}
200 行铺到 A4 上大约是 8 页。每一页顶部都坐着深蓝色的表头;表体从上一页停下的地方接着来。这段代码里唯一暗示「跨页」的,只有循环上界的 200。
它是怎么工作的
值得了解一下,这样你才会信任它。布局引擎布局表格时,按顺序测量表体行,一直往当前页里加,直到下一行会超出可用高度。没装下的行变成一个溢出表格 —— 一个携带相同 Header、相同 Footer 和剩余表体行的新 *document.Table。gpdf 把已布局的部分刷到页面上,开下一页,再用新页的高度把溢出表格喂回布局引擎。重复,直到没有剩余。
由此得出两件事:
- 表头会重复,是因为它在
tbl.Header里,不在你的循环里。 溢出表格复用同一个切片,所以它在每页上渲染得一模一样。这是白送的。 - 不存在「表头装不下」这种边界情况。 引擎在测量能装几行表体之前就为表头预留了高度。如果一页装不下表头加至少一行表体,整个表格会被推到下一页,而不是被尴尬地切开。
页脚也会重复
如果想要一个合计行(或「本页小计」)也出现在每页底部,那就是 document.Table.Footer —— 在 document 层而不是通过 builder 构建表格时可用:
import "github.com/gpdf-dev/gpdf/document"
tbl := &document.Table{
Columns: []document.TableColumn{
{Width: document.Pct(20)}, {Width: document.Pct(20)},
{Width: document.Auto}, {Width: document.Pct(20)},
},
Header: headerRows, // []document.TableRow
Body: bodyRows,
Footer: []document.TableRow{footerRow},
}
Footer 切片在每个续页上重复,机制和表头一样。builder 的 c.Table(...) 没暴露页脚,是因为大多数短表格不需要 —— 一旦需要,你就已经离开了「常见情况」区。表格深入讲解会走一遍 document 层。
强制表格从新页开始
没有针对单个表格的「从新页开始」选项。在页级别做 —— 在持有表格的那一行之前加一页:
doc.AddPage() // 下面的表格从这一页顶部开始
page2 := doc.AddPage()
page2.AutoRow(func(r *template.RowBuilder) {
r.Col(12, func(c *template.ColBuilder) {
c.Table(header, rows /* , opts... */)
})
})
表格需要的「分页」控制就这一个,因为表格内部的分页已经替你处理好了,外部的分页只不过是「这个块从哪开始」。
做不到的事
- 「这几行保持在一起」。 每一行表体都可被切分。没有「第 4–7 行这组必须在同一页」这样的注解。这是已知的缺口。如果一条发票明细加它的子行绝对不能被跨页撕开,变通办法是在那组之前开新页,或在 document 层构建表格并插入自己的分页提示。
- 只在最后一页的页脚。
document.Table.Footer按设计在每一页重复(每页列合计是常见情况)。文档末尾要一次性的总计,就在表格之后作为单独的块加进去,而不是放在里面。 - 表格里的页码。 「第 3 页/共 8 页」属于文档页脚,不属于表格。它放在哪见页码、页眉和页脚。
浪费十分钟的错误
- 去找
PageBreak选项。 没有,你也不想要 —— 你在手动调它的时候就已经输了。把所有行都传进去就行。 - 自己把数据切成每页一块。 有人在第 1 页
rows[0:40]、第 2 页rows[40:80]…… 别。你会在某处算错行,最后一页会变短,表头样式会漂移。把整个切片交给 gpdf。 - 以为表头只在第 1 页。 有些库是那样。gpdf 在每一页重复,这正是给打印出来翻阅的报表所需要的。
- 150 页的表格配 6 MB 的 CJK 字体。 字体会被子集化到实际用到的字形,所以没问题 —— 输出仍然很小。但如果你出于某种原因关掉了子集化,长表格就是它咬人的地方。把子集化保持开启(默认)。
相关菜谱
- Go PDF 里的表格:列宽、斑马纹、分页 —— 含
document.Table和页脚的长文。 - 如何为表格设置自定义列宽? ——
ColumnWidths的边界情况。 - 如何给表格加斑马纹行? —— 以及斑马纹为何跨页仍然对齐。
- 用 Go 在 50 行以内生成发票 PDF —— 一份含分页表格的真实文档。
试试 gpdf
gpdf 是一个 Go 的 PDF 生成库。MIT 许可、零外部依赖、原生支持 CJK。
go get github.com/gpdf-dev/gpdf