LaTeX表格合并单元格的宽度指定方法

发布于 2022-06-05 17:23:28

LaTeX 的表格绘制,大多数使用者都处于并将长期处于知其然不知其所以然的阶段。往往就是简单的表会画,稍微复杂一点,只能去网上搜怎么写,帖子倒是有很多,就是代码抄过来用不了。甚至费很大功夫,把一个个宏包英文文档看完,还是似懂非懂。要不就是用 table generator 之类的网站,机器生成的代码丑就不说了,太复杂的效果还是整不出来。所以啊,了解一点基本原理还是很有必要的。

关于 LaTeX 表格的大致介绍,可以先看LaTeX下的表格处理 - 知乎这篇神文。阿玲写的东西向来很好,如果没有阿玲这样高的水平,很难在写得简明易懂的同时不出知识性错误。文章写于2014年,8年间的改变是 tabu 宏包由于 LaTeX 内核的破坏性更新已经不能用了,其替代者 tabularray 可以解决表格绘制的绝大多数问题。这里是耿楠老师翻译的中文文档:https://gitee.com/nwafu_nan/tabularray-doc-zh-cn。前提是不能违背基本法,要是“第一行第一列的的单元格是第一行第二列宽度的50%,第一行第二列宽度又是第一行第一列的50%”这种现实中不可能实现的要求,那什么宏包都无法达成您的期望。

有很多离谱的要求,可能比上面提到的例子更隐蔽,其中几条要求分开都合理,合在一起就不可能做到。宏包代码无法判断各种要求组合是否合理(总不能弄出个停机问题),不可避免用户有的需求,排版失败,LaTeX 还往意识不到出错的真正原因,结果就是报错信息人类完全看不懂。所以进阶用户有必要去了解表格绘制的流程。否则像tabularx如何用在合并单元格中这样的问题,光是尝试基本上不太可能猜出解决方法。

绝大多数表格实现,如 LaTeX 默认提供的tabular 环境、longtable宏包的longtable环境、tabularx宏包的tabularx环境等等,基本原理都是依赖 TeX 的原始命令halign,这一类表格环境的问题是既有上层抽象的行列,又有底层的对齐处理机制,LaTeX 试图隐藏底层实现却没有做到完全隔离,导致结果常常不可控;与之相对的是tabularray 宏包,它的方案是先检查一遍表格内容,算好尺寸后拿段落盒子手动拼一个表格出来,各种效果都比较完美,代价是编译更慢以及一些兼容问题。

一、\halign 的基本语法

\halign{-\hfil#-&-\hfil#\hfil-&-\fbox{#}\hfil-\cr
  1 & 2 & 3\cr
  Alpha & Beta & Gamma \cr}

image.png

\halign的行以\cr结束,其中第一行叫做“模版”。

  • 排版的时候,TeX 用每一栏的内容去依次替换模版里的#,于是上面的代码会变成如下形式
-\hfil 1-&-\hfil 2\hfil-&-\fbox{3}\hfil-\cr
-\hfil Alpha-&-\hfil Beta\hfil-&-\fbox{Gamma}\hfil-\cr
  • 然后 TeX 计算栏宽,每一栏的宽度是所有行中该栏宽度的最大值。

    • 把第一行第一栏装在一个盒子里:\hbox{-\hfil1-},假设其宽度是w1。第二行第一栏装成\hbox{-\hfil Alpha-} ,宽度是w2。因为w2>w1,第一栏的宽度就是w1。
    • 再把每一行第一栏的盒子伸缩到w2。因为第一行宽度不够,\hbox to w2 {-\hfil1-}里的\hfil粘连就伸长到w2-w1。(hbox 是为了说明方便假设的,不代表这个阶段真的存在这么一个盒子)
    • 这之后在第一二栏添加宽度为\tabskip的粘连。
    • 依次处理第二栏、第三栏……

不引起歧义的情况下,本文通常用“栏”来表示\halign的各列,而用“列”来表示表格的各列。于是1是表格第一行第一列的内容,而-\hfil1-是第一行第一栏的内容。

二、tabular 环境

\begin{tabular}{|lcr|}
1 & 2 & 3\\
x & y & z\\
\end{tabular}

首先从对齐说明lcr生成\halign的模版。

  • 左对齐l\hfil#
  • 右对齐r#\hfil
  • 居中对齐c\hfil#\hfil
  • p{2cm}\parbox{2cm}{#}
  • |\vrule width\arrayrulewidth

试在表格里用\show\@preamble查看模板的内容:

\everycr {}\tabskip \z@skip \halign \@halignto \bgroup
\relax \unhcopy \@arstrutbox
\hskip -.5\arrayrulewidth \vrule width\arrayrulewidth \hskip -.5\arrayrulewidth
\hskip \tabcolsep {\hskip 1sp\ignorespaces \@sharp \unskip \hfil }\hskip \tabcolsep 
&\hskip \tabcolsep {\hfil \hskip 1sp\ignorespaces \@sharp \unskip \hfil }\hskip \tabcolsep
&\hskip \tabcolsep {\hfil \hskip 1sp\ignorespaces \@sharp \unskip }
\hskip \tabcolsep \hskip -.5\arrayrulewidth \vrule width\arrayrulewidth \hskip -.5\arrayrulewidth
\tabskip \z@skip \cr

解释一下,

  • \tabskip \z@skip\tabkskip设为0pt。
  • \@halignto 要么是空白,要么在tabular*环境中是to <width>,表示\halign整体的宽度。
  • \bgroup就是一个左括号。
  • \@arstrutbox是一个支撑盒子,用来撑起每行的高度,保证即使某行内容为空表格也有基本的高度。前面的\relax不用管,是一个展开技巧的结果,在这里没有什么用。
  • \arrayrulewidth典型地是0.4pt,跟TeX默认的线宽相等。
  • \hskip -.5\arrayrulewidth \vrule width\arrayrulewidth \hskip -.5\arrayrulewidth就是画竖线的代码,它使框线不占宽度。现在普遍认为这个做法不妥当,array宏包会把它改为\vrule width\arrayrulewidth,所以加载array宏包后线间距会变大一点。
  • \hskip\tabcolsep用来生成列与列的粘连,LaTeX把\tabskip设为0而用\tabcolsesp来控制,否则横线会断开。
  • {\hskip 1sp\ignorespaces \@sharp \unskip \hfil }里的\@sharp就是#,所以这就是{#\hfil},表示左对齐。
    • \hskip 1sp的1sp是一个极小极小的尺寸,可以视为0pt。其目的是如果用户在单元格开头写了\unskip,避免\tabcolsep被吃掉。曾经的实现就是0pt,改成1sp是为了配合LaTeX内部命令\@bsphack, \@esphack的判断。
  • 最后还有一个\tabskip \z@skip。有的时候会在表格模板中修改\tabskip,而 TeX 会在第一栏左边和最后一栏右边插入制表粘连。这里的作用是保证前后的粘连都是0。

★★ 为什么模板里{\hskip 1sp\ignorespaces \@sharp \unskip \hfil }要用括号包起来?这是因为 LaTeX 的部分字体相关命令定义中含有\aftergroup,如果这样的命令出现在最后一行的最后一栏,就会将紧随\aftergroup的token插入到\cr后,导致\halign的内容没有以\cr结尾。

这个“最后一行的最后一栏”,实际可以是任意一栏,比如

\halign{#&#\cr
a\aftergroup b\cr}

虽然模板有两栏,但这行中第二栏没有出现,第二栏的模板就不会用到,而第一栏就成了这一行的“最后一栏”。
同样地,如果一行的&个数不够,就会出现单元格没填的情况

\begin{tabular}{|c|c|c|}\hline
1 & 2 & 3\\\hline
x & y \\\hline
\end{tabular}

image.png

三、跨栏的命令 \multicolumn

3.1 \span\omit

TeX 底层有两个命令\span\omit。当 TeX 查找栏的内容时,把&, \span\cr视为一栏结束的分隔符。\span&的地位一样,它的意义是将左右两栏的内容合并成一栏。

\halign{*#&#-\cr
x\span y\cr}

的结果就是*xy-

\omit如果出现在一栏开头,就表示忽略这一栏的模板

\halign{&*#*\cr
1&\omit 2\cr}

的结果是*1*2

模板开头的&表示循环。一般地,模板一栏里如果遇到多余的&,就表示后边的部分无限重复,如&#1&#2&#3就表示#1&#2&#3&#1&#2&#3&#1&#2&#3...#0&&#1&#2&#3表示#0&#1&#2&#3&#1&#2&#3&#1&#2&#3...

TeX 为了判断一栏开头是否为\span,\omit,\cr,&,会不断展开第一个 token 直到遇到无法展开的记号,如前面的\relax \unhcopy \@arstrutbox,其实是\@arsturt在构建模板的时候被\edef展开的结果,其定义为

\def\@arstrut{\relax\ifmmode\copy\@arstrutbox\else\unhcopy\@arstrutbox\fi}

如果我们在某一栏开头有这样的代码

%\usepackage{array}
\begin{tabular}{>{$}c<{$}}
\ifmmode X\else Y\fi
\end{tabular}

展开的结果就是Y,因为\ifmmode在替换模板之前就被展开了。它的前面如果加了一个\relax,展开就在遇到\relax时停止,送到模板里变成$\relax\ifmmode...$,最终\ifmmode展开的结果为X

3.2 \multicolumn的实现

\begin{tabular}{|c|c|c|}
\hline
1 & 2 & 3\\ \hline
\multicolumn{2}{|c|}{xy} & z\\ \hline
\end{tabular}

\multicloumn首先展开成\omit\span\omit...\span\omit,跨 n 栏就是 n 个\omit夹着 n-1 个\span

第一个\omit忽略第一栏的模板,\span合并第二栏,第二个\omit忽略第二栏的模板……。

然后将multicolumn的对齐说明像前面所说生成表格模板一样去解析,并去跨列的内容替换临时模板里的#,比如|c|就是

\def\@sharp{xy}
\hskip -.5\arrayrulewidth \vrule width\arrayrulewidth \hskip -.5\arrayrulewidth
\hskip \tabcolsep {\hfil \hskip 1sp\ignorespaces \@sharp \unskip \hfil }\hskip \tabcolsep
\hskip -.5\arrayrulewidth \vrule width\arrayrulewidth \hskip -.5\arrayrulewidth

默认的模板生成算法中,每一根框线属于其左方的一栏,除了第一列左边的,它属于第一栏。|c|c|c|就是对齐说明分别为|c|, c|, c|的三列。

\multicloumn的时候也要遵循同样的原则,上面的例子中\multicolumn忽略了第一栏左右、第二栏右边的三根框线,所以合并栏两边的框线都要指定出来。

3.3 \halign宽度的确定

  1. 如果没有合并栏的存在,TeX将每一栏的宽度设为所有行中这一栏最大的宽度。
  2. 如果存在合并的栏,首先筛选出所有合并的终点,然后从左到右,每一个以第 j 栏为终点的合并栏,如果某行第 i 栏到第 j 栏合并为一个盒子,就用它的最终宽度减去第 i, i+1, ...j-1 栏及各自紧随其后的\tabskip,当作这一行第 j 栏的宽度,然后取所有第 j 栏的最大值。

于是

\begin{tabular}{|c|c|c|c|}
\hline
\multicloumn{2}{|c|}{1} & 2 & 3\\ \hline
\multicloumn{3}{|c|}{abcdefghijkmnopqrtuvwxy} & z\\ \hline
\end{tabular}

image.png

可以看出合并单元格多余的宽度都放在其合并的最后一栏了。

首先两行合并的结果是第一行的栏分为1-2, 3, 4,第二行1-3, 4。所有合并的终点是2, 3, 4。

  • 第一栏忽略掉。
  • 以第二栏为终点的只有\multicloumn{2}{|c|}{1},所以第二栏宽度就是它的宽度w12。
  • 以第三栏为终点的有2\multicloumn{3}{|c|}{abcdefghijkmnopqrstuvwxy},合并单元格的宽度换成其原本的宽度w23减去w2+tabskip,由于w23-w2-tabskip>w13,所以w3=w23-w2-tabskip。
  • 以第四栏为终点的有3z,w4就是max(w14,w24)。

四、tabularx环境

tabularx提供了一个新的对齐说明符X
用lshort-zh-cn里的例子

% \usepackage{tabularx}
\begin{tabularx}{14em}%
{|*{4}{>{\centering\arraybackslash}X|}}
\hline
A & B & C & D \\ \hline
a & b & c & d \\ \hline
\end{tabularx}

image.png

首先设置了总宽度14em,第一趟tabularx就把X当成p{14em}来试排版,于是格式就是

|>{\centering\arraybackslash}p{14em}|>{\centering\arraybackslash}p{14em}|>{\centering\arraybackslash}p{14em}|>{\centering\arraybackslash}p{14em}|

tabularx宏包自动加载了array包,所以框线有宽度,每条0.4pt;每格两个\col@sep(相当于原来的\tabcolsep)各6pt,试排出来的表格总共宽14em+4*2*6pt+0.4*5=610.00082pt,

从试探的宽度减去超出的长度除去X栏的个数:14em-(610.00082pt-14em)/4= 22.50006pt。

第二趟就把X当成p{22.50006pt}来排。

五、一些例子

想画如下表格

image.png

直接想法就是等分成6栏,前两行每格占2栏,第三行每格占3栏。

\newcolumntype{Y}{>{\centering\arraybackslash}X}
\begin{tabularx}{9cm}{|XXXXXX|}
\hline
\multicolumn{2}{|Y|}{\textbf{A}} & \multicolumn{2}{Y|}{\textbf{Table}} & \multicolumn{2}{Y|}{\textbf{B}} \\ \hline
\multicolumn{2}{|Y|}{$DC_A$}     & \multicolumn{2}{Y|}{$S$}            & \multicolumn{2}{Y|}{$DC_B$}     \\ \hline
\multicolumn{3}{|Y|}{\textbf{Common Ground} \textit{cg}}              & \multicolumn{3}{Y|}{\textbf{Projected Set} \textit{ps}}              \\ \hline
\end{tabularx}

image.png

显然结果不对。

环境里总共有14个Xtabularx没有区分哪些是导言中的哪些在表格正文(其实是没想到用户这么鸡贼\multicolumn里也用X,它以为导言里就写了14个,而某些列全部被\multicolumn合并后忽略了X),所以除以均分的栏数的时候从14开始,每次减一来逐个试探。而第一二行X栏总长度为3\TX@col@width,第三行为2\TX@col@width,尝试12次之后,将多余的宽度分成3分刚好可以达到最终宽度,即\TX@col@width = \TX@target-(第一次试排版总宽度-\TX@target)/3

问题来了,

  • \halign中没有以第一栏为终点的,忽略;
  • 第二栏,一个X宽度;
  • 第三栏,\multicolumn{3}{|Y|}{\textbf{Common Ground} \textit{cg}}宽度-w2=0pt,宽度为0,由于右框线属于这一栏,就画在宽度为0的第三栏右边,与第二栏右边界对齐;
  • 第四栏宽度为一个X宽度;
  • 第五栏忽略;
  • 第六栏,前两行给出一个X宽度,第三栏给出\TX@col@width减去第四栏的\TX@col@width还是0,最后宽度以前两行为准。

可见在跨栏里用X会导致意想不到的结果,如果不用会怎样呢?

\begin{tabularx}{9cm}{|XXXXXX|}
\hline
\multicolumn{2}{|c|}{\textbf{A}} & \multicolumn{2}{c|}{\textbf{Table}} & \multicolumn{2}{c|}{\textbf{B}} \\ \hline
\multicolumn{2}{|c|}{$DC_A$}     & \multicolumn{2}{c|}{$S$}            & \multicolumn{2}{c|}{$DC_B$}     \\ \hline
\multicolumn{3}{|c|}{\textbf{Common Ground} \textit{cg}}              & \multicolumn{3}{c|}{\textbf{Projected Set} \textit{ps}}              \\ \hline
\end{tabularx}

image.png

试排版第一趟宽度比最终总宽度还小,不再尝试直接排出结果,并报警告Underfull \hbox

  • 同样,第一栏忽略;
  • 第二栏,前两行第一格自然宽度的最大值;
  • 第三栏,第三行第一格的自然宽度-w2;
  • 第四栏,前两行第二格的最大自然宽度-w3,宽度为负;
  • 第五栏忽略;
  • 第六栏,第三行第二格的自然宽度-w4。
  • 再右边横框线继续补足到目标总宽度。

分析到这里,正确的办法已经呼之欲出了:

\newcolumntype{Y}{>{\centering\arraybackslash}X}
\begin{tabularx}{9cm}{|cccccc|}
\hline
\multicolumn{2}{|Y|}{\textbf{A}} & \multicolumn{2}{Y|}{\textbf{Table}} & \multicolumn{2}{Y|}{\textbf{B}} \\ \hline
\multicolumn{2}{|c|}{$DC_A$}     & \multicolumn{2}{c|}{$S$}            & \multicolumn{2}{c|}{$DC_B$}     \\ \hline
\multicolumn{3}{|>{\hsize=1.5\hsize\linewidth=\hsize}Y|}{\textbf{Common Ground} \textit{cg}} &
\multicolumn{3}{>{\hsize=1.5\hsize\linewidth=\hsize}Y|}{\textbf{Projected Set} \textit{ps}}              \\ \hline
\end{tabularx}

>{\hsize=1.5\hsize\linewidth=\hsize}Y也可以写成>{\centering\arraybackslash}p{1.5\csname TX@col@width\endcsname}

分四栏也是一样的

\newcolumntype{Y}{>{\centering\arraybackslash}X}
\begin{tabularx}{15cm}{|Y|cc|Y|}
\hline
\textbf{A} & \multicolumn{2}{Y|}{\textbf{Table}} & \textbf{B} \\ \hline
$DC_A$     & \multicolumn{2}{c|}{$S$}            & $DC_B$     \\ \hline
\multicolumn{2}{|>{\hsize=1.5\hsize\linewidth=\hsize}Y|}{\textbf{Common Ground} \textit{cg}}  &
\multicolumn{2}{>{\hsize=1.5\hsize\linewidth=\hsize}Y|}{\textbf{Projected Set} \textit{ps}}   \\ \hline
\end{tabularx}

image.png

模板不要X,前两行随便选一行去用它对齐,只要我们保证设定的 9cm / 3 = 3cm 不小于前两行每一格的自然宽度就行。

仔细观察会发现上图并没有完全等分,前两行比第三行多了一个竖线以及两个\col@sep,稍微修补一下

\newcolumntype{Y}{>{\centering\arraybackslash}X}
\edef\colsep{\expandafter\noexpand\csname col@sep\endcsname}
\begin{tabularx}{9cm}{|Y|cc|Y|}
\hline
\textbf{A} & \multicolumn{2}{Y|}{\textbf{Table}} & \textbf{B} \\ \hline
$DC_A$     & \multicolumn{2}{c|}{$S$}            & $DC_B$     \\ \hline
\multicolumn{2}{|>{\hsize=\dimexpr1.5\hsize+\colsep+.5\arrayrulewidth\relax\linewidth=\hsize}Y|}{\textbf{Common Ground} \textit{cg}}  &
\multicolumn{2}{>{\hsize=\dimexpr1.5\hsize+\colsep+.5\arrayrulewidth\relax\linewidth=\hsize}Y|}{\textbf{Projected Set} \textit{ps}}   \\ \hline
\end{tabularx}

回过头想一想,其实我们一开始的代码没什么不对,只是tabularx的机制限制画不出来。如果用更强大的tabularray,同样的做法就变得可行了。

\begin{tblr}{colspec={|XXXXXX|},
hlines,vlines,width=9cm,rowsep=0pt, % width 可以不用设置,rowsep 设为0是为了跟tabular环境长得更像
cell{1-2}{odd}={c=2}{c},
cell{3}{1,4}={c=3}{c}
}
\textbf{A} && \textbf{Table} && \textbf{B} & \\
$DC_A$       && $S$            && $DC_B$       &\\ 
\textbf{Common Ground} \textit{cg} &&& \textbf{Projected Set} \textit{ps} \\ 
\end{tblr}

image.png

0 条评论

发布
问题