Excel VBA再谈程序结构
VBA 代码一长就乱?这时候你就得重新认识“程序结构”了
前面已经讲了这么多关于 VBA 的一些语法和基本操作,大家学了这些之后,就能够做更多的事情。
但代码一旦开始变复杂、变长,很快就会出现一个很现实的问题:阅读起来费劲,后期维护也越来越难。
这件事,很多人刚开始学 VBA 的时候其实感受还不明显。因为前面写的代码大多比较短,十几行、二十几行,看起来还算清楚。
可只要后面开始套循环、套判断,再加上一点格式处理、汇总逻辑、批量处理,代码马上就容易写成一大坨。这个时候别说改了,有时候隔几天自己回头看,都会有点不想看。
所以这里,我们就得重新认识一下 Excel VBA 的程序结构。
看看怎么通过组织代码,让代码不只是“能跑”,还能够在后面更方便地修改、维护和复用。
这一块,最核心的就是两个东西:
一个叫 子过程,一个叫 函数。
子过程更像是在帮主程序“打工”,把一件大事拆成几件小事分别去做。函数则更像是在完成计算之后,把结果返回给你。
把这两个东西理顺了,后面很多代码都会顺很多。
一、为什么代码一长,维护就开始变难
先看一个很常见的办公场景。
比如你在做一张 6 月渠道投放表,现在要完成下面三件事:
-
计算每个渠道的净结余 -
如果净结余小于 0,就标红 -
最后再写一行合计
表格可以先这样放:
|
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
如果这个需求只是一次性的,很多人会直接把所有逻辑全写进一个 Sub 里。
这样不是不行。
但只要需求再多一点,比如:
-
后面还要处理别的表 -
想把“算明细”和“写汇总”拆开 -
还要加更多格式处理 -
还要给别人继续接着改
你就会发现,代码越写越长以后,最大的麻烦已经不是“能不能写出来”,而是:
后面不好维护。
所以这个时候,写代码的重点就要开始从“能跑”往前走一步,变成:
怎么组织代码。
二、子过程,说白了就是给代码“打工”的
上面那个需求,我一般不会写成一整个大过程。
更稳一点的写法,是把它拆开。
比如拆成三个部分:
-
一个主过程,负责统一安排 -
一个子过程,负责计算每个渠道的净结余 -
一个子过程,负责写合计行
这里就先要认识一个关键词:Call
1)Call 是什么
Call 是 VBA 里的一个关键词。
你可以把它理解成“调用”。
当我们写:
-
1
Call 某个过程名(...)
意思其实就是在告诉 VBA:
去执行这个子过程里的代码。
所以,子过程你可以把它理解成“专门负责干某一小块活”的过程。主过程不是什么都自己做,而是把不同的小任务分给不同的子过程去做。
这就是为什么我前面说,子过程很像是在给代码“打工”的。
2)先看代码
你先看这个版本:
-
1 -
2 -
3 -
4 -
5 -
6 -
7 -
8 -
9 -
10 -
11 -
12 -
13 -
14 -
15 -
16 -
17 -
18 -
19 -
20 -
21 -
22 -
23 -
24 -
25 -
26 -
27 -
28 -
29 -
30 -
31 -
32 -
33 -
34 -
35
Sub BuildChannelReport()Dim reportWs As WorksheetSet reportWs = Worksheets("6月渠道投放") '指定要处理的工作表Call CalcChannelBalance(reportWs) '调用“计算净结余”的子过程Call WriteChannelTotal(reportWs) '调用“写合计行”的子过程EndSubSub CalcChannelBalance(reportWs As Worksheet)Dim rowIndex AsLongFor rowIndex = 2To5reportWs.Cells(rowIndex, 5).Value = _reportWs.Cells(rowIndex, 2).Value - _reportWs.Cells(rowIndex, 3).Value - _reportWs.Cells(rowIndex, 4).Value 'E列=回款-广告费-执行费If reportWs.Cells(rowIndex, 5).Value < 0ThenreportWs.Cells(rowIndex,5).Font.Color=vbRed '亏损就标红ElsereportWs.Cells(rowIndex,5).Font.Color=vbBlack '否则恢复黑色EndIfNext rowIndexEndSubSub WriteChannelTotal(reportWs As Worksheet)Dim colIndex AsLongFor colIndex = 2To5reportWs.Cells(6, colIndex).Value = WorksheetFunction.Sum( _reportWs.Range(reportWs.Cells(2,colIndex),reportWs.Cells(5,colIndex)) _) '把第2行到第5行的数据汇总到第6行Next colIndexEndSub
你会发现,这样一拆,整个结构立刻清楚很多:
-
BuildChannelReport负责总调度 -
CalcChannelBalance负责算净结余 -
WriteChannelTotal负责写合计
这就是子过程真正的价值。
不是为了写得花,而是为了让每一段代码只负责一件事。这样你后面改的时候,心里会清楚很多。
三、参数一加,子过程就不容易写死
刚才这段代码里,还有一个很关键的点,就是这里:
-
1 -
2
Call CalcChannelBalance(reportWs)Call WriteChannelTotal(reportWs)
这里的 reportWs,就是参数。
很多初学者一开始会喜欢在子过程里面直接写死工作表名字,比如:
-
1
Set reportWs = Worksheets("6月渠道投放")
这样当然也能跑。
但问题是,只要表名一改,或者你想批量处理多张表,后面就会很麻烦。
所以更稳的思路是:
由主过程决定要处理谁,再把这个目标传给子过程。
这就是参数的作用。
主过程负责定目标,子过程负责干活。这样一来,子过程就不会被某一张固定工作表绑死。
比如后面你有很多张项目表,都想做同样的处理,那主过程可以这样写:
-
1 -
2 -
3 -
4 -
5 -
6 -
7 -
8 -
9 -
10
Sub BuildAllReports()Dim reportWs As WorksheetForEach reportWs In WorksheetsIf reportWs.Name <> "总表"ThenCall CalcChannelBalance(reportWs)Call WriteChannelTotal(reportWs)EndIfNext reportWsEndSub
你会发现,下面两个子过程完全不用改。
这时候,参数的价值就非常明显了。
四、作用域这件事,越早懂越省事
很多人学到这里会有个疑问:
为什么不同过程里都能写变量,而且名字就算一样,也不一定报错?
原因就在于 作用域。
你可以先简单理解成:
变量写在哪个过程里,就只归哪个过程管。
比如你在 CalcChannelBalance 里写了一个 rowIndex,在 WriteChannelTotal 里写了一个 colIndex,它们互不影响。
哪怕两个过程里你都写 i,VBA 也不会自动把它们当成同一个变量。
这个点看起来有点抽象,但放到实际工作里很好理解。
就像一个公司里销售部有个小张,运营部也有个小张。名字一样,不代表是同一个人。
程序里也是一样。
变量除了名字之外,它还带着自己所在的位置。这个“位置”,就是它的作用域。
五、多参数子过程,才是真正开始像“写程序”
前面传进去的是一个工作表对象,这算单参数。
但实际工作里,很多时候一个子过程不可能只接一个参数。
比如你想写一个“高亮标记”的小工具:
-
传入要处理的区域 -
传入颜色 -
传入操作方式
这时候就会用到多参数子过程。
先看一个场景。
下面这张客户跟进表里,我现在想做两种动作:
-
把“未跟进”的状态填上黄色底色 -
或者把重点客户的名字改成红色字体
表格可以先这样:
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这时候我会写成这样:
-
1 -
2 -
3 -
4 -
5 -
6 -
7
Sub MarkCells(targetRange As Range, markColor AsLong, markMode AsLong)If markMode = 1ThentargetRange.Interior.Color = markColor '模式1:填充背景色ElseIf markMode = 2ThentargetRange.Font.Color=markColor '模式2:修改字体颜色EndIfEndSub
调用的时候这样写:
-
1 -
2 -
3 -
4
Sub DemoMarkCells()Call MarkCells(Range("C3"), vbYellow, 1) '把C3填成黄色Call MarkCells(Range("A5"), vbRed, 2) '把A5字体改成红色EndSub
这里你最好记住 3 个规则:
第一,参数之间要用逗号隔开。第二,调用时要写全。第三,顺序不能乱。
因为 VBA 默认就是按顺序去认参数的。
你要是把颜色和模式的位置写反了,代码不一定马上报错,但结果很可能就完全不对了。
六、Call 可以写,但很多时候你会更常看到“不写”
刚开始学的时候,很多人会习惯这样写:
-
1
Call MarkCells(Range("C3"), vbYellow, 1)
这当然没问题。
但你后面看别人代码,会发现另一种写法也很多:
-
1
MarkCells Range("C3"), vbYellow, 1
这也能正常运行。
原因很简单,因为在 VBA 里,Call 关键字是可以省略的。
但这里有个细节,最好一起记住:
如果你把 Call 省略了,后面的括号通常也要一起去掉。
也就是说,这两种写法都可以:
-
1 -
2
Call MarkCells(Range("C3"), vbYellow, 1)MarkCells Range("C3"), vbYellow, 1
但刚开始学的时候,我还是更建议你先把写法记规范一点。
也就是:
-
写 Call,参数就带括号 -
不写 Call,通常就把括号也拿掉
这样你不容易混。
等后面写熟了,再去适应不同人的代码风格会更轻松。
这里还有一个细节:当你在需要用到子过程的返回值时,括号必须要加上,否则会报错。
这是一个小细节。
再来解释一下,什么叫用到子过程的返回值?
-
1 -
2 -
3 -
4 -
5 -
6 -
7
Range("A1").Value = mySqr 5'报错Range("A1").Value = mySqr(5) '正确'计算参数 k 的平方Function mySqr(k)mySqr = k * kEndFunction
七、函数和子过程,最核心的区别就一句话
到了这里,就该把函数接上了。
子过程和函数,最核心的区别就一句话:
-
子过程负责做事 -
函数负责把结果返回给你
比如你在做一张销售激励表,要根据销售额和完成率自动算奖金。
表格可以这样放:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这时候就很适合写函数:
-
1 -
2 -
3 -
4 -
5 -
6 -
7 -
8 -
9
Function CalcReward(monthSales AsDouble, finishRate AsDouble) AsDoubleIf finishRate >= 1ThenCalcReward=monthSales*0.03'完成率100%以上,奖金按3%ElseIf finishRate >= 0.8ThenCalcReward=monthSales*0.015'完成率80%以上,奖金按1.5%ElseCalcReward=0'低于80%,不发奖金EndIfEndFunction
然后你在工作表 C2 里直接写:
-
1
=CalcReward(A2,B2)
往下填充就行了。
这就是函数很有价值的地方。
你不是只写了一段“运行一次就结束”的宏,而是在给 Excel 造一个自己的公式。
再啰嗦一句,其实函数和子过程(Sub)这两个概念之间的界限,并没有那么清晰。
我们能写子过程的地方,正常来说都是可以用函数去直接覆盖的,这个没有问题。只是说:
-
子过程写的代码是能够在“宏”里面去调用的。 -
如果你把这两者之间弄混了,还是会有点奇怪。
一般我们的分工是:
-
函数:最常用来写自定义公式。 -
子过程:负责做真正正儿八经的“干活”代码。
八、传参还有一个坑:里面改了,外面也可能跟着变
这个点很多人第一次碰到都会愣一下。
看这个例子:
-
1 -
2 -
3 -
4 -
5 -
6 -
7 -
8 -
9 -
10 -
11 -
12
Sub ArrangePlan()Dim followCount AsLongfollowCount = 6Call DoubleTask(followCount)Range("F2").Value = followCountEndSubSub DoubleTask(taskValue AsLong)taskValue = taskValue * 2EndSub
很多人会以为,taskValue 是子过程里的变量,改了就改了,外面的 followCount 不该变。
但在 VBA 里,参数传递这块有时候很容易把外面的变量也一起带动。
你可以先把它理解成:
传进去的参数名,很多时候更像是外面变量的一个“临时别名”。
所以你在子过程里动它,外面对应的值也可能受到影响。
我自己平时有个习惯:
如果这个参数只是拿来参与计算,那就尽量别在子过程里直接改它。
这样会更稳,后面排错也省心。
这里其实还涉及一个难点,就是函数与子过程的值传递与参数传递。
这里的话我想放在后面来重点讲,这里我只是单纯地来提一下,大家要把这个东西给记住。因为它确实是有坑的,这个地方大家要规范一下自己写代码的习惯。
九、这一篇最值得带走的,不是某一句语法,而是“拆着写”的习惯
VBA 学到这里,真正开始有点“写程序”的感觉了。
因为从这里开始,你不只是想着:
-
这一句该怎么写 -
这个循环该怎么跑 -
这个判断该怎么判断
你会开始想一件更重要的事:
这段代码,应该怎么组织。
我自己一直觉得,很多人后面写 VBA 写得越来越累,不是因为语法太难,而是因为所有逻辑都堆在一起,最后自己都不想再打开。
所以这篇你要是只记住几件事,就够了:
第一,一个过程别什么都干,能拆就拆。第二,让主过程做调度,让子过程专心干活。第三,凡是会重复用到的逻辑,尽量做成带参数的子过程。第四,多个参数时,逗号、顺序、完整性都别写乱。第五,Call 可以省略,但省略时括号通常也一起去掉。第六,需要返回结果,而且希望在单元格里反复调用的规则,更适合写成函数。
你把这些东西慢慢用顺手了,后面很多 VBA 代码都会明显清楚很多。
夜雨聆风