简介:《EXCEL_VBA常用代码实战大全》是一份面向Excel VBA入门与进阶学习者的代码技巧汇编,适合希望用宏自动化重复操作、提升表格处理效率的办公与数据分析人员。压缩包内含1个docx文档,大小约15.12MB,内容以章节化方式组织,便于按知识点检索。已有2071人学习下载,说明其在VBA自学群体中具有一定参考价值。文档围绕Range与Cells对象、单元格区域选定、末行末列定位、Find查找替换、选择性粘贴、数组与集合、错误处理及用户窗体等常用主题展开,配有技巧编号、目录结构和代码解析,可帮助读者理解对象引用方式、常用语句组合与典型应用场景。整体更像一份可随时翻阅的VBA常用代码手册,适合边练边查、逐步积累。
1. 从一张三十个工作表的台账说起:这套 VBA 技巧到底解决什么问题
接手一张用了五年的台账最怕什么?不是数据量大,是点开之后三十个工作表、命名全靠手气,每天早上还要重复一遍筛选、复制、粘成新表、另存发送。这种活干到第三周,人自然会琢磨用 VBA 把流程固定下来。这份《EXCEL_VBA常用代码实战大全》正是对着这个场景写的:它不铺垫语法,直接按 Excel 对象模型分成 Range、Worksheet、Workbook、Shape/Chart、Application、对话框六章,把 70 个常用技巧逐条落到能粘贴就运行的代码上。适合已经会写公式、想跨过 Excel+VBA编程入门那道坎的人,也适合写了几年脚本、想回头补对象模型细节的老手当手册翻。
2. 对象模型与目录映射:六章 70 个技巧该按什么顺序啃
拿到一份几十页的技巧汇编,最容易犯的错是从第一页开始顺着读。这类资料的组织逻辑不是教学顺序,是对象模型的层级顺序,顺着读会在「技巧 30」卡住,因为那里已经在用第 20 个技巧里的遍历写法了。先把骨架搭起来,再挑需要的段落查,效率差好几倍。
2.1 三层对象模型:Workbook → Worksheet → Range 的引用链
VBA 操作 Excel 的一切动作,都落在一条从大到小的引用链上:Application 是 Excel 本体,往下是 Workbooks 集合里的某一本工作簿,再往下一层是 Worksheets 集合里的某张表,最末端才是 Range 单元格区域。这条链子写全了很长,所以 VBA 允许省略前缀,代价是省略之后指向谁由「当前状态」决定。
Sub ShowChain() ' 写全的引用链,任何时候都指向同一本书、同一张表、同一个格子 Application.Workbooks("台账.xlsm").Worksheets("明细").Range("A1").Value = "写全了" ' 省略前缀:Range 默认指活动工作表,Workbooks 默认指活动工作簿 Range("A1").Value = "省略了" ' 活动表不是「明细」时,这行就写错地方了 End Sub上面两行代码在只有一张表的时候表现完全一样,一旦活动表被用户点到别处,第二行就会把数据写到错误的表上,而且不报错。这是新手最常遇到、也最难自己发现的错误,因为它不抛异常,只是静默写到了防不胜防的地方。
我一般的做法是:凡是跨表、跨簿的操作,一律先Dim ws As Worksheet再Set ws = ThisWorkbook.Worksheets("明细"),后面所有 Range 都挂在这个ws上。ThisWorkbook指代码所在的那本书,ActiveWorkbook指当前激活的那本,两者在加载宏场景下经常不是同一本,这个区别后面第 4 章还会再提。
至于Selection、ActiveCell这类对象,只在确实要响应用户操作的场景下用,纯计算流程里一律不用,因为它们的状态你控制不住。
2.2 把 70 个技巧映射成六个模块
这份资料的目录结构本身就是一张能力地图。把章节和实际场景对照起来看,就知道自己该从哪一章补:
| 章 | 核心对象 | 典型技巧 | 什么时候会翻到它 |
|---|---|---|---|
| 第 1 章 | Range | 引用方式、最后一个非空单元格、Find、合并单元格、Target 参数 | 90% 的日常脚本都在这一章里 |
| 第 2 章 | Worksheet | 引用与遍历、增删表、深度隐藏、删除空行重复行、保护 | 做多表汇总、做模板分发 |
| 第 3 章 | Workbook | 新建打开、判断是否已打开、关闭不弹框、跨簿取数 | 做月度报表合并、做数据抽取 |
| 第 4 章 | Shape/Chart | 插入图形图片、遍历图形、自动生成与导出图表 | 做看板、做图表批量生成 |
| 第 5 章 | Application | 版本与用户名、定时器、屏蔽组合键、隐藏主窗口 | 做工具壳、做加载宏 |
| 第 6 章 | 对话框 | 内置对话框、文件夹选择器 | 做交互入口 |
阅读顺序建议按使用频率来,不是按目录顺序。Range 一章必须吃透,因为其余五章的动作最终都要落到单元格上;Worksheet 和 Workbook 两章是把它扩展到多表多簿;Shape/Chart 和 Application 属于锦上添花,等前面跑顺了再看。对话框那一章代码量最小,用到的时候现查就行。
2.3 上手前的环境准备:三个必改的设置
代码没错但运行结果诡异,八成是环境问题。动手之前先花十分钟把这几项配好,能省掉后面大量排查时间。
第一,文件必须存成.xlsm或.xlsb。存成.xlsx时 VBA 代码会被直接丢弃,关闭再打开就没了,而且 Excel 不会给你任何强提醒。这不是小概率事件,我见过不止一个人写了两天的脚本因为一次「另存为」蒸发。
第二,把常放脚本的文件夹设为受信任位置(文件 → 选项 → 信任中心 → 信任中心设置 → 受信任位置),这样打开时不会每次都在顶部弹一条黄色安全警告条。没配的话,宏默认被禁用,代码一动不动。
第三,在 VBE 里打开「要求变量声明」。具体操作是 VBE 菜单 工具 → 选项 → 编辑器 → 勾选「要求变量声明」,之后新建的每个模块顶部会自动加上Option Explicit。
Option Explicit ' 强制声明变量,拼错变量名会直接报编译错误而不是静默得到空值 Sub CheckEnv() Debug.Print "Excel 版本: " & Application.Version Debug.Print "当前用户: " & Application.UserName Debug.Print "本书路径: " & ThisWorkbook.Path Debug.Print "本书名称: " & ThisWorkbook.Name End SubDebug.Print的结果输出到 VBE 的「立即窗口」(快捷键 Ctrl+G),这是排查时最轻量的观察手段,比弹 MsgBox 打断流程好用得多。上面这段代码同时演示了第 5 章 Application 对象里最常用的三个属性,把它当成新环境的第一段验证代码写下来,路径、名称符合预期,说明文件位置和环境都对。
提示:把常用的排查语句整理成一个
Sub Env()放在个人宏工作簿里,换机器复制过去就能用。
3. Range 核心拆解:引用、定位、查找、合并单元格的落地写法
Range 是这份资料里篇幅最大的一章,二十个技巧全围着「怎么准确地指到那块格子」和「指到之后怎么改」这两件事转。下面按引用方式、边界定位、条件查找、事件与合并四个场景拆开讲,每段都能直接粘进模块跑。
3.1 六种引用方式与 Offset、Resize、Union 的组合用法
引用一块区域的写法有六种,各自的适用场景不一样。写死地址适合固定模板,Cells 适合循环里的行列号,方括号快捷记号适合调试时临时用,Offset 和 Resize 适合在已知锚点的基础上推出周边区域,Union 用来把几块不相邻的区域当成一个对象批量处理。
Sub RefDemo() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("明细") ws.Range("A1").Value = "直接地址" ' 1 地址字符串,最直观 ws.Cells(2, 1).Value = "Cells 行列" ' 2 行列号,配合循环最方便 ws.[A3].Value = "方括号" ' 3 快捷记号,等价 Range("A3") ws.Range(ws.Cells(1, 1), ws.Cells(5, 3)).Interior.Color = RGB(255, 240, 200) ' 4 两点定区域 ws.Range("B2").Offset(1, 2).Value = "Offset" ' 5 以 B2 为基准下移 1 行右移 2 列 → D3 ws.Range("B2").Resize(3, 2).Borders.LineStyle = xlContinuous ' 6 从 B2 扩成 3 行 2 列 → B2:C4 End SubOffset(行偏移, 列偏移)的两个参数可以为负,Offset(-1, 0)就是上一行,负数越界会直接报错,循环里用之前一定要判断行号。Resize(行数, 列数)改的是区域大小,起点不变,Resize(1, 1)就是把它缩回单个格子。
真正让人少写循环的是 Union 和 UsedRange:
Sub AreaDemo() Dim u As Range Set u = Union(Range("A1:B2"), Range("D5:E6")) ' 两块不相邻区域合成一个对象 u.Interior.Color = vbYellow ' 一次赋值,两块同时上色 Debug.Print u.Areas.Count ' 2,Areas 才是真实块数 Debug.Print ActiveSheet.UsedRange.Address ' 已使用区域,含仅有格式的空单元格 Debug.Print Range("A1").CurrentRegion.Address ' 由空行空列围起来的连续块 End Sub这里有两个容易搞混的点。Union出来的对象是一个 Range,但它的Count统计的是单元格总数,要数「几块」得用Areas.Count。UsedRange会把只设过格式、没有内容的单元格也算进去,所以拿它来确定数据边界经常偏大,处理完数据后手动删掉多余行再保存,UsedRange才会缩回来;CurrentRegion则是从锚点往外扩到第一个空行空列为止,适合读取规整的二维表,前提是表中间不能有整行整列的空隙。
3.2 找最后一个非空单元格:三种写法与各自的坑
只要是做动态区域,就一定要取边界。资料里给了好几种找最后一行的方法,实际用下来差别很大。
最省事的是 End(xlUp):
Function LastRowEnd(ByVal ws As Worksheet, ByVal col As Long) As Long LastRowEnd = ws.Cells(ws.Rows.Count, col).End(xlUp).Row ' 从最底下一格往上顶 End Function这行代码的逻辑是从该列的最后一个单元格(第 1048576 行)按 Ctrl+↑ 的效果往上跳,落到第一个非空单元格。它的坑有两个:整列全空时返回 1,不是 0,调用方要自己判断Cells(1, col)是不是真的有值;中间有大段空行时也可能跳过头,因为它只认「连续的块」。
更稳的是 Find,用通配符*从下往上搜:
Function LastRowFind(ByVal ws As Worksheet, ByVal col As Long) As Long Dim r As Range Set r = ws.Columns(col).Find(What:="*", _ After:=ws.Cells(1, col), _ LookIn:=xlFormulas, _ LookAt:=xlPart, _ SearchOrder:=xlByRows, _ SearchDirection:=xlPrevious) If r Is Nothing Then LastRowFind = 0 Else LastRowFind = r.Row End Function参数含义值得逐个数一遍:LookIn:=xlFormulas让公式单元格也参与匹配,哪怕它算出来是空字符串;SearchDirection:=xlPrevious表示从后往前搜,第一个命中的就是最后一行;After:=ws.Cells(1, col)是起始位置,Find 是从起点之后开始找的,起点如果给成默认值,搜索区域的第一格就永远搜不到,这是无数人「明明有值却返回 Nothing」的根源。
第三种是SpecialCells(xlCellTypeLastCell),它取的是表格里最后被用过的单元格,包含了格式残留,实际位置往往比真实数据大,只适合粗判。日常我基本只用 Find 版本,稳定且不受筛选、分类汇总的干扰。
3.3 Find 与 Like:精确查找、模糊匹配、批量替换
Find只返回第一个命中结果,要拿全部命中必须配FindNext循环,而且得自己记下起始地址做终止判断,否则会绕圈跑到死循环。
Sub FindAll() Dim ws As Worksheet, rng As Range, hit As Range, firstAddr As String, n As Long Set ws = ThisWorkbook.Worksheets("明细") Set rng = ws.Range("A2:A5000") Set hit = rng.Find(What:="华东", LookIn:=xlValues, LookAt:=xlPart, MatchCase:=False) If hit Is Nothing Then MsgBox "没有命中" Exit Sub End If firstAddr = hit.Address ' 记住第一个命中,用来判断是否绕回 Do n = n + 1 hit.Offset(0, 6).Value = "已核对" ' 命中行右侧第 6 列打标记 Set hit = rng.FindNext(hit) Loop While Not hit Is Nothing And hit.Address <> firstAddr Debug.Print "共命中 " & n & " 处" End SubLookAt:=xlPart是包含匹配,xlWhole是整格相等,默认是前者,做编码匹配时如果不改,会误命中一批前缀相同的记录。MatchCase控制大小写。另外Find会记住上一次调用时的参数,如果上次设置过xlWhole又没显式传参,这次就可能查不到——所以每次调用都把关键参数写全,别指望默认值。
字符串模式匹配用Like更直接,尤其是格式校验:
Sub LikeCheck() Dim v As String, i As Long For i = 2 To 100 v = CStr(Cells(i, 1).Value) ' 一个字母开头 + 6 位数字,例如 A123456 If v Like "[A-Za-z]######" Then Cells(i, 2).Value = "格式正确" Next i End Sub方括号里是字符集合,#代表任意数字,?代表任意单个字符,*代表任意长度。它比正则轻得多,格式规则简单的场景用它足够,复杂规则才需要去 VBE 的「工具 → 引用」里勾选脚本运行库引正则。
批量替换走Range.Replace,比循环单元格快一个数量级:
Sub BatchReplace() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("明细") ws.Range("A2:A" & LastRowFind(ws, 1)).Replace _ What:="有限公司", Replacement:="", _ LookAt:=xlPart, SearchOrder:=xlByRows, MatchCase:=False End Sub3.4 合并单元格与 Worksheet_Change 的 Target 参数
合并单元格是表格里最漂亮、代码里最麻烦的东西:合并后只有左上角那格有值,其余全空,删除行、排序、循环遍历都会出错。取消合并时要保留内容的正确写法是先接住整个合并区域:
Sub UnmergeKeepValue() Dim c As Range, m As Range, v As Variant For Each c In Selection If c.MergeCells Then Set m = c.MergeArea ' 先记住整块,取消合并之后 MergeArea 就只剩一格了 v = c.Value m.UnMerge m.Value = v ' 整块回填同一个值 End If Next c End Sub如果按直觉写成c.UnMerge之后c.MergeArea.Value = v,结果是把值写回了左上角一格,其余格子依旧是空的——因为 UnMerge 之后MergeArea只返回它自己。这个先后顺序必须颠倒过来,我在这上面查过半小时。
判断区域里有没有合并单元格用Range.MergeCells,注意它返回 True 的条件是「这个区域本身就是一个完整的合并块」,拿一个大区域去判断常常得到 False。稳妥的做法是遍历其中的格子挨个判断。
工作表事件里的Target参数同理,它可能是一个格子,也可能是一整片(用户一次性粘贴几千行),甚至还可能是带合并区域的单元格。所以处理前必须限流:
Private Sub Worksheet_Change(ByVal Target As Range) If Target.Cells.CountLarge > 1 Then Exit Sub ' 批量粘贴直接放过 If Intersect(Target, Me.Columns("D")) Is Nothing Then Exit Sub ' 只管 D 列 On Error GoTo Finally Application.EnableEvents = False ' 避免自己触发自己 Target.Offset(0, 1).Value = Format(Now, "yyyy-mm-dd hh:nn") ' 写入修改时间 Finally: Application.EnableEvents = True ' 无论如何都要恢复 End SubIntersect(区域1, 区域2)返回两者的交集,为空说明这次变化跟我们关心的列无关。CountLarge是为了防止大区域时Count溢出,是老手才会注意的细节。EnableEvents = False必须在写值之前打开,否则写值这个动作又会触发一次 Change 事件,形成递归,最后 Excel 直接卡死。配套的On Error GoTo Finally保证中途报错时事件开关能恢复,不然会一直处于关闭状态,后面所有事件都像失联一样。
4. Worksheet 与 Workbook 层:增删、隐藏、保护与跨簿取数
单表脚本写顺了,迟早要面对多表多簿。这一层的难点不在于语法,在于「引用的稳定性」和「动作的可撤销性」——删错一张表、关错一本簿,Ctrl+Z是救不回来的。
4.1 工作表引用的四种方式与两种遍历写法
引用的四种方式各有代价,按稳定性从高到低排大概是:代码名称 > 名称 > 索引号 > 活动表。
Sub SheetRef() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("明细") ' 按名称,最常用 Debug.Print ThisWorkbook.Worksheets(1).Name ' 按索引号,用户拖动表顺序就变了,不建议写死 Debug.Print Sheet1.Name ' 按代码名称,改表名也不影响,最稳 Debug.Print ActiveSheet.Name ' 活动表,状态不可控 For Each ws In ThisWorkbook.Worksheets ' 遍历写法一,推荐 If ws.Visible = xlSheetVisible Then Debug.Print ws.Name Next ws Dim i As Long For i = 1 To ThisWorkbook.Worksheets.Count ' 遍历写法二,注意删除表时要倒序 Debug.Print ThisWorkbook.Worksheets(i).Name Next i End Sub代码名称在 VBE 的工程窗口里能看到,默认是 Sheet1、Sheet2 这种,可以在属性窗口(F4)里改。用户改表名是家常便饭,代码名称他改不了,所以核心脚本挂代码名称更安全。用索引号遍历的前提下如果要删除表,必须从Count往 1 倒着循环,正序删会跳表——删掉第 3 张之后,原来的第 4 张就变成第 3 张了,循环变量再加 1 正好越过去。
4.2 禁止删除、深度隐藏、防止改名:保护类需求的实现
给模板加保护,本质是在合适的事件里拦截用户动作。禁止删除指定表靠SheetBeforeDelete事件里的Cancel:
' 这段必须写在 ThisWorkbook 模块里,放标准模块不生效 Private Sub Workbook_SheetBeforeDelete(ByVal Sh As Object) If Sh.Name = "汇总" Or Sh.Name = "参数" Then MsgBox "「" & Sh.Name & "」是系统表,不允许删除", vbExclamation Cancel = True End If End SubCancel = True在较老的版本里对删除事件无效,只能退而求其次用「工作簿结构保护」把整个增删表的入口锁住。这也顺带解决了另一个需求——防止改名:
Sub LockStructure() ThisWorkbook.Protect Password:="admin123", Structure:=True ' 只锁结构,不锁窗口 End SubStructure:=True锁的是工作簿结构,用户可以继续编辑单元格、排序、筛选,但不能增删表、改名、移动。改代码里的密码要跟实际使用的保持一致,忘记密码基本等于工作簿结构永久锁死,没有后悔药。
深度隐藏是xlSheetVeryHidden,它和普通隐藏的区别是:右键菜单的「取消隐藏」里看不到,只能在 VBE 的属性窗口把 Visible 改回xlSheetVisible才能恢复。
Sub DeepHide() ThisWorkbook.Worksheets("参数").Visible = xlSheetVeryHidden End Sub资料里有个很实用的组合技:把配置表设为深度隐藏,在Workbook_Open里再隐藏一次,同时那张表上写着「请启用宏」。用户在打开时选择禁用宏,Open 事件不执行,配置表就露出来并被看到——这是「禁用宏则关闭工作簿」这类需求的典型实现思路,本质上是拿可见性当信号灯。
4.3 关闭不弹保存框与 BeforeClose 的取舍
自动化流程跑到最后一步弹出一个「是否保存」对话框,整个无人值守的设想就作废了。处理方式有两种,选哪种取决于你想不想留下修改。
' 方式一:关闭时强制保存 Private Sub Workbook_BeforeClose(Cancel As Boolean) If Not ThisWorkbook.Saved Then ThisWorkbook.Save End If End Sub ' 方式二:代码主动关闭,明确丢弃修改 Sub CloseNoAsk() Application.DisplayAlerts = False ThisWorkbook.Close SaveChanges:=False Application.DisplayAlerts = True End Sub方式一的关键是ThisWorkbook.Saved这个属性,它标记「自上次保存后有没有改动」,比直接无脑 Save 高效,也不会有多余的写盘。方式二里DisplayAlerts必须在关闭之后立刻改回来,如果中途报错跳过了恢复那一行,后面所有需要弹确认的操作(删除表、覆盖文件)都会静默执行,很容易酿成事故。所以我一般写成On Error GoTo Finally加Finally: Application.DisplayAlerts = True的结构。
保存指定工作表为单独文件也是同一类需求,用Sheets.Copy:
Sub SaveSheetAsBook() Dim p As String p = ThisWorkbook.Path & "\导出_" & Format(Now, "yyyymmdd_hhnn") & ".xlsx" ThisWorkbook.Sheets("汇总").Copy ' 不传参数就是复制到一个新工作簿 ActiveWorkbook.SaveAs p, FileFormat:=xlOpenXMLWorkbook ActiveWorkbook.Close False End Sub注意Copy之后ActiveWorkbook已经变成新书了,原来那本降到后台,接着所有不带前缀的 Range 都会指向新书——这也是前面反复强调要显式声明工作簿对象的原因。
4.4 跨簿取数:五种方案的取舍
资料第 3 章后半段列了五种不打开工作簿就取数的方案,实际选型按「要不要实时」「数据量多大」「文件在不在本机」三个维度定:
| 方案 | 是否打开源文件 | 适用场景 | 主要限制 |
|---|---|---|---|
| 公式外部引用 | 否 | 少量单元格、要求实时更新 | 路径写死,源文件移动即断链 |
| GetObject | 后台打开 | 结构复杂、要读取格式 | 大文件慢,容易残留进程 |
| 隐藏 Application | 后台打开 | 批量处理多个文件 | 必须成对恢复 Visible,否则 Excel 隐身 |
| ExecuteExcel4Macro | 否 | 只取单个值、追求速度 | 语法古老,只能取数值 |
| ADO/SQL 连接 | 否 | 大表按条件筛选、做汇总 | 需要引用数据访问库,字段类型要处理 |
数据量在几万行以内、只取一个汇总数,用 GetObject 最省事;几十个文件批量抽取,我会用隐藏 Application 的写法,但一定在On Error的收尾里把Application.Visible = True补回来。曾经有一次调试中断在这一行之前,整个 Excel 界面消失,只能在任务管理器里结束进程,那次之后我把恢复语句全部挪到了Finally段。
Sub GetValueFromClosedBook() Dim p As String, wb As Workbook, v As Variant p = ThisWorkbook.Path & "\源数据.xlsx" Application.ScreenUpdating = False On Error GoTo Finally Set wb = GetObject(p) ' 后台加载,不显示窗口 v = wb.Worksheets("明细").Range("B2").Value wb.Close False ThisWorkbook.Worksheets("汇总").Range("B2").Value = v Finally: Application.ScreenUpdating = True End Sub5. 避坑排查:宏点不动、事件不触发、保存后代码消失这五个现场
这一章是血泪经验合集。下面五条都是我在实际项目里真实翻过车的,每条按「看到什么 → 为什么 → 怎么修」写,遇到相似症状可以直接对号入座。
5.1 运行到 Select 就报 1004
现象:Range("A1").Select报「Select 方法作用于 Range 类失败」,错误号 1004,代码其他地方看着都没问题。
原因:Select 只能作用于活动工作表上的区域,如果Range挂在一张非活动表上,或者执行时活动表根本不是你引用的那张,就必失败。另一种情况是工作簿处于保护状态,选择被禁止。
解决:首选根本不用 Select,直接对区域赋值,绝大多数场景压根不需要「先选中再操作」;确实必须选中的,先ws.Activate再ws.Range("A1").Select。
Sub SafeWrite() Dim ws As Worksheet Set ws = ThisWorkbook.Worksheets("明细") ws.Range("A1").Value = "直接写" ' 不需要激活、不需要选中 End Sub5.2 改了单元格,Worksheet_Change 不触发或者卡死
现象:手动改 D 列能触发,运行代码改就不触发;或者触发一次之后 Excel 直接转圈无响应。
原因:一是Application.EnableEvents被某段代码关掉之后没恢复,常见于中途报错跳过了恢复语句;二是事件处理程序内部又去改了单元格,形成递归,这是卡死的元凶。
解决:所有改单元格的事件代码都用On Error GoTo Finally包起来,Finally段无条件恢复EnableEvents;写值之前先把开关关掉。调试阶段可以在立即窗口执行Application.EnableEvents = True手动救回来,不用重启 Excel。
5.3 关掉再打开,写的代码全没了
现象:昨天写好的脚本,今天打开文件点按钮没反应,按 Alt+F11 进去一看模块是空的。
原因:文件被另存成了.xlsx。这个格式不支持 VBA 工程,保存时 Excel 会弹一次提示,但很多人顺手点了「是」,代码就丢了。另存为.xlsm才是正确选项。
解决:养成习惯,新建文件第一件事就是另存为.xlsm,再做其他设计。另外把常用脚本集中放在个人宏工作簿或加载宏里,和业务文件解耦,业务文件怎么另存都不影响工具本体。
5.4 Find 明明有值却返回 Nothing
现象:A 列肉眼可见有「华东」,Find("华东")返回 Nothing,或者只在某几段区域里找不到。
原因:三个高发点。LookIn默认是xlFormulas,如果那些单元格是公式且公式文本里没有「华东」,就搜不到;After参数默认从区域第一格之后开始,导致第一格永远被跳过;Find会沿用上一次调用的参数,前一次设过xlWhole没显式复位,这次就变成整格匹配。
解决:每次调用把LookIn、LookAt、SearchDirection、After全部写全,不依赖默认值。
Set hit = rng.Find(What:="华东", _ After:=rng.Cells(1, 1), _ LookIn:=xlValues, _ LookAt:=xlPart, _ SearchOrder:=xlByRows, _ SearchDirection:=xlNext, _ MatchCase:=False)5.5 删除行之后数据整体错位,少删了一行
现象:循环删除符合条件的行,结果每次都会漏掉紧邻的下一行,或者删完之后还有漏网的。
原因:正序循环删除时,删掉第 i 行之后,原来的第 i+1 行上移顶到第 i 行,而循环变量已经加到了 i+1,正好把它跳过去。
解决:两种写法,一是从最后一行倒着循环到第 2 行,二是用Union把所有命中行攒成一个大区域,最后一次EntireRow.Delete。
Sub DeleteBlankRows() Dim i As Long Application.ScreenUpdating = False For i = Cells(Rows.Count, 1).End(xlUp).Row To 2 Step -1 ' 倒序删,不会跳行 If Trim(Cells(i, 1).Value) = "" Then Rows(i).Delete Next i Application.ScreenUpdating = True End Sub倒序删的另一个好处是可以省掉屏幕刷新,一万行以内基本秒完。批量删除之前务必先备份一份,删行是没法撤销的。
6. 二次开发:把 70 个技巧收进自己的加载宏工具箱
零散脚本写到一定数量,一定会想「为什么每次新项目都要重新复制这些函数」。我现在的做法是建一个.xlsam加载宏,把资料里的技巧按职责拆成四个标准模块:modRange放区域查找、边界定位、复制粘贴;modSheet放增删表、遍历、保护;modBook放跨簿取数和保存;modUtil放日志、路径、错误处理这些杂项。加载宏的好处是所有工作簿打开时都能调用,且业务文件怎么另存都不影响工具本身。
真正让工具箱好用的是两件事:所有列号集中管理,所有入口带错误兜底。
' modConst:表结构只在这一处描述,字段位置变了只改这里 Public Const COL_DATE As Long = 1 Public Const COL_REGION As Long = 3 Public Const COL_AMOUNT As Long = 6 ' modUtil:带日志的执行壳,出错时能追溯是哪一步挂的 Public Sub LogIt(ByVal msg As String) Dim f As Integer f = FreeFile Open ThisWorkbook.Path & "\run.log" For Append As #f Print #f, Format(Now, "yyyy-mm-dd hh:nn:ss") & " " & msg Close #f End Sub Public Function LastRow(ByVal ws As Worksheet, ByVal col As Long) As Long Dim r As Range Set r = ws.Columns(col).Find("*", ws.Cells(1, col), xlFormulas, xlPart, , xlPrevious) If r Is Nothing Then LastRow = 0 Else LastRow = r.Row ' 空列返回 0,调用方要判 End FunctionmodConst里那几个常量看着不起眼,但它是整份脚本能不能撑过半年的分界线。业务表加一列、调一次顺序是常事,常量集中之后改一行就够,散落在各处的Cells(i, 6)才是维护灾难。LastRow用 Find 而不是 End(xlUp),是为了避开整列为空返回 1 的老坑,返回 0 让调用方显式处理空表。
改造顺序上,我的建议是先挑一个每天都要跑的手工流程,用它跑通「加载宏 + 常量表 + 日志」这条链路,再往里加功能。上来就照着 70 个技巧全实现一遍,通常做到第 20 个就没动力了。验证方法很简单:把同一个流程手工做一遍、脚本做一遍,比对结果行数和关键字段的合计值,对不上就去看run.log的最后一个时间戳,问题基本就在那之后几步之内。
还有个小技巧值得单独说:把第 5 章那些排查语句做成一个Sub Diag()挂在加载宏里,内容包括当前活动表名、UsedRange 地址、EnableEvents 状态、行列数统计,遇到「跑不动」先执行它,五行输出往往就能定位到是环境问题还是数据问题。
从那以后我每接一张新表,都先把它的字段位置抄进modConst,再动第一行代码,宁可多花十分钟也不让硬编码散出去。希望帮到你。
本文还有配套的精品资源,点击获取