記事一覧

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 / AutoRowPageBuilder 側にしかありません。シンタックスを探してここに来たなら、無いのが答えです。下では代替の 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: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) のような展開しにくい数字を選んでいたなら、それは実際の紙の上でも意味のある比率ではないはず。展開できるフラット版を選びましょう。

なぜネストしないのか

フラットグリッドは幅を 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 行で書ける上にあなたのドキュメントに合わせて書いた版の方が、私たちが用意する版より絶対に良いからです。

関連レシピ

gpdf を試す

gpdf は Go の PDF 生成ライブラリ。MIT、外部依存ゼロ、日本語ネイティブ対応。

go get github.com/gpdf-dev/gpdf

⭐ GitHub で Star · ドキュメントを読む