Files
ouc_latex_template/试卷/ouckey.sty
T
laoyang 93b03c8a78 试卷字体统一为思源宋体 #20
用户要求:「试卷的字体,需要改一下,全部都用思源宋体就可以了。」

pymupdf 提取两份成品 PDF 定位到四类需要统一的字体:

  试卷题型示例(6 页)            参考答案与评分标准示例(3 页)
  OUCSerif-Regular 203 中文        OUCSerif-Regular 333 中文
  TeXGyreTermes-Regular 133 西文   TeXGyreTermes-Regular 197 西文
  TeXGyreTermes-Bold 85 题号分值   OUCSans-Regular 37 黑体小标题
  OUCSerif-Bold 81 中文加粗         CMR10/CMMI10/CMSY10 49 数学公式
  OUCKai-Regular 25 卷头与成绩表    TeXGyreTermes-Bold 21 数字

一、西文与数字:Termes → 思源宋体(oucexam.sty / ouckey.sty)

先在包里用 \newfontfamily 登记 \OUCLatinSerif / \OUCLatinSerifBold
(四个档位全给,斜体指回 Regular —— 思源宋体没有真斜体),再把
\tnr / \tnrb / \sunfont 都 \let 过去。

⚠️ 只做这一步**不够**,这点踩了一轮:模板里大量文字根本没走 \tnr,
而是走 \setmainfont(默认西文族)——\textbf{learning outcome} 的裸西文、
页脚的页码与总页数、任何 \selectfont 之后的裸 ASCII。
第一轮只 \let 时,PDF 里仍残留 22 个 TeXGyreTermes span:
页脚 6 个页码 + 6 个总页数 + 第 4 页的 A./B./C./D. 与 learning outcome 等。
⇒ 必须连 \setmainfont / \setsansfont 一起指到 OUCSerif。

二、卷头与成绩表题号:霞鹜文楷 → 思源宋体(oucexam.sty)

\kh 与 \scoretable 用 \kaiti。包里 \let\kaiti\songti 一次性收口,
不动 ../fonts.tex 的 \def\kaiti —— 别的模板还要用楷体。

三、答案卷黑体小标题:思源黑体 → 思源宋体(ouckey.sty)

\heiti 共 37 处(卷头大标题与各节小标题)。同样 \let\heiti\songti。

四、数学公式:保持不动

$x$ / $\dfrac{1}{45}\sum f_i x_i$ 等 49 处仍走 Computer Modern。
思源宋体没有 MATH 表(实测只有 GSUB/GPOS),不能作数学字体;
强行塞进正文字体会破坏斜体变量、正体数字、运算符间距。用户已确认。

五、装订线虚线:连字符排字 → 用线画(oucexam.sty)

原卷那 6 段竖虚线是 18bp Times 连字符排出来的。换成思源宋体后连字符
每个宽 0.346 em(Termes 0.333),每串会长 4~7bp,破坏"复刻原卷长度"。
⇒ 改为几何画的短横线段,长度锁死。实测原卷(720dpi 逐笔画测量):
  单个连字符笔画 长 4.40bp、粗 1.10bp、x 中心 51.60bp
  相邻笔画步距 6.00bp
新增 \ouc@dashseg{起点y}{笔画数},六段坐标与笔画数逐段照抄原卷
(含原卷被白底遮断成 13+3 两截的那一段)。
⚠️ \newcommand 必须定义在 \ouc@dobindstrip 外面,否则每次 shipout
重新定义,第二页起报 already defined。
⚠️ y 坐标不能写 \dimexpr#1bp+\i*6bp —— TikZ 坐标解析器不认小数 +
\dimexpr 组合,报 Illegal unit of measure。要先 \pgfmathsetmacro。

六、直引号归入 CJK(ouckey.sty)

答案卷正文用 ASCII 直引号 "" 表示中文引号(32 处)。U+0022 在
Basic Latin,xeCJK 判为西文 ⇒ 引号走 \setmainfont,与两侧汉字不同字体,
并挤压出 Overfull(第 88 行 +8.07pt)。
\xeCJKDeclareCharClass{CJK}{"0022} 归位后解决。
⚠️ 不在 fonts.tex 里改:那里全库共用,会让正文真正的英文引号(代码
   片段 "hello world")也变全角。

七、Overfull 的修法:改源码断行,不调参数(ouckey.sty 留有完整记录)

换字体后 \keyitem{3}{4} 那段 Overfull 2.61pt,实际压出版心
(最右 527.01bp,版心止于 524.41)。试过并确认**全部无效**的五种:
  · \emergencystretch 1.5em —— 本行没有弹性胶可拉,无从施展
  · \tolerance 3000 —— 不是不满意断点,是没有更早的合法断点
  · PunctStyle=kaiming —— 能消 Overfull,但标点悬挂探出版心 6.5bp
  · PunctStyle=quanjiao —— 同样探出 8bp
    (xeCJK 这几个 PunctStyle 做的是"标点悬挂"不是"标点压缩",
      指望它消 Overfull 必翻车)
  · \rightskip 0pt plus 6bp(含 \AtBeginDocument 版)—— 6bp 远小于
    需要的 150bp,TeX 仍选"撑破"这个解
正解:第 124 行句号「。」后加 \\。分项给分的答案本来就该一句一行,
读者靠句号扫分点 —— 既解决排版,又让内容结构更清楚。

验证(两套环境各跑,逐项同数)

  WSL / TeX Live 2025    试卷 rc=0 err=0 over=0 6页
                         答案卷 rc=0 err=0 over=0 3页
  Windows / TeX Live 2022 试卷 rc=0 err=0 over=0 6页
                         答案卷 rc=0 err=0 over=0 3页

pymupdf 提取字体分布:

  试卷题型示例      OUCSerif-Regular 291 + OUCSerif-Bold 70
                    → 非宋体文字字体:无
  参考答案示例      OUCSerif-Regular 235 + OUCSerif-Bold 53
                    + CMR10/CMMI10/CMSY10/CMMI7/CMEX10(数学,保留)
                    → 非宋体文字字体:无

几何核对(逐 span 比对,均与基线一致或更优):
  · 成绩表 17 栏逐字符坐标与基线**完全相同**(硬编码坐标,不受字体影响)
  · 装订线 96 笔全部绘出:x 49.40~53.60(宽 4.40bp)、步距 6.00bp,
    六段位置与原卷实测一致
  · 三页最右墨迹均为 524.41bp,与版心严丝合缝,无越界
  · 试卷 6 页最右最大 523.33bp(第 4 页 \mtpair 的 44% 栏宽设计,
    基线同位置同样数值,非本次引入)

涉及文件:
  试卷/oucexam.sty                 默认族、西文字族、\kaiti、装订线虚线
  试卷/ouckey.sty                  默认族、西文字族、\heiti、直引号、踩坑记录
  试卷/参考答案与评分标准示例.tex  第 124 行句号后加断行
2026-10-29 21:29:15 +08:00

515 lines
24 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
%======================================================================
% ouckey.sty — 中国海洋大学 期末考试 参考答案及评分标准 版式包
%
% 设计依据:学校无统一模板,本包按国内高校通行体例提炼,要点四条——
% ① 抬头须有:学年学期、课程名、试卷类型、学院/年级/专业、
% 开课学院、任课教师、考试方式、(可选)满分与考试日期;
% ② 客观题(填空/选择/判断)答案集中列出,附统一给分规则;
% ③ 主观题(名词解释/简答/论述/分析/计算/作图…)答案内嵌
% "分步给分"标记,每个得分点后直接标(X 分);
% ④ 每个大题末尾给【评分标准】,写清"答错/多选/漏选/未答"如何计分。
%
% 与 oucexam.sty(试卷版式)配套使用,字号字体同名同义:
% \Fivhao 五号正文 / \Sihao 小四 / \Xiaowu 小五
% \TNR{} Times / \TNRB{} Times Bold / \SUN{} 宋体 / \heiti 黑体
%
% 纸张:默认 A4(阅卷、归档、打印都按 A4 走)。
% 需要与试卷同尺寸(195mm×270mm 专用试卷纸)时加选项 exampaper:
% \usepackage[exampaper]{ouckey}
%
% 用法见同目录 参考答案与评分标准示例.tex
%======================================================================
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{ouckey}[2026/10/22 v1.0 OUC answer key & marking scheme]
% 本包大量使用 \ouckey@xxx 私有名,@ 必须当字母
\makeatletter
\RequirePackage{fontspec}
\RequirePackage{xeCJK}
\RequirePackage{graphicx}
\RequirePackage{lastpage}
\RequirePackage{keyval}
\RequirePackage{array}
\RequirePackage{tikz} % 作图题的参考答案要配图
\usetikzlibrary{calc,positioning}
%----------------------------------------------------------------------
% 纸张选项
% exampaper → 195mm × 270mm(与试卷同纸,便于和试卷一起装订归档)
% 默认 → A4(阅卷现场用,字大好认)
%----------------------------------------------------------------------
\newif\ifouckey@exampaper \ouckey@exampaperfalse
\DeclareOption{exampaper}{\ouckey@exampapertrue}
\ProcessOptions\relax
\ifouckey@exampaper
\RequirePackage[paperwidth=195mm,paperheight=270mm,
left=82.2bp,textwidth=436.55bp,
top=46bp,textheight=650bp,
headheight=0pt,headsep=0pt,footskip=30bp]{geometry}
\else
\RequirePackage[paperwidth=210mm,paperheight=297mm,
left=25mm,right=25mm,top=25mm,bottom=25mm,
headheight=0pt,headsep=0pt,footskip=28bp]{geometry}
\fi
\setlength{\parindent}{0pt}
\setlength{\parskip}{0pt}
\pagestyle{empty}
%----------------------------------------------------------------------
% 字体(#20:与 oucexam.sty 同步,全部统一为思源宋体)
% 全部取自公共字体包 ../fonts.tex,再在包内重定向。
%
% ⚠️ 必须和 oucexam.sty 做**完全相同的处理**,否则两份文件观感不一致:
% oucexam 是试卷、ouckey 是答案卷,学校通常一起印发。
%
% ⚠️⚠️ 只重定向 \tnr / \tnrb 不够 —— 模板里大量文字没走它们:
% · \textbf{...} 的裸西文 → 走 \setmainfont
% · 「(4 分)」里的数字 → 走 \setmainfont
% · \thepage / \pageref 页码 → 走 \setmainfont
% ⇒ 必须连 \setmainfont 一起改,否则 PDF 里会残留 Termes span。
% 实测第一轮只做 \let 时,答案卷残留 197 个 Termes span。
%
% ⚠️ 数学公式($x$ / $\dfrac{}{}$ / $\sum$)**不在统一范围内**,
% 仍走 Computer Modern。原因见 oucexam.sty 的说明:思源宋体没有
% MATH 表,不能作数学字体。用户已确认保持不动。
%----------------------------------------------------------------------
\input{../fonts.tex}
%----------------------------------------------------------------------
% 西文字族:思源宋体(与 oucexam.sty 同一套定义,复制而非 \input,
% 因为两个 .sty 是并列关系,谁先加载不确定,不能互相依赖)
%----------------------------------------------------------------------
\newfontfamily\OUCLatinSerif{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\newfontfamily\OUCLatinSerifBold{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Bold,
BoldFont = *-Bold,
ItalicFont = *-Bold,
BoldItalicFont = *-Bold,
]
% 默认西文字体 → 思源宋体(这一步不能省,见上面的说明)
\setmainfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
\setsansfont{OUCSerif}[
Extension = .otf,
Path = ../fonts/ouc-serif/,
UprightFont = *-Regular,
BoldFont = *-Bold,
ItalicFont = *-Regular,
BoldItalicFont = *-Bold,
]
% \sunfont 原指 SimSun,现统一指思源宋体
\let\sunfont\OUCLatinSerif
\let\tnr\OUCLatinSerif
\let\tnrb\OUCLatinSerifBold
% 答案卷里的黑体小标题(\heiti,共 37 处)一并换成思源宋体,#20 要求统一。
% ⚠️ 用 \let 而非改 \def\heiti —— 后者会破坏 ../fonts.tex,影响别的模板。
% ⚠️ 若将来要恢复黑体标题,注释掉下面这行即可。
\let\heiti\songti
% 局部字体切换必须显式收尾,否则 xeCJK 字体会"溢出"到后一段
\newcommand{\TNR}[1]{{\tnr #1\normalfont}}
\newcommand{\TNRB}[1]{{\tnrb #1\normalfont}}
\newcommand{\SUN}[1]{{\sunfont #1\normalfont}}
% 中西文间距 2.70bp(原卷实测值,不随字号缩放)。
\xeCJKsetup{CJKecglue={\hskip 2.70bp\relax},PunctStyle=plain}
%----------------------------------------------------------------------
% 直引号 " (U+0022) 归入 CJK 字符类(#20 补充修正)
%
% ⚠️ 问题:本文件正文用 ASCII 直引号 "" 表示中文引号(共 32 处),
% 而 xeCJK 按 Unicode 区段分类 —— U+0022 在 Basic Latin,被判成**西文**,
% 于是引号走 \setmainfont(原为 Termes),与两侧汉字不同字体、不同宽度。
% 换字体后表现为一行里出现 TeXGyreTermes-Regular 的 ",且因为西文引号
% 比中文引号窄、字距算法不同,挤压出 Overfull(实测第 88 行 +8.07pt)。
%
% ⚠️ 为什么不在 fonts.tex 里改:那里是**全库共用**的,把 U+0022 全局归 CJK
% 会让正文里真正的英文引号(如 code 片段 "hello world")也变成全角,
% 破坏那些地方。这是试卷的局部需求 —— 试卷正文基本是中文,直引号就是
% 中文引号的意思。
% ⚠️ 必须在 \input{../fonts.tex} **之后**声明,否则被随后加载的默认分类覆盖。
%----------------------------------------------------------------------
\xeCJKDeclareCharClass{CJK}{"0022} % " 直引号(当中文引号用)
%----------------------------------------------------------------------
% 【#20 踩坑记录】换字体引起的 Overfull 怎么修(结论:改源码断行,别调参数)
%
% 现象:字体统一为思源宋体后,参考答案与评分标准示例.tex 第 123-125 行
% (\keyitem{3}{4})出现 Overfull 2.61pt,渲染后压出版心右边界
% (最右墨迹 527.01bp,版心止于 524.41bp)。
% 基线同一行本来就有 0.21pt 轻微 Overfull,换字体后西文变宽
% (数字 +6%、小写字母 +13~20%),把 0.21 放大到 2.61。
%
% 根因:该行末尾是「…正迁移与负迁移(1 分),」。
% · 「,」按中文标点禁则不能出现在行首 ⇒ 不许在「)」与「,」之间断
% · \pt 定义里有 `~`(\nobreakspace)⇒ 「(1 分)」内部也不可断
% ⇒ 「(1 分),」约 16bp,是不可断尾部,必须整体塞进本行。
% 而本行没有弹性胶可拉(汉字定宽、唯一的西文空格已满),
% 往前找更早的合法断点(句号「。」之后)的话,第一行只填 65%,
% 欠宽 150bp 无处拉伸 ⇒ badness 爆掉 ⇒ TeX 宁可撑破 2.61bp。
%
% ⚠️⚠️ 试过且**全部无效**的方案(别再花时间重复,每种都实测过):
% ① \emergencystretch 1.5em —— 无效。只追加"可拉伸量",
% 本行没有胶可拉,给多少额度都无从施展。
% ② \tolerance 3000 —— 无效。不是不满意断点,是没有更早的**合法**断点。
% ③ PunctStyle=kaiming —— 能消 Overfull,但句末标点悬挂探出版心
% 6.5bp(第 3 页右缘 530.93 > 524.41),比不修更难看。
% ④ PunctStyle=quanjiao —— 同样探出 8bp(532.5),一样不可接受。
% 根子在于 xeCJK 的这几个 PunctStyle 做的是"行末标点**悬挂**",
% 不是"标点**压缩**"—— 中文排版里这是两回事,指望它消 Overfull 必翻车。
% ⑤ \rightskip 0pt plus 6bp(含 \AtBeginDocument 版)—— 无效。
% 6bp 远小于需要的 150bp,TeX 仍选"撑破"这个解。
% 注意:preamble 里直接 \setlength 还会被 ctex/xeCJK 初始化重置,
% 必须 \AtBeginDocument 才可能生效 —— 这类"设了没反应"的常见原因。
%
% ✅ 正解:**在源码的自然断点处手动断行**。
% 第 124 行末尾句号「。」之后本来就是语义上的句子边界,加 \\ 即可:
% \keyitem{3}{4}{%
% \textbf{学习迁移}:指一种学习对另一种学习的影响\pt{2}。\\
% 按方向分正迁移与负迁移\pt{1},其发生依赖于共同要素与概括水平\pt{1}。}
% 已实测:Overfull 归零,三页最右墨迹均为 524.41bp,与版心严丝合缝。
%
% 为什么这是对的做法而不是"绕过问题":
% 分项给分的答案本来就该一句一行,读者(阅卷人)靠句号扫分点。
% 手动断行既解决排版,又让内容结构更清楚 —— 不是妥协,是双赢。
% ⚠️ 若将来修改了这段文字导致又不齐,先看这里:多半还是在句号处补 \\。
%----------------------------------------------------------------------
\newcommand{\Fivhao}{\fontsize{10.5bp}{16bp}\selectfont}
\newcommand{\Sihao}{\fontsize{12bp}{18bp}\selectfont}
\newcommand{\Xiaowu}{\fontsize{9bp}{13bp}\selectfont}
\AtBeginDocument{\Fivhao\selectfont}
%======================================================================
% 1. 抬头信息
% \keysetup{ ... } 里填;留空的项自动不印(不留空横线)
%======================================================================
\def\ouckey@year{}\def\ouckey@term{}\def\ouckey@course{}
\def\ouckey@type{}\def\ouckey@college{}\def\ouckey@grade{}
\def\ouckey@major{}\def\ouckey@dept{}\def\ouckey@teacher{}
\def\ouckey@examiner{}\def\ouckey@mode{}\def\ouckey@date{}
\def\ouckey@total{}
\define@key{ouckey}{year}{\def\ouckey@year{#1}}
\define@key{ouckey}{term}{\def\ouckey@term{#1}}
\define@key{ouckey}{course}{\def\ouckey@course{#1}}
\define@key{ouckey}{type}{\def\ouckey@type{#1}}
\define@key{ouckey}{college}{\def\ouckey@college{#1}}
\define@key{ouckey}{grade}{\def\ouckey@grade{#1}}
\define@key{ouckey}{major}{\def\ouckey@major{#1}}
\define@key{ouckey}{dept}{\def\ouckey@dept{#1}}
\define@key{ouckey}{teacher}{\def\ouckey@teacher{#1}}
\define@key{ouckey}{examiner}{\def\ouckey@examiner{#1}}
\define@key{ouckey}{mode}{\def\ouckey@mode{#1}}
\define@key{ouckey}{date}{\def\ouckey@date{#1}}
\define@key{ouckey}{total}{\def\ouckey@total{#1}}
\newcommand{\keysetup}[1]{\setkeys{ouckey}{#1}}
% 判空:\ifx 比不了多 token 参数,用 \detokenize 展成字符串再比
\newcommand{\ouckey@ifempty}[1]{%
\if\relax\detokenize{#1}\relax
\expandafter\@firstoftwo
\else
\expandafter\@secondoftwo
\fi}
% 一格信息:标签加粗,值用宋体;值为空则整格不出
\newcommand{\ouckey@cell}[2]{%
\ouckey@ifempty{#2}{}{\textbf{#1:}#2}}
% 一行三格,等宽
\newcommand{\ouckey@inforow}[6]{%
\par\noindent
\makebox[0.3333\linewidth][l]{\ouckey@cell{#1}{#2}}%
\makebox[0.3333\linewidth][l]{\ouckey@cell{#3}{#4}}%
\makebox[0.3333\linewidth][l]{\ouckey@cell{#5}{#6}}\par}
%----------------------------------------------------------------------
% 抬头:标题 + 信息栏 + 一条分隔线
% 标题 = 学年学期 + 课程名 + 试卷类型 +「参考答案及评分标准」
%----------------------------------------------------------------------
\newcommand{\makekeyhead}{%
\par\noindent
\begin{minipage}{\linewidth}\centering
{\fontsize{16bp}{24bp}\selectfont\heiti
中国海洋大学%
\ouckey@ifempty{\ouckey@year}{}{\quad\TNR{\ouckey@year}—\TNR{\the\numexpr\ouckey@year+1\relax}~学年}%
\ouckey@ifempty{\ouckey@term}{}{\ouckey@term 学期}\par}
\vspace*{6bp}
{\fontsize{15bp}{22bp}\selectfont\heiti
《\ouckey@course》期末考试%
\ouckey@ifempty{\ouckey@type}{}{(\ouckey@type~卷)}%
参考答案及评分标准\par}
\end{minipage}\par
\vspace*{10bp}
\hrule height 1.2bp
\vspace*{8bp}
\ouckey@inforow{开课学院}{\ouckey@dept}%
{课程名称}{\ouckey@course}%
{试卷类型}{\ouckey@ifempty{\ouckey@type}{}{\ouckey@type~卷}}
\ouckey@inforow{学生学院}{\ouckey@college}%
{年级专业}{\ouckey@major}%
{考试方式}{\ouckey@mode}
\ouckey@inforow{任课教师}{\ouckey@teacher}%
{命题人}{\ouckey@examiner}%
{满分}{\ouckey@ifempty{\ouckey@total}{}{\TNR{\ouckey@total}~分}}
% ★ 抬头【不印考试日期】。
% date 这个键仍保留(老文件写了 date= 不会报 unknown key),
% 但不再渲染,对话框里也不显示,避免抬头行数随日期有无而浮动。
\vspace*{6bp}
\hrule height 0.6bp
\vspace*{10bp}}
%======================================================================
% 2. 分值标记
% \pt{2} → 「(2 分)」——嵌在答案文字中间,标"这一点值 2 分"
% \ptline{2}{要点} → 要点靠左、分值靠右对齐(长要点更清爽)
%======================================================================
% 分值标注「(X 分)」。
% ⚠️ 里面用 `~`(\nobreakspace)是**有意为之** —— 不让「X」和「分」
% 分处两行,否则会看到「(1」在行末、「分)」在下一行,很难看。
% 代价是「(X 分)」成了不可断的整体,极端行宽下会撑破版心。
% ⇒ 这个代价接受,解决办法是在**源码的句子边界手动断行**,
% 不是在这里放开可断。详见上面「#20 踩坑记录」一节。
\newcommand{\pt}[1]{{\small(\TNR{#1}~分)}}
\newcommand{\ptline}[2]{%
\par\noindent #2\unskip\par\nobreak
\vspace*{-1.2\baselineskip}%
\par\noindent\hfill{\small(\TNR{#1}~分)}\par\vspace*{2bp}}
% 阿拉伯数字 → 中文(1..16)★必须在使用之前定义
\newcommand{\ouckey@cnnum}[1]{%
\ifcase\numexpr#1\relax\or 一\or 二\or 三\or 四\or 五\or 六%
\or 七\or 八\or 九\or 十\or 十一\or 十二%
\or 十三\or 十四\or 十五\or 十六\fi}
%======================================================================
% 3. 大题标题
% \keysection{一}{单项选择题}{10}{2}
% → 一、单项选择题(本题共 10 题,每小题 2 分,共 20 分)
% 小计自动算,不用手填。
% \keysectionfree{二}{填空题}{共 10 空,每空 1 分}
% → 二、填空题(共 10 空,每空 1 分) —— 分值不整齐时用这个
%======================================================================
\newcommand{\keysection}[4]{%
\par\addvspace{10bp}%
{\Sihao\heiti
#1、#2(本题共~\TNR{#3}~题,每小题~\TNR{#4}~分,共~\TNR{\the\numexpr#3*#4\relax}~分)\par}%
\addvspace{5bp}}
\newcommand{\keysectionfree}[3]{%
\par\addvspace{10bp}%
{\Sihao\heiti #1、#2(#3)\par}%
\addvspace{5bp}}
%======================================================================
% 4. 客观题答案
% \objkeys{C,B,A,B,C,A,C,A,C,A} → 每行 6 个,自动换行
% \objkeys[5]{...} → 每行 5 个
% 答案串逗号分隔;题号自动递增。
%
% ★ 这个宏反复改过很多次,下面几条是实测结论,动之前先看清:
%
% 【不能用 \par 分行】
% 在 \@for 循环体里写 "\hbox to \textwidth{...}\par\noindent",
% 整段放进文档正文是好的,但一旦包进宏体就失效——
% 十几项全排在同一个基线(pdftotext -bbox 的 yMin 完全相同),
% 后续直接溢出纸边。\@for 先把循环体抓成参数,\par 的展开时机不对。
%
% 【不能用 \unvbox + \prevdepth 思路】
% \unvbox 执行完寄存器即被清空,紧跟的 \ifdim\ht...>0pt 恒为假;
% 而且 \unvbox 后 TeX 把 \prevdepth 设成 -1000pt 抑制行间胶水,
% 各行会叠在一起(实测间距只有一个行高)。
%
% 【不能用 \makebox[<变宽>]】
% 在 \@for 循环里 \makebox[\dimexpr\textwidth/\perrow\relax] 或
% \makebox[\cw] 会**静默吞掉内容**(PDF 里整行消失,且不报错,
% 只在 log 里留 Undefined control sequence / Missing number)。
% → 用 \hbox to <定宽> 代替,定宽预先算进 \dimen 寄存器。
%
% 可靠做法(本实现):
% 逐项往 \ouckey@cellbox 塞一个 "\hbox to \ouckey@cellw" 单元格;
% 攒满 \\ouckey@perrow 个就整理成一行,追加进 \ouckey@allbox(\vbox);
% 行与行之间自己补胶水 = \baselineskip - \ht(行) - \dp(行);
% 最后 \unvbox 一次性铺出。全程不碰 \par、不碰 \makebox。
%======================================================================
\newbox\ouckey@cellbox
\newbox\ouckey@linebox
\newbox\ouckey@allbox
\newcount\ouckey@objn
\newcount\ouckey@objrow
\newcount\ouckey@perrow
\newcount\ouckey@rowsdone
\newdimen\ouckey@cellw
\newdimen\ouckey@textw
\newdimen\ouckey@gap
\newcommand{\ouckey@flushline}{%
\setbox\ouckey@linebox\hbox to \ouckey@textw{\unhbox\ouckey@linebox\hss}%
\ouckey@gap=\dimexpr\baselineskip-\ht\ouckey@linebox
-\dp\ouckey@linebox\relax
\ifnum\ouckey@rowsdone>0
\setbox\ouckey@allbox\vbox{\unvbox\ouckey@allbox
\vskip\ouckey@gap \box\ouckey@linebox}%
\else
\setbox\ouckey@allbox\vbox{\unvbox\ouckey@allbox
\box\ouckey@linebox}%
\fi
\advance\ouckey@rowsdone by 1
\setbox\ouckey@linebox\hbox{}}
\newcommand{\ouckey@addcell}[1]{%
\advance\ouckey@objn by 1
\setbox\ouckey@cellbox\hbox to \ouckey@cellw{%
\TNR{\the\ouckey@objn}.#1\hss}%
\setbox\ouckey@linebox\hbox{\unhbox\ouckey@linebox
\box\ouckey@cellbox}%
\ouckey@objrow=\ouckey@objn
\divide\ouckey@objrow by \ouckey@perrow
\multiply\ouckey@objrow by \ouckey@perrow
\ifnum\ouckey@objn=\ouckey@objrow \ouckey@flushline\fi}
\newcommand{\objkeys}[2][6]{%
\par\addvspace{4bp}%
\begingroup
\ouckey@textw=\textwidth
\ouckey@perrow=#1
\ouckey@cellw=\dimexpr\ouckey@textw/\ouckey@perrow\relax
\ouckey@objn=0
\ouckey@rowsdone=0
\setbox\ouckey@linebox\hbox{}%
\@for\ouckey@item:=#2\do{\expandafter\ouckey@addcell\expandafter{\ouckey@item}}%
\ouckey@flushline
\noindent\unvbox\ouckey@allbox
\par
\endgroup
\addvspace{4bp}}
%======================================================================
% 5. 主观题
% \keyitem{1}{8}{答案正文……} → 「1.(8 分)答案正文……」
% \keyitemstar{2}{答案} → 只出题号+答案,不印分值
%
% ★ 注意:参数正文里可以正常用 \\ 换行与 \pt{..};
% 但**不要在里面嵌 tabular/tikzpicture 环境**——那些环境的 & 与 \\
% 会被当成参数分隔符,导致 "File ended while scanning"。
% 表格/图请写在 \keyitem 之后(先写完文字,再另起环境)。
%======================================================================
\newcommand{\keyitem}[3]{%
\par\addvspace{5bp}\noindent
{\bfseries \TNR{#1}.}\pt{#2}\hspace*{3bp}#3\relax\par}
\newcommand{\keyitemstar}[2]{%
\par\addvspace{5bp}\noindent
{\bfseries \TNR{#1}.}\hspace*{3bp}#2\relax\par}
%======================================================================
% 6. 【评分标准】块
% 左侧一条 2bp 竖线 + 灰底标题,视觉上和"答案正文"分开,但不抢眼
% \scoring{内容}
% \scoring*{内容} → 不带左侧竖线(行内短说明用)
%======================================================================
\newcommand{\scoring}{%
\@ifstar{\ouckey@scoring@plain}{\ouckey@scoring@rule}}
\newcommand{\ouckey@scoring@rule}[1]{%
\par\addvspace{4bp}%
\noindent
\begin{minipage}{\linewidth}%
\hspace*{2bp}%
\vrule width 2bp \hspace*{6bp}%
\begin{minipage}{\dimexpr\linewidth-10bp\relax}%
{\bfseries【评分标准】}#1%
\end{minipage}%
\end{minipage}%
\par\addvspace{4bp}}
\newcommand{\ouckey@scoring@plain}[1]{%
\par\addvspace{2bp}\noindent{\bfseries【评分标准】}#1\par\addvspace{2bp}}
% 备注 / 说明(比评分标准低一级,用于"其他解法同等给分"这类话)
\newcommand{\keynote}[1]{%
\par\addvspace{3bp}\noindent
{\small\bfseries 说明:}#1\par\addvspace{3bp}}
%======================================================================
% 7. 分值汇总表
% \keytotal{20,20,15,10,15,20}
% → 自动画"题号 / 一 二 三 … / 合计"三行表,合计自动求和
% 用于阅卷前核对"各大题分值相加是否等于满分"。
%======================================================================
\newcount\ouckey@sum
\newcount\ouckey@cnt
%----------------------------------------------------------------------
% ★ 这里踩过三个坑,都记下:
%
% 坑1 \edef\head{\head&...} 自引用 → 无限展开,parameter stack 爆掉。
% 坑2 \g@addto@macro / \toks 存 token 序列 → 存进去的是 \ouckey@cur
% 这个"宏名",等表格排版时循环早走完,16 格全变成最后一个值。
% 坑3 本想用 \xdef 拼,但在里面包 \TNR{...}(内含 \normalfont,
% 不可展开)→ 又撞栈。
%
% 正解:\xdef 拼装,且**只拼纯字符**,字体命令留到表格里再加。
% 数字用 Times 是题号行的既有风格,直接靠正文西文字体即可。
%----------------------------------------------------------------------
\newcommand{\keytotal}[1]{%
\par\addvspace{12bp}%
\ouckey@sum=0 \ouckey@cnt=0
\def\ouckey@head{}\def\ouckey@body{}%
\@for\ouckey@v:=#1\do{%
\edef\ouckey@cur{\ouckey@v}%
\advance\ouckey@cnt by 1
\advance\ouckey@sum by \ouckey@cur\relax
\edef\ouckey@cn{\ouckey@cnnum{\the\ouckey@cnt}}%
\xdef\ouckey@head{\ouckey@head&\ouckey@cn}%
\xdef\ouckey@body{\ouckey@body&\ouckey@cur}}%
\noindent
\begin{tabular}{|c|*{\the\ouckey@cnt}{c|}|c|}
\hline
\textbf{题号}\ouckey@head&\textbf{合计}\\
\hline
\textbf{满分}\ouckey@body&\the\ouckey@sum\\
\hline
\end{tabular}%
\par\addvspace{12bp}}
%======================================================================
% 8. 结尾签署栏
% ★ 只保留命题人一行。审核人、系(教研室)主任、日期都不印——
% 这三个位置是纸质归档时由相应人员手签,答案稿本身不预印。
% 换标签就直接改下面这行文字;想整栏不要,删掉 tex 里的 \keysign 即可。
%======================================================================
\newcommand{\keysign}{%
\par\addvspace{18bp}%
\noindent
\makebox[0.5\linewidth][l]{命题人(签字):\hrulefill}\par}
%======================================================================
% 9. 页脚:第 X 页 共 Y 页(居中)
%======================================================================
\def\ps@ouckeypage{%
\def\@oddfoot{\hfill{\small 第\,\thepage\,页\quad 共\,\pageref{LastPage}\,页}\hfill}%
\let\@evenfoot\@oddfoot
\def\@oddhead{}\let\@evenhead\@oddhead}
\pagestyle{ouckeypage}
% 包结尾必须恢复,否则会污染文档类里 @ 的处理
\makeatother
\endinput