要问 TeX 最根本的想法是什么,答案既不是字体,也不是公式,更不是断行算法,而是 盒子。LaTeX 放到页面上的一切都是盒子:一个带有宽度、高度和深度的矩形,用胶(glue)与相邻元素连接,一旦装配完成就再也不会被打开查看。\mbox 之所以能阻止断词,溢出的行之所以报作 Overfull \hbox,\parbox 与 minipage 之所以在某处分道扬镳,都源于这一点。本页按照这个统一的想法,逐一走过你真正会敲下的盒子命令——\mbox、\makebox、\framebox、\raisebox、\parbox、minipage、\newsavebox。
宽度、高度与深度:盒子为什么有三个尺寸
盒子的尺寸不是两个而是三个:宽度、高度 和 深度。高度自基线向上量,深度自基线向下量——所以盒子不是围住墨迹的矩形,而是 挂在字母所立的那条线上的矩形。这三个值可以用 \the\wd、\the\ht、\the\dp 读出。在 10pt 的 Computer Modern 下把 Hx 装进盒子,深度是 0.0pt;换成 Hxg,深度变成 1.94444pt,因为 g 伸到了基线以下。宽度当然也变了,但深度变化的原因完全不同。
这一点正好解答了几乎人人都会先撞上、却在懂得盒子之后才明白的谜题:为什么 \fbox{Hx} 的边框贴着字母,而 \fbox{Hxg} 的边框看起来往下坠? 因为 \fbox 框住的是盒子而不是字母,而 Hxg 的盒子深了 1.94pt。把一排带框的词并列到表格的一行里,边框就会随着下伸部(descender)的有无而上下跳动。解法是加 支柱(strut):\fbox{\strut Hx} 与 \fbox{\strut Hxg} 的高度和深度完全一致,因为 \strut 是一个零宽的隐形盒子,向上占据当前 \baselineskip 的 0.7 倍、向下占据 0.3 倍。支柱在本页后面还有专门一节。
\newsavebox{\probe}
\newcommand{\dims}[1]{\sbox{\probe}{#1}%
\typeout{[#1] w=\the\wd\probe\space h=\the\ht\probe\space d=\the\dp\probe}}
\dims{Hx} % w=12.77782pt h=6.83331pt d=0.0pt <- no descender
\dims{Hxg} % w=17.77783pt h=6.83331pt d=1.94444pt <- the g adds depth
% same words framed: only the second frame sinks below the baseline
\fbox{Hx}\quad\fbox{Hxg}\quad \fbox{\strut Hx}\quad\fbox{\strut Hxg}用 \mbox 阻止断行与断词
\mbox{...} 会把内容留在一行里,并在其内部关闭断词。原因就写在定义里:LaTeX 把 \mbox 声明为 \leavevmode\hbox{#1},仅此而已。\hbox 是 已经装配完成的盒子,而 TeX 绝不会回头钻进一个成品盒子里去寻找断行点或断词位置。所以 \mbox{antidisestablishmentarianism} 不是被客气地请求「别断开」,而是根本 无法 断开。这种只把内容从左往右排下去、绝不折行的盒子,LaTeX 称为 LR 盒子(其内部处于 LR 模式);\mbox、\makebox、\fbox、\framebox 都属这一族,与固定宽度并折行的 段落盒子 \parbox 和 minipage 相对。TeX 的整套世界观——排版就是用胶把盒子连起来——被浓缩进了这一条命令。
代价是:不可断开的盒子有时会宽过一行,这时 TeX 就会抱怨。那条消息值得逐字细读:Overfull \hbox (29.62221pt too wide) in paragraph at lines 5--5。它写的是 \hbox 而不是「行」,因为 TeX 产出的每一行本身就是一个 hbox——断段器把单词和可伸缩的胶装进每行一个的 hbox 里,这正是高德纳与迈克尔·普拉斯在 1981 年发表的《Breaking paragraphs into lines》所描述的模型。给文档类加上 draft 选项,LaTeX 会把 \overfullrule 设为 5pt,在每一条越界的行旁边的页边打印一根黑棒;那根黑棒本身就是一个被填满的盒子。\mbox 该用在真正不能拆开的东西上——产品名、电话号码、数值与单位——而不是当作糟糕断行的万能药。
% never split, never hyphenated
Released for \mbox{Mac OS X} and \mbox{Windows 11}; call \mbox{+81 3-1234-5678}.
% too wide for the measure -> Overfull \hbox (...pt too wide) in the log
\parbox{3.4cm}{aaa \mbox{antidisestablishmentarianism}}
% \documentclass[draft]{article} sets \overfullrule=5pt and marks the line用 \makebox 指定宽度与对齐——零宽盒子、\rlap 与 \llap
\makebox[width][position]{text} 就是把宽度的决定权从 TeX 手里拿过来交给你的 \mbox。[position] 指定内容在你要求的宽度中的位置:l 左对齐,r 右对齐,c 居中(默认),而 s 会拉伸词间的胶,让内容正好铺满整个宽度——\makebox[3cm][s]{a b c d} 确实会排成 3cm 宽。其他字母也会被接受,但 LaTeX 会给出 Unexpected alignment 警告并退回到 c。
在宽度参数内部,有四个名字代表「正要装进盒子的东西的自然尺寸」:\width、\height、\depth 以及 \totalheight(高度加深度)。它们在宽度被求值之前就由内容设定,所以 \makebox[2\width]{word} 会先把 word 量为 21.4167pt,再造出 42.8334pt 的盒子。这就是「宽度是它的两倍」这句话的干净写法,而不必先确定「它」到底是什么。同样这四个名字也可用于 \framebox、\savebox 和 \raisebox 的参数。
而最有用的宽度是 0pt。零宽盒子会被排出来,却不推进当前位置,于是它会叠印在随后的内容之上。\makebox[0pt][l]{...} 把内容垂到当前点的右侧,\makebox[0pt][r]{...} 垂到左侧,[c] 则跨在两边。这两者各有简称——\rlap 与 \llap——LaTeX 正是照此定义的:\hbox to 0pt{#1\hss} 与 \hbox to 0pt{\hss#1}。叠印、悬挂标点、页边记号,以及绝大多数自制的删除线宏,全都是这一个把戏。
\makebox[3cm][l]{left}|\makebox[3cm][r]{right}|
\makebox[3cm][s]{a b c d}| % interword glue stretched to exactly 3cm
\makebox[2\width]{word}| % twice the natural width of "word"
% zero width: the slash overprints the space that follows the X
X\rlap{/}\quad Y % identical to X\makebox[0pt][l]{/}\quad Y\fbox 与 \framebox 的区别,以及决定边框的两个长度
\fbox{text} 与 \framebox[width][position]{text} 的区别,恰恰就是 \mbox 与 \makebox 的区别:\fbox 按内容的自然宽度加框,\framebox 则在你指定的宽度内加框,位置参数同样是 l/c/r/s。两者都会在盒子的四边画线,外观由两个长度决定:线条粗细 \fboxrule 为 0.4pt,线条与内容之间的间距 \fboxsep 为 3pt。算一算即可验证:Hx 宽 12.77782pt,而 \fbox{Hx} 宽 19.5778pt——差值正好是 3.4pt 的两倍。写 \setlength{\fboxsep}{0pt},边框就会紧贴字母。
\fbox 是 LR 盒子,所以它只能框住一行。要给段落加框,就 给段落盒子加框——\fbox{\parbox{6cm}{...}}。外层盒子不关心里面装的是什么,只看它有多大。这种可组合性正是盒子模型的要义,同时也解释了为什么围绕 \parbox 画的边框会原样继承上一节的下伸部问题;在框内文字的开头和结尾各放一个 \strut,一叠这样的框就能保持齐平。一旦需要圆角、背景色或可跨页的边框,那就是 framed、tcolorbox 等专用宏包的地盘,留给「边框」那一页。
\setlength{\fboxrule}{1pt} % default 0.4pt
\setlength{\fboxsep}{6pt} % default 3pt
\fbox{one line}\quad\framebox[4cm][r]{4cm wide, flush right}
% frame a whole paragraph: put a paragraph box inside the frame
\fbox{\parbox{6cm}{\strut A framed paragraph. The frame follows the box,
and the struts keep its height independent of ascenders and descenders.\strut}}\parbox 与 minipage 的区别
两者都固定宽度并让文字在其中折行。区别 并不是 常被复述的那句「\parbox 只能放一个段落」。它想放几段就放几段,列表也可以——\parbox{4cm}{First.\par Second.} 确实会排出两个段落。真正的区别在于:minipage 是 环境,\parbox 是 命令,这决定了里面可以合法出现什么。其余写法是一致的:\parbox[position][height][inner-pos]{width}{text} 与 \begin{minipage}[position][height][inner-pos]{width}。两者都会把 \parindent 和 \parskip 重置为 0,并把 \linewidth 设为盒子的宽度——这正是在其中写 \includegraphics[width=\linewidth]{...} 能得到正确结果的原因。
[position] 决定盒子如何与周围的行相接,也是最容易被误读的参数。[t] 并不是 把盒子的顶端对齐:它把盒子 第一行 的基线放到周围的基线上。量一个三行的 \parbox[t],高度是 4.30554pt——那是小写 o 的高度,不是盒子的高度——而深度是 24pt,也就是有两整行悬在基线之下。[b] 对最后一行做镜像的事,默认的 [c] 则把盒子居中对齐到该行。所以两个 [t] 的 minipage 是按 第一条基线 对齐的;若其中一个以图片或更大的字号开头,顶端依然会参差不齐。补救办法是在各自开头加一个 \strut,或者用 \raisebox 挪动其中一个。四参数写法 \parbox[position][height][inner-pos]{width}{text} 还能强制指定盒子的高度,[inner-pos] 则决定内容在这个高度里靠上(t)、居中(c)、靠下(b),还是用 s 上下撑满。
两者真正分道扬镳的地方是 里面能装什么。\parbox 的正文是作为宏参数读入的,此时类别码已经固定,所以 \parbox{5cm}{\begin{verbatim}...\end{verbatim}} 会直接以 ! Argument of \@xverbatim has an extra }. 失败——而同样的 verbatim 在 minipage 里可以正常工作。脚注比报错更麻烦:\parbox 内的 \footnote 会印出标记,然后 把脚注正文整个弄丢,因为插入物被困在盒子里,永远到不了页面。minipage 则是有意处理它的:用自己的 mpfootnote 计数器生成 斜体小写字母 作标记,把脚注放在 minipage 正下方。浮动体在两者中都被禁止:在 minipage 里写 figure 会以 ! LaTeX Error: Not in outer par mode. 中止。而且两者都不能跨页断开,因为盒子按定义就是整块放置的。
| 放入的内容 | \parbox | minipage |
|---|---|---|
\par | 可以放多个段落 | 可以放多个段落 |
itemize | 可用 | 可用(列表深度也会重置) |
verbatim | ! Argument of \@xverbatim has an extra }. | 可用 |
\footnote | 标记会印出,正文丢失 | 出现在盒子正下方,斜体小写字母 |
figure | 不可用 | ! LaTeX Error: Not in outer par mode. |
\parindent | 重置为 0 | 重置为 0 |
minipage 最常见的用途是并排的两栏:两个各约占 \textwidth 的 0.48,中间放一个 \hfill。总和要小于 1,否则第二个放不下。还有一个人人都会栽一次、耗掉半天的陷阱:\end{minipage} 与 \begin{minipage} 之间源文件里的 换行会变成一个普通空格,于是两个盒子被一个你从未敲过的词间空白撑开。在行末加一个 %,空格就消失了。若要让两栏按高度对齐,或希望文字绕排在盒子周围,请转到「浮动体与放置」那一页。
\noindent
\begin{minipage}[t]{0.48\textwidth}
\strut Left column. Several paragraphs, lists and a local footnote
are all fine here.\footnote{Printed under this box, marked a, b, \dots}
\end{minipage}\hfill% <- the % kills the newline; without it the boxes drift apart
\begin{minipage}[t]{0.48\textwidth}
\strut Right column. [t] aligns the FIRST BASELINES, not the tops --
the struts are what make the tops line up as well.
\end{minipage}用 \raisebox 上下移动——以及谎报尺寸的参数
\raisebox{lift}{text} 会把内容抬高指定的量,值为负则下移;它是把两个不同尺寸的素材对齐到基线的常规工具。有意思的是那两个可选参数:\raisebox{lift}[height][depth]{text} 会让 LaTeX 对外声明这个高度和深度,而不管实际画出来的是什么。\raisebox{0pt}[0pt][0pt]{\Huge Ag} 仍然完整占据 28.66008pt 的宽度,量出来却是高 0pt、深 0pt(真实值是 16.99782pt 与 4.83937pt),于是周围的行距会按「那里什么都没有」来计算。装饰字符或超大字形能落进正文而不撑开行距,靠的就是这个机制;\width、\height、\depth、\totalheight 这四个简写在这里同样可用。当然,声明与墨迹不符就必然会与邻行重叠——所以只在「重叠正是目的」时才谎报尺寸。
\rule 与支柱(strut)——用看不见的盒子撑出行高
\rule[lift]{width}{height} 会画出一个实心的黑色矩形——它几乎是唯一一个以盒子本身的形状把墨迹落到页面上的 LaTeX 命令。\rule{0.5\linewidth}{0.4pt} 就是一条宽为行宽一半的细横线。可选的 [lift] 在保持矩形尺寸不变的前提下,改变它相对基线的位置:\rule[-2pt]{10pt}{8pt} 量出来是宽 10pt、高 6pt、深 2pt——这 8pt 高的矩形里,有 2pt 悬在基线以下。
把宽度设为 0,线条就不再印出,却仍继续占位——这就是 支柱(strut)。\rule{0pt}{2.6ex} 不可见,宽 0pt,高 11.19443pt;把它放在表格一行的开头,这一行就保证至少有那么高。LaTeX 自带的 \strut 是按当前行距做好的成品:当 \baselineskip 为 12pt 时,它高 8.39996pt、深 3.60004pt——正是这一行的 0.7 与 0.3 的划分。这也解释了为什么 \fbox{\strut Hx} 与 \fbox{\strut Hxg} 完全一致:支柱比这两个词都更高更深,于是下伸部再也决定不了盒子的尺寸。它是文字挤到 \hline 上时的经典解法,也是让一排盒子保持统一竖直节奏的工具。
% a centred horizontal rule
\noindent\hfil\rule{0.5\linewidth}{0.4pt}\hfil
% a strut forces a taller, roomier table row
% (\strut is the ready-made one, sized to the current \baselineskip)
\begin{tabular}{|l|}
\hline
\rule{0pt}{2.6ex}Tall, uncramped row \\
\hline
\strut Same idea, sized to the current leading \\
\hline
\end{tabular}
% put two differently sized boxes on one baseline
big \raisebox{-0.4ex}{\Huge A} small用 \newsavebox/\sbox/\usebox 保存并重复使用盒子
出现不止一次的素材,可以只排版一次然后回放。\newsavebox{\name} 保留一个盒子寄存器,\sbox{\name}{text} 把内容填进去——若想要 \makebox 式的写法,则用 \savebox{\name}[width][position]{text}——再用 \usebox{\name} 取出。内容真正被排版只发生在 \sbox 的那一刻,此后不再重复。因此,一张长表或一幅复杂图形若要用在三处,排版开销只有一次,而三份副本分毫不差。
盒子寄存器是有限资源,而且有大约二十年是 稀缺 资源。最初的 TeX 只有整整 256 个,编号 0 到 255;分配得太多的文档会以 ! No room for a new \box 停下。eTeX 把上限提到 32768,LuaTeX 提到 65536,所以如今几乎不会撞上——LaTeX 在构建格式时按引擎选定上限,内部的 \e@alloc@top 分别是 255、32767 或 65535。关于接口有两点提醒:保存盒子本身是 LR 盒子,所以多行内容要先用 \parbox 或 minipage 包起来;而当内容含 verbatim 时,应改用环境形式 \begin{lrbox}{\name} … \end{lrbox}。另外,旧资料常说「\savebox 是脆弱的(fragile)」,在当前的 LaTeX 中并非如此:\mbox、\makebox、\fbox、\parbox、\raisebox、\sbox 和 \savebox 全都被声明为稳健的(robust)。
\usebox 的搭档是 \settowidth{\len}{text}、\settoheight 和 \settodepth。它们会以不可见的方式排版内容,并把结果的某一个尺寸存入长度变量。它们的存在是为了「画出与另一样东西完全等大的东西」——先量出签名栏的宽度,再用 \rule 画出下划线。
\newsavebox{\mylogo}
\sbox{\mylogo}{\fbox{\textbf{Draft}}} % typeset once, here
Header: \usebox{\mylogo} \dots\ Footer: \usebox{\mylogo}
% verbatim inside a save box needs the environment form
\newsavebox{\snippet}
\begin{lrbox}{\snippet}
\begin{minipage}{6cm}\begin{verbatim}int main(void) { }\end{verbatim}\end{minipage}
\end{lrbox}
\usebox{\snippet}
% measure a box into a length, then rule exactly that wide
\newlength{\siglen}
\settowidth{\siglen}{Signature}
Signature\par\rule{\siglen}{0.4pt}什么时候用哪个盒子命令
| 命令 | 类型 | 什么时候用 |
|---|---|---|
\mbox | LR | 这段文字绝不能被拆开 |
\makebox | LR | 想自己决定宽度,或用 [0pt] 叠印 |
\rlap | LR | \makebox[0pt][l] 的简写(\llap 向左) |
\fbox | LR | 按自然宽度加一个边框 |
\framebox | LR | 指定宽度和位置的边框(用 \fboxsep/\fboxrule 调整) |
\parbox | 段落 | 按固定宽度折行,例如放在表格单元格里 |
minipage | 段落 | 需要 verbatim、脚注,或并排分栏 |
\raisebox | 变换 | 上下移动,或把高度和深度声明为 0 |
\rule | 填充盒子 | 画线;零宽时成为看不见的支柱 |
\strut | 支柱 | 让行或表格单元格的高度整齐一致 |
\usebox | 存储 | 取出用 \newsavebox + \sbox 造好的盒子 |
\settowidth | 测量 | 把宽度(还有 \settoheight/\settodepth)存入长度 |