Col の中に Row をネストする方法は?
できません。gpdf の ColBuilder に Row メソッドはなく、12 カラムグリッドは意図的にフラットです。代わりに使う 3 つのイディオムを示します。
質問を別の言葉で
Bootstrap や Tailwind なら .row の中に .col、その中にまた .row、と自由にネストできる。gpdf でも同じ r.Col(span, fn) の文法が見えるので、Col のクロージャの中で c.Row(...) と書いてみる。補完が出ない。これは設計漏れですか?
結論
違います。gpdf の 12 カラムグリッドは意図的にフラットです。 ColBuilder が受け付けるのはコンテンツだけ — Text / Image / Table / Box / List / Spacer — で、Row / AutoRow は PageBuilder 側にしかありません。シンタックスを探してここに来たなら、無いのが答えです。下では代替の 3 イディオムを紹介します。
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, ...) — ですが Box が受け取るのもまた *ColBuilder で、Row ではありません。カラムをカラムに入れることは(Box 経由で擬似的に)できても、カラムの中に横並びの行を開くことはできません。これが制約です。
イディオム 1 — ページレベルの兄弟 Row
「ネスト Row」と書きたくなった場面の 9 割はこれで足ります。
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("著者: ノダ タイキ")
})
})
data, err := doc.Generate()
if err != nil {
log.Fatal(err)
}
_ = os.WriteFile("flat.pdf", data, 0o644)
}
2 つの AutoRow が同じ 8+4 を共有しているので、視覚的にはカラムが揃って見えます。サブグリッドではなく、フラットに並んだ Row が同じカラム配分を使っているだけ。CSS で .col-8 の中に .row を入れたときと出力は同じです — そもそもネストが買っていたのは構文上の局所性だけで、gpdf はその予算を「カラム幅の一致」に振り直しています。
イディオム 2 — グルーピングは c.Box
「このカラムの中に枠線付きの箱を置いて、中に 2 行積みたい」が本当の動機なら、欲しかったのは Box です:
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 に戻ります。ただし「カード」状の見た目を欲しがってネスト Row を書こうとしたなら、正解はこれです。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) のような展開しにくい数字を選んでいたなら、それは実際の紙の上でも意味のある比率ではないはず。展開できるフラット版を選びましょう。
なぜネストしないのか
フラットグリッドは幅を 1 パスで解決できます。Row は (ページ幅 − マージン) のパーセンテージ、各 Col(span) はその span / 12。これだけ。再帰なし、幅 of 幅 of 幅 なし、レイアウトエンジンに親コンテキストを引き回す必要なし。grid.go でカラム幅を計算している行は文字通り 1 行です:
Width: document.Pct(float64(col.span) / float64(gridColumns) * 100),
ネストを許すと、この 1 行が木の探索になります。Col(12) の中の Col(8) の中の Col(6) の 6 は親カラムの 50% なのか、Row の 50% なのか、ページの 50% なのか — その判断が必要になる。Bootstrap は「親の 50%」を選び、それを耐えうるものにするためにブレイクポイントとガター (gutter) を入れました。PDF にはブレイクポイントがありません。流動コンテナもありません。ネストの構文を借りると、解決すべきでない 3 つの問題を、必要のない構文糖と引き換えに抱え込むことになります。
「でも構文の局所性が欲しい」
分かります。フラットにする欠点は、概念的に一緒の 2 つの 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 を同梱しないのは、3 行で書ける上にあなたのドキュメントに合わせて書いた版の方が、私たちが用意する版より絶対に良いからです。
関連レシピ
- 12 カラムグリッドの動き方 — グリッド本体の詳しい説明
- 請求書 PDF を Go で 50 行以内に作る — フラットグリッドでドキュメント 1 枚を組む例
- Layout guide — Row / Col / Box のリファレンス
gpdf を試す
gpdf は Go の PDF 生成ライブラリ。MIT、外部依存ゼロ、日本語ネイティブ対応。
go get github.com/gpdf-dev/gpdf