第2章 查找引用
每个工作表有17179869184个单元格,即234;每个工作簿又有无数个工作表。在海量的数据中获取目标数据是一项使用频率较高且繁重的工作。而图片的存放方式没有数据存储的规范性,其查找与引用是工作中的又一个难点。本章通过30个案例演示数据与图片的查找引用,包括本表查找、多表查找、图片查询及磁盘中引用图片文件等,引导读者轻松驾驭查询技术。
2.1 本表查找
Excel 2010自带数据查询功能,但远远无法满足工作中需求的多样性。本节通过12个案例展示在活动工作表中进行数据查询的技巧,包括按范围查找、按格式查找、模糊查找、逐步查找、利用自定义工具栏查找,以及罗列目标数据相关信息的查询技巧,大大扩展内置的查询功能。
疑难24 如何查找不及格学生姓名并突出显示
图2-1所示的工作表包含某年级所有学生的语文成绩,如何将不及格的所有学生姓名所在单元格背景设为黄色?需要忽略未参考人员,程序需要具备通用性。
█ 图2-1 语文成绩表
解决方案
Find方法无法使用比较运算符进行按范围查找,只能采用For...Next循环或者Loop循环语句遍历C列成绩区域,对每个非空单元格进行数值判断。如果符合条件“不及格”则逐一合并所有单元格到同一个Range对象变量中,最后将该变量所代表的区域进行背景着色。
操作方法
步骤1 确定活动工作表为“语文成绩”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 查询并标识() Dim rng As Range, RngTemp As Range, cell As Range '声明3个Range对象变量 '将C2到C列最后一个非空单元格所代表的区域赋值给变量Rng,这是被查找的区域 Set rng = Range([c2], Cells(Rows.Count, "C").End(xlUp)) For Each cell In rng '利用For...Next循环遍历Rng区域每个单元格 If Len(cell) > 0 Then '仅仅对长度大于0的单元格进行查询 If cell.Value < 60 Then '如果值小于60 If RngTemp Is Nothing Then '如果变量RngTemp未初始化,那么将找到的单元格左边偏移两位的单元格赋值给变量 RngTemp Set RngTemp = cell.Offset(0, -2) Else '否则将变量RngTemp与找到的单元格向左偏移两位的单元格合并,然后赋值给变量RngTemp Set RngTemp = Union(RngTemp, cell.Offset(0, -2)) End If End If End If Next cell '如果变量RngTemp未初始化则提示 If RngTemp Is Nothing Then MsgBox "不存在不及格学生", 64, "提示" Else '否则对变量所代表的区域添加黄色背景并选择目标单元格 RngTemp.Interior.ColorIndex = 6 RngTemp.Select End If End Sub
步骤3 返回工作表,在功能区的“开发工具”选项卡中单击【宏】按钮,并执行过程“查询并标识”。程序执行结果如图2-2所示。
█ 图2-2 执行结果
原理分析
本例中涉及两个重要的知识点:让程序自动适应查询的目标区域,以及合并已找到的多个单元格。
其中获取待查询的目标区域使用了“Range([c2], Cells(Rows.Count, "C").End(xlUp))”语句,表示C2到C列最后一个非空单元格之间的整个区域。它具有延展性,可以提升程序的通用性能,即可以随C列数据的增减变化而自动适应。其通用性主要体现在两个方面:首先是利用Cells(Rows.Count, "C")获取C列最后一个单元格,使程序可以在Excel 2003早期版本和Excel 2010都能正确执行,防止出错。而采用“Range("C65536")”或者“Range("C1048576")”则兼容性不足;其次,配合End(xlUp)属性取C列最后一个非空单元格。它可以让程序自动适应成绩表的增减变化,从而提升程序的适应性能。远远优于 Range("C2:C21")这种硬性定位区域的思路。
※ 利用Union方法合并多区域 ※
合并已找到的多个单元格则主要是为了提升程序的性能。本例中采用Union方法将所有找到的符合条件的单元格合并为一个Range对象,最后对这个对象进行着色操作,即仅仅需要一次着色操作。如果每找到一个目标就进行着色,则在效率上会有偏差。
知识扩展
For Each...Next 语句表示针对一个数组或集合中的每个元素,重复执行一组语句,其语法如下:
For Each 变量 In 集合 一条或多条语句 [Exit For] 一条或多条语句 Next [变量]
其中Exit For是可选参数,用于中途退出循环。
本例也可以采用数组来执行,当工作表中数据较多时,执行效率会有明显的提升,代码如下:
Sub 查询并标识2() Dim rng As Range, RngTemp As Range, i As Integer, j As Integer Dim arr(), cell '声明一个数组和一个变体型变量 '将C2到C列最后一个非空单元格所代表的区域赋值给数组Arr arr = Range([c2], Cells(Rows.Count, "C").End(xlUp)).Value i = 1 '因第一行是标题,不参与查找,所以初始化变量值为1 For Each cell In arr '利用For...Next循环遍历数组arr,比遍历单元格区域更快 i = i + 1 '累加变量i,该值对应于查到的目标值所在行 If Len(cell) > 0 Then '仅仅对长度大于0的成绩进行查询 If cell < 60 Then j = j + 1 '如果值小于60则累加变量J,该变量对应于目标值数量 If j = 1 Then '如果变量RngTemp未初始化,那么将A列i行单元格赋值给变量RngTemp Set RngTemp = Cells(i, "A") Else '否则将变量RngTemp与A列i行单元格合并,然后赋值给变量RngTemp Set RngTemp = Union(RngTemp, Cells(i, "A")) End If End If End If Next cell '如果J=0则提示,否则对变量所代表的区域添加黄色背景 If j = 0 Then MsgBox "不存在不及格学生", 64, "提示" Else RngTemp.Interior.ColorIndex = 6: RngTemp.Select End Sub
疑难25 如何一次性罗列表中三省员工的姓名
公司员工分布在多个省份,如图2-3所示,现需要查找并罗列出四川、湖南、湖北三个省的员工姓名,如何一次性完成?
█ 图2-3 职工信息表
解决方案
将四川省、湖南省和湖北省转换成数组,然后查找C列数组区域中每一元素。如果有符合条件的单元格,那么整行选择,并复制到工作表“查询结果”中。在复制数据之前,需要检查是否存在“查询结果”工作表,没有则创建一个。
操作方法
步骤1 确定活动工作表为“职工信息表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 一次性罗列表中三省员工的姓名() 'Find循环法,通用于Excel 2002、2003、2007和2010 Dim rng As Range, RngTemp As Range, firstAddress As String '声明需要使用的变量 Dim i As Byte, FindCell As Range, ShtName As String, sht As Worksheet '将待查找的目标区域C2到C列最后一个非空单元格赋予对象变量 Set rng = Range([c2], Cells(Rows.Count, "C").End(xlUp)) '将四川省、湖南省和湖北省转换成数组,然后循环数组每个元素。数组的基数默认为0,从0递增 For i = 0 To UBound(Array("四川省", "湖南省", "湖北省")) '本例中数组的上限为2,但为了提升程序的通用性,利用UBound计算而不是手工指定 '开始查找数据,按值精确查找,不区分大小写 Set RngTemp = rng.Find(What:=Array("四川省", "湖南省", "湖北省")(i), LookIn:=xlValues, Lookat:=xlWhole, MatchCase:=False) If Not RngTemp Is Nothing Then '如果变量RngTemp不再是Nothing,即找到目标值 firstAddress = RngTemp.Address '记录第一个目标的地址 Do '只要找到的单元格的地址不等于刚才记录的地址就一直循环下去 '如果变量FindCell未初始化则将查找结果赋予它,否则合并该变量到查到的单元格 If FindCell Is Nothing Then Set FindCell = RngTemp Else Set FindCell = Union(FindCell, RngTemp) Set RngTemp = rng.FindNext(RngTemp) '查找下一个 Loop While RngTemp.Address <> firstAddress End If Next i '如果变量FindCell已初始化,就选择所有目标的整行,否则提示并退出程序 If Not FindCell Is Nothing Then FindCell.EntireRow.Select Else MsgBox" 没有符合条件的数据": Exit Sub On Error Resume Next '防错,有错误时继续下一步 ShtName = ActiveSheet.Name '记录当前表的名称 Set sht = Sheets("查询结果") '将工作表“查询结果”赋予变量sht。目的是判断有没 '有这个工作表 If Err.Number <> 0 Then '如果出错,则表示不存在“查询结果”工作表 Sheets.Add after:=Sheets(Sheets.Count) '在最末尾创建一个工作表 Sheets(Sheets.Count).Name = "查询结果" '将新表命名为“查询结果” Else '否测,清空“查询结果”原有数据 Sheets(Sheets.Count).Cells.Clear End If Sheets(ShtName).Select '返回刚才查找的工作表 '将选择的所有对象复制到“查询结果”表中 Selection.Copy Sheets(Sheets.Count).[a1] End Sub
步骤3 返回工作表,在功能区的“开发工具”选项卡中单击【宏】按钮,并执行过程“一次性罗列表中三省员工的姓名”。程序执行结果如图2-4所示。
█ 图2-4 查询结果
原理分析
本例是按值查找,所以不再使用疑难27的For...Next循环方式查询数据。Find的查询速度远远高于For...Next循环。在本例中Find每查找到一个目标,就将单元格合并到变量FindCell中,然后查找下一个,直到返回第一次查到的单元格时停止。在查询结束后,利用 IF 判断是否存在符合条件的单元格,有则整行选择对象,没有则中断程序,不再执行后续的操作。
复制到目标工作表中时,为了程序的通用性,不能直接一句代码实现:“Selection.Copy Sheets("查询结果").[a1]”,而是判断是否存在目标工作表,有则清空数据,没有则新建该名称的工作表。判断工作表是否存在的方法,是基于“引用不存在的对象时会产生错误”的原理。先利用“On Error Resume Next”语句让程序遇到任何错误都继续执行,然后引用目标工作表,如果此时Err对象的Number属性值不等于0,则表示不存在这个工作表。
跨表复制数据时,不需要进入目标工作表,Copy 方法的第二参数可以指定跨表区域。但如果是选择性粘贴某个选项,就必须利用 Activate 方法激活目标工作表后,再用 PasteSpecial方法进行粘贴数据。
知识扩展
Range.Find方法用于在区域中查找特定信息,它的语法如下:
表达式.Find(What, After, LookIn, LookAt, SearchOrder, SearchDirection, MatchCase, MatchByte, SearchFormat)
各参数含义如表2-1所示。
█ 表2-1 Range.Find参数列表
在查找数据后,如果未发现匹配项,Find 将返回 Nothing,通常利用此属性判断是否有符合条件的值,同时也利用它配合 IF 来决定是否进行后续的操作,从而提升程序的防错性,避免没有目标时却进行复制、着色等操作产生的错误。
※ Find方法的使用技巧 ※
每次使用 Find 方法后,参数 LookIn、LookAt、SearchOrder 和 MatchByte 的设置都将被保存,如果下一次执行时忽略参数则调用上一次的设置。为了防错,需要养成指定以上参数的习惯。
从 Excel 2007开始,筛选 AutoFilter 的条件支持数组表达式,所以本例也可以采用AutoFilter方法来实现。AutoFilter方法完成本例需求在效率上比FIND要高出很多,它不需要循环,一次性执行产生结果。不过代码仅仅在Excel 2007和2010中产生作用。代码如下:
Sub 一次性罗列表中三省员工的姓名2() '筛选法,通用于Excel 2007和2010 Dim sht As Worksheet With Range("a1").CurrentRegion '利用WITH语句获取A1的当前区域,避免多次引用,提升速度 '以第三列为条件,筛选出“湖北省”、“湖南省”、“四川省”三类数据,Excel 2003 '无此功能 .AutoFilter Field:=3, Criteria1:=Array("湖北省", "湖南省", "四川省"), Operator:=xlFilterValues On Error Resume Next '防错,有错误时继续下一步 ShtName = ActiveSheet.Name '记录当前表的名称 Set sht = Sheets("查询结果") '将工作表“查询结果”赋予变量sht,目的是判断有 '没有这个工作表 If Err.Number <> 0 Then '如果出错,则表示不存在“查询结果”工作表 Sheets.Add After:=Sheets(Sheets.Count) '在最末尾创建一个工作表 Sheets(Sheets.Count).Name = "查询结果" '将新表命名为“查询结果” Else '否则,清空“查询结果”原有数据 Sheets(Sheets.Count).Cells.Clear End If Sheets(ShtName).Select '返回刚才查找的工作表 '将筛选后的所有对象复制到“查询结果”表中 .SpecialCells(xlCellTypeVisible).Copy Sheets(Sheets.Count).[a1] .AutoFilter ' 取消筛选状态 End With End Sub
以上过程首先利用Range("a1").CurrentRegion获取当前表中A1单元格所在的整个区域,后续的操作都在此基础上进行。这可以提升程序的通用性,如果采用单元格地址如“Range(“A1:D21”)”则无法适应数据的增减。
Range.AutoFilter表示对区域进行筛选。本例中Field参数为3表示以第三列为条件进行筛选,其条件为湖北省、湖南省和四川省。筛选后利用SpecialCells(xlCellTypeVisible)方法定位可见区域,并将符合条件的单元格复制到工作表“查询结果”中。
疑难26 如何在输入时逐步查找
工作表中存放某班级的成绩表,如何实现输入一个字符即进行查找,然后逐步缩小范围。例如输入“黄”,罗列出姓名中包括“黄”的所有人员姓名,继续输入“天”,则罗列出所有包括“黄天”的姓名。
解决方案
创建一个窗体,并在窗体中绘制一个文本框供用户输入查询字符串,再绘制一个ListView控件用于显示查询结果。为了让输入时实现逐步查询,需要使用文本框的 KeyUp 事件,从而每输入一个字符即通过Find进行模糊查找,并将找到的所有信息罗列在ListView控件中。其中ListView控件默认不显示在工具箱中,需要通过附加控件调用它。
操作方法
步骤1 按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“窗体”,从而产生一个默认名称为“UserForm1”的窗体。
步骤3 单击窗体,此时会自动出现一个工具箱,如图2-5所示。如果没有出现则可以选择菜单“视图”→“工具箱”来调出工具箱。
步骤4 单击工具箱中的“标签”控件,并在窗体左上角拖放(按下左键拖动,然后松开鼠标),从而在窗体中绘制一个标签。
步骤5 如果默认状态没有显示属性对话框,可以按【F4】键调出“属性”窗口,并找到“Caption”属性,将其值修改为“请输入姓名:”。
步骤6 单击工具箱中的“文本框”控件,并在窗体中上部拖放绘制一个文本框。
步骤7 在工具箱中间的空白区单击右键,从弹出的快捷菜单中选择“附加控件”,并在打开的“附加控件”对话框中找到“Microsoft ListView Control, version 6.0”并将其选中,再单击【确定】按钮。“附加控件”对话框如图2-6所示:
█ 图2-5 默认状态的工具箱
█ 图2-6 附加ListView控件
步骤8 单击工具箱中的“ListView”控件,并在窗体中拖放,拖放时适当控制其大小,使ListView控件在不覆盖文本框的前提下可以填充整个窗体。
步骤9 双击窗体任意位置进入窗体的代码窗口,将自动产生的代码删除,然后输入以下代码:
'启动窗体时执行,功能是对窗体中的ListView1控件进行基本设置 Private Sub UserForm_Initialize() With ListView1 .ColumnHeaders.Add , , "姓名", 60 '添第一列表头,宽度为60 .ColumnHeaders.Add , , "语文", 55 '添第二列表头,宽度为55 .ColumnHeaders.Add , , "数学", 55 '添第三列表头,宽度为55 .View = lvwReport '报表形式显示ListView1控件 .Gridlines = True '显示网格线 End With TextBox1.SetFocus '对文本框设置焦点,类似于单击文本框 End Sub '文本框中输入字符时执行此命令 Private Sub TextBox1_KeyUp(ByVal KeyCode As MSForms.ReturnInteger, ByVal Shift As Integer) Dim firstAddress As String, rng As Range '声明需要用到的变量 ListView1.ListItems.Clear '清除ListView控件的所有值 If TextBox1.Text = "" Then GoTo line '如果文本框是空白则执行Line标签后面的命令 With Range("a:a") '对A列进行查找,按值模糊查找 Set rng = .Find(TextBox1.Text, LookIn:=xlValues, Lookat:=xlPart) If Not rng Is Nothing Then '如果找到目标 firstAddress = rng.Address '记录第一个找到单元格的地址 Do '继续查找,直到找到的单元格地址等于刚才记录的单元格地址时停止 Set Item = ListView1.ListItems.Add() '对ListView1控件添加列表项 Item.Text = rng.Text '第一列显示找到的单元格字符 Item.SubItems(1) = rng.Offset(0, 1).Text '第二列显示右移一个单元格 Item.SubItems(2) = rng.Offset(0, 2).Text '第三列显示右移两个单元格 Set rng = .FindNext(rng) '查找下一个 Loop While rng.Address <> firstAddress End If End With line: '指定一个标签,让程序在指定条件下可以跳转到此处继续执行 '让ListView1控件的高度随查到的值的多少而变化,从而让窗体更美观 ListView1.Height = ListView1.Font.Size * ListView1.ListItems.Count + 20 Me.Height = ListView1.Height + 70 '让窗体随ListView1的高度自动变化 End Sub
步骤10 选择菜单“插入”→“模块”,并在模块中输入以下代码:
Sub 查询() UserForm1.Show 0 End Sub
步骤11 返回工作表,依次选择“开发工具”选项卡→“插入”→“按钮(窗体控件)”,并在工作表中拖动,从而绘制一个命令按钮,且将其默认名称修改为“逐步查询”。
步骤12 在弹出的“宏”对话框中选择“查询”并单击“确定”按钮返回工作表。单击按钮弹出“逐步查询”窗体,此时窗体中显示空白。在文本框中输入“不”,“ListView”控件将会罗列出所有包含“不”的人员信息且窗体的高度刚好适应信息列表,如图2-7所示。
█ 图2-7 查询包括“不”的人员信息
步骤13 继续输入“败”,则列表中显示所有包含“不败”的人员信息,同时自动调整高度使其美观,如图2-8所示。
█ 图2-8 查询包括“不败”的人员信息
原理分析
※ 通过KeyUp事件自动执行查询 ※
文本框的KeyUp事件在按下任意键时触发事件,从而执行指定的SUB过程,适用于逐步运行程序的需求。通常还用它来检查输入的字符是否符合要求,例如是否输入有效数值或者长度是否超过需求。
为了让ListView控件可以多列显示多个信息,需要对“ListItems.Add()”添加的项目追加子项目,即SubItems(1)、SubItems(2)的赋值。
让ListView控件高度具有自动适应信息量的原理是:ListView控件的字体大小乘以显示行数,再加标题、边框所占用的高度,通常用20即可。窗体的高度也随ListView控件相应变化,可以更美观。
知识扩展
在没有自定义窗体时,工具箱不会显示出来,在选择窗体时通常可以自动显示。
工具箱中默认包括12个控件(第一个不算),需要使用其他控件时必须手工附加控件。比较有价值的附加控件还有很多,例如Web控件、Flash控件、ImageList控件和Dialog控件、Chart控件等。
显示窗体可以用代码“UserForm1.Show 0”调用,也可以将光标定位于代码中的任意位置并按【F5】键显示窗体。通常在工作表中创建一个按钮配合命令调用更方便。也可以生成菜单来调用窗体,菜单的生成方式请参阅本书的第10章。
疑难27 如何按指定的格式查找
如何查找某个格式的所有单元格?例如图2-9所示的成绩表按不同班级和成绩范围设定了不同的格式。如何查找到字体为“Arial Black”、加粗且黄色背景的所有单元格?
█ 图2-9 成绩表
解决方案
指定FindFormat,并通过Range.Find的“SearchFormat”参数来查找指定格式的区域。
操作方法
步骤1 确定活动工作表为“成绩表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 按格式查找() Dim FindStr As String, Rng As Range, mRng As Range Application.FindFormat.Clear '清除原有格式 With Application.FindFormat '设置查找格式 .Font.Name = "Arial Black" '指定字体 .Font.Bold = True '加粗 .Interior.Color = 65535 '黄色背景 End With With Range("C:C") '在C列查找 Set Rng = .Find(What:="", LookIn:=xlFormulas, LookAt:=xlPart,Search Format:=True) '按格式查找 If Rng Is Nothing Then MsgBox "没有找到此类单元格": Exit Sub '如果找不到则退出 Set mRng = Rng '将找到的对象赋予另一个变量 FindStr = Rng.Address '取第一个找到的单元格的地址 Do Set mRng = Union(mRng, Rng) '合并变量mrng和找到的单元格(如果有多个符合条件,则逐一合并) Set Rng = .Find(What:="", After:=Rng, SearchFormat:=True) '查找下一个 '直到找到的单元格的地址等于第一个单元格地址时停止 Loop While FindStr <> Rng.Address mRng.Select '选择所有符合条件的单元格 End With End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,程序会按指定的三种格式查找所有单元格,并选择符合条件的单元格。本例中符合条件的单元格为C4、C9和C10,如图2-10所示。
█ 图2-10 选择所有指定格式的单元格
原理分析
Application.FindFormat属性用于指定查找格式,将它与Find方法的“SearchFormat”参数配合使用也能实现按格式查找。
※ FindFormat的使用技巧 ※
FindFormat属性有记忆上次设置的格式的功能,那么在进行按格式设置之前必须清除以前的所有设置,否则会查找不准确。
知识扩展
通过以下方法可以证明“原理分析”中的理论。首先复制过程“按格式查找”的所有代码,并将过程名修改为“按格式查找2”,删除代码中的“Application.FindFormat.Clear”语句,表示不清除原有查找格式;再删除表示加粗和指定字体的两句代码,表示仅仅查找黄色背景的单元格。最后分别执行过程“按格式查找”和“按格式查找2”,可以发现,“按格式查找2”虽然只查找黄色背景的单元格,但是实际上执行结果和“按格式查找”完全一致,因为过程“按格式查找2”所指定的加粗和字体属性都保留了下来,所以影响了“按格式查找2”过程的查找结果。
查找可以跨表,但Select方法无法跨表,所以多表查找时不能选择符合条件的单元格。
疑难28 如何实现字体格式替换
成绩表中有多种字体,如何将其中的10号字替换为红色12号字?
解决方案
Excel 对数据可以替换,对格式也可以替换。只要设置好对应的 FindFormat 属性和ReplaceFormat属性,利用Range.Replace的“SearchFormat”和“ReplaceFormat”两个参数即可实现替换格式。
操作方法
步骤1 按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 替换字体格式() Application.FindFormat.Clear '清除原有查找格式 Application.ReplaceFormat.Clear '清除原有替换格式 Application.FindFormat.Font.Size = 10 '查找10号字体 Application.ReplaceFormat.Font.ColorIndex = 3 '替换为红色字体色 Application.ReplaceFormat.Font.Size = 12 '替换为12号字体 '开始替换 [c:c].Replace What:="", Replacement:="", LookAt:=xlPart, SearchFormat:=True, ReplaceFormat:=True End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,C列中10号字的单元格立即替换为12号红色字体,如图2-11所示。
█ 图2-11 将10号字体替换为12号红色
原理分析
※ 替换格式的条件 ※
Application.FindFormat和Application.ReplaceFormat分别对应查找格式和替换格式,都可以使用一种或者多种格式,替换前需要同时指定两种属性,且将 Range.Replace 的“SearchFormat”和“ReplaceFormat”两个参数设置为True,否则不会执行。
知识扩展
按格式替换比按格式查找的效率更高,可以一次性替换所有符合要求的单元格的格式,而不需要循环。
如果是将10号字体删除,那么代码可以修改为:
Sub 删除10号字体单元格() Application.FindFormat.Clear '清除原有查找格式 Application.ReplaceFormat.Clear '清除原有替换格式 Application.FindFormat.Font.Size = 10 '查找10号字体 '开始替换 [c:c].Replace What:="*", Replacement:="", LookAt:=xlPart,Search Format:=True End Sub
疑难29 如何查找所有“#”并标为上标
图2-12所示的所有“#”符号表示机台号,如何查找到所有“#”号并上标显示?
█ 图2-12 生产表
解决方案
使用Range.Find方法查找每个包含“#”的单元格,并将其中的“#”字符设置字体属性为Superscript,表示上标显示。为了体现程序的通用性,允许用户自定义需要上标显示的字符,以及让程序对单元格逐个字符检查,将每一个“#”都上标显示。
操作方法
步骤1 确定活动工作表为“生产表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 替换指定字符标识为上标() Dim rng As Range, i As Integer, First As String, inputt As String inputt = InputBox("请指定需要上标显示的字符,只有一个字符", "指定字符", "#") '指定需要上标的字符 Application.ScreenUpdating = False '关闭屏幕更新,从而提速 Set rng = Cells.Find(inputt, LookAt:=xlPart, LookIn:=xlFormulas) '开始查找 If Not rng Is Nothing Then '如果找到 First = rng.Address '记录首个符合条件的单元格的地址 Do '循环查找,直到返回第一个找到的单元格时停止 For i = 1 To Len(rng) '循环检查每一个字符 '如果某字符等于用户输入的字符,则将它上标显示 If Mid$(rng, i, 1) = Left(inputt, 1) Then rng.Characters(Start:=i, Length:=1).Font. Superscript = True Next Set rng = Cells.FindNext(rng) '查找下一个 Loop Until rng.Address = First End If Application.ScreenUpdating = True '恢复屏幕更新 End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,所有表示机台号的“#”都自动上标显示,如图2-13所示。
█ 图2-13 将#标识为上标
原理分析
对Range.Find方法的“LookAt”参数设置为“xlPart”可以实现模糊查找,将包括“#”的单元格逐个找到。而 Characters 属性可以定位于单元格中部分字符串,配合 MID$函数可以逐个字符检查,并对符合条件的字符标识为上标。
知识扩展
※ 区分上标、下标的应用对象 ※
Superscript表示上标,Subscript表示下标字符。但上标和下标都只对文本生效,如果是数值,需要将其数字格式转换成文本后再执行标识。
如果需要对“M2”中的“2”设置为上标,则可以改用以下代码:
Sub 将M后面的2标识为上标() Dim rng As Range, i As Integer, First As String, inputt As String Application.ScreenUpdating = False '关闭屏幕更新,从而提速 Set rng = Cells.Find("M2", LookAt:=xlPart, LookIn:=xlFormulas) '开始查找 If Not rng Is Nothing Then '如果找到 First = rng.Address '记录首个符合条件的单元格的地址 Do '循环查找,直到返回第一个找到的单元格时停止 For i = 2 To Len(rng) '循环检查每一个字符,从第二位开始 '如果某字符等于2且前一位是“M”,则将它上标显示 If Mid$(rng, i, 1) = "2" And Mid$(rng, i - 1, 1) = "M" Thenrng. Characters(Start:=i, Length:=1).Font.Superscript = True Next Set rng = Cells.FindNext(rng) '查找下一个 Loop Until rng.Address = First End If Application.ScreenUpdating = True '恢复屏幕更新 End Sub
疑难30 如何找出还款时间超过一年及未还款的客户信息
图2-14所示的C列为客户还款的起始时间,D列为实际还款时间。如何实现提取出所有未还款及超过1年未还款者的所有信息?
█ 图2-14 还款登记表
解决方案
利用 For...Next 循环逐一对还款时间和购货时间进行比较,如果购货时间到还款时间在1年之内则将该行隐藏,剩下的所有数据即为还款时间超过一年及未还款的客户信息。
操作方法
步骤1 确定活动工作表为“还款记录表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 取出还款时间超过一年及未还款的客户() '其余客户的资料隐藏 Dim rng As Range, cell As Range For Each rng In Range([c2], Cells(Rows.Count, 3).End(xlUp)) '对C列所有数据区域进行查找 If Len(rng.Offset(0, 1)) > 0 Then '如果对应的D列值长度大于0 '如果还款时间超过购货时间12个月以内 If Evaluate("Datedif(" & rng.Address & ", " & rng.Offset(0, 1).Address & ", ""M""" & ")") < 12 Then '将所有符合条件的单元格合并 If cell Is Nothing Then Set cell = rng Else Set cell =Application. Union(cell, rng) End If End If Next rng '如果有符合条件的单元格,则将它隐藏(剩下的是还款时间超过一年以及未还款的客户) If Not cell Is Nothing Then cell.EntireRow.Hidden = True End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,所有1年之内完成还款者全部隐藏,而其余行是被筛选出来的,如图2-15所示。
█ 图2-15 保留还款时间超过一年及未还款的客户
原理分析
Datedif是工作表函数,可以计算两个日期的差值,其单位可以是天、月和年。在本例中先取得间隔的月数,然后判断其是否大于12。
当第一或者第二参数引用的单元格为空时,Datedif的计算结果是错误值,所以在程序中需要利用IF和Len$函数排除空单元格再进行日期计算。
知识扩展
※ 日期函数Datedif的特性 ※
Datedif是工作表函数,其语法如下:
Datedif(start_date,end_date,unit)
其中,参数start_date 为起始日期,end_date表示结束日期,而unit为所需信息的返回类型,包括年、月、天。
VBA中也有一个相同功能的函数:DateDiff,其语法如下:
Datediff(interval, date1, date2[, firstdayofweek[, firstweekofyear]])
参数interval表示所需信息的返回类型,相当于Datedif函数的第三参数;date1表示起始日期;date2表示结束日期;firstdayofweek 和firstweekofyear为可选参数,分别用于指定一个星期的第一天及指定一年的第一周。
Datediff和Datedif在计算上有些差异。大多数情况下两者计算结果相同,在部分情况下会相差1。其中Datediff以跨月为计算标准,而Datedif要满月为标准。例如上月30日到本月1日,Datediff将它当做差异1月处理,而Datedif则表示仅仅2天不足一月而按0计算。
疑难31 可以将查找到的所有数据串连并写入剪贴板中吗
图2-16所示包括三个班的成绩表,按姓名进行拼音排序。如何找到每个班第一名的姓名和成绩,且写入到剪贴板,让它可以粘贴到其他对象中?利用 For...Next 循环查找目标值,并将所有找到的字符串合并成一个字符串,最后通过SetText和PutInCl1ipboard方法将字符串存入剪贴板中。
解决方案
操作方法
步骤1 确定活动工作表为“三个班成绩表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“工具”→“引用”,打开图2-17所示的引用对话框,选择“Microsoft Forms 2.0 Object Library”。如果找不到,则可以通过“浏览”按钮找到System32文件夹中的“M2.0.DLL”文件并双击即可。
█ 图2-16 某年级三个班的成绩表
█ 图2-17 引用Microsoft Forms 2.0 Object Library
步骤3 选择菜单“插入”→“模块”,并输入以下代码:
Dim MyData As DataObject '必须引用MS Form 2.0 Sub 查找三个班第一名并写入剪贴板() Dim arr, rng, i As Integer, One As Byte, Two As Byte, Three As Byte Dim a As Byte, b As Byte, c As Byte Set MyData = New DataObject '建立一个DataObject 对象 arr = Range([a1], Cells(Rows.Count, 3).End(xlUp)).Value '将所有数据赋予数组,从而提速 For i = 1 To UBound(arr) '遍历数组所有行 '如果某行第一列等于“一班”,而且该行第三列大于变量One,那么取出其行号和成绩 If arr(i, 2) = "一班" And arr(i, 3) > One Then One = arr(i, 3): a = i If arr(i, 2) = "二班" And arr(i, 3) > Two Then Two = arr(i, 3): b = i If arr(i, 2) = "三班" And arr(i, 3) > Three Then Three = arr(i, 3): c = i Next '将找到的所有数据串接起来,复制到 DataObject对象 MyData.SetText arr(a, 1) & ": " & One & Chr(10) & arr(b, 1) & ": " & Two & Chr(10) & arr(c, 1) & ": " & Three MyData.PutInClipboard '最后写入剪贴板 End Sub
步骤4 光标置于代码中任意位置,并按【F5】键执行过程。
步骤5 选择F1单元格,按下【Ctrl+V】组合键,可以看到F1:F3区域将产生一、二、三班的最高成绩及学生姓名,表示目标对象已经存在于剪贴板中。
原理分析
VBA 读写数组远远快过读写单元格,所以将整个工作表中的数据转换为数组,然后通过变量从循环语句中提取需要的数据。
DataObject对象是转移操作中所使用的带格式文本数据的中转区,利用它可以将文本转入Office剪贴板中。可以在任意单元格或者任意应用程序中通过快捷键取出该文本。
知识扩展
※ 引用“Microsoft Forms 2.0 Object Library”的两种方法 ※
使用 DataObject 对象必须引用动态链接库文件“M2.0.DLL”。除了本文所述通过引用对话框的方式引用外,还有一个最方便的办法:插入窗体,然后删除窗体,VBA会自动完成“Microsoft Forms 2.0 Object Library”对象的引用。
也可以将写入剪贴板中的文字提取到对话框。例如在程序最末处加入以下代码,结果如图2-18所示。
If MyData.GetFormat(1) Then MsgBox MyData.GetText(1)
█ 图2-18 三个班第一名者信息
还可以不用循环而通过公式来计算,Evaluate函数将公式转为文本再写入剪贴板:
Sub 查找三个班第一名并写入剪贴板2() Set MyData = New DataObject '建立一个DataObject 对象 Dim str As String str = Evaluate("=INDEX(A2:A37,MATCH(MAX(IF(B2:B37=""一班"",C2:C37)), IF(B2:B37=""一班"",C2:C37),0))") str = str & ": " & Evaluate("MAX(IF(B2:B37=""一班"",C2:C37))") str=str & Chr(10) & Evaluate("=INDEX(A2:A37,MATCH(MAX(IF(B2:B37= ""二班"",C2:C37)),IF(B2:B37=""二班"",C2:C37),0))") str = str & ": " & Evaluate("MAX(IF(B2:B37=""二班"",C2:C37))") str=str & Chr(10) & Evaluate("=INDEX(A2:A37,MATCH(MAX(IF(B2:B37= ""三班"",C2:C37)),IF(B2:B37=""三班"",C2:C37),0))") str = str & ": " & Evaluate("MAX(IF(B2:B37=""三班"",C2:C37))") MyData.SetText str MyData.PutInClipboard End Sub
疑难32 可以创建一个工具栏来方便查找吗
如何实现工具栏查找?即创建一个可以输入文本的工具栏,在工具栏中输入字符后按回车键即可对工作表中对应的单元格进行查找,并报告所有找到的单元格地址,以及选中所有单元格。
解决方案
利用 CommandBars.Controls.Add 方法在工具栏创建新工具按钮,包括一个msoControlButton和一个msoControlEdit对象,其中msoControlEdit对象可以输入字符。而通过对按钮指定“OnAction”参数的方式可以实现msoControlEdit对象中输入字符并按回车键后可以调用一个查找程序。该程序调用Range.Find方法在工作表中查找用户输入的字符,并选中所有符合条件的单元格。
操作方法
步骤1 按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub auto_open() 'auto_open表示打开文件时就执行 On Error Resume Next '防错 CommandBars("Formatting").Controls("请输入查找内容").Delete '删除上次产生的工具栏 CommandBars("Formatting").Controls("查找").Delete '创建新的工具栏,位于格式工具栏末尾 With CommandBars("Formatting").Controls.Add(Type:=msoControlButton, Temporary:=True) .Caption = "请输入查找内容" '工具栏显示的标题 .BeginGroup = True '显示一条分隔线 .TooltipText = "请输入查找内容" '鼠标指向时出现提示 .Style = msoButtonCaption '显示文字 End With '再创建一个文字框菜单 With CommandBars("Formatting").Controls.Add(Type:=msoControlEdit,Temporary: =True) .Caption = "查找" '指定显示标题 .Text = "" '默认显示空白 .OnAction = "intos" '关联的宏,表示输入文字后按回车键时执行的过程名称 End With End Sub Sub intos() With ActiveSheet.UsedRange '在当前表已用区域中查找 Dim rng As Range, rngg As Range, firstAddress As String Set rng = .Find(CommandBars("Formatting").Controls("查找").Text, LookIn: =xlValues,lookat:=xlPart) If Not rng Is Nothing Then '如果找到 firstAddress = rng.Address '记录第一个单元格地址 Do '循环执行,直到返回第一个单元格 '将找到的所有单元格合并为一个Range对象 If rngg Is Nothing Then Set rngg = rng Else Set rngg = Union(rng,rngg) Set rng = .FindNext(rng) '查找下一个 Loop While rng.Address <> firstAddress rngg.Select '选择所有符合条件的单元格 MsgBox "已找到目标所在地址:" & rngg.Address(0, 0) '报告地址 End End If MsgBox "没找到" '未找到时也提示 End With End Sub
步骤3 光标置于“auto_open”过程的代码中,并按【F5】键执行,那么在工具栏将产生两个自定义按钮。不过在Excel 2010和2003中显示方式不同,分别如图2-19和图2-20所示。
█ 图2-19 Excel 2010菜单样式
█ 图2-20 Excel 2003菜单样式
步骤4 返回工作表中,在刚才创建的文字框控件中输入“丽”并按下回车键,工作表中所有包括“丽”的单元格都会呈选中状态,同时提示所有单元格地址,如图2-21所示。
█ 图2-21 查找所有包括“丽”的单元格
原理分析
CommandBars.Controls.Add方法创建的自定义工具栏可以通过“OnAction”参数调用一个过程,那么将它指向一个具有查找功能的SUB过程即可实现查找。
※ 设置lookat参数实现模糊查找 ※
本文需要模糊查找,Range.Find 的 lookat 参数需要设定为“xlPart”。如果有多个单元格符合条件,利用Union进行合并,最后选择该区域。
知识扩展
CommandBars("Formatting")表示格式工具栏,本例中在格式工具栏最末处添加两个新按钮。如果需要创建在最前面,可以再加一个参数“before:=1”,表示新按钮位于第一个按钮之前。不过该排序只对Excel 2003有影响,在Excel 2010中感觉不到变化。
也可以将对话框提示改为状态栏显示,Msgbox语句改为赋值给Application.StatusBar。
疑难33 如何快速罗列出每个产品最新报价
图2-22所示的产品价格表在不同时间有不同的报价,如何单独提取其中每个产品最近一次报价?
█ 图2-22 产品价格表
解决方案
利用Collection对象取不重复值,将每个产品的品名和报价、日期都仅取一次。同时为了取得最新的报价,For...Next循环需要从后向前循环。最后将所有目标值取出后导出到单元格,再利用辅助区倒序排序的方法将其校正,使其按原来的升序排列。
操作方法
步骤1 确定活动工作表为“价格表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Sub 查找最后一次报价() On Error Resume Next '防错,确保Collection对象能正常执行 Dim Only As New Collection, i As Integer, j As Integer Dim arr, Arr2() arr = Range([a1], Cells(Rows.Count, 3).End(xlUp)).Value '将区域转换成数组,从而提速 For i = UBound(arr) To 1 Step -1 '遍历数组所有行,为了保留最后日期的价格,则从后向前循环 '将数组第一列i行的品名添加到Collection对象中,如果存在重复,仅取首次出现者 Only.Add arr(i, 1), CStr(arr(i, 1)) If Err = 0 Then '如果没有错误(表示当前产品名称是第一次写入Collection对象) j = j + 1 '累加变量,该变量等于产品个数(不含重复值) ReDim Preserve Arr2(1 To 3, 1 To j) '重新分配存储空间(只能修改最末一维的大小) Arr2(1, j) = Only(j) '将Collection对象中的值导入数组arr2 Arr2(2, j) = arr(i, 2) '将数组arr中j行的值也写入数组 Arr2(3, j) = arr(i, 3) End If Err.Clear '每循环一次自动清除错误(必需的,否则不能取出后面的产品、价格和日期) Next i [e1].Resize(j, 3) = WorksheetFunction.Transpose(Arr2) '将数组arr2的值转置后写入单元格 [g1].Resize(j, 1).NumberFormatLocal = "m月d日" '将存放日期的单元格设置为日期格式 [h1].Resize(j, 1) = Evaluate("=row(1:" & j & ")") '创建一个辅助区,利用它将倒序的数据修正 '对产生的数据进行排序,以辅助区为标准倒序排列.可以使原本倒序的按原来的升序显示 [e1].Resize(j, 4).Sort Key1:=[h1], Order1:=xlDescending, OrderCustom:=1, Orientation:=xlTopToBottom, DataOption1:=xlSortNormal [h1].Resize(j, 1).Clear '清除辅助区 End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,在E到G列将罗列出每个产品最新报价,如图2-23所示。
█ 图2-23 每个产品最新报价
原理分析
Collection对象是项目所组成的有序集合,它的特点是集合中不可能存在多个相同的项目。基于此特点,提取不重复值时通常利用Collection对象来排除重复出现的项目。
VBA 读取数组的速度远远快于单元格,所以本例中在取不重复的品名和对应的价格、日期前将区域转成数组,且提取目标值时也都写入数组中,最后一次性将数组导出到单元格。
知识扩展
※ Collection对象的应用技巧 ※
Collection对象通常用于取不重复值,建立Collection集合的方法如下:
Dim X As New Collection
可以用Add方法添加成员,用Remove方法删除成员。而For Each...Next语句可以逐个取中集合每个成员值。
对数组排序必须使用循环。本例采用了将值写入单元格再创建辅助区,并通地辅助区对整个区域进行排序的思路,从而在代码的编写工作上简单许多,且不需要循环。
疑难34 如何在具有合并单元格的区域中多条件逐步查找
图2-24所示的A、B列都有合并单元格,如何在这类表格中查询评分?
█ 图2-24 具有合并单元格的评分表
解决方案
创建一个用户窗体,并使用三个组合框分别引用部门、项目组和姓名。利用组合框的Change事件实现修改“部门”、“项目组”和“姓名”三者之一时自动从工作表中查找得分。
操作方法
步骤1 确定活动工作表为“程序员评分”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“用户窗体”,并在工具箱中将标签控件、组合框控件和文本框控件拖到窗体中,按图2-25所示方式排列及命名;并且将三个组合框和Label5的“名称属性”分别修改为“部门”、“项目组”、“姓名”和“得分”。
█ 图2-25 查询窗体控件布局
步骤3 双击窗体任意位置进入代码窗口,删除自动产生的代码并输入以下新的代码:
Dim rng As Range '声明一个公共变量 Private Sub UserForm_Activate() '窗体激活时执行 For Each rng In [A2].Resize(ActiveSheet.UsedRange.Rows.Count - 1, 1) If Len(rng) > 0 Then 部门.AddItem rng.Text '将A列除A1之外的所有非空单元格添加到“部门”组合框 Next 部门 = 部门.List(0) '默认显示列表中第一个值 End Sub Private Sub 部门_Change() 项目组.Clear '当“部门”组合框修改时清除“项目组”的列表 Dim i As Integer '在A列查找“部门”组合框的值,并记录其行号 i = [a:a].Find(What:=部门.Text, After:=[a1], LookIn:=xlValues, LookAt:=xlPart).Row '在A列查找到的单元格所在的合并区域对应的B列单元格中循环,如A2:A5对应B2:B5 For Each rng In Cells(i, 2).Resize(Cells(i, 1).MergeArea.Rows.Count, 1) If Len(rng) > 0 Then Me.项目组.AddItem rng.Text '如果非空则追加到组合框 Next rng 项目组=项目组.List(0) '默认显示列表中第一个值 End Sub Private Sub 项目组_Change() If Len(项目组) = 0 Then Exit Sub '如果组合框“项目组”为空时则退出程序 姓名.Clear '清空“姓名”组合框 Dim i As Integer '在B列查找“项目组”组合框的值,并记录其行号 i = [B:B].Find(What:=项目组.Text, After:=[B1], LookIn:=xlValues, LookAt:=xlPart).Row For Each rng In Cells(i, 3).Resize(Cells(i, 2).MergeArea.Rows.Count, 1) If Len(rng) > 0 Then 姓名.AddItem rng.Text '如果非空则追加到组合框 Next rng 姓名 = 姓名.List(0) '默认显示列表中第一个值 End Sub Private Sub 姓名_Change() If Len(姓名) = 0 Then Exit Sub '如果“姓名”组合框为空时则退出程序 Dim i As Integer '在C列查找“姓名”组合框的值,并记录其行号 i = [C:c].Find(What:=姓名.Text, After:=[C1], LookIn:=xlValues, LookAt:=xlPart).Row 得分 = Cells(i, 4) '将D列i行的值赋予“得分”控件,即最后需要查询的目标值 End Sub
步骤4 选择菜单“插入”→“模块”,并在模块中输入以下代码:
Sub 评分查询() UserForm1.Show 0 End Sub
步骤5 执行过程“评分查询”,将显示图2-26所示的窗体,默认显示第一个部门第一个项目第一个人的评分。修改部门、项目或者姓名时,得分会相应变化,如图2-27所示。
█ 图2-26 评分查询窗体
█ 图2-27 修改查询对象
原理分析
窗体的所有控件都有各自的事件。本例中利用组合框的“Change”事件可以实现修改每个组合框的显示字符时自动查找对应的得分。
※ 通过“MergeArea”属性返回合并区域 ※
合并单元格有一个“MergeArea”属性,可以返回其合并区域。本例利用该属性查找对应的子项目,即“部分”合并区域对应的子项目是“项目组”,而“项目组”合并区域对应的是“姓名”,“姓名”则不存在合并区域,它与“评分”是一对一的关系,可以直接取值。
知识扩展
本例中利用了窗体的Activate事件,用于显示窗体时自动对组合框和标签控件进行初始化;而组合框则使用Change事件实现修改列表时自动更新相关联的其他列表和评分。
疑难35 如何查找成绩并分批发邮件
图2-28所示为某班成绩及考生邮件地址,现需要将成绩利用邮件分别发送到各学生邮箱中,可以使用VBA完成吗?
█ 图2-28 成绩表
解决方案
API函数ShellExecute可以调用当前默认的邮件程序,配合For...Next循环可以将成绩逐个发向指定的邮箱。
操作方法
步骤1 确定活动工作表为“成绩表”,按【Alt+F11】组合键打开VBE窗口。
步骤2 选择菜单“插入”→“模块”,并输入以下代码:
Private Declare Function ShellExecute Lib "shell32.dll" Alias "ShellExecuteA" (ByVal hWnd As Long, ByVal lpOperation As String, ByVal lpFile As String, ByVal lpParameters As String, ByVal lpDirectory As String,ByVal nShowCmd As Long) As Long Sub 批量发邮件() '每次发一人的资料 Dim rng As Range, MyMail As String For Each rng In Range([b2], Cells(Rows.Count, 2).End(xlUp)) MyMail = "mailto:" & rng.Offset(0, 1).Text & "?subject=成绩通知&body=" &rng.Offset(0, -1) & ":%0A你的总分为:" & rng.Text & "%0A%0A" + Space(20)& "通知日期:" & Date ShellExecute 0&, vbNullString, MyMail, vbNullString, vbNullString, 1 Next End Sub
步骤3 光标置于代码中任意位置,并按【F5】键执行,将弹出图2-29所示的窗口,将每一个考生的姓名和成绩都发送到对应的邮箱中。
█ 图2-29 向第一个学生发送邮件
原理分析
ShellExecute函数可以调用Windows默认的邮件程序,且可以指定收件人邮箱地址、主题和正文。相对于其他仅仅调用Outlook的VBA程序,ShellExecute函数的优越性在于可以调用多种不同的邮件程序,默认是什么邮件程序就能调用什么程序。
知识扩展
※ VBA中发编写邮件正文时的换行符的表示法 ※
ShellExecute函数调用邮件程序时,正文中换行不用chr(10)或者vbCrLf,而是用“%0A”。每插入一个“%0A”换一行显示。
如果不需要将成绩分发,而是一次性将所有成绩发给所有人,同样可以利用ShellExecute函数实现,完整代码如下:
Sub 批量发邮件2() '一次发给多人邮件 Dim rng As Range, MyMail As String, str As String, Temp As String, i As Integer For Each rng In Range([c2], Cells(Rows.Count, 3).End(xlUp)) str = str & rng.Text & ";" Next For Each rng In Range([A2], Cells(Rows.Count, 2).End(xlUp)) i = i + 1 Temp = Temp & rng.Text & IIf(i Mod 2, " ", "%0A") Next MyMail = "mailto:" & Mid$(str, 1, Len(str) - 1) & "?subject=成绩通知&body= 成绩表:%0A" & Temp & "%0A%0A" + Space(20) & "通知日期:" & Date ShellExecute 0&, vbNullString, MyMail, vbNullString, vbNullString, 1 End Sub