乐于分享
好东西不私藏

Excel VBA再谈程序结构

Excel VBA再谈程序结构

VBA 代码一长就乱?这时候你就得重新认识“程序结构”了

前面已经讲了这么多关于 VBA 的一些语法和基本操作,大家学了这些之后,就能够做更多的事情。

但代码一旦开始变复杂、变长,很快就会出现一个很现实的问题:阅读起来费劲,后期维护也越来越难。

这件事,很多人刚开始学 VBA 的时候其实感受还不明显。因为前面写的代码大多比较短,十几行、二十几行,看起来还算清楚。

可只要后面开始套循环、套判断,再加上一点格式处理、汇总逻辑、批量处理,代码马上就容易写成一大坨。这个时候别说改了,有时候隔几天自己回头看,都会有点不想看。

所以这里,我们就得重新认识一下 Excel VBA 的程序结构。

看看怎么通过组织代码,让代码不只是“能跑”,还能够在后面更方便地修改、维护和复用。

这一块,最核心的就是两个东西:

一个叫 子过程,一个叫 函数

子过程更像是在帮主程序“打工”,把一件大事拆成几件小事分别去做。函数则更像是在完成计算之后,把结果返回给你。

把这两个东西理顺了,后面很多代码都会顺很多。


一、为什么代码一长,维护就开始变难

先看一个很常见的办公场景。

比如你在做一张 6 月渠道投放表,现在要完成下面三件事:

  1. 计算每个渠道的净结余
  2. 如果净结余小于 0,就标红
  3. 最后再写一行合计

表格可以先这样放:

A
B
C
D
E
1
渠道
回款金额
广告费
执行费
净结余
2
抖音
15600
8200
2100
3
小红书
9800
5700
1800
4
视频号
7200
5100
1700
5
公众号
4600
2900
900
6
合计

如果这个需求只是一次性的,很多人会直接把所有逻辑全写进一个 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 = 2To5        reportWs.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 < 0Then   reportWs.Cells(rowIndex,5).Font.Color=vbRed '亏损就标红Else   reportWs.Cells(rowIndex,5).Font.Color=vbBlack '否则恢复黑色EndIfNext rowIndexEndSubSub WriteChannelTotal(reportWs As Worksheet)Dim colIndex AsLongFor colIndex = 2To5        reportWs.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 也不会自动把它们当成同一个变量。

这个点看起来有点抽象,但放到实际工作里很好理解。

就像一个公司里销售部有个小张,运营部也有个小张。名字一样,不代表是同一个人。

程序里也是一样。

变量除了名字之外,它还带着自己所在的位置。这个“位置”,就是它的作用域。


五、多参数子过程,才是真正开始像“写程序”

前面传进去的是一个工作表对象,这算单参数。

但实际工作里,很多时候一个子过程不可能只接一个参数。

比如你想写一个“高亮标记”的小工具:

  • 传入要处理的区域
  • 传入颜色
  • 传入操作方式

这时候就会用到多参数子过程。

先看一个场景。

下面这张客户跟进表里,我现在想做两种动作:

  • 把“未跟进”的状态填上黄色底色
  • 或者把重点客户的名字改成红色字体

表格可以先这样:

A
B
C
D
1
客户姓名
负责人
跟进状态
备注
2
华新科技
林青
已跟进
二次沟通中
3
星耀传媒
周宁
未跟进
需要回访
4
远川贸易
顾言
已跟进
等报价
5
北辰文化
许舟
未跟进
首轮建联

这时候我会写成这样:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
Sub MarkCells(targetRange As Range, markColor AsLong, markMode AsLong)If markMode = 1Then        targetRange.Interior.Color = markColor   '模式1:填充背景色ElseIf markMode = 2Then   targetRange.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 * k EndFunction

七、函数和子过程,最核心的区别就一句话

到了这里,就该把函数接上了。

子过程和函数,最核心的区别就一句话:

  • 子过程负责做事
  • 函数负责把结果返回给你

比如你在做一张销售激励表,要根据销售额和完成率自动算奖金。

表格可以这样放:

A
B
C
1
销售额
完成率
奖金
2
58000
1.05
3
43000
0.91
4
31500
0.77

这时候就很适合写函数:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
Function CalcReward(monthSales AsDouble, finishRate AsDoubleAsDoubleIf finishRate >= 1Then   CalcReward=monthSales*0.03'完成率100%以上,奖金按3%ElseIf finishRate >= 0.8Then   CalcReward=monthSales*0.015'完成率80%以上,奖金按1.5%Else   CalcReward=0'低于80%,不发奖金EndIfEndFunction

然后你在工作表 C2 里直接写:

  • 1
=CalcReward(A2,B2)

往下填充就行了。

这就是函数很有价值的地方。

你不是只写了一段“运行一次就结束”的宏,而是在给 Excel 造一个自己的公式。

再啰嗦一句,其实函数和子过程(Sub)这两个概念之间的界限,并没有那么清晰。

我们能写子过程的地方,正常来说都是可以用函数去直接覆盖的,这个没有问题。只是说:

  1. 子过程写的代码是能够在“宏”里面去调用的。
  2. 如果你把这两者之间弄混了,还是会有点奇怪。

一般我们的分工是:

  • 函数:最常用来写自定义公式。
  • 子过程:负责做真正正儿八经的“干活”代码。

八、传参还有一个坑:里面改了,外面也可能跟着变

这个点很多人第一次碰到都会愣一下。

看这个例子:

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
Sub ArrangePlan()Dim followCount AsLong    followCount = 6Call DoubleTask(followCount)    Range("F2").Value = followCountEndSubSub DoubleTask(taskValue AsLong)    taskValue = taskValue * 2EndSub

很多人会以为,taskValue 是子过程里的变量,改了就改了,外面的 followCount 不该变。

但在 VBA 里,参数传递这块有时候很容易把外面的变量也一起带动。

你可以先把它理解成:

传进去的参数名,很多时候更像是外面变量的一个“临时别名”。

所以你在子过程里动它,外面对应的值也可能受到影响。

我自己平时有个习惯:

如果这个参数只是拿来参与计算,那就尽量别在子过程里直接改它。

这样会更稳,后面排错也省心。

这里其实还涉及一个难点,就是函数与子过程的值传递与参数传递。

这里的话我想放在后面来重点讲,这里我只是单纯地来提一下,大家要把这个东西给记住。因为它确实是有坑的,这个地方大家要规范一下自己写代码的习惯。


九、这一篇最值得带走的,不是某一句语法,而是“拆着写”的习惯

VBA 学到这里,真正开始有点“写程序”的感觉了。

因为从这里开始,你不只是想着:

  • 这一句该怎么写
  • 这个循环该怎么跑
  • 这个判断该怎么判断

你会开始想一件更重要的事:

这段代码,应该怎么组织。

我自己一直觉得,很多人后面写 VBA 写得越来越累,不是因为语法太难,而是因为所有逻辑都堆在一起,最后自己都不想再打开。

所以这篇你要是只记住几件事,就够了:

第一,一个过程别什么都干,能拆就拆。第二,让主过程做调度,让子过程专心干活。第三,凡是会重复用到的逻辑,尽量做成带参数的子过程。第四,多个参数时,逗号、顺序、完整性都别写乱。第五,Call 可以省略,但省略时括号通常也一起去掉。第六,需要返回结果,而且希望在单元格里反复调用的规则,更适合写成函数。

你把这些东西慢慢用顺手了,后面很多 VBA 代码都会明显清楚很多。