Professional Documents
Culture Documents
原版载于 http://speedfirst.mysmth.net
speedfirst
speedfirst@sina.com
2008-12-4
目录
目录
0. 序言 ................................................................................................................................... 1
1. 基本编程元素 ................................................................................................................... 6
2. 对象 ................................................................................................................................. 12
3. 开始编写完整的程序 ..................................................................................................... 19
4. 操作文件 ......................................................................................................................... 24
5. 事件 ................................................................................................................................. 34
6. 用户窗体 ......................................................................................................................... 38
9. 操作数据库 ..................................................................................................................... 55
0. 序言
某圣贤说过,人和动物的最大之不同在于人知道怎么利用工具。VBA 就是一种工
具,一种可以创造工具的工具。VBA 提供了给你充分的自由,做几乎任何其他编
程语言或者环境能做的事情,避免重复的手动劳动。当然,有太多人对编程充满
了敬畏,认为编程是类似周小川做金融决策,或者爱因斯坦在思考问题那样很玄
幻、难以理解的事情,更不用说运用了。但其实,这更多的是误解(我觉得这些
误解源自于国内的编程教科书上那些高深莫测的概念和诘屈聱牙的讲解)。事实
上,20 多年来编程正朝着越来越人性化,越来越容易学的方向发展。VBA,确切
的说是 VB(我在下面会解释这个微妙的差别)是众多语言中最容易学的,正如
其名字一样,又”Visual”,又”Basic”。所以大可不必担心诸如“我没有编
程基础”之类的心理障碍。
For i = 1 To 100
ActiveWindow.Captain = i
Next
sum = 0
For i = 1 To 100
sum = sum + i
Next
1
序言
For i = 1 To 100
MsgBox i
Next
然后在“开发工具”选项卡的“代码”区域里按”Visual Basic”按键,就可以
打开同样的界面。(我用 Excel 举例)
2
序言
Sub test()
MsgBox "Hello, VBA"
End Sub
然后点击上面的绿色箭头,就可以运行它了。看看出了什么?:)这就是你的第
一个 VBA 程序了。记住这个写代码的过程,以后我们会反复用到它。并且我不会
再重复。如果你有兴趣,在桌面上新建一个文本文件,将 MsgBox "Hello, VBA"
这句放到里面(不要第一句和第三句),然后保存为 test.vbs。(注意不是
test.vbs.txt,一定要把默认的扩展名去掉)然后双击这个文件,你又看到了什
么?(如果有杀毒软件或者防火墙问你是否执行脚本,请确认允许。这段代码不
会带来任何伤害)。
以下是对一些常见问题的回答:
3
序言
2. 既然可以录制宏,为何还要手写代码?首先,宏录制的局限性很大,很多
动作(比如鼠标行为)是录不下来的。另外宏只能录制顺序的多个指令,
如上面的比较复杂一点的循环就不可能录制。而且宏录制不能覆盖 VBA
的强大功能。例如 VBA 可以直接读写文件系统中的任何文件。这些动作是
录不下来的。最后,录制宏得到的代码非常死板,不灵活,而且很冗余。
如果你知道了怎么写,就会立刻抛掉这个宏录制功能。事实上
PowerPoint2007 已经取消了宏录制。
3. VBA 能干什么?如果你想避免重复劳动,或者需要多个文档协作,VBA 是
最好的选择。虽然默认下,VBA 只可以操作 VB 自带的库和 Office 的库,
但可以通过添加引用的方式来为其扩充功能。理论上 VBA 可以操作任何
Windows API(代表你的 Windows 能做的任意事情),和任意用 COM 封装
的组件。比如像 Acrobat,EndNote 等都是这么做的。它们可以使自身的
功能和办公紧密结合。如果你高兴,大可以写一个 Word 用的聊天插件:)
4. VBA 不能干什么?什么时候不适合使用 VBA?VBA 的功能取决于其库。如
果没有库提供一个功能(比如让 Excel 一个单元格显示一副图片),那么
VBA 也不能帮你。此外,你还可以用 VBA 编写插件来完成普通 Office 不
能完成的功能。此外,如果你开发的项目过于庞大,则应该换一门更专业
的编程平台(比如.Net),以便更好的组织代码和查错。VBA 比较适合短
小的,与工作任务很直接的问题。
5. VBA 能访问某个文本文件么?当然可以。VBA 直接提供 OPEN 关键字来访问
文件。
6. VBA 能模拟键盘输入么?当然可以。VBA 直接提供 SendKeys 函数来做到这
一点。
7. VBA 会被保存在哪里?VBA 是保存在 Office 文档中的,具体可以是某一个
文档,或者某一个文档的模板中。具体的位置需要在工程视图中选择(如
上图)。如果你希望一批文档都能用到同一个功能,最好先写个模板,然
后将 VBA 写到模板里。然后利用此模板来生成多个文档。当然,VBA 代码
也可以单独导出保存。在工程视图中右键单击某一个模块然后点“导出文
件”即可。
8. 不同 Office 组件可以互操作么?当然可以。VBA 的功能是由 COM 库来添
加的。默认情况下某一个 Office 组件只引用了当前这个组件的库。但是
你也可以手动添加其他库的引用。在 VBA 开发环境的“工具”菜单里点击
“引用”,就可以添加 Windows 上安装的任意 COM 组件。这样,不光不同
Office 组件可以互操作,你可以操作比如 Media Player,IE 等程序。
9. VBA 可以写图形界面么?当然可以。不然就说不上”Visual”了。VB 一向
支持一种被称为 UserForm 的模块。你可以在上面添加各种控件。
10.VBA 还提供哪些其他的功能?VBA 可以捕捉 Office 的事件,于是可以编写
如“当打开一个文档,要做某事”;“当 Excel 计算发生了错误,则做某
事”这样的程序。VBA 还有定时器,让你可以做周期性的工作。
11.除了 VBA,还有哪些开发平台可以操作 Office?实际上,任何可以使用
COM 组件的语言都可以访问 Office 的功能。在微软平台上的各种语言基
本上都满足这个条件。不过有几个平台是专门针对 Office 的。如.Net 上
的 VSTO(Visual Studio Tools for Office Developers)。这是一个利
用 Visual Studio 进行 Office 开发的集合环境,适合大型项目。不过为
4
序言
5
基本编程元素
1. 基本编程元素
这一期就是要介绍在 VBA 环境下你能使用什么东西。一般来讲,新手往往对到底
写什么会引起什么结果感到比较混乱。比如写 MsgBox 可以出现对话框,但写
MessageBox 为什么就不可以。在 VBA 中,直接写代码,而不是在 Sub 里写就会
报错?为何是 Sub,而不是 Suc,不是 Sud?等等。我会将最常见的做概要性的
讲解。但是我不会深入去介绍每个细节的语法。精简够用就好。如果有必要,你
可以查任何的关于 VB 的参考手册。
Sub 与 Function
最基本的问题是:如果你写了很多模块,每个模块有很多代码,那么你的程序代
码是从哪一句开始执行?答案是,VB 将代码组成一段一段的,每段从头到尾执
行(当然,这是粗略的说,因为代码还可能有跳转和循环)。每一段叫做一个
Sub 或着一个 Function。Sub 是子过程“Subroutine”的简称。比如像如下的代
码就定义了一个 Sub。
Sub Test()
MsgBox "Hello" 'Show Hello with a messagebox
End Sub
参数和类型在下面的“变量与类型”一节更详细的介绍。即使没有参数也要写个
空括号来占位置。这是 VB 的要求。之后,当你将键盘光标放在这个 Sub 里,并
点击上面的绿色箭头;或者到“宏”对话框执行 Test,这个 Sub 就被执行了。
另外值得注意的是单引号后边的文字,称为注释。VB 中所有写在单引号后边的
文字都会被解释器忽略掉,它们仅仅是给人看的。所以你可以在写代码时加上注
释,方便其他人看懂你的程序。
6
基本编程元素
这个例子实现了一个 Function,含义是将两个整数加起来。这次我添加了两个
参数 a1 和 a2 用于输入,其类型均为 Integer。参数的语法 Sub 和 Function 是
一样的。形式上,Function 和 Sub 仅仅有关键字的差别。Function 与 Sub 最大
的不同就在于 Function 需要返回一个值,而 Sub 不需要。第一行最后那个“As
Integer”是指返回值的类型。返回的语法是:
Function 名称=想要返回的值
那么什么叫“返回”呢?这是因为程序段之间是可以互相调用的。Sub 和
Function 可以调用其他的 Sub 和 Function。整个程序的执行顺序大致如下图所
示:
Sub Test()
sum = Add(3,4)
MsgBox sum
End Sub
变量与类型
我们在算数的时候需要草稿纸,将计算的中间结果记录下来。计算机算数也不例
外,也得需要在内存中开辟一些空间来记录这些内容。定义一块空间的方法的代
码是:
Dim var
或者
可见,在类型转换中,可能会有数据损失。此外对于对象类型(下文将介绍)来
讲,将一个类型的值赋给另一个类型的变量是会报错的。因此建议大家尽量减少
混用的情况。
一种特别的类型是 Variant。这种变量可以放置任何类型的数据。比如上文中的
Dim var 就等价于 Dim var As Variant。但是要注意的是 Variant 并不意味着变
量内容的类型可以变化。它仅仅是能放置任何类型的数据而已,同一时间只有一
个”实际类型”。为此,其内部必须维护当前到底是什么类型,还需要在被访问
时进行内部的二次解释。所以它消耗的空间更大,并且性能更差。另外如果用
Variant 变量表示一个对象,在写变量名后输入一个点,就不会有自动提示出现。
因此,建议大家少用 Variant 类型。
最后说一下作用域的问题。每一个变量都有一个作用域。如果在一个
Sub/Function 内定义变量,那么这个变量的作用域就仅仅限于这个
Sub/Function,称为局部变量。也就是说,在其他 Sub/Function 中是访问不到
这个变量的;或者说,如果在其他 Sub/Function 中定义了同名的变量,二者完
全是两码事,互相不会有任何影响。
在 Sub/Function 中的参数也是一种局部变量,其定义的方式就是比常规方法省
略了 Dim。其形式正如上文所写的那样。当然,你也可以在参数列表里不写类型,
这就意味着参数类型是 Varaint。同样的,Function 第一行的最后的 As Type
表明 Function 返回值的类型。不写这个返回值就意味返回 Variant 类型。
也许你会问,如果某个局部变量恰好和全局变量同名会怎么样?这时局部变量会
“遮蔽”全局变量,使你只能访问到局部变量。
下面是一个例子,大家可以来复习一下本次的内容。
Sub ShowSum()
Range("A1").Value = CalSum(200)
End Sub
End If
Next
CalSum = sum
End Function
总结
问答
比如
sub a()
aa=1
msgbox aa
end sub
是的,小程序无所谓,方便就好。但是如果程序稍微大了些,对象多了些,类型
检查是一个很好的防止错误的功能。那个时候,写的对比写的少更重要。其实我
觉得在目前的硬件平台上,性能可以不做最优先考虑。反倒是编程的便利性(写
正确的类型后 VBA 编辑器有自动提示),可读性和类型错误检测让我不喜欢用
Variant。
3.VBA 的变量必须先声明再使用么?
—————————————————
当然不是,VBA 允许你直接使用变量而不经任何声明。但是这样做可能会引起难
以调试的错误。比如你在第一行直接使用了变量 abc,结果到了后边由于笔误写
成了 acb。这一定会出逻辑错误,但是解释器因为语法没问题而不会报错。你自
己也很难在密密麻麻的代码中将这个错误挑出来。为了强迫变量必须先声明,再
使用,在程序的第一行加上这句:
Option Explicit
这样,如果不经声明直接使用变量就会报语法错误。这对于比较大的程序十分必
要。
11
对象
2. 对象
当你叫一个人做什么事情的时候,一般来讲你会如何做呢?你会说,比如,“周
正龙,拍老虎去”。如果你想知道一个人的信息时,你大概会这么问,“姚明,
你有多高”。这是我们平时的直观的交流方式。对象就提供了这样一种机制,使
得我们的编程更加符合人的思维习惯。这样,做基于对象的编程就很方便了。比
如你可以用程序指令写 Application.Workbooks.Add 的代码让 Application 这个
对象新建一个 Workbook。
对象在物理上是一段内存的区域,维护着一组数据,这些数据管理着实际的各种
看得见,摸得着的实体。比如应用程序 Application 是一个对象,窗口 Window
是一个对象,单元格是一个对象,段落是一个对象……在 Office 中,几乎任何
实体都可以找到对应的对象。如果你希望操作某个实体,那么你就操作它的对象
就好了。这样,编程的任务被转变成了找到合适的对象,并让这个对象做一些事
情,或者从对象上获得一些信息。
12
对象
每个组件定义了若干“对象类型”。回想上一期讲解的类型就能知道任何一个变
量都有一个类型。而对象也有类型。每个类型可以生成不同的“对象实例”。这
就是说,如果有一个 Excel.Application 的对象类型,我可以生成很多个此类型
的对象实例,即开启多个 Excel(其实类似的,如果有 Integer 这个类型,就可
以定义很多个不一样的 Integer 变量)。在下文中我会简称“对象类型”为“类
型”,而“对象实例”为“对象”。
13
对象
默认情况下,VBA 已经为我们自动添加了一些必要组件的引用,所以大多是时候
我们可以完全忽略以上的步骤,直接写代码。但如果想用一些额外的组件,就需
要手动添加了。
每个对象会有如下几种成员:
值得注意的是有两种特别的属性。第一种是“只读”的属性。就是说你不能改变
它,而仅仅能读取它。比如单元格的 Text 属性就是只读的。Excel 根据单元格
的 Value 和应用于此单元格的 Format 来决定 Text 是什么。所以如果非得要改的
话,需要改 Value 或者 Format。另一种是所谓“集合”的属性。表示当前对象
包含一组子对象。直观上,一个 Application 包含多个 Workbook,一个 Workbook
包含多个 Worksheet,一个 Worksheet 包含若干的 Range。对象也正是这么嵌套
的。比如你想获得第一个 Workbook 的第一个 Worksheet 的 A10 单元格需要这么
写:
Application.Workbooks(1).Worksheets(1).Range("A10")
14
对象
可以看到大多数集合对象比常规对象的名称多一个 s,表示复数(这是一种习惯,
而不是语法要求的。比如 Range 就没有 s)。另外取得集合中的某一个对象即可
以用对象索引值来获得(记得索引值是从 1 开始算的,而不是 0),也可以用过
对象的名称,比如如果你的第一个工作表又叫“MySheet”,那么上面的语句可
以等价的写为:
Application.Workbooks(1).Worksheets("MySheet").Range("A10")
你可以注意到访问对象成员的语法就是在对象名成后边加”.”。确切的语法是:
[对象名].[成员名]
15
对象
16
对象
我们可以定义一个变量来指向某个对象。这称为“对象的引用”(注意要和组件
引用的概念区分开)或者引用变量。之所以用引用的方式访问对象的原因是对象
比起常规变量都大的多。如果像普通变量那样每 Dim 一次就分配整个对象的内存,
非常浪费空间。所以,对象的引用只包含对象在内存地址而已。当然,我们通过
对象的引用还是能够直接操作对象。比如下面的代码:
a = "abc"
等价于
a.Value = "abc"
但是如果你写
a = Range("A2")
那么什么时候对象会释放掉内存呢?每个对象有一个内部的引用计数。每添加一
个引用就能使引用计数增加 1。如果你确认某个引用不再使用了,就写
a = Nothing
17
对象
所以,如果想找一个对象,完全没必要去录制宏那样去找。你仅仅把需要的对象
的名字翻译成英文(工作簿->Workbook,工作表->Worksheet,……)然后去查
文档。如果你对常用对象不熟悉的话,那么请等待下一期教程:)
18
开始编写完整的程序
3. 开始编写完整的程序
前几期介绍了一些基本概念。是该拿真东西练练手的时候了。本期的主题就是拿
OfficeSoft 一个真实的问题作为例子,讲解如何开始编写一个程序。当然这个
例子也很简单:)这个问题是:如何将一组单元格内红色的数字求和。Excel 自
己提供的函数 Sum 不能对待求和数据进行条件判断,而 Sumif 只允许对待求和数
值进行数值比较上的判断,也无法处理“字体是红色”这种格式条件。这时正是
VBA 发挥作用的时候,它可以将一些基本的功能组织到一起,然后完成自定义的
任务。
开始编程之前,应该先想清楚几个关键的问题,也就是一个设计的过程。编程可
不是上来就开写。对于本问题,首先想明白如何表达问题中的几个关键点:
• 如何表示一个单元格范围,并以此作为问题的输入?即到底要对哪些单元
格应用这个自定义的求和?
• 如何遍历一个个的单元格?
• 如何获取一个单元格的值?
• 对于一个单元格如何判断一个单元格的文字是红色的?
• 如何求和?
• 如何输出?
对于第一个问题。我们一开始可以用最简单的输入方法来做这个事情,即选择。
用户在单元格上拖动一下,选中了一组单元格。我们在 VBA 中可以用 Selection
对象来取得所有选中的单元格。Selection 对象的类型是 Range,以后我会用“对
象名{类型名}”的方法来表示对象和其类型的关系,如 Selection{Range}。顾
名思意,Range 就表示一个单元格的范围。值得注意的是,之前可以看到有这种
写法 Range("A1")。这里的 Range 是一个对象名。具体来讲,它等价于
ActiveSheet.Range("A1")——(这里 Excel 为我们提供了很好的快捷写法)。
也就是说类型名可以和对象名重名。所以需要你来区分什么时候是类型,什么时
候是对象。回到我们的问题上,Selection{Range}可以表示选中的所有单元格,
并为我所用。第一个问题解决。
使用的时候,将<obj>和<Collection>替换成实际的变量和对象即可。每次<obj>
这个变量都会指向 Collection 的一个元素。对于 Selection,其类型是 Range,
那么其包含的元素是什么类型呢?答案是,也是 Range,只是这些 Range 只代表
19
开始编写完整的程序
第五个问题,求和。可以用最基础的方法——累加:设定一个变量 sum,初始值
为 0。然后遍历所有单元格,如果单元格符合条件,就将其值累加到 sum 上。需
要注意,求和之前应该根据要求和数据的总量估算求和结果,考虑 sum 应该用什
么类型。Integer 的范围是-32768~32767,而 Long 是-2,147,483,648 到
2,147,483,647。这里为了简单,就直接用 Integer 了。
好了,所有问题都解决。你可以根据这些知识尝试自己编写一下,然后在看以下
的完整代码:
Sub SumIfRed()
Dim sum As Integer
Dim r As Range
sum = 0
For Each r In Selection
If r.Font.Color = vbRed Then
sum = sum + r.Value
End If
Next
MsgBox sum
End Sub
也许你对整个运算过程的细节非常感兴趣,也许你写代码后发现除了错误。这时
就可以对代码进行调试。在 VBA 编辑器写代码区域的左边空白单击一下,就可以
20
开始编写完整的程序
增加一个“断点”。你也可以显示“调试”工具栏,并点击上面的小手做到这一
点。
增加完断点之后运行程序,当程序执行到断点时就会停下来。你可以借此机会查
看当前各个变量的值。使用“监视”窗口(Watch View),或者“本地”窗口
(Local View),可以帮你做到这一点。调试工具栏上点击对应的按钮就可以让
它们显示出来。本地窗口自动显示当前 Sub/Function 的所有局部变量(本地、
局部、Local 是等价的,这是翻译问题)。如果一个变量指向对象的话,可以点
击其左边的展开符号查看其所有成员。你还可以利用“类型”那列的信息显示出
一个 Variant 变量当前的实际类型是什么。监视窗口与本地窗口类似,只不过你
需要指定让它显示哪些属性或者变量的信息。你可以直接在代码上选中一段代码,
然后拖拽到监视窗口,添加对那些数据的监视。
21
开始编写完整的程序
让代码停下来之后,你还可以一步一步的执行你的代码。用调试工具栏上的“逐
语句”(Step In),“逐过程”(Step)和“跳出”(Step Out)可以单步执
行代码。它们的区别体现在如果执行到了一个你自己写的 Sub/Function,是否
单步进入那个 Sub/Function 代码。
=SumIfRed(A1:A10)
就能得到想要的结果了。
22
开始编写完整的程序
23
操作文件
4. 操作文件
在使用 Excel 的工作中,很常见的就是收集磁盘上的文件,并整理成 Excel 文件。
被收集的文件可能是 Excel 文件,XML 文件,数据库文件等特种文件,也可能是
普通的文本文件,或者自己定义的文件。VBA 提供了相当丰富的文件操作工具。
当然,本教程是不会流水般的列举所有操作的,如有这个需要请看 VBA 的文档或
者 OfficeSoft 的精华区。本期教程还是集中精力解决一个实际问题,用以说明
各种文件的操作方法。在以下的代码中需要注意的地方是,对于文件的基本操作,
如打开、关闭、进入目录、查找、列举等操作是 VBA 直接支持的,它们是 VBA
库提供的 Sub/Function,甚至是关键字。针对 Excel 格式文件的操作自然由
Office 库来提供,比如 Workbook 对象。
我们的问题是:每个月公司都会收集大量的日志数据,它们是以文本的方式记录
的,格式如下:
当然,文本文件中并不存在那行列头,仅包含数据。记录中间是以 1 个空格分开
的(上面为了排版的需要夸大了空格的长度)。已知我们的系统有 5 个服务器,
每个服务器每天都会记录一份日志。每天所有服务器的日志都被保存在一个以日
期为名称的目录里,每份日志以服务器名称作为文件名。其目录结构是:
2008-4-1\
server1.txt
server2.txt
server3.txt
server4.txt
server5.txt
2008-4-2\
server1.txt
server2.txt
server3.txt
server4.txt
server5.txt
….
24
操作文件
就像上期那样,编程前应该先弄清楚有哪些问题要解决:
2. 如何打开文本文件?
这条语句用指定的打开模式打开指定路径上的文件,并连接到指定的一个号码。
比如代码
对于打开的文件要时刻记住一定要在不用文件的时候关闭它。不然就会出现别的
程序无法打开,或者无法删除的情况(因为你在占着嘛)。利用关键字 Close
就可以关闭文件,如:
Close #1
25
操作文件
就可以关闭上边打开的文件。
3. 如何遍历一个目录下的所有文件夹?
fileName = Dir([path],[attribute])
4. 如何一行一行的读取文本文件的内容,并按照规则将其拆分?
26
操作文件
5. 如何拆分字符串?
在这里引入了数组的概念。简单来讲,一个数组就是可以用“变量名(下标)”访
问的变量。数组的下标具有下界(Lower Bound,简称 LBound)和上界 (Upper Bound,
简称 UBound)。默认情况下数组下界从 0 开始,但也可以采用一些方法来改变
这个,例如在文件头写 Option Base 1,可以让默认下界变为 1。或者明确的用
Dim 声明一个数组,如 Dim arr(3 to 9) As String,就定义了一个上界为 9,
下界为 3 的字符串数组。不过就我的经验,修改上下界没有太大的意义,只会引
起混乱。只有一个下标的数组被称为“一维数组”,同理就会有“二维”、“三
维”、……等数组。但多维数组一般并不常用。数组必须在定义时就指定下标范
围才能使用,即其大小需要在使用前确定。
与数组对应的就是 Collection,读者可以将其理解为一个可变长的数组,不需
要提前制定大小。通过 Add 方法来添加元素,通过 Count 来取得其包含元素的个
数。比如
27
操作文件
6. 如何将数据放到数据表恰当的位置?
其实这已经不是个问题了。看过前面教程的都知道用 Range(”XX”).Value=XXX
的形式来做到这一点。
7. 如何在工作表里插入公式,计算总额?
8. 如何在工作表里插入柱状图?
Range("A1:A10").Select
Charts.Add
9. 如何删除原始的文本文件?
OK,所有的关键问题到目前为止都已经解决。是时候将他们组合起来了。不过由
于问题比较多,还是先画个流程图,进一步明确要做的事情。除非你能一次想明
所有问题,写代码之前最好先规划一下。对于我们的程序,大致应该是这样的:
28
操作文件
以下给出了这个程序的源代码 。代码的长度比之前的程序都要长。因此我将程
序拆分成了若干 Sub 和 Function:
Sub CollectMain()
'Create a workbook
Workbooks.Add
'the result is automatically "activated", so we can get it through
ActiveWorkbook
'now start to read files
Dim root As String, folder As String, filename As String, servername As String,
fullfilename As String
Dim curDate As Date
Dim curSheet As Worksheet 'current worksheet
Dim folders As New Collection 'hold all folders under C:\log\
'Find all folders In C:\log\
root = "C:\log\"
folder = Dir(root, vbDirectory)
While folder <> ""
If folder <> "." And folder <> ".." Then
folders.Add folder
End If
folder = Dir
Wend
29
操作文件
'Now all files are read, create a new worksheet to take statistics and add chart
TakeStatistics
'All jobs are done, clear source files, and other temp resource
ClearAll
'Finally, save the workbook and tell user all jobs are done
ActiveWorkbook.SaveAs filename:="C:\log\ServerLogStatistics_" & Date 'put
current date As suffix of result file name
MsgBox "All jobs are done, enjoy"
End Sub
30
操作文件
resultSheet.Name = sheetname
'write header
resultSheet.Range("A1").Value = "交易日期"
resultSheet.Range("B1").Value = "交易时间"
resultSheet.Range("C1").Value = "交易额"
End If
Set GetWorksheet = resultSheet
End Function
'Read each line of files and put the content to the sheet
Sub ReadFile(d As Date, fullfilename As String, sheet As Worksheet)
sheet.Activate
Dim record As String, dealtime As Date, dealamount As Currency
Dim startRange As Range
curRow = sheet.UsedRange.Rows.Count + 1 'get the first empty line
Open fullfilename For Input As #1
While Not EOF(1)
Line Input #1, record ' read one line
items = Split(record, " ") 'split record
dealtime = items(0) 'the first item in a record is deal time
dealamount = items(2) 'the third itme in a record is deal amount
'Write them to the last row of worksheet
Range("A" & curRow).Value = d
Range("B" & curRow).Value = dealtime
Range("C" & curRow).Value = dealamount
curRow = curRow + 1
Wend
'Caution: you must close the opened file
Close #1
End Sub
31
操作文件
32
操作文件
Exit Function
End If
Next
ContainSheet = False
End Function
• 插入新的工作表时,最好将新表插到当前所有工作表的最后边。否者最后统计的时
候,会遍历前面的工作表,从而使得统计结果是倒序的(这样就还得排序) 。为此在
Sheets.Add 时要加入 After 参数,给出要 After 的那个工作表 Sheets(Sheets.Count),
即最后一个工作表对象。
• 在遍历目录的时候,无法使用嵌套的方式遍历。即没法在 Dir 目录的时候,同时 Dir
一个目录中的文件。这是由 Dir 的使用方式造成的——第一次使用时指定路径,之后
每次使用就不需要参数。如果嵌套使用,无参的 Dir 就不知道去遍历哪个路径了
(C:\log 的?还是 C:\log\2008-4-1 的?)
。所以在代码中使用了一个 Collection 来缓
存目录项,然后再遍历每个目录项下面的文件。
• 代码中很多时候采用&来拼接字符串,这十分方便,而且如果被拼接的不是字符串
(比如一个整数,一个日期),也能正确的变成相应的字符串。
• 在最后删除sheet1,sheet2,sheet3 的时候,比如Sheets("sheet1").Delete,
会让Excel为每个sheet的删除弹出一个对话框提示,让我们确认是否要删除这个shet。
为了让提示不出现,可以用Application.DisplayAlerts=False来暂时关闭这个删除提示 1。
但是不用再写一个Application.DisplayAlerts=True恢复,因为VBA执行结束后会自动将
这个状态恢复。也许可以在GetWorksheet中重用sheet1~sheet3,但因为会让代码更
复杂就没有做。读者可以自己尝试。
• 在代码中,很多地方需要获得一个 sheet 的最后一行在哪里。这里使用了
Sheet.UsedRange.Rows.Count。UsedRange 返回用户使用区域的外接矩形形成的 Range。
其 Rows 属性返回所有行,而 Rows.Count 就代表行数了。这也就是最后一行的行号。
本期教程讲解了如何使用 VBA 解决一个复杂的实际问题的全过程。从局部技术,再
到流程设计,最后到实现。本文希望开发者在做程序的时候可以遵从这个过程,这
样可以保证程序十分清晰流畅。程序的规模稍大时,应该利用 Sub/Function 将任务
分解,这样非常利于代码的理解,重用和调试。本文把调试中发现的问题作为注意
点放在了代码的最后,提示了一些程序中细节的地方。在下期中,将重点介绍 VBA
的事件模型和事件处理。
1
感谢水木网友 iam0 的细心指正
33
事件
5. 事件
在前几期教程中,我们的程序都有一个共性——它们都是从头执行到尾的。在正
常执行时,你无法中断它们。这对于完成某个具体的任务是足够了,但是对于具
有交互性的程序就没有办法了。为此 Office 提供了另外一种编程模型——基于
事件的程序。其实说起来很简单,就是 Office 有什么事情发生了就直接通知你
(比如鼠标单击,键盘被按,某个 Workbook 被打开……),如果你有兴趣,就
可以处理这个事件。事件和事件处理的关系如下图所示:
作为开发者只需要编写绿色的部分就可以了,完全不需要其余的部分。
那么如何编写一个事件处理代码呢?在处理之前,首先要明确到底要处理哪一个
层面的事件。例如,在 Excel 中,有三大类事件——Worksheet、Workbook 和
Application。在 Word 中,可以处理 Document 和 Application 两大类事件。以
Workbook 事件为例。不同于之间的“插入模块”,想编辑 Workbook 的事件,需
要在 VBA 开发界面的 Project 视图中的“This Workbook”中右键单击鼠标,点
击“查看代码”。在编辑界面中最上边的左边的下拉框选中“Workbook”,右边
就可以选择处理哪一种事件了。对于 worksheet 的事件是类似的,只不过需要查
看某个 Sheet 的代码。
34
事件
35
事件
通过查找帮助可以找到所有可以处理的事件的列表。
Sub DoSomething()
MsgBox "DoSomething"
End Sub
36
事件
37
用户窗体
6. 用户窗体
大多数人更加偏爱图形界面的程序,因为它看起来很直观,用起来很方便。不过
之前讲述的编程内容无法做到这一点。但是,“Visual Basic”的本意就是可以
快速的编写可视化的程序。VBA 当然可以快速制作图形界面。VBA 称这种图形界
面为“用户窗体”(User Form)。
为了添加一个窗体,要在工程视图中右击鼠标,并新建一个“用户窗体”。结果
如图所示:
这时可以看到工程视图中多出了一个“UserForm1”,这和之前我们书写的“模
块”、“ThisWorkbook”和“Sheet”等是平级的。在主编辑窗口显示出了我们
的窗体显示出来的样子,被称为“对象窗口”(窗体上面的若干点是用来定位控
件的,并不会真的显示在实际的窗体里)。通过这个视图,就可以使得编程“所
见即所得”。工具箱上有若干可以使用的控件。如果希望在窗体上添加控件,可
以直接用拖拽的方式将工具箱的控件拉到窗体上。不过可以用的工具并不是像显
示的那么少,右击工具箱,选择“附加工具…”可以添加相当多的工具。这些工
具来自于诸多的 COM 控件。
左下侧是“属性”视图。这个属性和之间讲述的一个对象的属性是同一个东西
(用户窗体本身就是一个对象),只不过这里明确的列了出来,而不需要像以前
那样使用“对象名.属性名=值”的形式来访问它们。当然,利用编写代码访问窗
体属性仍然是可以的,两种方式等价。只不过利用属性视图修改的属性并不灵活,
只适合那种设好就一直不变的属性,比如窗体的标题、大小、背景色等;而利用
编写代码的方式更具有动态性,比如实现单击窗体上某一按键就改变某个属性这
样的功能。
38
用户窗体
UserForm1.Show
对于特别复杂的窗体,显示可能需要耗费一些时间。这时,可以在用户不知情的
时候使用 Load UserForm 的方法预先将窗体加载到内存中。等到用户想看这个窗
体的时候再 Show,因为窗体已经加载好了,仅仅是显示出来,就会显得快很多。
如果希望暂时隐藏窗体,可以使用 UserForm.Hide 方法。如果希望彻底将
UserForm 从内存中清除掉,则需要用 Unload UseForm。
UserForm 和用户是通过事件来进行的。为了处理事件,首先将“对象窗口”切
换到“代码窗口”,做法是右键单击工程视图中的 UserForm1,并选择“查看代
码”。或者右键单击窗体本身,然后选择“查看代码”。甚至直接双击窗体也可
以直接进入代码编辑状态,并自动生成 UserForm_Click 事件的处理代码框架。
切换到代码视图后,编写如下代码:
39
用户窗体
可以在用户尝试关闭窗体时检测窗体的高度是否达到要求,如果没有达到,就取
消掉关闭这个动作,并且将窗体的标题改为提示信息。更现实一点,你可以将条
件判断改为对窗体内容的合法性校验,并指定如果窗体内容不合法,就禁止关闭
窗体。
当然,空空如也的窗体没什么用。所以需要加一些控件才能完成实际的工作。一
些常见的控件如 CommandButton 是按键;Lable 是静态文本;TextBox 是文本框;
ListBox 是列表框。通过从工具箱中拖拽,可以将一些控件添加到窗体上。比如,
在下面的例子中添加了一个 ListBox,一个 Lable,一个 TextBox 和两个
CommandButton。添加时,可以用“格式”工具栏来对齐控件,这样界面就不会
显得凌乱了。
其中 ListBox.AddItem 可以将一个字符串加到列表框当中,并显示出来。利用条
件判断,可以指定只有当 txtValue 非空才会添加列表框项目。添加后,将
txtValue 清空,并重新将输入焦点返还给它,以便于下次的输入。
40
用户窗体
为了使得我们的程序可以在“宏”中出现,还是需要添加一个常规的“模块”,
然后在里面写一个 Sub 即可:
Sub StartTestForm()
frmTest.Show
End Sub
这样就能利用宏对话框来启动窗体了。
41
根据 VBA 制作展示 PPT
Sub CreateShowPPT()
AutoShowForm.Show
End Sub
接下来的工作是制作一个用户窗体 AutoShowForm,使其可以提供比较丰富的配
置功能,使得程序更加灵活。窗体的效果如下图所示(我标出了控件的名称,和
下文代码对照):
42
根据 VBA 制作展示 PPT
相关代码为:
Option Explicit
Private Sub btnBrowseDict_Click()
Dim dd As FileDialog
Set dd = Application.FileDialog(msoFileDialogFolderPicker)
If dd.Show = -1 Then
txtImagePath.Text = dd.SelectedItems(1)
End If
End Sub
End If
If txtColumn.Text = "" Then
MsgBox "请输入图像文件路径所在的列名"
Exit Sub
End If
Dim bAbsoluteImgPath As Boolean, bHeader As Boolean
bAbsoluteImgPath = ckbAbsolutePath.Value
bHeader = ckbHeader.Value
Insert txtXlsPath.Text, txtImagePath.Text, txtColumn.Text,
bAbsoluteImgPath, bHeader
MsgBox "任务完成"
End Sub
这部分代码定义了当几个按键被按下后要进行的工作。其中,btnBrowseFile 按
键即为图中“Excel 文件路径”后边的“浏览”按键。它被按下后就会启动一个
“Open File”对话框。这个对话框是由 Application.FileDialog 来直接提供的,
不需要我们自己编写。你只要告诉它打开“msoFileDialogFilePicker”这种对
话框即可——即用于选取文件的文件对话框。在显示之前,添加一些文件类型过
滤器,使得只有 Excel 文件会被显示出来,如下图所示。这是由
FileDialog.Filters.Add 来添加的。
FileDialog.Show 函数就会使得文件对话框被显示出来。当用户选好文件,按
“确定”退出文件对话框时,Show 函数就会返回一个-1(如果是按“取消”退
出,则会返回 0)。所以就会有 If fd.Show = -1 Then …这样的语句,将用户
44
根据 VBA 制作展示 PPT
在将所有的配置填好之后,点击 btnRun,程序就开始进行简单的输入检查,防
止用户输入的一些漏洞。当一切没有问题之后,所有的配置会以参数的形式传递
给 Insert 这个 Sub。这是本程序的核心 Sub。下面是这个 Sub 的代码:
...
Dim excelApp As New Excel.Application
Set Workbook = excelApp.Workbooks.Open(FileName:=xlsPath)
Workbook.Activate
...
之后,程序定义了一个用于幻灯片母版(SlideMaster)的 Layout:
...
Set layout = ActivePresentation.SlideMaster.CustomLayouts(7) '7 means a
blank layout
...
可以看到 7 正是“空白”。因为我们不需要任何“占位符”(如果你不清楚什么
是 PowerPoint 的母版和占位符,请查阅相关帮助——它们对于日常的
PowerPoint 应用很重要)。
...
If (bHeader = True) Then
i=2
Else
i=1
End If
...
终于,可以进行实际的数据读取工作了。因为每个幻灯片要放四幅图,所以如果
当前的图片刚好是一个幻灯片里的第一幅图片,就需要新建一个幻灯片,然后把
图插进去:
...
Set Cell = workbook.Sheets(1).Range(column & i)
If Cell.Value = "" Then workbook.Close: excelApp.Quit: Exit Sub
With ActivePresentation
.Slides.AddSlide .Slides.Count + 1, layout 'Create a new slide
Set curr = .Slides(.Slides.Count)
...
新建幻灯片之后,需要构造图片的实际路径:
得到图片路径之后,就要进行实际的在幻灯片中加入图片的工作了。这是由
PutImage 来实现的,其代码为:
48
根据 VBA 制作展示 PPT
'Pos 1, 2, 3, 4 means left top, right top, lef bottom, right bottom
Private Sub PutImage(imgPath As String, pos As Integer, slide As Slide)
'Set postion
Dim left As Integer, top As Integer, width As Integer, height As Integer
Select Case pos
Case 1: left = 5: top = 5
Case 2: left = ActivePresentation.PageSetup.SlideWidth / 2 + 5: top = 5
Case 3: left = 30: top = ActivePresentation.PageSetup.SlideHeight / 2 + 5
Case 4: left = ActivePresentation.PageSetup.SlideWidth / 2 + 5: top =
ActivePresentation.PageSetup.SlideHeight / 2 + 5
End Select
width = ActivePresentation.PageSetup.SlideWidth / 2 - 10
height = ActivePresentation.PageSetup.SlideHeight / 2 - 10
slide.Shapes.AddPicture imgPath, msoFalse, msoTrue, left, top, width, height
End Sub
workbook.Close
excelApp.Quit
程序运行完之后,会弹出对话框提示:
49
根据 VBA 制作展示 PPT
...
MsgBox "任务完成"
...
好啦,可以看一下最后的运行结果。
50
提取 Word 中未样式化的标题
8. 提取 Word 中未样式化的标题
在本期中,我会将注意力放在 Word 上。网友 dinosaurhxe 曾经提出了一个问题:
他们单位的老干部不是太会用 Word,写标题的时候完全没理会标题样式,全部
用了正文。每个标题分为三部分:标号、中文标题和英文标题。dinosaurhxe 很
苦恼,因为领导要求它产生三份列表,分别以标号、中文标题和英文标题排序,
同时结果还要包含标题对应的页码。这样的话,就必须把这些标题全提取出来
——那可是好几百页啊。不过,尽管老干部们不会用样式,还是留下了一些线索
——标题都很工整的被分为以上三个部分,均以空格隔开,并且全被设为了粗体。
问题的数据规模比较大,问题没有通用的解法,并且很有规律。这刚好符合 VBA
施展功力的条件。
我将这个问题的解决方案定为:如果碰到了一段的段首是粗体的数字,就认为这
个东西是标题。当然,这里可以加很多条件来使得结果更不容易错,比如最大长
度不能超过某某值等。将标题拆解后放到 Excel 中,每个部分一列,这样想怎么
排序都可以了。VBA 的代码写在 Word 中。依照老规矩,还是先看看有哪些子问
题需要解决。
Dim p As Paragraph
For Each p In ActiveDocument.Paragraphs
'do something...
Next
来遍历段落了。
51
提取 Word 中未样式化的标题
p.Range.Select
pageNum = Selection.Information(wdActiveEndAdjustedPageNumber)
所谓 ActiveEndAdjused 是指获取选中区域结尾所在的(因为一个段落可能跨页),
经过调整过的页码。所谓调整是指,如果对文档分了节,并做了页码设置(如首
页页码不是 1,或者加了分页符),就返回设置后得到的页码。如果只想要那种
文档第一页为 1,忽略任何页码设置的页码,可以改用 wdActiveEndPageNumber。
更多的枚举值可以看这里:
ms-help://MS.WINWORD.DEV.12.2052/WINWORD.DEV/content/HV10076103.htm
对于问题 7,相信经过那么多次的教程,这个就没有必要再讲了。详细的程序如
下所示:
Sub ExtractTitle()
Dim p As Paragraph
Dim firstChar As Range
Dim num As String, chTitle As String, enTitle As String 'title number, Chinese
52
提取 Word 中未样式化的标题
53
提取 Word 中未样式化的标题
54
操作数据库
9. 操作数据库
Excel 一个重要的数据分析平台,自然就少不了与数据库连接。在 Excel 上就有
直接打开数据源的选项,如下图所示:
尽管这种方式用起来很方便,这种连接数据源的方式和其他功能一样,不是很灵
活。查到的数据会被放到一个工作表里,并做自动筛选。如果我们有特别的要求,
比如希望将数据表的左左上角定位在 B10 单元格,这种方式就不能满足要求。另
外,鉴于 SQL 的强大,这些“好用”的功能到了高级用户手里就会一无是处。最
后,这种方式不能把 Excel 的数据 Insert 到数据库中。所以,利用 VBA 连接并
操作数据库就显得很必要了。
55
操作数据库
con.ConnectionString = "Provider=Microsoft.ACE.OLEDB.12.0;Data
Source=D:\MyDoc\Documents\mydb.accdb"
每种数据库都有自己的连接字符串格式,你需要查阅文档才能知道到底该怎么写
这个连接字符串。不过连接字符串的一般格式是一致的,即一组被分号;分开的
key=value 的项目。Provider 是指数据库的驱动类型;Data Source 是指数据库
文件;Database 是指数据库名;UID 是用户名;pwd 是密码;有的时候,需要用
Windows 集成帐户校验,这时还要加上 Integrated Security=sspi。其外还有很
多其他的项目,例如通讯方式,永续化方式,字符编码等。这些项目大多可选,
如果不写也可以连上数据库,只有在希望对连接方式进行精细调整的情况下才需
要写它们。
设置好了连接字符串后,就可以用以下代码打开数据库
con.Open
最常见的数据库使用方式就是查询了。为了存储查询结果,必须首先新建一个记
录集对象。
56
操作数据库
For i = 1 To rs.Fields.Count
ActiveSheet.Cells(1, i).Value = rs(i - 1).Name
Next
rs 内部会有一个游标(Cursor)。刚查询完后,游标指向查询结果的第一条记
录。每条记录有若干列,每一列被称为一个域(Field)。rs(index)就是指当前
指向记录的第 index 个域。上面这段代码遍历每一个域,并将其 Name 显示出来,
放到工作表第一行的对应列中。需要注意,Office 的库中的索引一般从 1 开始
计数,而数据库的索引都是从 0 开始计数的(因为它们都是用 C/C++开发的)。
所以 rs 的索引要用 i-1。执行效果如下:
接下来就是遍历每条记录,将所有的数据显示出来。
r=2
While (Not rs.EOF)
For i = 1 To rs.Fields.Count
ActiveSheet.Cells(r, i).Value = rs(i - 1).Value
Next
rs.MoveNext
r=r+1
Wend
57
操作数据库
当然,如果你想直接把 rs 的所有数据显示在单元格里有更简单的做法。比如希
望显示位置的左上角在 A2 单元格,直接写一句 Range.copyFromRecordSet 就可
以了。
Range("A2").CopyFromRecordSet rs
这个和上面显示数据的效果完全相同。不过它不会帮你把列头显示出来。
好了,说完了读取数据库,再说说修改数据库。修改数据库有 Insert,Update
和 Delete 三种。可以书写 SQL 语言直接进行这种操作。这里以 Insert 为例。在
数据库连接打开后,直接用 Connection.Execute 来执行一个修改数据库的 SQL
命令,如下所示。
然而 RecordSet 为我们提供了更方便的方式。在查询完成之后,我们可以直接对
RecordSet 进行修改,比如:
...
rs.Delete 'delete the current record
等等。最后执行
rs.Update
RecordSet 就会根据我们的修改,自动将插入,删除,和更新应用到数据库里。
所以如果希望将工作表数据导入到数据库,就可以直接将数据放到一个空白的数
据集中,然后再 Update 即可。当然 SQL Server 的 T-SQL 有更加方便的语句 Open
Source 可以直接导入 Excel 数据。不过因为这是 VBA 教程,就不介绍它了。
不过为了使得这种修改可以执行,必须在 rs 被打开时指定可进行读写操作。就
是说必须书写 Record.Open 的第四个参数。下面具体说一下其第三个和第四个参
数的含义。
其中 cursorMode 可以是:
0 = adOpenForwardOnly 打开仅向前类型游标。
1 = adOpenKeyset 打开键集类型游标。
2 = adOpenDynamic 打开动态类型游标。
3 = adOpenStatic 打开静态类型游标。
而 lockMode 可以是:
程序的最后请务必记住要关闭数据库连接。
conn.Close
另外,最好将两个对象的引用取消掉,使其可以释放空间:
conn=Nothing
rs=Nothing
59
操作数据库
这样做可以避免资源的浪费。
60
操作 Windows API
不过在使用这个功能之前,一定要搞清楚是否真的没有别的办法了。因为调用
Windows API 函数可能会影响应用程序的可靠性。当你从自己的代码中直接调用
API 函数时,会跳过 VBA 在正常情况下提供的一些安全机制。如果您错误地定
义或调用 API 函数(任何程序员都难免犯这类错误),就可能会产生应用程序
错误。如果使用了 API,那么在运行代码前一定要保存项目,并确保已理解调用
API 函数的原理。因此,你可能需要实现了解一下 C/C++的初步知识,还要稍微
的学习一下 Windows API 和 Windows 的内部机制。比如,可以阅读
MarkE.Russinovish 和 DavidSolomon 合著的 Microsoft Windows Internals。
• Kernel32.dll :低级操作系统函数,例如内存管理和资源处理函数
• User32.dll :Windows 管理函数,例如用于信息处理、计时器、菜单和
通信的函数
• GDI32.dll:图形设备接口 (GDI) 库(包含设备输出函数),例如用于绘
图、显示环境和字体管理的函数
如果想改变一个窗口的文字,必须首先找到要改变的是哪个窗口。这就必须得用
到 Windows API 中的 FindWindowA、GetWindow 和 GetWindowTextA 函数。前者根
62
操作 Windows API
据一定的条件来查找一个窗口,并返回其句柄值。在这里,我们不给其任何参数,
就让它将一个任意一个窗口句柄,然后再用 GetWindow 来进行窗口遍历。而
GetWindowTextA 则可以获得一个窗口的文字。为了使用它们,首先用上面的语
法声明它们。
然后简单书写一下大致的流程。
获得第一个窗口句柄
While 句柄不为空(如果为空就意味着遍历完了)
如果句柄代表的窗口文字中包含“有道”,就将其改名,并退出
否则,获得下一个窗口的句柄
Wend
FindWindow(vbNullString, "abc")
是找不到窗口文字为”abcdef”的窗口的。所以才需要进行遍历。这对于找到那
些窗口标题是动态变化生成的(例如 QQ 之类的)是必须的。
第二个则是相对位置。2 表示获得当前窗口的下一个窗口(自然也能获得上一个,
第一个,或者最后一个窗口,详情可以看 WinAPI.txt 或者 MSDN)。你可以先定
一个常量,使这段代码语义更清晰。
当找到了期望的窗口后,就需要改变其文字。这里,需要再声明一个 API 函数
SetWindowTextA:
然后,在上面代码中,将相应的注释改为:
这就完成了所有的工作。以下是完整的代码:
Sub ReplaceCaptain()
Dim WinHandler As Long, captain As String * 26
WinHandler = FindWindow(vbNullString, vbNullString)
While WinHandler <> 0 ' 0 means there is no window to retrasverse
GetWindowText WinHandler, captain, 26
64
操作 Windows API
本期教程是最后一期教程,只要你收到了一点点启发,就达到了本教程的目的。
最后感谢大家的支持,希望大家都能用 VBA 来提高自己的工作效率,让 Office
的高级功能真正的为自己服务。
65
END
66
END
67