类选项与自定义编写

现成的 class 不够用时,可以自己编写 class。本页讲的是 编写 class 的一侧。我们先整理 class 通常会接受的标准选项(字号、纸张、栏数等),再一步步搭出 .cls / .sty 文件的骨架:用 \ProvidesClass 标识自己,用 \DeclareOption 声明选项,用 \ProcessOptions 执行选项,用 \LoadClass 以现有 class 为基础,最后完成一个扩展 article 的完整例子。若要了解在文档侧 使用 选项,请参见“Document class & preamble”。

Class 还是 package?

开始写之前,先决定要做的是 class(.cls 还是 package(.sty。class 定义文档类型本身,并通过 \documentclass 只加载一个;论文、书籍、幻灯片等整体骨架和默认体裁由 class 提供。package 则通过 \usepackage 加载,可以加载任意多个,并添加不依赖文档类型的功能(如数学的 amsmath、图形的 graphicx 等)。

借用官方 clsguide 的说法,判断标准很简单:如果某个功能可以与任何 document class 一起使用,就做成 package;否则就做成 class。 想定义整套外观,就是 class;只想添加功能,就是 package。两者的编写规约几乎相同,只是命令名分成 Class 版和 Package 版(\ProvidesClass\ProvidesPackage\LoadClass\RequirePackage\PassOptionsToClass\PassOptionsToPackage)。

自制 class 接受的标准选项

用户会像给标准 class 一样给你的自制 class 传选项,所以至少应支持这些常见选项:选择正文基准字号的 10pt / 11pt / 12pt,纸张的 a4paper / letterpaper,栏数的 onecolumn / twocolumn,单双面的 oneside / twoside,以及草稿显示 draft(用黑条标出溢出行;反义是 final)。这些不必逐个自行实现;正如下面“把选项交给基础 class”所示,常规做法是把它们 原样转发article 等基础 class。

选项含义默认值
10pt / 11pt / 12pt正文基准字号10pt
a4paper / letterpaper纸张大小(也有 b5paperlegalpaper 等)letterpaper
onecolumn / twocolumn单栏 / 双栏onecolumn
oneside / twoside单面 / 双面版式oneside(但 booktwoside
draft / final是否用黑条标出溢出的行final

关于在文档的 \documentclass[...] 一侧 指定并使用 这些选项的具体例子,以及 book 特有的 openright 等内容,请参见姊妹页“Document class & preamble”。从这里开始,我们专注于 如何编写接收这些选项的 class

在文件开头声明身份

class 文件(myclass.cls)的前两行几乎是固定写法。首先用 \NeedsTeXFormat{LaTeX2e} 声明这个文件面向 LaTeX2e(如果在更旧格式下加载,会发出警告)。接着用 \ProvidesClass{myclass}[2026/01/01 v1.0 ...] 报出 class 名、发布日期、版本和简短说明。方括号部分可以省略,但写上后,用户就能通过 日期(YYYY/MM/DD 格式) 要求最低版本,例如 \documentclass{...}[2026/01/01],这些标识也会记录到日志中。

如果写的是 package,对应命令是 \ProvidesPackage{mypackage}[2026/01/01 v1.0 ...]\NeedsTeXFormat 两者共用。铁则是 \ProvidesClass / \ProvidesPackage 中的名字必须与实际文件名一致:在 myclass.cls 中一定写 \ProvidesClass{myclass}

latex
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]

声明选项 — \DeclareOption

class 能接受的每个选项都用 \DeclareOption{option}{code} 逐一声明。用户指定该选项时,等执行到后面说明的 \ProcessOptions,对应 code 就会运行。例如可以准备自己的 draft 开关,并设置一个布尔标志。

未声明选项的接收口是带星号的 \DeclareOption*{code}(catch-all)。在其中,\CurrentOption 会展开为“当前正在处理的选项名”。自制 class 中最常见的一行,是把未知选项 原样转发 给基础 class。正因为有这一行,即使你没有自己声明 10pta4paper 这类标准选项,用户也能理所当然地传入它们。

latex
% pass anything we do not handle ourselves on to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}

编写 package 时,使用对应的 \PassOptionsToPackage{\CurrentOption}{base}。如果想在 \ProcessOptions 之前注入默认值,可以写 \ExecuteOptions{a4paper,11pt};这样即使用户什么也没指定,这些选项的代码也会先执行。

处理选项,再交给基础 class

只声明选项不会发生任何事;只有调用 \ProcessOptions 时,各选项的代码才会执行。实务中几乎总是写 \ProcessOptions\relax。因为还存在带星号的 \ProcessOptions*,末尾的 \relax 是可靠选择“无星版本”的惯用写法。无星的 \ProcessOptions声明顺序 处理选项;带星号的版本按 调用方指定的顺序 处理。

从零搭版式很费力,所以大多数自制 class 都以现有 class 为基础。这个命令就是 \LoadClass[options]{article},它会把 article.cls 的命令和体裁整体读入(也可以在方括号中传入 twocolumn 等选项)。\LoadClass 只能在 class 文件内部使用。 顺序很重要:为了让用户在 \documentclass[...] 中传入的选项影响基础 class,\LoadClass 要放在选项处理(\ProcessOptions)之后。先设置转发,再由 \ProcessOptions 分派选项,最后加载基础 class。

如果只是想把“自己收到的同一组选项”原封不动传给基础 class\LoadClassWithOptions{article} 很方便(它会原样继承 class 的全局选项)。编写 package 时,用 \RequirePackage 代替 \LoadClass(相当于作者端的导言区 \usepackage);若要全部原样传递,则用 \RequirePackageWithOptions

\LoadClass 之后,才是真正写出 这个 class 个性 的地方:用 \renewcommand 改标题体裁,用 \setlength 调整边距,用 \newcommand / \newenvironment 定义新命令和环境。需要的附加包也从这里用 \RequirePackage 加载。

完整示例:扩展 article 的最小 class

把以上内容合在一起,就得到下面这个最小 .cls。它以 article 为基础,添加自己的 draft 选项,把未知选项转发给 article,最后整理边距并定义一个自用命令。把它保存为 myclass.cls,在文档中写 \documentclass[11pt,a4paper]{myclass} 就能使用。

latex
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/01 v1.0 My example class]

% --- declare options ---
\newif\ifmy@draft
\DeclareOption{draft}{\my@drafttrue}
% forward everything else to article
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{article}}

% --- execute and load the base class ---
\ExecuteOptions{a4paper}      % a default if the user gives none
\ProcessOptions\relax
\LoadClass{article}

% --- this class's own setup ---
\RequirePackage[margin=25mm]{geometry}
\setlength{\parindent}{0pt}
\newcommand{\version}{v1.0}
\ifmy@draft \RequirePackage{draftwatermark}\fi

\endinput

末尾的 \endinput 告诉 LaTeX 从这里以后不要再读取;按惯例会写上它。如果要写 package,只需把开头换成 \ProvidesPackage,把 \LoadClass 换成 \RequirePackage,同一骨架就会变成 .sty 文件。

现代写法:key-value 选项(expl3)

上面介绍的 \DeclareOption 机制现在仍然完全有效,但它主要面向“有 / 无”式的开关选项。若新代码想自然地编写 name=value 这样的 key-value 选项,当前推荐使用 LaTeX kernel 自带的接口:用 \DeclareKeys 声明 key,用 \ProcessKeyOptions 处理它们。这个机制基于 expl3(LaTeX3)的 l3keys,已经作为近年 LaTeX kernel 的一部分提供。

过去同样的事情要用 l3keys2e 包完成,但其核心功能现在已经并入 kernel,因此新代码无需加载该包即可直接调用 \ProcessKeyOptions。不过,现有教程和许多包仍以 \DeclareOption 风格为主,所以理解本页的骨架依然重要。转向 key-value 方式时,请参阅最新版 clsguide(“LaTeX2e for class and package writers”)中的相关章节。

发布前的最小测试

class 一旦加载就会影响整篇文档,因此在写真正内容前,先用一个很小的测试文档固定其行为。首先确认 10pt11pta4paperdraft 等标准选项能到达基础 class,而只有自己的独有选项由自制代码处理。如果这里出错,原因通常是 \ProcessOptions / \ProcessKeyOptions 的位置、未知选项的转发,或 \LoadClass 的顺序。

latex
\documentclass[11pt,a4paper,draft]{myclass}
\begin{document}
\section{Smoke test}
本文の基準サイズ、用紙、下書き表示、見出し、余白を確認する。
\end{document}
  • 日志中是否标识了 class? 确认 \ProvidesClass 的日期和版本出现在 .log 中。文件名和 class 名不一致会在之后造成混乱。
  • 是否保留了标准选项? 如果 11pttwocolumn 被忽略,请检查把未知选项转发给基础 class 的处理。
  • 文档用宏在导言区和正文中是否正常? 放入一个标题、注、图和表分别编译,因为这些是真实用户最先会触碰的元素。
  • \endinput 之后保持无效。 在末尾留下笔记或示例,如果标记缺失或被移动,可能导致意外文字被读入。

连发布形态一起考虑

自制 class 的真正考验,不是它在你手上刚能运行的瞬间,而是别人用另一套环境加载它的时候。最低限度,把 .cls、简短示例文档、README 和变更记录放在同一目录,并确认示例可原样编译。在 README 中把转发给基础 class 的选项和只由自制 class 接收的选项分开写,用户就能追踪 11pta4paper 究竟在哪里生效。示例中放入一个标题、列表、图表和参考文献,一起检查 class 最容易首先破坏的元素。

terminal
myclass/
  myclass.cls
  sample.tex
  README.md
  CHANGELOG.md

发布前测试时,在示例中加入 \listfiles,让日志列出被加载的文件和版本。这样可以诊断用户本地是否放着旧的 myclass.cls,以及必要的包是否按预期加载。class 是整篇文档的基础;对长期使用的文档而言,仔细处理加载顺序、选项处理和日志信息,比再添加一个正文宏更有价值。