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}   % class supplied by the template
\usepackage{mytemplate} % sometimes a .sty instead of .cls
\begin{document}
\title{My Title}
\author{Me}
\maketitle
% <content>
\end{document}

创建自己的模板需要同时使用cls类完全控制文档结构,同时使用stl样式文件包含额外的宏或者格式。编写cls的方式一是可以通过编写dtx文件另一个是直接基于现有cls模板进行调整。考虑到常见情况,其实基于cls模板进行更改更实际。

LaTeX软件工程
dtxsource code
insbuild script
cls/stybinary
pdfdocumentation

dtx文件

而dtx本质是文学化编程(literate programming)+docstrip.

一个标准 dtx 文件 = 3 个部分:

  1. driver(生成文档)
  2. documentation(说明文档)
  3. tagged code(可提取代码)

文学化编程含义就是在源代码中包含注解并且生成的内容也会包含注解pdf,让人看dtx源码就能容易看懂。

dtx包含内容

driver部分

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 文件中正确配置,内容才会进入 .clsltxdoc 类的核心行为

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 会区分:

类型编译结果注释方式
文档文字PDF 普通文本% 开头普通文字
文档命令PDF 按样式渲染% 开头,但行中有 \section, \subsection, \begin{macro}
宏代码PDF 代码块显示% \begin{macrocode} … % \end{macrocode}

核心规律:ltxdoc 会自动识别 % 注释中的 LaTeX 文档命令并执行它来生成 PDF 样式,而普通文字不会被执行

documentation部分

1
2
3
4
5
6
% \section{Introduction}
% This is my custom class.
%
% \subsection{Features}
% - Custom section style
% - Custom layout

规则:

  • 每行必须以 % 开头
  • 可以写完整 LaTeX 文档
  • 会出现在 PDF 文档中

tagged code部分

1
2
3
4
5
%<*class>
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/22 My class]
\LoadClass{article}
%</class>

%<*tag> 开始

%</tag> 结束

中间是纯 TeX 代码(不能加 % 注释)。tagged code 是 源码块,可以被 docstrip 提取生成可执行的 cls/sty 文件,同时也可以用来生成 PDF 中的代码显示:

  • 定义 class/package 的宏命令、环境、选项
  • 可以写文档注释(% 开头)用于 PDF 文档显示
  • 提供 可控模块化(通过 tag 提取不同内容生成不同文件)核心目标:把源码和文档组织在同一个文件,方便提取和展示。

\begin{macro} 用来文档化一个宏(命令),并生成“宏索引”和结构化说明。

它的主要作用是:

  1. 给某个宏建立“文档条目”
  2. 在 PDF 中生成“宏说明块”
  3. 在索引中登记宏名(配合 \CodelineIndex
  4. 让 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
%<*base>
基础宏定义
%</base>

%<*layout>
版式控制
%</layout>

%<*section>
章节结构
%</section>

%<*option>
class options
%</option>

%<*class>
主类文件
%</class>

简单来说,%开头的都会在pdf中显示,除非tagged code块中除开%后是\指令则会执行,如果不是%开头,则会出现在cls文件中,如果是在tagged code中类似。

docstrip 会把 非注释行(不以 % 开头)的内容 当作 真正源码。也就是说,无论它在 %<*tag> 中还是不在,docstrip 都会把这些行写进目标文件。有了 tag,你可以控制 docstrip 只提取 classmacro如果不使用 tag,所有非 % 行都会默认被提取到生成文件

如何让dtx生成cls

使用dtx生成cls文件流程:

  1. 写 dtx
  2. 写 ins
  3. 运行 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文件

cls文件格式往往如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
% mytemplate.cls – a very simple article‑style class
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{mytemplate}[2025/12/03 v1.0 My custom class]
\LoadClass[12pt]{article} % inherit from article

% ---- custom settings ----
\RequirePackage{geometry}
\geometry{margin=2cm}
\RequirePackage{fontspec}
\setmainfont{Latin Modern Roman}

% ---- user‑level commands ----
\newcommand\mytitle[1]{\centerline{\LARGE\bfseries #1}}
\endinput

cls文件底层是Tex,而不是Latex,其核心问题包含三个:

  1. 文档类如何加载?
  2. 类如何定义结构和命令?
  3. 类如何控制版式?

下面围绕这三点说明。

1
2
3
4
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{myclass}[2026/01/22 My custom class]

\LoadClass{article}
命令作用
\NeedsTeXFormat声明 LaTeX 版本
\ProvidesClass声明类信息
\LoadClass继承已有类
\RequirePackage加载宏包

宏定义

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 已存在 → 什么都不做
  • 如果不存在 → 定义
类别命令作用
TeX\def定义宏
TeX\gdef全局宏
TeX\edef展开定义
TeX\xdef全局展开
TeX\let赋值宏
LaTeX\newcommand新宏
LaTeX\renewcommand重定义
LaTeX\providecommand安全定义
LaTeX\DeclareRobustCommand鲁棒宏
LaTeX\NewDocumentCommand现代宏
LaTeX\NewExpandableDocumentCommand可展开宏

\newcommand

\renewcommand

\def

\let

\DeclareRobustCommand

\NewDocumentCommand

常用宏

\newifif

\newif 用来创建一个布尔型开关宏(true/false),通常用于:

  • 选项开关(class/package options)
  • 控制是否执行某段代码

它会生成三个宏:

  1. \<name>true — 设置开关为真
  2. \<name>false — 设置开关为假
  3. \<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=phdfont=adobe 这样“键-值对”格式的复杂选项。

定义状态开关 (\newif)

这一部分定义了大量的布尔变量(即只有“真”或“假”两种状态的开关)。

注意:命令中包含 @ 符号,说明这些是内部命令,防止普通用户在正文中误改。

命令对应功能默认含义
\if@npu@lang@chs语言环境如果为真,则排版中文版;为假则排版英文版。
\if@npu@type@phd博士学位标记当前是否为博士论文。
\if@npu@type@mst硕士学位标记当前是否为硕士论文。
\if@npu@type@bcl本科毕业设计标记当前是否为本科生论文。
\if@npu@academic学术/专业用于区分“学术学位”和“专业学位”(工程硕士等)。
\if@npu@output@blindreview盲评模式重要开关。开启后会隐藏作者姓名、导师等敏感信息。
\if@npu@font@adobeAdobe 字体是否使用 Adobe 系列字体进行排版。
\if@npu@font@winfontsWindows 字体是否强制调用 Windows 系统内置字体(如中易宋体)。

\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}

  1. xkeyval 会解析出 type=phd
  2. 模板内部会调用 \set@type@phd
  3. 随后模板会根据 \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}} % 传递参数到 book 类
1
\DeclareOptionX{键名}[默认值]{执行代码}
  • 键名 (Key):用户在 \documentclass[...] 中写的参数名(如 langdegree)。
  • 默认值 (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{ *** } % 替换成 3 个星号
\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),且带有默认值。
组成部分是否必选作用描述
{\命令名}必选你要创建的新命令名称,必须以反斜杠 \ 开头。
[参数个数]可选定义该命令接受多少个参数,取值范围是 1–9。如果不写,默认为 0(即无参数)。
[默认值]可选如果写了这一项,新命令的第一个参数就变成了“可选参数”。如果不填,则使用这个默认值。
{具体定义}必选命令实际执行的内容。使用 #1, #2#9 来代表传入的参数。
命令行为逻辑适用场景
\newcommand如果已存在,报错定义全新的、确定没有重名的命令。
\renewcommand如果不存在,报错修改现有的命令(比如修改西工大模板默认的标题格式)。
\providecommand无论是否存在,都不报错“如果没有就定义,有就跳过”。常用于编写兼容性代码。
\DeclareRobustCommand无论是否存在,直接覆盖增强型定义,防止命令在页眉、目录等“移动参数”中失效。

另外也有利用\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 %
% \addcontentsline{toc}{chapter}{\contentsname} %
\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 %
}

相关资料

  1. 【LaTex】cls文件编写和使用入门 - 知乎
  2. LaTeX 模板 - 快速入门指南
  3. LaTeX .cls 文件编写经验 | 学,行之,上也
  4. latexdoc-chinese-translation/clsguide-current-zh-cn/clsguide-zh-cn.pdf at main · rockyzhz/latexdoc-chinese-translation
  5. rockyzhz/latexdoc-chinese-translation: LaTeX 文档中文翻译项目
-------------本文结束感谢您的阅读-------------
感谢阅读.

欢迎关注我的其它发布渠道