1. 中英文混排字号分离到底是个什么问题
先说清楚场景。WPS表格里做双语对照表、产品参数表、合同条款清单、科研数据记录,单元格里常常是“中文说明 + 英文缩写或数值”挤在同一格,比如“额定功率 Power 2.5kW”“备注 Note: 待确认”。中文用12号看着舒服,英文用12号就明显偏大、行距被撑开、表格显得松散;反过来英文缩到9号,中文又显得单薄。于是就有了这个诉求:同一个单元格里,中文一套字号、英文另一套字号,各走各的。这个动作我一般叫它“中英文混排字号分离设置”。
需要先打个预防针:WPS表格的单元格格式面板里,中文字体和西文字体是能分别指定的,但字号只有一个输入框。这意味着“一键设置中文12英文9”在原生UI里并不存在,微软的表格软件也是同样的设计。所以网上很多“改字体对话框就能分离字号”的说法是错的,字体能分离,字号不能。真正能落地的方法只有两条路:一是手工用富文本方式在单元格内部逐段刷格式,二是写宏按字符类型批量切段设置。
这篇内容适合三类人看:经常做双语表格但只会点格式面板的新手、需要一次处理几百上千行的办公族、以及想搞清楚“为什么不行、宏到底怎么写的”的技术型读者。下面从原理讲到代码,从单个单元格讲到整表批量,中间还会把我踩过的坑一条条摆出来。代码可以直接抄,参数可以按你的表格微调,不需要你有编程基础,会复制粘贴、会改两个数字就够了。
2. 先搞清楚原理:字体能分离,字号为什么不能
2.1 字符格式的两个层级
WPS表格的格式设置其实分成两个层级,很多人没意识到这一点。第一个层级是单元格级格式,选中一片区域点“字体”,改的是整个单元格的属性;第二个层级是字符级格式,也就是所谓的富文本,选中单元格里的一段文字单独改颜色、加粗、换字号——这个能力表格软件一直是有的,只是藏得比较深,UI上不给入口,只能通过代码调用Characters对象来操作。
关键点在于:单元格格式里的“字体”其实是一组属性,包含Name(西文字体,比如 Arial)、NameFarEast(中文字体,比如 宋体)、Size(字号)。前两个是分开的,第三个是共享的。所以你在字体对话框里选“西文 Times New Roman、中文 宋体”,软件知道英文段落用哪个字形、中文段落用哪个字形;但你只有一个字号输入框,它作用于整个Size属性,中英文一起变。这就是为什么“字体分离”是产品设计好的功能,“字号分离”只能靠字符级格式曲线救国。
2.2 富文本格式的能力边界
字符级格式不是万能的,它只对纯文本内容生效。具体说:
| 单元格内容类型 | 能否设置字符级字号 | 说明 |
|---|---|---|
| 手工输入的文本 | 可以 | 最理想的场景,宏处理的主战场 |
| 数值、日期、百分比 | 不行 | 显示值由数字格式决定,没有“字符”概念 |
| 公式结果 | 不稳定 | 设置后一旦重算或编辑,格式大概率被重置 |
| 合并单元格 | 高风险 | 部分版本会报错或格式错位,建议先拆再设 |
| 超长文本(几千字符) | 可以但慢 | 每次调用Characters都有开销,需要优化算法 |
这张表建议先看一遍,因为它决定了后面宏里那些If判断的来历。我见过太多人拿着宏去跑数值列,跑完发现一点变化都没有,然后以为代码写错了——其实是数据类型不对,宏对数字单元格无能为力,得先把列转成文本格式(设置单元格格式为“文本”,或者用TEXT函数包一层)。
2.3 为什么非要分离,不分离会怎样
不分离的代价比想象中大。中文方块字的字面高度约等于字号,英文小写字母的 x-height 只有字号的一半左右,大写字母接近字号的 70%。同一个12号字,中文看上去是“满格”,英文看上去是“半格”,在一行里并排时英文会显得又小又飘,视觉上密度不均。反过来,如果为了迁就英文整体放大到14号,中文就过于粗壮,行高被撑到22磅以上,一页纸放不下几行。
实际排版里比较稳妥的比例是:中文 12 号(小四)配英文 9 号,中文 10.5 号(五号)配英文 8 号,中文 9 号(小五)配英文 7 号。大致规律是英文字号取中文字号的 0.75 到 0.8 倍,这个比值来自两种文字的字面率差异,不是拍脑袋定的。当然也要看你用的西文字体,等线、Calibri 这类字形偏小的可以往上调半号,Arial、Times New Roman 就按 0.75 来。
3. 手工方案:字体对话框加富文本微调
3.1 字体对话框能做到的和做不到的
先说清楚这一步能干什么,免得白折腾。选中单元格,右键“设置单元格格式”切到“字体”页,你会看到“中文字体”和“西文字体”两个下拉框,下面只有一个“字号”框。两个字体框可以分别选,选完之后单元格里的中文会自动套用中文字体的字形,英文和数字套用西文字体——这个效果是实时的,不需要额外操作。
但字号框只有一个。改它,中文英文一起变。有人试过先设中文字体再设一次西文字体,想靠两次操作让字号“覆盖”,不行。属性是同一个,后设的值会覆盖前设的值,两边永远是同一个数字。所以字体对话框这条路,止步于“字形分离”,到不了“字号分离”。把这个认知先建立起来,可以省掉很多无效尝试。
顺手提一句“字符缩放”这个折衷方案。字体对话框底部有个“缩放”比例,可以设成 80%,效果是字符宽度压缩到 80%、高度不变。把英文单独选中设成 80% 或 90%,视觉上英文会收窄一些,跟中文的密度能靠近一点。这是不改字号也能改善观感的办法,代价是英文变形、笔画会显得细,字重不匹配。如果只是想让表格别那么难看、又不想折腾宏,这条能用;如果对印刷质量有要求,别用。
3.2 单元格内部怎么单独选中一段文字
手工处理单个单元格,靠的是“编辑状态下选中”——双击单元格进入编辑状态,或者按 F2,然后用鼠标拖选你要改的那几个英文单词,再在工具栏上把字号改成 9 号。回车确认后,被选中的那部分字符就带上了独立的字号,同格内的中文不受影响。
这个操作有几个细节值得说:
- 必须双击进入编辑态,不能只在选中单元格的状态下操作。选中状态下的格式改动作用于整格,会全部变。
- 拖选时如果只选英文单词,标点符号容易漏,比如括号、冒号。半角标点跟着英文走一般更协调,所以选中时把紧邻的半角符号一起框进去。
- 改完之后工具栏的字号框会显示空白或“混合”状态,这是正常的,说明格内存在多种字号,不是bug。
- 千万别在改完富文本之后再动单元格级别的字体设置,那一改会把字符级格式全部冲掉,前功尽弃。
单个单元格里如果中英文交替出现五六次,这个手工操作会非常费劲,点错一处就得重来。所以手工方案只适合“表格总数不超过二三十格”的情况,再多就必须上宏。
3.3 手工方案的适用范围要心里有数
我的经验是,手工整一个单元格大约要 15 到 30 秒,取决于是纯英文段还是交替段。50 个单元格就是 15 到 25 分钟,中间还要不断确认有没有改错。而且手工改的东西有个隐患:后续任何人编辑这个单元格,只要改动幅度大一点,格式就可能被打乱。
所以我的建议是把分界线画在“30 格”上:30 格以下手工干,快、稳、不用调试代码;30 格以上直接上第 4 节的宏,别犹豫。另外还有一类情况必须上宏,就是数据是通过公式或外部导入产生的,你手工改完格式,下次数据一刷新,格式全没了,纯属浪费。这种场景要求整个流程自动化,每刷一次跑一次宏。
4. 批量方案:写个宏按字符切段设字号
4.1 VBA宏还是JS宏,怎么选
WPS表格现在两套宏环境都支持。VBA 宏(.bas 文件)是老牌方案,语法通用、代码资料多、迁移到别的表格软件也能跑;JS 宏是 WPS 后来推的,用的是 JavaScript 语法,不用额外装组件,在“工具-开发工具-宏编辑器”里直接新建就能写,保存成 .xlsm 或 WPS 自己的格式都行。
怎么选?如果你要发给同事用、或者需要在多台机器上复用,VBA 通用性更好;如果你自己机器上用、不想折腾组件、顺手会点 JS,那就 JS 宏,语法写起来还更顺手。下面的核心算法两套都给了完整版本。
4.2 核心算法:连续段压缩,别逐字符刷
这是整个方案里最值得讲的技术点。最直觉的写法是对单元格里每个字符都判断一次中文还是英文,然后Characters(i, 1).Font.Size = ...。这个写法能跑,但慢得离谱——每次调用Characters都是一次跨进程的格式操作,一个 20 字的单元格要调 20 次,1000 行 20000 个字符就是两万次调用,实测能跑好几分钟。
正确做法是游程压缩(run-length):从头扫描字符串,记录当前位置是中文还是英文,遇到类型切换时才提交一次设置,把从上一个切换点到当前点的整段一次性设完。这样 20 个字符的单元格,如果只有一段中文一段英文,就只需要 2 次调用。性能差距是数量级的,实测 1000 行 × 20 字,压缩后 2 到 4 秒能跑完。
判断中文还是英文,用字符编码:AscW返回字符的 Unicode 码点,大于 255 的按中文处理,否则按英文处理。这个规则有个好处是会自动把全角标点归入中文——全角逗号、句号、括号的码点都在 65280 以上,判为中文,正好符合“中文标点跟中文字号”的排版习惯。半角标点则跟着英文走,也合理。
4.3 完整VBA代码和逐段说明
Option Explicit ' 主过程:对选区内的文本单元格做中英文分号 Sub SplitCNENSise() Dim cnSize As Single, enSize As Single cnSize = 12 ' 中文字号 enSize = 9 ' 英文字号 Dim rng As Range If TypeName(Selection) <> "Range" Then Exit Sub Set rng = Selection Application.ScreenUpdating = False Application.EnableEvents = False Dim cell As Range, s As String, n As Long, i As Long Dim segStart As Long, segIsCN As Boolean, curIsCN As Boolean Dim done As Long For Each cell In rng.Cells ' 只处理非空、非公式、非合并的单元格 If cell.MergeCells Then GoTo NextCell If cell.HasFormula Then GoTo NextCell If Not IsTextLike(cell) Then GoTo NextCell s = CStr(cell.Value) ' 注意:取.Value,不取.Text n = Len(s) If n = 0 Then GoTo NextCell ' 先把整格统一成中文字号,清掉历史残留格式 cell.Font.Size = cnSize segStart = 1 segIsCN = IsCN(s, 1) For i = 2 To n curIsCN = IsCN(s, i) If curIsCN <> segIsCN Then If Not segIsCN Then cell.Characters(segStart, i - segStart).Font.Size = enSize End If segStart = i segIsCN = curIsCN End If Next i ' 收尾:处理最后一段 If Not segIsCN Then cell.Characters(segStart, n - segStart + 1).Font.Size = enSize End If done = done + 1 NextCell: Next cell Application.EnableEvents = True Application.ScreenUpdating = True MsgBox "处理完成,共 " & done & " 个单元格。", vbInformation End Sub ' 判断第 i 个字符是否为中文(含全角标点) Function IsCN(s As String, i As Long) As Boolean Dim code As Long code = AscW(Mid$(s, i, 1)) If code < 0 Then code = code + 65536 ' 处理负数返回值 IsCN = (code > 255) End Function ' 判断单元格是否适合做字符级格式 Function IsTextLike(cell As Range) As Boolean Dim v As Variant v = cell.Value If IsEmpty(v) Then Exit Function If IsNumeric(v) And VarType(v) <> vbString Then Exit Function IsTextLike = True End Function几个地方专门解释一下,都是血泪教训换来的。
cell.Font.Size = cnSize这一行看着多余,其实很关键。如果单元格之前被人手工改过富文本,格内会残留各种字号,你只在切换点设英文段,那些残留的中文段字号还是乱的。先整体刷一遍中文字号,等于把画布清干净,后面的分段设置才有确定性。
If cell.HasFormula Then GoTo NextCell跳过公式单元格,是因为公式结果的字符格式在重算后会被重置,设了也白设。如果你的数据是公式来的,正确做法是先复制整列 → 选择性粘贴为“数值” → 再跑宏。
If cell.MergeCells Then GoTo NextCell跳过合并单元格,是因为部分版本对合并单元格设Characters会直接抛错,整个循环中断,前面的活白干。需要处理合并格的话,先取消合并、处理完再合回去,或者单独写个分支只设整格字号。
CStr(cell.Value)而不是cell.Text,因为.Text是显示值,受列宽影响,列窄了会返回一堆###,字符数完全对不上,Characters的索引就会错位,出现“改到隔壁字符”的诡异现象。
4.4 JS宏版本,WPS新版直接用
function SplitCNENSise() { var cnSize = 12, enSize = 9; var rng = Application.Selection; if (!rng) return; var rows = rng.Rows.Count, cols = rng.Columns.Count; var done = 0; for (var r = 1; r <= rows; r++) { for (var c = 1; c <= cols; c++) { var cell = rng.Cells.Item(r, c); try { if (cell.MergeCells) continue; if (cell.HasFormula) continue; var v = cell.Value2; if (v === null || v === undefined) continue; if (typeof v === "number") continue; var s = String(v); var n = s.length; if (n === 0) continue; cell.Font.Size = cnSize; var segStart = 0; var segIsCN = isCN(s.charAt(0)); for (var i = 1; i < n; i++) { var curIsCN = isCN(s.charAt(i)); if (curIsCN !== segIsCN) { if (!segIsCN) { cell.Characters(segStart + 1, i - segStart) .Font.Size = enSize; } segStart = i; segIsCN = curIsCN; } } if (!segIsCN) { cell.Characters(segStart + 1, n - segStart) .Font.Size = enSize; } done++; } catch (e) { // 单格异常不中断整体流程 } } } alert("处理完成,共 " + done + " 个单元格。"); } function isCN(ch) { return ch.charCodeAt(0) > 255; }JS 版本有两个地方和 VBA 不同,容易踩。一是Characters的起始索引从1开始,而 JS 字符串索引从 0 开始,所以传参时要segStart + 1,漏了这个加一会整体往前偏一格,第一个字符永远改不到。二是用try...catch包住单格处理,因为 JS 宏遇到异常会直接终止整个函数,不像 VBA 可以On Error Resume Next,包一层能保证一个坏单元格不至于毁掉剩下的活。
4.5 参数怎么定,给几个能直接抄的组合
字号搭配不是纯审美问题,跟字体、行高、打印缩放都有关系。下面这张表是我自己在不同场景下用的,可以直接拿去改:
| 场景 | 中文字号 | 英文字号 | 行高(磅) | 备注 |
|---|---|---|---|---|
| 正式报告、打印稿 | 12 | 9 | 18 | 小四配小五偏小,视觉最稳 |
| 内部数据表 | 10.5 | 8 | 15 | 五号配小五,一屏信息量大 |
| 双语合同条款 | 12 | 10 | 20 | 英文调大半号,可读性优先 |
| 参数表、规格书 | 9 | 7 | 13 | 小五配七号,密集排版 |
| 演示用大屏表 | 16 | 12 | 26 | 等比放大,比例不变 |
行高这一列别忽略。字符级字号设完之后,行高如果是固定值,英文变小了但行高没变,会显得上面空一块;中文 12 号配行高 15 磅又会把中文切头。最稳的做法是设完字号把行高改成“自动调整”,或者按中文字号 × 1.5 到 1.6 手动给值。
5. 实测中的坑和排查速查表
5.1 三个最常被问到的“为什么没效果”
第一个:跑完宏,格子里的字一点没变。九成是数据类型问题。那列是数字或者公式,IsTextLike判断直接跳过了。解决办法:选中整列 → 设置单元格格式 → 选“文本” → 重新输入或粘贴数据(注意,改格式后原数据不会自动转,要重新“分列”或者粘贴一次值)。
第二个:改完之后只有英文变小了,中文没变或者一部分没变。大概率是列宽太窄,cell.Text返回了###,导致长度判断错位。检查一下是不是用了.Text而不是.Value,或者干脆把那列拉宽再跑。
第三个:跑完当场是好的,第二天打开文件又变回去了。富文本格式在排序、筛选、套用表格样式、合并单元格这四种操作之后会被清掉。这不是宏的问题,是软件行为。所以流程上要固定:所有数据处理完 → 最后一步才跑字号分离宏 → 之后不再动这张表的结构。
5.2 常见问题速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 宏报“下标越界” | 单元格是合并格或空值没判 | 加MergeCells和长度判断 |
| 只有第一行生效 | 循环写成了单次执行 | 检查For Each/ 双重for是否包住全部 |
| 中文英文全变成9号 | 收尾段判断写反 | 确认最后一段的If Not segIsCN逻辑 |
| 首个字符永远改不到 | JS 宏索引没 +1 | Characters(segStart + 1, ...) |
| 数字也被缩小了 | 数字判为西文,属预期行为 | 若不想缩数字,把code > 47 && code < 58单独排除 |
| 处理 5000 行卡死 | 未关屏幕刷新 | 加ScreenUpdating = False,或用 JS 的批量数组方案 |
| 英文里的连字符断开后字号不一致 | 半角符号被判为独立段 | 属正常,半角符号按西文走,视觉上无影响 |
| 粘贴到别的软件后格式丢失 | 富文本格式不跨软件 | 只能转成图片或 PDF 再交付 |
5.3 几个只有实际跑过才知道的细节
全角数字的处理。表格里如果混着全角数字“123”,码点在 65296 到 65305,会被判为中文,跟着中文字号走。多数情况下这是对的,因为全角数字本来就是配合中文排版的。但如果你的表格里全角数字是误输入,跑完会发现这些数字特别大,得先把它们批量转成半角。
中英文之间的空格。排版规范里中英文之间要加一个空格,这个半角空格会被判为西文,用英文字号。空格的宽度本来就随字号变化,所以视觉上没影响,不用特殊处理。
冒号和括号的归属。“备注:Note” 里的全角冒号判为中文,跟着 12 号走;“Power(2.5kW)” 里的半角括号判为西文,跟着 9 号走。这个分配是符合排版惯例的,不需要调。唯一别扭的是中英文紧挨着、中间用半角符号的情况,比如“功率(Power)”,半角括号缩到 9 号后跟中文的间距会变大一点,属于可接受范围。
复制粘贴会破坏格式。把处理好的单元格复制到另一个位置,如果是“粘贴全部”,字符格式能带过去;但如果目标区域原本有格式,或者用了“只保留文本”粘贴,字符级字号就全没了。跨工作簿复制更不可靠。所以批量处理要在数据最终落位之后做,别做完再搬。
撤销栈会被宏清空。跑完宏按 Ctrl+Z 是撤不回来的,VBA 的格式操作不进撤销记录。所以跑之前一定先存盘,或者先拿十行数据试跑。
6. 让成品真正好看的两步收尾
6.1 行高、对齐和边框的联动调整
字号分离做完,表格往往还差一口气,问题通常出在垂直对齐上。默认是“底端对齐”,中文字面满格、英文只占下半部分,两者底边对齐之后,英文的顶部会空出一大块,看起来英文“掉下去了”。把垂直对齐改成“居中”,中英文的视觉重心才对得上,这一改效果立竿见影,比调字号还明显。
行高建议设成“自动调整行高”,让软件按内容算。如果非要固定值,取中文字号 × 1.6 比较稳:12 号 → 19 磅,10.5 号 → 17 磅。行高给小了中文会被切掉上下沿,给大了整张表稀稀拉拉。
还有一个容易忽略的点:英文段如果带下划线或倾斜,字号缩小后线宽会变细,跟中文的笔画粗细对不上。这种情况把英文换成稍粗一点的字重,或者干脆不加下划线,用单元格边框代替,观感会统一很多。
6.2 把宏挂到按钮上,做成一次点击的活
宏写完每次都要开编辑器按 F5,太麻烦。WPS 里可以插一个形状当按钮:插入 → 形状 → 画个圆角矩形 → 右键“指定宏” → 选你写的过程名。之后选中数据区域,点一下按钮就完事。给同事用的时候,把“中文字号”“英文字号”这两个变量提到过程最上面,加个InputBox让用户自己填:
Dim sCn As String, sEn As String sCn = InputBox("请输入中文字号", "字号设置", "12") sEn = InputBox("请输入英文字号", "字号设置", "9") If sCn = "" Or sEn = "" Then Exit Sub cnSize = CSng(sCn) enSize = CSng(sEn)这样一份文件发出去,谁都能按自己的表格调,不用改代码。注意InputBox返回的是字符串,要CSng转成数值,直接赋值给Single类型在某些版本会报类型不匹配。
我个人在实际操作中的体会是,这套东西真正的价值不在于“省了多少次点击”,而在于把排版标准固化了。以前一张双语表三个人做,字号搭配三种风格;现在跑同一个宏,出来就是一个样。表格这种东西,一致性比精致更重要,而一致性只能靠代码保证,靠手工点不出来。