\chapter{盒子} 在 \in[item-sym-diy] 节中,你已经见过盒子了,只是那时可能你还不知其究竟,本章将揭开它们的一些端倪。也有可能你早已钻研过 Knuth 的《The \TeX\ Book》,对盒子的研究之深已经让我望风而拜,但是未必熟悉 \ConTeXt\ 的盒子,故而本章仍有部分内容值得一睹。 \section{\TeX\ 盒子} 前面已经多次暗示和明示,\TeX\ 系统是 \ConTeXt\ 的底层,二者的关系犹如引擎(发动机)和汽车的关系。对 \TeX\ 引擎丝毫不懂,并不影响你学习和使用 \ConTeXt\ 排版一份精致的文档,但是懂一些引擎层面工作原理,未必会有用处,但是你现在也并不能确定将来会不会成为一名 \TeX\ 黑客,如同你从前也从未想过有一天会学习 \ConTeXt。 在 \TeX\ 系统中,盒子是很重要的部件。例如,在 \ConTeXt\ 的排版的每一个段落,是一个竖向盒子,即 \type{\vbox},该盒子之内又有一些横向盒子,即 \type{\hbox},它们是段落的每一行。我们可以直接用这两种盒子构造一个不甚规整的段落: \startsample \vbox{ \hbox{离离原上草}\hbox{一岁一枯荣}\hbox{野火烧不尽}\hbox{春风吹又生} } \stopsample \sample[option=TEX][todo-list]{竖向盒子和横向盒子}{\externalfigure[11/vbox-and-hbox.pdf]} 横向盒子可以指定它的长度,竖向盒子可以指定它的高度。例如, \starttyping[option=TEX] \hbox to 5cm {赋得古原草送别} \vbox to 3cm { \hbox{离离原上草}\hbox{一岁一枯荣}\hbox{野火烧不尽}\hbox{春风吹又生} } \stoptyping 在示例 \in[rsquare] 中,\type{\hbox} 用于包含一副使用 \METAPOST\ 语言绘制的矢量插图,关键代码如下: \starttyping[option=TEX] \startuseMPgraphic{foo} ... ... ... \stopuseMPgraphic \lower.2ex\hbox{\useMPgraphic{foo}} \stoptyping \noindent 其中 \type{useMPgraphic} 环境中的 \METAPOST\ 代码会被 \type{\useMPgraphic{foo}} 提交给 \TeX\ 引擎。\TeX\ 引擎会调用 metapost 程序将 \METAPOST\ 代码编译成 PDF 格式的图片,然后交给 \ConTeXt。最后由 \ConTeXt\ 将图片插入到 \type{\useMPgraphic{foo}} 所在的横向盒子里。\type{\lower .2ex} 可令位于其后的横向盒子下沉 0.2 ex。长度单位 ex 类似于 em,也是一个相对尺寸,它是当前所用字体中的字母 x 对应的字形的高度。从现在开始要记住,横向距离用 em,竖向距离用 ex,不过这只是建议,并非 \TeX\ 世界的法律。 还有一种横向盒子 \type{\line},它的宽度是当前段落的宽度,可让其中的文字向两边伸展并与边界对齐,例如 \starttyping[option=TEX] \line{\darkred\bf 我能吞下玻璃而不伤身体。} \stoptyping \line{\darkred\bf 我能吞下玻璃而不伤身体。} 看到上述示例,想必你想起了之前我们曾在文章标题的样式设定时,用 \type{{middle,broad}} 抑制的 \ConTeXt\ 触发汉字间隙的伸长特性,该特性与\type{\line} 的效果非常相似。 使用 \type{\hfill} 或 \type{\hss} 可对横向盒子里的内容进行挤压。例如 \starttyping[option=TEX] \line{\hfill 我能吞下玻璃而不伤身体。} \line{我能吞下玻璃而不伤身体。\hfill} \line{\hfill 我能吞下玻璃而不伤身体。\hfill} \line{\hss 我能吞下玻璃而不伤身体。} \line{我能吞下玻璃而不伤身体。\hss} \line{\hss 我能吞下玻璃而不伤身体。\hss} \stoptyping \blueframed{ \vbox{ \line{\hfill 我能吞下玻璃而不伤身体。} \line{我能吞下玻璃而不伤身体。\hfill} \line{\hfill 我能吞下玻璃而不伤身体。\hfill} \line{\hss 我能吞下玻璃而不伤身体。} \line{我能吞下玻璃而不伤身体。\hss} \line{\hss 我能吞下玻璃而不伤身体。\hss} } } \type{\hfill} 和 \type{\hss},都是可无限伸缩的粘连,还有一个伸缩能力弱于 \type{\hfill} 的 \type{\hfil}。竖向的可无限伸缩的粘连有 \type{\vfil},\type{\vfill} 和 \type{\vss}。 \section{\ConTeXt\ 盒子} 在 \ConTeXt\ 层面,通常很少使用 \TeX\ 盒子,而是使用 \type{\inframed} 和 \type{\framed}——前者是后者的特例。与 \TeX\ 盒子相比,\ConTeXt\ 层面的盒子可以显示边框,且有非常多的参数可以定制它们的外观。 \type{\inframed} 用于正文,可用于给一行文字增加边框,例如 \starttyping[option=TEX] \type{\inframed{\type{\inframed{...}}}} \stoptyping \noindent 结果为 \inframed{\type{\inframed{...}}}。倘若使用 \type{\framed},例如 \starttyping[option=TEX] \type{\framed{\type{\framed{...}}}} \stoptyping \noindent 结果为 \framed{\type{\framed{...}}}。可以发现,\type{\inframed} 更适合在正文中使用,因为它能与文字基线对齐。事实上,\type{\inframed} 与 \type{\framed[location=low]} 等效,故而前者是后者的特例。例如 \starttyping[option=TEX] \framed[location=low]{\type{\framed[location=low]{...}}} \stoptyping \noindent 结果为 \framed[location=low]{\type{\framed[location=low]{...}}}。 如果不希望 \type{\framed} 显示边框,只需 \type{\framed[frame=off]{...}},也可以单独显示某条边线,并设定边线粗度和颜色: \starttyping[option=TEX] \line{ \framed[frame=off,leftframe=on,rulethickness=4pt,framecolor=red]{foo} \framed[frame=off,topframe=on,rulethickness=4pt,framecolor=green]{foo} \framed[frame=off,rightframe=on,rulethickness=4pt,framecolor=blue]{foo} \framed[frame=off,bottomframe=on,rulethickness=4pt,framecolor=magenta]{foo} } \stoptyping \line{ \framed[frame=off,leftframe=on,rulethickness=4pt,framecolor=red]{foo} \framed[frame=off,topframe=on,rulethickness=4pt,framecolor=green]{foo} \framed[frame=off,rightframe=on,rulethickness=4pt,framecolor=blue]{foo} \framed[frame=off,bottomframe=on,rulethickness=4pt,framecolor=magenta]{foo} } \blank 可以设定盒子的宽度和高度,例如宽 10cm,高 2 cm 的盒子: \starttyping[option=TEX] \hbox to \textwidth{\hfill\framed[width=10cm,height=2cm]{foo}\hfill} \stoptyping \hbox to \textwidth{\hfill\framed[width=10cm,height=2cm]{foo}\hfill} \section{对齐} \ConTeXt\ 盒子的内容默认居中,即 \type{align=center},此外还有 8 种对齐方式: \starttyping[option=TEX] \line{ \setupframed[width=1.95cm,height=1.95cm] \framed[align={flushleft,high}]{A} \framed[align={middle,high}]{A} \framed[align={flushright,high}]{A} \framed[align={flushright,lohi}]{A} \framed[align={flushright,low}]{A} \framed[align={middle,low}]{A} \framed[align={flushleft,low}]{A} \framed[align={flushleft,lohi}]{A} } \stoptyping \line{ \setupframed[width=1.95cm,height=1.95cm] \framed[align={flushleft,high}]{A} \framed[align={middle,high}]{A} \framed[align={flushright,high}]{A} \framed[align={flushright,lohi}]{A} \framed[align={flushright,low}]{A} \framed[align={middle,low}]{A} \framed[align={flushleft,low}]{A} \framed[align={flushleft,lohi}]{A} } \type{\setupframed} 所作的设定会影响到其后的所有 \type{\framed},但是不会影响到其所处编组\footnote{还记得 \TeX\ 编组吗?即 \type[escape=no]{{...}}。}之外的 \type{\framed}。 \section{背景} 可将颜色作为 \type{\framed} 的背景。例如 \starttyping[option=TEX] \inframed [background=color, backgroundcolor=lightgray, width=2cm, frame=off]{\bf foo} \stoptyping \noindent 结果为 \inframed[background=color,backgroundcolor=lightgray,width=2cm,frame=off]{\bf foo}。 通过 overlay,可将一些代码片段的排版结果作为 \type{\framed} 的背景。例如 \starttyping[option=TEX] \defineoverlay [foo] [{\framed[width=3cm,frame=off,bottomframe=on]{}}] \midaligned{\inframed[background=foo,frame=off]{你好啊!}} \stoptyping \defineoverlay [foo] [{\framed[width=3cm,frame=off,bottomframe=on]{}}] \midaligned{\inframed[background=foo,frame=off]{你好啊!}} \blank 通过上述示例,现在你应该能有有七八分明白 \in[item-sym-diy] 节中带圈数字是如何实现的了,其关键代码如下: \starttyping[option=TEX] \startuseMPgraphic{foo} path p; p := fullcircle scaled 12pt; draw p withpen pencircle scaled .4pt withcolor darkred; \stopuseMPgraphic \defineoverlay[rsquare][\useMPgraphic{foo}] \def\fooframe#1{% \inframed[frame=off,background=rsquare]{#1}% } \stoptyping \startuseMPgraphic{foo} path p; p := fullcircle scaled 12pt; draw p withpen pencircle scaled .4pt withcolor darkred; \stopuseMPgraphic \defineoverlay[rsquare][\useMPgraphic{foo}] \def\fooframe#1{% \inframed[frame=off,background=rsquare]{#1}% } \noindent 无非是将一个圆圈图形作为 \type{\inframed} 的背景罢了,而且对圈内的文字的长度有限制,字数略多一些,便出圈了,例如 \type{\fooframe{123}},结果为 \fooframe{123}。不过,只需对上述代码略加修改, \starttyping[option=TEX] p := fullcircle scaled \overlaywidth; \stoptyping \startuseMPgraphic{foo} path p; p := fullcircle scaled \overlaywidth; draw p withpen pencircle scaled .4pt withcolor darkred; \stopuseMPgraphic \defineoverlay[rsquare][\useMPgraphic{foo}] \def\fooframe#1{% \inframed[frame=off,background=rsquare]{#1}% } \noindent 便可解除该限制。技巧是,用 overlay 的实际宽度作为单位圆的放大倍数,故而可保证圆圈的直径即圈内文字的长度,例如 \type{\fooframe{我有一个大房子}},结果为 \fooframe{我有一个大房子}。 \section{盒子的深度} 如果你仔细观察,\ConTeXt\ 盒子里的内容在水平方向是精确居中的,但是在并非如此。例如 \starttyping[option=TEX] \inframed{ajk\framed{我看到一棵樱桃树}}。 \stoptyping \noindent 结果为 \inframed{\framed{我看到一棵樱桃树}}。可见 \type{\inframed} 内部的 \type{\framed} 的底部有着看似多余的空白。使用盒子的参数 \type{depth} 可以消除这些空白,但问题在于这处空白从何而来及其高度是多少。该问题与底层 \TeX\ 的西文排版机制有关。 首先,我们需要明白,西文排版,一行文字是有基线的,但基线并非位于该行文字的最底端,而是距最底端有一段较小的距离,否则所有西文字母都在同一条线上了。在上述示例中,\type{\framed} 的底线便是其外围的 \type{\inframed} 所在行的基线。西文字体在设计时,每个字形(Glyph)是有基线位置,因此在排版时可根据实际的基线位置对字形进行对齐。一行文字的基线到该行的最底端的这段距离称为深度。一行文字的基线到该行最顶端的这段距离称为高度。因此,一行文字的真正高度等于深度 + 高度;在 \ConTeXt\ 中,它等于我们使用 \type{\setupinterlinespace} 设定的尺寸。 无论是 \TeX\ 还是 \ConTeXt\ 盒子,它们本身是没有深度的,但是当它们里面的文字或盒子有深度时,它们便有了深度。至于深度值具体是多大,可以借助 \TeX\ 的盒子寄存器进行测量。例如,定义一个盒子寄存器 box0: \starttyping[option=TEX] \setbox0\hbox{\inframed{\framed{我看到一棵樱桃树}}} \stoptyping \setbox0\hbox{\inframed{\framed{我看到一棵樱桃树}}} \noindent 现在我们有了一个 0 号盒子,使用 \type{\wd},\type{\ht} 和 \type{\dp} 可分别测量该盒子的宽度、高度和深度……顺便复习一下表格的用法: \startsample \starttabulate[|c|c|c|] \HL \NC 宽度 \NC 高度 \NC 深度 \NC\NR \HL \NC \the\wd0 \NC \the\ht0 \NC \the\dp0 \NC\NR \HL \stoptabulate \stopsample \simplesample[option=TEX]{\getsample} 将上述所得深度信息取负作为 \type{\inframed} 的参数 \type{depth} 的值,即 \starttyping[option=TEX] \inframed[depth=-\dp0]{\framed{我看到一棵樱桃树}} \stoptyping \noindent 便可消除深度,结果为 \inframed[depth=-\dp0]{\framed{我看到一棵樱桃树}}。 \section{小结} \TeX\ 盒子是无形的。\ConTeXt\ 盒子是有形的。老子曾说过,恒无欲,以观其妙;恒有欲,以观其所徼。故而,\TeX\ 要懂一些,\ConTeXt\ 也要懂一些。