cls文件编写实战
已经使用latex写论文一段时间了,但对于cls模板还是缺乏认识。这里尝试从基础命令开始写一个简单模板。
当我们撰写一个Latex文档时,可能会使用.cls以及.sty包,在 LaTeX 体系中,.cls 和 .sty 是两类地位完全不同、但经常配合使用的文件。Latex的模板就捆绑了 class (.cls)、style (.sty)、示例 .tex 文件和支持资源(徽标、参考书目文件等),这些资源定义了特定文档类型(期刊文章、论文、简历、海报、幻灯片等)的布局、排版和结构。
.cls 定义的是 文档的“身份”和全局结构”,例如:
- 单栏 / 双栏
- 页面尺寸、边距
- 字号体系
- 章节结构(section / subsection)
- 浮动体整体规则(figure / table)
.sty 是 “功能或样式插件”,用于扩展或局部修改 LaTeX 的能力。
一个tex文件内容如下
1
2
3
4
5
6
7
8
\documentclass[options]{mytemplate}
\usepackage{mytemplate}
\begin{document}
\title{My Title}
\author{Me}
\maketitle
\end{document}
创建自己的模板需要同时使用cls类完全控制文档结构,同时使用stl样式文件包含额外的宏或者格式。编写cls的方式一是可以通过编写dtx文件另一个是直接基于现有cls模板进行调整。考虑到常见情况,其实基于cls模板进行更改更实际。
而dtx本质是文学化编程(literate programming)+docstrip.
一个标准 dtx 文件 = 3 个部分:
- driver(生成文档)
- documentation(说明文档)
- tagged code(可提取代码)
文学化编程含义就是在源代码中包含注解并且生成的内容也会包含注解pdf,让人看dtx源码就能容易看懂。
driver 部分主要用于 生成 PDF 文档,也就是文档化源码(ltxdoc 风格):
- 显示文档说明文字
- 显示代码块示例
- 可以生成宏索引、代码行索引等
- 不执行 cls/sty 的核心功能(否则可能报错)
核心目标:把 dtx 文件生成 可读的文档。作用:生成说明文档 PDF。
1
2
3
4
5
6
7
8
9
10
11
% \iffalse
%<*driver>
\documentclass{ltxdoc}
\EnableCrossrefs
\CodelineIndex
\RecordChanges
\begin{document}
\DocInput{myclass.dtx}
\end{document}
%</driver>
% \fi
% \iffalse ... % \fi
防止 driver 被 docstrip 提取
<*driver> tag
只用于文档,不用于 cls
\EnableCrossrefs 和 \CodelineIndex:自动为宏包生成代码索引。
\PrintChanges:根据 % \changes 记录自动生成修订历史,这对大型项目
\ProvidesFile{latex.dtx}:向 LaTeX 系统声明当前文件的名称。
\documentclass{ltxdoc}:加载 LaTeX 官方专门为编写宏包文档设计的 ltxdoc 文档类。它提供了诸如列出宏定义、生成索引等特殊功能。
\usepackage{hypdoc}:为文档添加超链接支持(基于 hyperref),并针对文档中的宏和索引进行了优化。
\EnableCrossrefs:开启宏定义的交叉引用功能,自动记录每个宏在代码中的位置。
\CodelineIndex:设置索引以行号为基准,而不是页码,方便读者精确定位代码。
\RecordChanges:开启变更记录功能,配合下文的 \changes 命令生成版本修订历史。
开头的 %<*driver> 部分是告诉 LaTeX 如何编译这份文档。它通常加载 ltxdoc 类,这是专门为记录 LaTeX 宏包设计的类。
macrocode 环境:这是 .dtx 最特殊的地方。它内部的代码会被提取到 .sty 或 .cls 文件中,而在 PDF 文档里则会带有行号显示。也就是对于pdf排版和cls文件都有用,主要是告诉 PDF 排版引擎“这是代码,请原样打印并加行号”。提取为cls:必须配合 Guard Tags(如 %<*...>)且在 .ins 文件中正确配置,内容才会进入 .cls。ltxdoc 类的核心行为
dtx 文件的 driver 部分通常是:
1
2
3
4
\documentclass{ltxdoc}
\begin{document}
\DocInput{myclass.dtx}
\end{document}
ltxdoc 的会把 dtx 文件中 特殊注释(如 % \section{…}、% \begin{macro}{…})识别为文档命令,而不是普通注释,% 只是标记注释行,driver 编译时会 去掉前导 % 并执行其中的 LaTeX 文档命令。
\begin{macro}当读者快速翻页查找某个特定宏的实现时,眼睛只需要盯着左侧边缘的标签,而不需要阅读中间密集的代码。
ltxdoc 会区分:
核心规律:ltxdoc 会自动识别 % 注释中的 LaTeX 文档命令并执行它来生成 PDF 样式,而普通文字不会被执行
1
2
3
4
5
6
规则:
- 每行必须以
%开头 - 可以写完整 LaTeX 文档
- 会出现在 PDF 文档中
1
2
3
4
5
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/22 My class]
\LoadClass{article}
%<*tag> 开始
%</tag> 结束
中间是纯 TeX 代码(不能加 % 注释)。tagged code 是 源码块,可以被 docstrip 提取生成可执行的 cls/sty 文件,同时也可以用来生成 PDF 中的代码显示:
- 定义 class/package 的宏命令、环境、选项
- 可以写文档注释(% 开头)用于 PDF 文档显示
- 提供 可控模块化(通过 tag 提取不同内容生成不同文件)核心目标:把源码和文档组织在同一个文件,方便提取和展示。
\begin{macro}用来文档化一个宏(命令),并生成“宏索引”和结构化说明。
它的主要作用是:
- 给某个宏建立“文档条目”
- 在 PDF 中生成“宏说明块”
- 在索引中登记宏名(配合
\CodelineIndex) - 让 dtx 文档变得像 API 文档
宏就是“自定义命令”。比如:
\newcommand{\hello}{Hello}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
基础宏定义
版式控制
章节结构
class options
主类文件
简单来说,%开头的都会在pdf中显示,除非tagged code块中除开%后是\指令则会执行,如果不是%开头,则会出现在cls文件中,如果是在tagged code中类似。
docstrip 会把 非注释行(不以 % 开头)的内容 当作 真正源码。也就是说,无论它在 %<*tag> 中还是不在,docstrip 都会把这些行写进目标文件。有了 tag,你可以控制 docstrip 只提取 class 或 macro如果不使用 tag,所有非 % 行都会默认被提取到生成文件
使用dtx生成cls文件流程:
- 写 dtx
- 写 ins
- 运行 ins
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
% \iffalse meta-comment
% \iffalse
%<*driver>
\documentclass{ltxdoc}
\begin{document}
\DocInput{myclass.dtx}
\end{document}
%</driver>
% \fi
% \section{MyClass}
% This is a demo class.
%<*class>
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/22 Demo class]
\LoadClass{article}
%</class>
% \iffalse meta-comment 是实现“文学编程”的一种核心机制。它允许你将排版文档所需的指令(如编译说明和驱动程序)隐藏起来,不让它们出现在最终提取的宏包代码中。
而ins文件是”安装文件”,需要利用ins文件生成cls
1
2
3
4
5
6
7
\input docstrip.tex
\generate{
\file{myclass.cls}{\from{myclass.dtx}{class}}
}
\endbatchfile
最后运行命令
1
latex myclass.ins
其中安装文件使用了docstrip生成cls.所以需要利用ins生成cls文件,如果直接执行latex *.dtx,执行 dtx 时:
driver 会被执行
documentation 会被排版进 PDF
tagged code 不会被提取
不会生成 cls/sty
换句话说:
.dtx被当成“说明文档”编译,而不是“源码”。
1
2
3
4
xelatex latex.dtx
makeindex -s gind.ist -o latex.ind latex.idx
makeindex -s gglo.ist -o latex.gls latex.glo
xelatex latex.dtx
如果使用了中文等,可以考虑xelatex
cls文件格式往往如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{mytemplate}[2025/12/03 v1.0 My custom class]
\LoadClass[12pt]{article}
\RequirePackage{geometry}
\geometry{margin=2cm}
\RequirePackage{fontspec}
\setmainfont{Latin Modern Roman}
\newcommand\mytitle[1]{\centerline{\LARGE\bfseries #1}}
\endinput
cls文件底层是Tex,而不是Latex,其核心问题包含三个:
- 文档类如何加载?
- 类如何定义结构和命令?
- 类如何控制版式?
下面围绕这三点说明。
1
2
3
4
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/22 My custom class]
\LoadClass{article}
LaTeX 中的宏定义分为:Tex原生宏以及Latex宏接口。
来自 TeX 引擎,cls/sty内部大量使用
\def\edef\xdef\gdef\let\futurelet
来自 LaTeX,用户和宏包作者推荐使用
\newcommand\renewcommand\providecommand\DeclareRobustCommand\NewDocumentCommand(xparse)\NewExpandableDocumentCommand
最常用的就是\def和\newcommand,
1
\def\foo{Hello}
特点:
不检查是否已定义(会覆盖)
语法灵活
可定义复杂参数模式
\newcommand(LaTeX 推荐)
1
\newcommand{\foo}{Hello}
特点:
- 如果宏已存在 → 报错
- 语法安全
- 参数规则固定
另外还有 \renewcommand(重新定义)
1
\renewcommand{\section}{...}
如果宏不存在 → 报错。
\providecommand(如果不存在才定义)
1
\providecommand{\foo}{Hello}
逻辑:
- 如果 \foo 已存在 → 什么都不做
- 如果不存在 → 定义
\newcommand
\renewcommand
\def
\let
\DeclareRobustCommand
\NewDocumentCommand
\newifif
\newif 用来创建一个布尔型开关宏(true/false),通常用于:
- 选项开关(class/package options)
- 控制是否执行某段代码
它会生成三个宏:
\<name>true— 设置开关为真\<name>false— 设置开关为假\<name>?— 查询开关状态(返回 true/false 可用在 if 语句中)
1
\newif\if<name>
<name>:开关名称(不带if前缀)- 自动生成
\if<name>条件宏
例子:
1
\newif\ifdraft
生成:
\ifdraft→ 条件宏\drafttrue→ 将\ifdraft设置为真\draftfalse→ 将\ifdraft设置为假
\def重定义宏
所谓文学编程,本质还是利用%表示注解并可以输出到pdf中供使用者查看,而不使用%可以输出到cls源码进行构建。因为dtx文件本质包含了cls文件和可以导出pdf的注释,所以我们直接看cls文件。
以一个硕士毕业论文模板讨论。yanputhesis.cls
1
2
3
\NeedsTeXFormat{LaTeX2e}[2005/12/01]
\ProvidesClass{yanputhesis}
[2023/03/07 v1.8.5 Yet Another NPU Thesis Template]
1
\NeedsTeXFormat{LaTeX2e}[2005/12/01]
- 作用:指定运行该文件所需的最低格式版本。
- 含义:它告诉编译器,该模板必须在核心宏包版本日期不能早于 2005年12月1日。如果用户使用的系统过于陈旧,编译器会发出警告。
1
\ProvidesClass{yanputhesis}
- 作用:向系统声明这个文件的名称。
- 重要性:这个名字必须与你文件夹中的文件名 严格一致(即文件名必须是
yanputhesis.cls)。当用户在.tex主文件中写下\documentclass{yanputhesis}时,系统就是通过这个命令来确认“找对人了”。
1
\RequirePackage{xkeyval}
- 作用:引入高级参数处理宏包。
- 意义:原生的 LaTeX 只支持简单的
[option]。使用xkeyval后,模板可以处理像type=phd或font=adobe这样“键-值对”格式的复杂选项。
定义状态开关 (\newif)
这一部分定义了大量的布尔变量(即只有“真”或“假”两种状态的开关)。
注意:命令中包含
@符号,说明这些是内部命令,防止普通用户在正文中误改。
\def 定义的宏是用来具体“拨动”上面那些开关的。最巧妙的是互斥(Mutex)逻辑:
代码段
1
\def\set@type@phd{ \@npu@type@phdtrue \@npu@type@mstfalse \@npu@type@bclfalse}
- 逻辑解析:当你设置论文类型为“博士(phd)”时,程序会自动将
phd设为true,同时强制将mst(硕士)和bcl(本科)设为false。 - 目的:防止用户由于误操作同时开启了多个学位类型,导致封面排版冲突。
当你写下: \documentclass[type=phd, lang=chs]{yanputhesis}
xkeyval会解析出type=phd。- 模板内部会调用
\set@type@phd。 - 随后模板会根据
\if@npu@type@phd这个开关是否为真,来决定封面上打印“博士学位论文”还是“硕士学位论文”。
代码中多次出现的 \csname ... \endcsname 是底层命令,用于将字符串动态转换为命令名。
- 示例:
\DeclareOptionX{lang}[chs]{\csname set@lang@#1\endcsname}- 如果用户写
lang=eng,#1就是eng。 - 系统会自动拼接成
\set@lang@eng并执行,从而调用你上一段代码中定义的语言设置宏。
- 如果用户写
使用模板,其中lang=chs等选项就是通过\DeclareOptionX{键名}[默认值]{执行代码}设置的
1
\documentclass[lang=chs, degree=phd, blindreview=false, winfonts=true, academic=true]{yanputhesis}
1
2
3
4
5
6
7
\DeclareOptionX{lang}[chs]{\csname set@lang@#1\endcsname}
\DeclareOptionX{degree}[phd]{\csname set@type@#1\endcsname}
\DeclareOptionX{blindreview}[true]{\csname @npu@output@blindreview#1\endcsname}
\DeclareOptionX{adobe}[true]{\csname @npu@font@adobe#1\endcsname}
\DeclareOptionX{winfonts}[true]{\csname @npu@font@winfonts#1\endcsname}
\DeclareOptionX{academic}[true]{\csname @npu@academic#1\endcsname}
\DeclareOptionX*{\PassOptionsToClass{\CurrentOption}{book}}
1
\DeclareOptionX{键名}[默认值]{执行代码}
- 键名 (Key):用户在
\documentclass[...]中写的参数名(如lang、degree)。 - 默认值 (Default):如果用户只写了键名而没给值(例如只写
[lang]而非[lang=chs]),系统会自动套用的值。 - 执行代码:当该选项被激活时运行的 LaTeX 指令。在代码中,
#1代表用户输入的数据。
\ExecuteOptionsX这些命令的作用是设定默认值。如果在 \documentclass{yanputhesis} 中没有输入任何参数,模板将自动按照以下配置运行
\ProcessOptionsX \relax 这是整个参数解析流程的“开关”。它会对比用户在文档开头输入的参数与上述默认值,最终决定当前编译环境的各项指标。
\LoadClass[...] {book} 这行代码表明 yanputhesis 是继承自基础 book 类开发的
所以上面的代码通过DecareOptionX声明选项,并设置变量,方便后续通过变量设置属性。
1
2
3
4
5
6
7
8
9
10
11
\newcommand\@npu@replaceitwithblank[1]{{\setlength{
\fboxsep}{0pt}\colorbox{white}{\phantom{#1}}}}
\newcommand\@npu@replaceitwithblack[1]{{\setlength{
\fboxsep}{0pt}\colorbox{black}{\phantom{#1}}}}
\newcommand\@npu@replaceitwithstars{ *** }
\newcommand{\blindreview}[1]{\if@npu@output@blindreview
\@npu@replaceitwithblank{#1}\relax\else #1\fi}
\newcommand{\blackbox}[1]{\if@npu@output@blindreview
\@npu@replaceitwithblack{#1}\relax\else #1\fi}
\newcommand{\markname}[1]{\if@npu@output@blindreview
\@npu@replaceitwithstars\relax\else #1\fi}
利用\newcommand自定义命令,现代latex也可以使用
1
\NewDocumentCommand{\命令名}{参数类型}{定义内容}
m:代表必选参数 (Mandatory)。O{默认值}:代表可选参数 (Optional),且带有默认值。
另外也有利用\let和\renewcommand重新定义声明,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
\let\old@toc\tableofcontents
\renewcommand{\tableofcontents}{
\sDefault\phantomsection
\bookmark[dest=\HyperLocalCurrentHref, level=0]{\contentsname}
\old@toc \cleardoublepage
}
\let\old@lof\listoffigures
\renewcommand{\listoffigures}{
\sDefault\phantomsection
\addcontentsline{toc}{chapter}{\listfigurename}
\old@lof \cleardoublepage
}
\let\old@log\listoftables
\renewcommand{\listoftables}{
\sDefault\phantomsection
\addcontentsline{toc}{chapter}{\listtablename}
\old@log \cleardoublepage
}