
时值端午,虽然假期只有三天,但也可以让大家在紧张的工作中稍微透一口气。不过对于已经在电脑前度过一个学期的孩子们来说,小长假恐怕也放松不得,因为马上就要进入疫情后的第一次期末考试了。

孩子考试,家长当然也不会轻松。这不,昨天一位朋友就跟我说:老师在群里发了一套初一代数的例题模板(Word文件),让家长自己在此基础上,随便修改每道题的数字系数和加减号,以便同学反复练习整式化简的技巧。
听到这里,马上明白了他的意思:能不能想个办法,自动修改上面这个 word 文件中每一道题的数字系数和加减号,然后对它们随机修改,从而生成无穷无尽的练习题,比如下面这页:

因为这个问题非常适合使用 Word VBA 解决,所以今天我们就聊一聊这个实用的小程序 —— 怎样用 Word VBA 随机“改题”。
从朋友的叙述可知,这个任务包括两个要求:(1)随机改变加减号,也就是正负号(2)随机改变多项式的数字系数。相对而言,加减号的实现思路比较简单,所以咱们就从它开始。
还是我们在《全民一起VBA 基础篇》中反复强调的那个思维:如果不知道怎样写程序,就看看我们人类自己处理这个问题时,会遵循怎样的步骤。比如对于修改加减号的操作,如果是我们自己手动解决,那么相必会是这样的流程:
- 让眼睛定位到第一行
- 从左向右读取每一个字符
- 如果发现该字符是一个加号或者减号,就停下来执行步骤 4,否则回到第2步继续读取下一个字符
- 如果发现是加减号并停下来,那么马上抛一个硬币,如果硬币正面朝上,就把它写成加号、如果背面朝上就写成减号,不管它以前是加号还是减号
- 如上反复,直到读完一行所有字符,就回到第1步再读取下一行。
如果对上述流程没有异议,那么我们就可以用一模一样的方式写出一个VBA程序。只不过对于其中“定位某一行”、“读取一个字符”、“抛硬币”三个环节,需要用到一点Word对象用法知识和随机数技巧:
技巧1:读取Word文档的每一段
前面的第1步“读取每一行”,在Word中对应的是“读取每一个段落”。我们在《全民一起VBA 提高篇》里学过:当前打开的Word文档(docx文件)在VBA中表示为 ActiveDocumen 对象,而Document类的对象都有一个集合性质的属性 Paragraphs,代表该文档中的所有段落。
所以,我们可以使用 For each p in ActiveDocument.Paragraphs 的方式,依次扫描当前文档中的每一段。每次循环时,循环变量 p 都是一个 Paragraph(段落)对象,代表当前扫描到的那个段落。框架代码如下:

技巧2:读取一段中的每个字符并修改
Paragraph(段落)对象有一个Range属性,代表该段落的所有内容,包括其中每一个字符以及它们的格式等等。其中 Range.Characters属性代表的就是该段落中的所有字符,Range.Characters(i) 就是其中第 i 个字符,而 Range.Characters.Count 则代表这一段字符的总数量。
所以,使用下面的循环就可以读取每一个段落中的每一个字符,并且将它们都改成 “*”:

技巧3:“抛硬币”决定加减号
学过《提高篇》的同学一定都记得“随机数”这个概念,显然,抛硬币就是最经典的随机变量问题。我们知道,VBA中可以使用 rnd() 函数生成一个0到1之间的随机小数,所以只要调用 rnd() 生成一个随机数,然后判断它是否大于0.5 —— 如果大于则返回“+”、否则返回“-”即可。
这里为了简化代码,杨老师使用了一个《全民一起VBA》中没有介绍过的函数 IIF 来表示随机返回加减号的过程:

IIF函数其实就是一个简化版的if语句,与Excel表格公式中的IF函数非常相似:假如第一个参数 Rnd()>0.5 为True,那么就返回第二个参数“+”,否则返回第三个参数“-”。因此上面语句执行后,s 就是一个随机的加号或减号。
攻克了上述三个技巧,稍加组合,VBA代码就已经呼之欲出了:

上面的代码基本就是前面三个技巧的组合使用,具体参见代码中的注释即可。运行之后,果然所有加减号都被随机处理,下面就是本人运行后的截图示例:

解决了加减号,接下来就可以解决数字的随机化。整体思路当然也与加减号一样,可以逐个扫描每一段落的每一个字符,如果发现是数字字符,就把它换成一个随机数字字符。
但是别急,咱们先观察一下原来的内容,会发现其中有三个场合会出现数字:
- 题目序号
- 多项式系数比如第一题中的23、15、8 等
- 指数,比如第一题中a的3次方、c的2次方等。
而在这三种场合中,只有多项式系数是需要随机修改的、其他应该保持不变。

那么怎样排除掉(1)和(2)两种情况呢?其实也不难。对于(1)题目序号,因为源文档使用的是Word“项目编号”功能,不会被认作段落中的内容字符,因而p.Range.Characters里面根本扫描不到它,所以我们完全可以忽略不管。
而对于出现在(2)“指数”中的数字,则与多项式中的数字系数一样,实实在在是 Range.Characters 中的一员,所以必须要找出来并排除掉。
正规的排除方法,应当是判断一个数字是否出现在加减号、括号、空格等分隔符之后、同时又位于本项的第一个字母之前,所以使用正则表达式更为合理,不过这样做比较复杂。事实上,本例的Word文档中有一个捷径可以利用:所有作为指数的数字,其字体格式都是“上标”。

在《全民一起VBA 提高篇》中我们学过,如果想知道某一段(用对象 p 代表)的第i个字符的字体,那么只要用 r=p.Range 得到 p 中所有内容范围,接下来可以使用下面的代码实现:

而在Word的Font对象中,又有一个 SuperScript 属性和 SubScript属性,分别代表这个内容的字体格式是否为上标或下标。假如 Font.SuperScript 是 -1 ,则说明是上标格式,0 则说明不是上标。
所以把上面这个判断也加到前面处理加减号的循环中,我们的程序就大功告成了:

程序运行,稍等片刻,所有习题果然连参数带正负号都被随机修改。这一次朋友再也不担心孩子没题可做了 —— 无论你小子做题多么快,老爸只要一按按钮,你就得从头再来200道!

当然,因为我们走了一个“捷径”,所以还是会产生一些遗留问题不便处理。比如随机数字只有1-9,没有考虑0;而且有时候还会出现下面这种“不专业”的随机系数:

对于这些问题,根本的解决方案是按照数学规范中对“系数”的定义去分析文本,比如“位于一个项的第一个字母之前、若干位连续数字、1可以省略 …… ”,大家有兴趣可以尝试改进。而今天这篇文章只是抛砖引玉,毕竟十几行代码就已经基本解决了随意改题的需求,虽有瑕疵但也足堪使用。反正大过节的,咱就不较真了吧 。