
咱们接着前两篇的进度往下写。前面已经把VBA的开发环境和基本的变量、数据类型讲透了这一篇进入真正的重头戏——程序流程控制。说白了不管你以后写的是多复杂的宏天天跟Excel里的数据打交道代码跑起来无非就是三件事从上往下执行、遇到条件分个岔、需要重复的地方转圈圈。流程控制就是把这三种节奏掌握好让你的代码学会“看情况办事”。这篇内容我会把选择结构、循环结构、跳转与错误处理、还有循环跟对象模型、字典的组合实战都过一遍顺便把调试技巧和常见坑也一起说了。适合刚学完变量和数据类型、准备写完整功能宏的朋友也适合已经写过一些代码但总感觉逻辑绕不清的同学。1. 流程控制的核心思路从顺序执行到让代码“懂事”1.1 三种基本结构任何一门编程语言讲流程控制绕不开三种基本结构顺序结构、选择结构、循环结构。顺序结构就是你代码写上往下一条条跑这个不用多做解释VBA默认就是这种执行方式。真正让代码变得“聪明”的是后面两个。选择结构也叫分支结构核心是“如果……就……否则……”。比如你要判断一个成绩是不是及格60分以上显示“通过”不到60显示“待提升”这就是一个典型的分支。VBA里对应的是If语句和Select Case语句。循环结构的核心是“重复做一件事直到满足某个条件”。比如你要把A1到A1000的数值全部乘以2手工操作累死人写个For循环瞬间跑完。VBA里的循环语句有For Next、Do While、Do Until、For Each这几种。这三种结构可以互相嵌套组合。实际项目里一个完整的宏往往是“顺序执行过程中在某一步根据条件决定要不要进入循环循环体里面又嵌套了选择判断”。打个比方就像你出门前判断天气选择如果下雨就带伞分支动作然后走到地铁站的过程是机械重复的循环等车的时候又根据站牌判断换乘路线嵌套选择。代码就是把这些日常逻辑翻译成机器能听懂的语言。1.2 为什么流程控制是VBA学习的分水岭我见过很多自学VBA的朋友变量、函数用得挺熟练但是一写到完整功能就卡壳。问题多半出在流程控制上——要么是不知道什么时候该用For循环还是Do循环要么是多重If嵌套把自己绕晕了要么是循环里处理单元格太慢导致整个Excel卡死。流程控制真的是VBA入门的“分水岭”。学会了它你才能从“录制宏、改改参数”进阶到“从零写一个完整功能”也才有能力处理那些需要批量判断、批量汇总、跨表筛选的真实工作场景。这篇不讲虚的直接上代码、讲原理、说坑点。2. 选择结构实战让代码学会“看情况办事”2.1 If语句的四种常见写法If语句是选择结构的基本盘VBA里有四种常见形态。第一种是单行If适合条件简单、只需要执行一条语句的场景If score 60 Then MsgBox 及格第二种是块If可以执行多行代码If score 60 Then MsgBox 及格 Range(B2) 通过 End If第三种是If Else结构条件成立和不成立各有各的处理If score 60 Then Range(B2) 通过 Else Range(B2) 待提升 End If第四种是If ElseIf多条件分支If score 90 Then Range(B2) 优秀 ElseIf score 80 Then Range(B2) 良好 ElseIf score 60 Then Range(B2) 及格 Else Range(B2) 待提升 End If这里有个关键点ElseIf会按顺序从上往下判断一旦某个条件成立后面的条件就不会再判断了。所以写多条件分支时条件顺序很重要。比如成绩90分它先判断“90”成立直接进优秀分支不会再去管后面那几个。你要是把“60”写在最前面那90分的人也直接进了“及格”分支后面的“优秀”“良好”永远不会被执行到。实际写代码我建议把限定范围更严格的判断放前面范围宽的放后面。像成绩等级这种从高往低排就是对的写法。2.2 多条件组合与优先级判断条件不是只能写一个比较表达式可以用逻辑运算符And、Or、Not组合多个条件If age 18 And age 25 Then MsgBox 青年 End If If subject 语文 Or subject 英语 Then MsgBox 文科科目 End If If Not IsEmpty(Range(A1)) Then MsgBox A1不为空 End If优先级方面Not的优先级最高其次是And最后是Or。如果条件比较复杂最好的习惯是加括号把逻辑范围明确出来不要靠记忆去判断优先级。比如If (age 18 And age 25) Or (age 55 And age 65) Then MsgBox 扶助对象 End If加括号不光是让自己看得清楚也让以后维护你代码的人不踩坑。还容易踩的坑是比较文本时的大小写问题。VBA默认的字符串比较是区分大小写的也就是说ABC不等于abc。如果业务上不区分大小写可以用UCase或者LCase把两边的字符串都转成同一格式再比较If UCase(Range(A1).Value) EXCEL Then MsgBox 匹配成功 End If2.3 Select Case多分支判断的利器当判断的条件特别多比如要根据成绩区间给等级、根据月份给季度、根据编号给分类用If ElseIf写出来会长得吓人而且层层嵌套的End If很容易漏写。这时候换成Select Case会清爽很多。Select Case score Case Is 90 Range(B2) 优秀 Case Is 80 Range(B2) 良好 Case 60 To 79 Range(B2) 及格 Case Else Range(B2) 待提升 End SelectSelect Case后面跟的表达式在进入结构时只计算一次然后逐个跟Case条件比对。Case后面可以写以下几种形式Case 5判断是否等于5Case 1, 3, 5, 7判断是否等于值列表中的任意一个Case 10 To 20判断是否在10到20这个区间Case Is 100配合Is关键字进行比较判断Case Else前面都不满足时的兜底分支选择用Select Case还是If语句我的经验是判断条件是单值或区间的场景优先用Select Case判断条件涉及多个变量的复杂组合逻辑用If更直观。比如“年龄大于30且工资小于5000”这种就写成If因为它同时牵扯两个不同变量。注意Select Case的判断顺序也是从上到下的。第一个匹配的Case被命中后后面的不会再执行。3. 循环结构实战让重复劳动交给机器3.1 For Next的固定次数循环For Next是最基础的循环结构适用于你明确知道要循环多少次的场景。语法是Dim i As Long For i 1 To 100 Cells(i, 1) i * 2 Next i上面这段代码把第1列第1行到第100行的值设为2、4、6……一直到200。这里的i是循环计数器每循环一次自动加1。你还可以用Step关键词自定义步长For i 10 To 1 Step -1 Cells(i, 1) 倒计时 i Next iStep -1表示每次减1从10倒着循环到1。For Next循环配合Range对象做批量操作是最常见的组合。比如你想给A列有数据的单元格全部加上黄色背景Dim lastRow As Long, i As Long lastRow Cells(Rows.Count, 1).End(xlUp).Row For i 1 To lastRow Cells(i, 1).Interior.Color vbYellow Next i这里用End(xlUp).Row来动态获取A列最后一条数据的行数而不是写死一个数字。写死数字是新手最常见的毛病换一份数据行数变了代码就出错。凡是跟数据范围打交道的循环优先考虑动态获取边界。3.2 Do While和Do Until的条件循环For循环适合已知次数的场景但有些时候你并不知道循环要跑多少次只知道“条件满足时就继续跑”。这种场景要用Do循环。Do While是“当条件成立时执行循环”条件检查保留在哪儿决定了循环体至少执行还是不执行 先判断后执行可能一次都不执行 Dim x As Long x 1 Do While x 100 x x * 2 Loop 先执行后判断至少执行一次 x 1 Do x x * 2 Loop While x 100Do Until是“直到条件成立为止”意思跟Do While正好相反x 1 Do Until x 100 x x * 2 Loop这段代码的效果和上面的Do While完全一样。所以你只需要记住一种就行想表达“条件满足就继续”用Do While想表达“条件满了就停”用Do Until看哪种读起来顺口用哪种。日常数据处理中Do循环比较典型的场景是逐行读取数据读取到空单元格就停止Dim r As Long r 1 Do While Cells(r, 1).Value Cells(r, 2).Value Cells(r, 1).Value * 1.1 r r 1 Loop这段代码从第1行开始把A列的数据乘以1.1写入B列遇到A列空单元格就停。这样不管数据是10行还是1000行代码都不用改。3.3 For Each遍历对象集合的最佳方式For Each是VBA里特别实用的一种循环用于遍历对象集合比如所有工作表、某个区域的所有单元格、所有图表、所有形状。它的语法是Dim ws As Worksheet For Each ws In ThisWorkbook.Worksheets Debug.Print ws.Name Next ws上面这段代码遍历当前工作簿里的所有工作表在立即窗口输出每个工作表的名称。遍历单元格的时候For Each比For i更有优势因为你不用手动管理行号列号也天然不会越界Dim cell As Range For Each cell In Range(A1:A10) If cell.Value 100 Then cell.Interior.Color vbRed End If Next cell这里每个进入循环的cell就是区域A1:A10里的一个单元格直接对cell操作就行。但注意For Each遍历单元格时你不用写计数器但如果需要在循环中知道当前是第几个单元格可以自己加一个变量Dim cell As Range, count As Long count 0 For Each cell In Range(A1:A100) count count 1 If cell.Value Then cell.Value 第 count 行为空 End If Next cell3.4 循环嵌套与性能优化循环嵌套在实际处理二维表格时很常见。比如你要遍历一个10行5列的区域里的每一个单元格通常就是外层循环行、内层循环列Dim i As Long, j As Long For i 1 To 10 For j 1 To 5 Cells(i, j).Value 行 i 列 j Next j Next i嵌套循环本身不难理解难的是别把外层循环和内层循环的边界搞混。我见过不少人把内层循环的计数变量写成了外层变量导致死循环或者数据错乱。建议外层变量用i内层变量用j再用Next i和Next j把对应的End标记清楚。嵌套循环的最大坑是性能。两层循环里如果还频繁操作单元格数据量一大就跑得奇慢无比。举例来说遍历10000行5列就是50000次操作每次读写单元格都涉及Excel界面的交互自然快不了。优化思路是把单元格区域整体读入VBA数组在内存里完成所有计算再一次性写回单元格。数组操作比单元格操作快几百倍。Dim arr As Variant Dim i As Long arr Range(A1:D10000).Value For i 1 To 10000 arr(i, 4) arr(i, 1) * arr(i, 2) arr(i, 3) Next i Range(A1:D10000).Value arr关于数组和内存操作后一篇讲数组时会专门展开但流程控制这里必须先把这个意识提前灌输进来循环里频繁碰单元格是VBA性能最大的杀手能整片读、整片写就尽量别一格一格操作。提示如果你在循环里需要修改单元格的显示状态背景色、字体颜色这类操作没法用数组替代那至少要加一句Application.ScreenUpdating False关闭屏幕刷新跑完再恢复成True速度提升很明显。3.5 退出循环Exit For和Exit Do循环不是必须完整跑完的。你在处理数据时经常遇到“找到一个符合条件的就停下”的场景。直接修改循环变量是一种粗暴办法但更规范的做法是用Exit For或Exit DoDim i As Long Dim targetRow As Long targetRow 0 For i 1 To 1000 If Cells(i, 1).Value 目标值 Then targetRow i Exit For End If Next i If targetRow 0 Then MsgBox 找到了在第 targetRow 行 Else MsgBox 没找到 End If循环里加退出条件的正确姿势是先把符合条件的行号存下来再Exit For等循环结束后统一判断该做什么而不是在循环体里直接弹窗或直接操作其他单元格。这样逻辑清晰也方便后续维护。注意多层嵌套循环时Exit For只退出当前那一层循环不会跳出外层循环。如果你需要从内层直接跳出所有嵌套循环常规做法是设置一个标志变量内层外层都判断这个变量再决定是否继续Dim found As Boolean found False For i 1 To 100 For j 1 To 100 If Cells(i, j).Value 停止 Then found True Exit For End If Next j If found Then Exit For Next i4. 流程控制的高级玩法跳出基础框架4.1 GoTo与错误处理的配合GoTo语句在很多编程教程里被警告少用因为它会让代码逻辑变得混乱、难以维护。不过在VBA里有一个场景GoTo是必不可少的那就是错误处理。配合On Error语句GoTo可以把代码执行流程跳转到指定标签实现“遇到错误统一处理”的效果Sub SafeProcess() On Error GoTo ErrHandler Dim result As Double result 100 / Range(A1).Value MsgBox result Exit Sub ErrHandler: MsgBox 发生错误请检查A1单元格是否为0 End Sub这段代码如果A1单元格是0除零错误就会触发流程直接跳转到ErrHandler标签后面的代码弹出友好提示而不是VBA默认的报错框。Exit Sub放在正常流程结束位置之后、标签之前这个很重要——没有Exit Sub的话没出错时也会继续往下执行错误处理代码。GoTo除了错误处理以外我建议尽量少用。程序流程控制有If和循环就足够了用GoTo强行跳转只会让逻辑支离破碎。4.2 循环与对象模型组合操作学了循环之后你能干的事情就多了。比如你想在每个工作表里插入一个页眉文字、设置打印方向用For Each遍历所有工作表分分钟搞定Dim ws As Worksheet For Each ws In ThisWorkbook.Worksheets ws.PageSetup.Orientation xlLandscape ws.PageSetup.CenterHeader ws.Name 报表 Next ws再比如批量创建图形热词里有“excel vba绘制矩形”你可以用Shapes.AddShape方法配合循环一次生成一排形状Dim i As Long Dim shp As Shape For i 1 To 5 Set shp ActiveSheet.Shapes.AddShape( _ Type:msoShapeRectangle, _ Left:10 (i - 1) * 60, _ Top:10, _ Width:50, _ Height:30) shp.TextFrame.Characters.Text 按钮 i Next i这段代码会在当前工作表的顶部生成5个并排的小矩形每个上面标注“按钮1”到“按钮5”。把Excel自带的绘图对象和循环结合批量制作图形、排列对齐一步到位。处理对象集合时For Each是首选的循环结构因为对象集合天然适合“逐个处理”的语义。4.3 字典与循环组合告别高成本的重复匹配字典Dictionary是VBA热词里的高频词它的核心特点是键值对存储、按键快速查找。很多时候我们要做“根据编号匹配其他表的数据”用循环加字典效率极高。比如张三、李四等人的销售业绩分布在两个表要按姓名匹配汇总用字典做法Dim dict As Object Set dict CreateObject(Scripting.Dictionary) Dim i As Long, key As String For i 2 To 100 key Sheets(数据源).Cells(i, 1).Value If Not dict.exists(key) Then dict.Add key, Sheets(数据源).Cells(i, 2).Value End If Next i Dim j As Long For j 2 To 50 key Sheets(汇总表).Cells(j, 1).Value If dict.exists(key) Then Sheets(汇总表).Cells(j, 2).Value dict(key) End If Next j这里字典相当于一个内存里的索引第一次循环把数据加载进去第二次循环直接按名字取值。比起两层循环一个一个配对字典的速度从本质上是另一种级别的提升——数据量一大差距就非常明显了。字典和循环的配合是VBA实战中最值得掌握的技巧之一后面写数据清洗、跨表汇总时基本离不开它。4.4 变量作用域与循环的关系热词里有“vba全局变量”这个跟流程控制也有关系。变量作用域决定了在哪里可以访问这个变量影响你在循环里怎么用它。过程级变量在Sub或Function内部声明循环之外也想看它的值就用Dim在过程顶部声明。循环里临时用的变量比如计数器i从规范上可以在过程顶部声明也可以在For语句里直接声明老版本VBA里For的计数器声明有讲究新版本VBE里建议统一在顶部声明。模块级变量需要在模块顶部声明它的作用域是整个模块的所有过程。你要是把一个变量声明成Public那就成了全局变量工作簿里的所有模块都能访问。实际应用时我建议能局部就局部别贪方便全用全局变量。全局变量太多代码执行顺序一变变量的值就不好追踪排查问题非常痛苦。5. 一个完整的综合案例把流程控制串起来5.1 需求描述纸上谈兵没意思来一个完整的案例把选择、循环、对象模型全部串起来。假设你手上有一个工作簿里面有多个工作表每个表里是某个月份的销售明细包括“产品编号”“产品名称”“销售额”三列。现在要求写一个宏完成以下任务遍历所有工作表跳过名为“汇总”的空表如果存在的话。在每个明细表里循环读取数据把销售额5000的产品的产品编号和产品名称收集起来。在所有表处理完后创建一个名为“汇总”的工作表把收集到的所有达标产品汇总写入。5.2 代码实现Sub SummaryHighSales() Dim ws As Worksheet Dim targetWs As Worksheet Dim i As Long, r As Long Dim lastRow As Long Dim productId() As String Dim productName() As String Dim count As Long 动态数组初始化 ReDim productId(1 To 1000) ReDim productName(1 To 1000) count 0 关闭屏幕刷新提升速度 Application.ScreenUpdating False 确保汇总表存在且清空旧数据 On Error Resume Next Set targetWs ThisWorkbook.Worksheets(汇总) On Error GoTo 0 If targetWs Is Nothing Then Set targetWs ThisWorkbook.Worksheets.Add(After:ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count)) targetWs.Name 汇总 Else targetWs.Cells.Clear End If 遍历所有工作表 For Each ws In ThisWorkbook.Worksheets If ws.Name 汇总 Then lastRow ws.Cells(ws.Rows.Count, 1).End(xlUp).Row For r 2 To lastRow If ws.Cells(r, 3).Value 5000 Then count count 1 productId(count) ws.Cells(r, 1).Value productName(count) ws.Cells(r, 2).Value End If Next r End If Next ws 写入汇总表 targetWs.Cells(1, 1).Value 产品编号 targetWs.Cells(1, 2).Value 产品名称 For i 1 To count targetWs.Cells(i 1, 1).Value productId(i) targetWs.Cells(i 1, 2).Value productName(i) Next i Application.ScreenUpdating True MsgBox 处理完成共找到 count 条达标记录 End Sub5.3 效果与优化这段代码的整体流程是关闭屏幕刷新 → 准备汇总表 → 循环所有工作表 → 内层循环判断每一行是否达标 → 达标数据存入数组 → 循环结束统一写入汇总表。你可能注意到我用数组而不是直接一行一行写入汇总表。这是因为在循环中每次写入单元格都会触发界面刷新数据量大时就明显卡顿。先把数据攒在内存数组里最后一次性写入速度会快很多。如果产品条数可能超过1000条动态数组可以改成用字典来存储更灵活Dim dict As Object Set dict CreateObject(Scripting.Dictionary) 在满足条件时 If Not dict.exists(ws.Cells(r, 1).Value) Then dict.Add ws.Cells(r, 1).Value, ws.Cells(r, 2).Value End If用字典的好处是天然去重还能存储和快速查找比较适合“编号不允许重复”的业务场景。不过要注意字典是无序的如果你希望汇总表按第一个出现的顺序展示用普通数组更可控。实际项目里按需选择就好。这个案例虽然不算复杂但基本覆盖了流程控制的所有要点If做条件过滤、For和For Each做遍历、ScreenUpdating优化性能、数组暂存数据。你把这个思路跑透后面写各类汇总清洗工具都不难。6. 调试方法与常见问题速查6.1 调试方法F8、断点、立即窗口写流程控制代码尤其是多重嵌套循环的时候一次跑通是理想状态更多时候你得盯着代码一点点查。VBA的调试工具必须练熟。F8是逐行执行每按一次执行一行配合“本地窗口”可以实时监控变量的变化。跑循环时你可以看着计数器i一行一行增加判断逻辑哪里不对一目了然。循环嵌套时F8逐行走能看清外层内层进入的顺序这也是理清嵌套逻辑最笨也最有效的办法。断点调试适合一次性跳到某个位置。在代码行左侧灰色区域点一下就会出现一个红点运行时代码会停在断点位置。比如你怀疑循环第3次之后出问题可以在If判断那里打一个断点配合条件断点右键断点设置条件快速跳到指定的循环次数。立即窗口Immediate Window快捷键CtrlG配合Debug.Print是调试流程控制最常用的组合。你可以在代码里写Debug.Print 第 r 行销售额 ws.Cells(r, 3).Value然后在立即窗口里就能看到每一行实际执行时打印出来的内容。这个习惯非常有用——数据一大你根本不知道代码执行到哪一步出的错打印日志是最直接的办法。6.2 常见问题速查表我总结几个写流程控制代码时最容易踩的坑一个一个说。第一个是死循环。Do While条件设置不当或者循环体内忘记修改变量的值就会无限循环。遇到程序卡住按Esc或CtrlBreak可以中断运行弹出一个“代码执行已中断”的对话窗口再点“调试”就能看到停在哪一行。这个操作新手一定要知道不然每次卡死只能强杀Excel进程没保存的数据直接就没了。第二个是循环边界错误。用For i 1 To lastRow的时候如果lastRow是0或者没正确赋值循环体一次都不执行或者反过来多执行了一次。检查边界最直接的方法就是在循环前后用Debug.Print打印lastRow的值。还有一个常见错误是用Cells(i, 1)循环时行数超出工作表最大行数直接报错那就要检查lastRow的取值逻辑了。第三个是条件判断不生效。这个坑在VBA里特别典型如果单元格里的数字存成了文本格式用“ 5000”比较会得到False。解决方法是比较前用Val或者IsNumeric转换一下或者用CDbl把值强制转成数值型再比较。还可以用VarType函数判断单元格的数据类型。我刚入门时在这个坑上愣是排查了半天最后发现是“数字”其实是“文本”。第四个是修改单元格导致运行慢。前面已经强调过循环里频繁读写单元格是性能杀手。除了用数组还有几个临时开关也别忘了Application.ScreenUpdating False Application.Calculation xlCalculationManual Application.EnableEvents False 循环处理代码 Application.EnableEvents True Application.Calculation xlCalculationAutomatic Application.ScreenUpdating True这三个开关分别关闭屏幕刷新、关闭自动计算、关闭事件触发。处理完数据一定要在End Sub之前恢复它们不然Excel会一直处于卡顿状态其他功能也不正常。这里尤其注意如果代码中途出错退出这些设置可能不会被恢复所以你可以在调试时手动在立即窗口里执行一遍恢复语句。第五个是For Each遍历对象时在循环里删除对象会报错。比如你遍历工作表循环体里要删除当前工作表运行到一半就出问题。处理方法是先把要删除的对象装进一个集合循环结束以后再统一删除。这个坑我在做工作表清理工具时踩过不止一次。为了让你更快对照排查我把上面的常见问题整理成一个速查表问题现象可能原因解决思路代码一直跑不停Do循环条件永远为真、循环内变量未变化按Esc中断检查循环条件和变量更新逻辑给循环加最大次数保护循环一次都没执行For循环的起始值大于结束值、Do While条件一开始就不成立检查边界变量的赋值用Debug.Print打印边界值条件判断结果总是False比较的是文本型数字、字符串包含不可见字符用Val或CDbl转换后再比较用Trim清除首尾空格数据多时运行极慢循环里频繁读写单元格用数组缓冲关闭ScreenUpdating、Calculation、EnableEventsFor Each遍历时报错循环体内删除了正在遍历的对象先收集删除目标到集合循环结束再统一操作修改后Excel异常卡顿关闭的自动刷新、自动计算没恢复在代码结束处设置恢复语句调试时用立即窗口手动恢复把表格里这些场景过一遍基本能覆盖流程控制阶段90%的日常报错。最后再分享一个小技巧。写循环之前先不要急着完整跑通而是先在代码里加一个计数器循环体每执行一次就Debug.Print一次。跑完以后看打印条数是否跟自己预想的一致。这个“打印确认法”花不了几秒钟却能帮你确认循环边界对不对、条件分支会不会漏掉数据特别是面对多表多条件嵌套循环时这个习惯真的能救命。我这些年写VBA处理上百个表汇总都靠这个方法兜底等调试顺了再把这个打印代码删掉干净利落。