超出版心的宽表格

表格超出了正文范围——这就是用 LaTeX 的人迟早都会遇到的 Overfull \hbox。来实测一下。article 中一张 12 列的 booktabs 表格宽 404.83pt,而 \textwidth 只有 345pt,日志里写着 Overfull \hbox (74.83432pt too wide)。多数人此时会试 \small,然后发现不够:即便缩到 \scriptsize(7pt),表格仍有 351.90pt,还是超出。原因在于缩小字号并不会缩小列间的空白。本页把让这张表格放上纸面的所有办法排列出来:用 tabularxtabulary 声明宽度,用 tabularraytblr 环境)重新构建,用 \resizeboxadjustbox 缩放,用 pdflscaperotating 把页面转成横向,以及用 \makebox 让它溢入页边空白。

为什么缩小字号还是超出

因为 列间空白 \tabcolsep 是绝对长度,改变字号并不会改变它。12 列的表格中,\tabcolsep(每侧 6pt)会出现在 24 个位置,包括表格的左右外缘。把同一张表用 \tabcolsep=0pt 重排,404.83pt 变成 260.83pt,差值恰好是 144pt,也就是说表宽的 35% 是留白。字号变小时这 144pt 一分不减——实测在 \scriptsize 下留白合计仍是 144pt。所以表格最多只能降到 351.90pt。

做法实测表宽(textwidth = 345pt)结果
(none)404.83ptOverfull \hbox (74.83432pt too wide)
\small385.27pt仍然超出
\footnotesize365.70pt仍然超出
\scriptsize351.90pt降到 7pt 仍然超出
\tabcolsep=3pt332.83pt放得下,且字号仍是 10pt
\resizebox345.01pt放得下,但字号实际缩到 8.52pt

这张表也给出了实务上的处理顺序:先削减留白,再重新审视列的设计,最后才考虑缩放或旋转。仅把 \tabcolsep 设为 3pt,表格就降到 332.83pt,而且字号仍与正文一样是可读的 10pt,所以这应当是首选(\tabcolsep@{} 的细节由「列规格、对齐与宽度」负责)。但可削的留白终究有限。如果罪魁祸首不是数字,而是含有 长段文字的列,那么削留白无济于事,必须让该列本身换行。这就是下一节的内容。

tabularx — 先定宽度,让 X 列去伸缩

tabularx第一个引数就是表格的目标宽度,并让 X 列填满剩余部分:\begin{tabularx}{width}{column spec}X 是一种会自动吸收剩余宽度的 p 列,多个 X 列会 平均分享 这部分宽度。如果溢出的原因是长说明文字,这就是最短的解决路径——把宽度声明为 \textwidth,表格在构造上就不可能溢出。它出自 longtable 的作者 David Carlisle 之手,随 tools bundle 发行,无需另行安装。

它的机制有点特别:tabularx 通过 多次试排表格 来求宽度。先把每个 X 都按完整目标宽度排出(此时表格过宽),再把超出部分按 X 列数量分摊,缩小每个 X 并重排,反复迭代直到收敛。最终 X 会被 \tabularxcolumn 定义的列替换,默认是 \newcommand{\tabularxcolumn}[1]{p{#1}},即 顶部对齐 的段落列。想让内容垂直居中就把它重定义为 m{#1}X 单元格默认两端对齐;若要左齐,在列指定前加 >{\raggedright\arraybackslash}X。关键是 \arraybackslash,它把 \raggedright 等命令对 \\ 含义的修改恢复回来。若同一前处理要反复书写,用 \newcolumntype 起一个单字母别名,列指定会重新变得易读。

document.tex
\usepackage{tabularx}
\newcolumntype{Y}{>{\raggedright\arraybackslash}X}
% ...
\begin{tabularx}{\textwidth}{l Y Y}
  \hline
  Package & Strength & Weakness \\
  \hline
  tabular  & simple and fast              & you compute the widths yourself \\
  tabularx & the table hits a fixed width & slower: the body is set several times \\
  \hline
\end{tabularx}

有两点需要注意。第一,由于表体会被展开多次,tabularx 中的 带副作用命令——推进计数器、写文件、\footnote\verb 等——可能误动作。惯用的修法是把这类单元格包进一个盒子或 minipage。第二,普通 tabularx 没有权重指定:写 X X 必然等分;若想要 2:1,只能用下文 tabularrayX[2],或用 \newcolumntype 手算 p{}

tabulary — 按各列内容多少分配宽度

tabularxX平均 分配,而 tabulary按各列内容的自然宽度成比例 分配。当一列是短标签、另一列是长说明时,平均分配反而难看。语法相同:\begin{tabulary}{width}{column spec},有四种列类型——L 左对齐、C 居中、R 右对齐、J 两端对齐。普通的 lcrp{width} 也可以混用,但它们 不参与测量,保持自然宽度。被测列按该列最宽单元格的自然宽度加权,并缩放到整表达到指定宽度;不过本来就比应得份额更窄的列会保持原样。为防止分配走向极端,被测列受 \tymin(默认 10pt)与 \tymax(默认 2\textwidth)约束——这是防止某个异常长的条目独占列宽的安全阀。

document.tex
\usepackage{tabulary}
% ...
\begin{tabulary}{\textwidth}{L L}
  \hline
  Term & Description \\
  \hline
  X column & Shares the width left over after the fixed columns, equally with the other X columns. \\
  \hline
\end{tabulary}

这里两列都是 L,但内容短的「Term」较窄,满是文字的「Description」得到更大的宽度。若用 tabularxX X,两列会完全等宽;所以对于标签列加正文列的表格,tabulary 看起来更自然。不过 tabulary 同样会多次处理表体,因此对脚注和计数器要同样小心。

tabularray — tblr 环境把宽度和线条统一到一套写法

tabularray 把过去需要 arraybooktabsmultirowcolortbldiagbox 协作才能完成的事,统一进 一个键值接口。作者是 吕建瑞(Jianrui Lyu,lvjr)。它抛弃旧的基于 \halign 的机制,用 LaTeX3(expl3)直接解析并排版表格。TeX Live 2024 附带的版本发布于 2024-02-16,内部版本号 2024A。主环境是 tblr,用 \usepackage{tabularray} 载入。

tblr 的结构与 tabular 略有不同:主体之前要给出 一个由逗号分隔的键值选项组成的引数,即内侧指定,写成 \begin{tblr}{ … inner spec … }。列对齐用 colspec 键声明。熟悉的一字母写法 colspec={lcr} 仍可使用,但真正的力量在 Q[…](通用列,在方括号中传入对齐、宽度等选项)和 X[…](类似 tabularx 的伸缩列)。X[c] 是居中的伸缩列;X[2,l]系数 2(普通 X 两倍宽)的左对齐伸缩列。在溢出这个语境下最令人高兴的是,width=colspec= 可以写在同一对花括号里,而且这些系数能 指定比例。实测中,指定了 width=\textwidthcolspec={X[2,l] X[1,r] X[1,r]}tblr 输出盒宽 正好 345.0pt,与目标宽度完全一致。

X 列扩展的目标宽度由 width 键给出(未指定时为 \linewidth)。行间和列间空白由 rowsepcolsep 键控制,tblr 默认是 rowsep=2ptcolsep=6pt。线条是声明式的:写 hlines / vlines 会画出所有横线 / 竖线,而 hline{n} / vline{n} 可设置特定线条的粗细和类型,如 hline{2}={1pt,solid}Z 是表示最后一行的特殊编号)。主体中不需要写 \hline。常用设置可以用 \SetTblrInner{rowsep=2pt} 统一提取出来。

单元格跨越在主体中用 \SetCell 完成。\SetCell[c=2]{c}跨 2 列 的居中单元格(相当于 \multicolumn),\SetCell[r=2]{c}跨 2 行 的单元格(相当于 \multirow);c=r= 给出列 / 行跨越数,花括号参数给出该单元格的对齐。若要装饰整行或整列,使用 \SetRow\SetColumn,例如 \SetRow{cyan8} 会把行涂成淡青色(颜色名是包内置的)。同样的设置也可以写在内侧指定里,如 row{1}={font=\bfseries}column{2}={halign=r}cell{2}{3}={c=2},这样主体更干净。

document.tex
\usepackage{tabularray}
% ...
\begin{tblr}{
    width        = \textwidth,
    colspec      = {Q[l] X[c] X[2,l]},
    rowsep       = 3pt,
    hline{1,Z}   = {1pt,solid},
    hline{2}     = {0.5pt,solid},
    row{1}       = {font=\bfseries},
  }
  Item & Kind & Description \\
  \SetCell[r=2]{l} Table environment & fixed width   & The table width is fixed; X columns share the rest. \\
                                     & content width & Width is apportioned by how much each column holds. \\
  Note & --- & Rules, colour and spans are all set through keys. \\
\end{tblr}

这个 tblrwidth=\textwidth 把表宽固定为正文宽度。三列分别是 Q[l](左对齐基本列)、X[c](居中伸缩列)和 X[2,l](系数 2、左对齐伸缩列),所以两个 X 列按 1 : 2 分配剩余宽度,第 3 列“Description”最宽。主体中没有 \hline;线条只来自内侧指定中的 hline{…} 键。首行通过 row{1}={font=\bfseries} 加粗,第 1 列的“Table environment”用 \SetCell[r=2]{l} 跨 2 行。无需同时引入 booktabsmultirowarray 就能做到这一步,正是 tabularray 的吸引力。

当表格不仅横向长、纵向也长时,有两个派生环境接手。longtblr 是能跨页的 tblr,支持重复表头、标题和脚注;talltblr 是单页表格,可以像 threeparttable 那样在表下放注记。两者都把标题、标签、note{…} 等写在主体之前的 可选引数 […](外侧指定) 中。longtblr 的表头重复只需 rowhead=1——用数字给出行数——比 longtable 的四个标记更简洁。

resizebox 会在字号上撒谎

graphicx\resizebox{\textwidth}{!}{...} 会把内容整体缩放到指定宽度。这是把表格放上纸面最快的办法,同时也 以一个源文件中根本看不到的比例缩小了字号。实测:把 404.83pt 的表格交给 \resizebox{\textwidth}{!},得到宽 345.01pt 的盒子,高度从 26.07pt 降到 22.22pt。两个比值都是 0.8522。也就是说,你以为是 10pt 的字,在纸上其实是 8.52pt。文档写着 \documentclass[10pt],成品却是 8.52pt——在审稿人或投稿规范会检查字号的场合,这就是事故。

那为什么 \resizebox 放得下,而 \scriptsize(7pt)却放不下?原因与本页开头相同:\resizebox 会连列间留白一起缩小。改变字号一点也减不掉那 144pt 留白,但按 0.8522 缩放会把它压到 122.7pt。这就是为什么表面上 8.52pt 的 \resizebox 竟胜过真实 7pt 的 \scriptsize。反过来说,\resizebox 做出的是一张 连留白也被挤扁 的表格,在可读性上是双重不利。

而且 \resizebox 还有一个更显眼的陷阱:给它一张小表,它会把表放大。实测中,把一张宽 4.44pt 的小表交给 \resizebox{\textwidth}{!},结果是 345.46pt——一个被拉伸约七十八倍的怪物。如果像用 \includegraphics 那样条件反射地给每张表都加上它,窄表就会膨胀。这时 adjustboxmax width=\textwidth 就派上用场:同样的 4.44pt 表格 仍是 4.44pt,只有 404.83pt 的那张缩到 345.01pt。需要时才缩,永不放大——在实际文档中把这个当默认更安全。

latex
\usepackage{graphicx}   % \resizebox
\usepackage{adjustbox}  % \adjustbox and the adjustbox environment
% ...

% scales in BOTH directions -- a narrow table gets blown up
\resizebox{\textwidth}{!}{\begin{tabular}{...}...\end{tabular}}

% only ever shrinks; a narrow table is left alone
\begin{adjustbox}{max width=\textwidth}
  \begin{tabular}{...}...\end{tabular}
\end{adjustbox}

把整页转成横向 — lscape、pdflscape、rotating 的区别

只有 pdflscape 能让页面在 PDF 阅读器中整页转向。 三者都通过 \begin{landscape}sidewaystable 把内容旋转 90 度,但只有 pdflscape 会往 PDF 里写入「此页请旋转显示」的指令,也就是 /Rotate 90。下面是同一份文档用三种方式排版后,用 pdfinfo 检查的结果。

宏包使用的环境pdfinfo 报告的 rot 与纸张尺寸
lscapelandscape所有页 rot: 0,仍是 612 x 792。只有内容旋转
pdflscapelandscape仅该页 rot: 90,612 x 792。阅读器会转过来显示
rotatingsidewaystablerot: 0,612 x 792。作为带编号的浮动体旋转

注意三者的 纸张尺寸都仍是 612 x 792,并没有把纸真的变成横向,旋转的只是内容。由此可定判断。若 PDF 主要在屏幕上阅读,就用 pdflscape,读者不必歪着脖子;pdflscape 会载入 lscape 再加上 /Rotate,所以 lscape 的写法可以原样沿用。若是打印装订,lscape 就够了——转一下纸即可。若希望表格保持为带编号和标题的浮动体,就用 rotatingsidewaystable(图则用 sidewaysfigure)。rotating 还提供 figuresright / figuresleft 选项来选择旋转方向。

document.tex
\usepackage{pdflscape}   % loads lscape and adds /Rotate 90 to the page
\usepackage{rotating}    % sidewaystable / sidewaysfigure
% ...

% a whole page turned sideways in the viewer
\begin{landscape}
  \begin{tabular}{...}...\end{tabular}
\end{landscape}

% a rotated float that still says "Table N"
\begin{sidewaystable}
  \centering
  \begin{tabular}{...}...\end{tabular}
  \caption{A very wide table}
\end{sidewaystable}

让表格溢入页边空白 — 用 makebox 超出正文宽度

正文宽度是 345pt,但纸面还有余地。在 article 的默认设置下,左边距实测 134.27pt,右边距 135.03pt——正文两侧空着 269pt 的白。伸进这片空白的惯用写法是 \noindent\makebox[\textwidth]{...}\makebox 会把内容放进一个宽为 \textwidth 的盒子,若内容更宽,就会 向左右均匀溢出。对于 404.83pt 的表格,每侧伸入页边 29.92pt:表格变宽,而版面的视觉中心保持不变。

有一点需要接受:由于内容超出盒子,TeX 仍会报告 Overfull \hbox这是唯一可以忽略该警告的场合——溢出本来就是有意为之。若只想向一侧溢出,用 \makebox[\textwidth][l]{...} 固定左边缘,让表格只向右伸。若想要更可控的做法,changepage 宏包的 adjustwidth 环境可以在文档的某一段区间内加宽正文块,这更规矩,也能让脚注和页眉保持一致。另外,页边的实际尺寸会随纸张与 geometry 设置而变,所以只要设计上要伸进页边,就务必用实际的 PDF 确认(页边距本身如何设定由「页边距与版面」负责)。

把表格拆开 — 纵向切分,还是行列互换

也有完全不依赖宏包的办法。把列拆成两张表,并在两张表中都重复关键列:对于 12 列的表格,可以把地区名放在两半各自的第一列,再把年份拆成 2–6 和 7–12。读者的视线不必左右往返,字号也与正文相同。既不缩放也不旋转,往往是可读性最好的结果。另一种做法是 行列互换。「12 年 × 5 地区」的表格会横向拉长,而「5 地区 × 12 年」则纵向拉长——这样一来就落进了 longtable 的管辖范围。

到底该用哪一个

选择取决于成因。若溢出的是长文本列,就用 tabularx(需要不等比例时用 tabularrayX[2],想按内容量分配时用 tabulary)——干脆声明宽度是最干净的做法。若是数值列太多,先动 \tabcolsep,再考虑是否真需要同时列出这么多列。若确实必须把所有列放在一页,就用 adjustboxmax width,而不是 \resizebox若连这样都放不下,就用 pdflscape 把页面转过来。 若是新写的表格,直接选 tabularray:宽度、线条、颜色、跨格都在同一套写法里。若表格不仅横向长、纵向也长,就转向 longtblrlongtable。无论走哪条路,最后都请看一眼成品 PDF,问问自己 8.5pt 的数字还读得清吗。

宏包宽度如何确定适合用途
tabularx固定总宽;X 列平均分配适配正文宽度的等宽文本列
tabulary固定总宽;按内容分配内容量不均的标签加正文表
tabularraywidth 键加 X 列(可用系数)统一线条、颜色和跨越的现代表格
adjustbox仅在溢出时缩小(max width)为既有表格设一个安全上限
pdflscape不改变宽度,把页面旋转 90 度无论如何都无法竖排放下的表格

列指定本身的细节(\multicolumnarray 宏包的 >{…} 等)由「列规格、对齐与宽度」负责,合并单元格的基础由「合并单元格」负责,跨页长表由「longtable」页面负责。