客户跟进表每天都会被改:有人补回访日期,有人把状态从“待回访”改成“已成交”,有人临下班前再另存一个版本。表被改得越频繁,越适合把提醒、校验和收尾动作交给事件去做。
事件在 VBA 里面是一个比较抽象的概念,我们可以这样去理解:
每当表格有变化,在 VBA 里面都可以理解成一个“事件”。比如:
这种操作都属于一个事件。
而 VBA 的“事件编程”,就是指在这些事件发生的时候,同时调用一段自己写的程序来运行。这样理解的话可能会好一点。
VBA 事件本身不难,难的是别把过程写错地方,也别让代码把自己再触发一遍。把工作簿事件、工作表事件,很多原本靠人记着做的动作,就能自己跑起来。
这里的学习,我觉得应该是先学常用的一些事件。
像我下面列出来的这些常用事件,我们可以先用起来。因为 VBA 里面的事件非常多,不要先求全。我们应该在实际工作的过程中,遇到了再去通过查资料的方式,找到我们需要的一个事件。
先分清谁在响应
| | |
|---|
| Workbook_Open、Workbook_BeforeSave、Workbook_NewSheet | ThisWorkbook |
| Worksheet_Change、Worksheet_SelectionChange、Worksheet_BeforeDoubleClick | |
| WorkbookOpen | |

入门阶段先把前两类写明白就够用了。只要记住一句话:工作簿级事件写在 ThisWorkbook,工作表级事件写在具体那张表的代码窗口里,别丢到普通模块里假装能触发。这是一个非常重要的问题,也是很容易犯错的一个地方。
我们在写事件的时候,必须把位置写清楚、写对:
- 属于工作簿的,就写在工作簿(ThisWorkbook)里面
先看一张客户跟进表
假设 B3:G8 是销售团队每天都在改的跟进总表:
这类表最常见的自动触发,通常就两组:
- 跟进状态或联系日期被改动后,自动补出回访日期和提醒内容。
打开和保存两个动作,放在 ThisWorkbook 里最合理
如果你想让工作簿一打开就跳到跟进总表,顺手提醒还有多少条待跟进记录,可以直接写在 ThisWorkbook:
PrivateSub Workbook_Open()With Worksheets("跟进总表") .Activate .Range("B3").Select' 打开时先提示今天还有多少条待回访 MsgBox "今天还有 " & WorksheetFunction.CountIf(.Range("D4:D8"), "待回访") & _" 条记录要继续跟。"EndWithEndSubPrivateSub Workbook_BeforeSave(ByVal SaveAsUI AsBoolean, Cancel AsBoolean)If SaveAsUI Then' 另存为前补一句提醒,避免版本乱放 MsgBox "这份跟进簿建议另存到本周归档文件夹,后面追版本会省事很多。"EndIfEndSub
这里的 SaveAsUI 很实用。它为 True 时,说明这次保存会弹“另存为”窗口,常见于新文件第一次保存,或者当前文件是只读打开。很多版本管理提醒,都适合挂在这里。
真正高频的,还是 Worksheet_Change
真正每天都在跑的,多半还是工作表里的输入变化。上面这张表里,我只关心 D4:E8 这块输入区,也就是“当前状态”和“本次联系”两列。状态或日期一变,就顺手把 F 列和 G 列补出来。
这段代码应该放在“跟进总表”那张工作表自己的代码模块里:
PrivateSub Worksheet_Change(ByVal Target As Range)Dim watchRange As RangeDim editCells As RangeDim oneCell As RangeDim dataRow AsLongSet watchRange = Range("D4:E8")Set editCells = Intersect(Target, watchRange)If editCells IsNothingThenExitSubOnErrorGoTo CleanUp Application.EnableEvents = FalseForEach oneCell In editCells dataRow = oneCell.RowIfCells(dataRow,"D").Value="待回访"AndIsDate(Cells(dataRow,"E").Value) Then Cells(dataRow,"F").Value=Cells(dataRow,"E").Value+3' 三天后回访 Cells(dataRow, "G").Value = "三天内回访"ElseIfCells(dataRow,"D").Value="已报价"AndIsDate(Cells(dataRow,"E").Value) Then Cells(dataRow,"F").Value=Cells(dataRow,"E").Value+2' 两天后追报价反馈 Cells(dataRow, "G").Value = "两天内催报价反馈"ElseIf Cells(dataRow, "D").Value = "已成交"Then Cells(dataRow, "F").ClearContents Cells(dataRow, "G").Value = "转售后跟进"Else Cells(dataRow, "F").ClearContents Cells(dataRow, "G").ClearContentsEndIfNext oneCellCleanUp: Application.EnableEvents = TrueEndSub
这段写法里,最关键的是下面三个点:
Intersect(Target, watchRange) 先把范围收窄,不让整张表任何一个改动都把事件带起来。Target 可能不止一格,批量粘贴时也一样会进来,所以直接遍历 editCells 更稳。- 事件过程里又写了
F 列和 G 列,如果不先 Application.EnableEvents = False,代码就会继续触发自己。
有三个触发细节别想当然
Worksheet_Change 不会因为公式重算而触发。公式结果变了,但用户没直接改单元格内容时,它不会进来。- 单纯改格式通常不触发
Change,但复制粘贴格式、清除格式这类动作又可能把它带起来,关键录入表别只按直觉判断。 - 只要事件过程里又改了工作表,Excel 的撤销栈经常就保不住了。录入表真要上事件,最好先接受这一点,再决定哪些动作值得自动化。
客户跟进表这类每天都在改的工作簿,最有用的不是把事件种类全背下来,而是先抓住三个动作:打开时准备好、保存前拦一下、输入后只盯住真正的业务区。把这几类写对了,后面再补 SelectionChange、BeforeDoubleClick 这些细一点的触发,学起来会容易一些。