|
前言
本文是依据本人自身的经验和体会,以及参考了众多网络资料写成,由于本人只是自学的VB和VBA,从未系统的学习过编程,编程水平有限,谬误之处,在所难免,还望各位不吝指正,万分感谢!
——Joforn
2009年11月21日星期六
-----------分隔线-----------
第一章 开头篇
——认识类
Visual Basic是基于对象的编程(注:本文所有的代码和讨论将都以VB为基础模型,不过我会尽量使用一些大家在VBA中常见的例子来做说明的。),所以我们常见的一些东西其实都与类有关。不需惊讶,是的,类其实离我们很近,它们正和我们天天相处。可以夸张的说,如果离开了类,我们的VB(VBA)就无用武之地了。
我们还是先来看一条非常简单的语句,让我们亲身体会一下类与我们的距离:
- '例一
- Sub Hello()
- Range("A1").Value = "Hello,world"
- End Sub
复制代码
我们就暂且称之为VBA的Hello world吧,看到这个简单到不能再简单的例子,或许你笑了,这个根本就和类没有关系嘛。是的,我们在这里并没有明显的看到类,但是我们看到了Range("A1")这个对象,而在Range("A1")前面,还隐藏着一个对象:ActiveSheet,而在ActiveSheet前面又有一个ActiveWorkbook,而在ActiveWorkbook前面还有一个Application。是的,这些都是对象,我们依旧没有看到类。先别急,我们再从后面往前面看一遍:Range("A1").Value="Hello,World" Value是什么?Value是一个Range("A1")这个对象的一个属性。那它是从哪来的呢?它是由Range类定义的,是的,我们发现了第一个出现在我们的面前的类 Range。你或许一下还接受不了,你刚刚在前不是还说Range("A1")是一个对象吗?怎么现在又说Range又是一个类呢。呵呵,不着急,我们还是看看下面的这个例子,或许你就很快就会明白了。
- '例二
- Sub Hello()
- Dim Range1 As Range '引用一个Range类
- Set Range1 = Range("A1") '将类实例化
- Range1.Value = "Hello,world" '这时,Range1又是一个对象了
- Set Range1 = Nothing '销毁一个类的实例
- End Sub
复制代码 或许你现在已经发现了,我们引用了一个Range类,并将之实例化后修改了它的属性。而在例一中,我们只不过是将这一切都以隐藏起来,直接对一个对象Range("A1")修改它的属性,但Range("A1")这个对象正是引用了Ragne这个类,才具有了Range类的属性"Value"。类本身并不直接为我们做什么,但是,它却又一直默默的隐藏在幕后规化着我们的动作。是的,这就是类,它往往都是通过对象的方式展现在我们的面前,让我们无时无刻与之交流,却又常常在不经意间忽视了它的存在。现在再回到例一去看,就会很容易的发现,ActiveSheet实际引用了Worksheet类、ActiveWorkbook引用了Workbook类,而Application则引用了和它同名的Application类(这也正是我们会经常被混淆一个概念,一个对象可以和被它所引用的类同名),原来我们在短短一个赋值的语句中,已经在与这么多的类打交道。
类就像是我们呼吸的空气,我们一直深在其中,却又常常忘却了它的存在。那我们又要怎样来区分类和对象呢?其实它们经常成对的出现在我们面前,只是一个看得见摸得着的,一个却深藏不露。我们可以这样去理解类与对象:类是一个概念或是一种定义,每个类拥有其自己的特征和行为方式,而对象就是某个类的实例。就像"人","人"是一个定义,你一看到"人"就会想到它是直立行走、有头、五官……等各种特征,但当"人"被具体到你、我、或是某一个人的时候,它就是一个对象了。
既然类与我们和程序息息相关,那么我们更应该去好好的了解它了。让我们准备好行囊,到神秘的类的领地去好好浏览一番吧。
第二篇 走近类
——类的基本写法及注意项
如果说上一篇让我们看到类隐藏在神秘面纱后的面孔,那么现在我们将要做的就是真正的看清楚它。不过,在此之前我还是要让大家再次熟悉一下面的几个关键词:Public 外部,可供类或是模块自身外调用;
Private 内部,只能由类或是模块内部直接调用;
Friend 友元,只能由工程内调用(即:在当前工程内部调用时相当于Public,而对外部工程相当于Private。)。
Sub、Function 过程(不返回值)和函数(返回值),在类中我们可将其视为类的方法;
Property Get 返回属性的值;
Property Let 设置属性的值;
Property Set 设置对象属性的值(即:该属性含有对象引用)。
[注:Property往往是成双出现在类中的某一个属性上,甚至可能三者同时出现,当一个类中某个属性只有Property Get时代表这一属性为只读属性,比如在VB工程中Form类的hWnd属性。]
Event 定义用户自定义的事件
[注:Event可以像声明过程的参数一样来声明事件的参数,但有以下不同:事件不能有带命名参数、Optional 参数、或者 ParamArray参数。事件没有返回值。];
RaiseEvent 引发在一个类、窗体、或者文档中的模块级中声明的一个事件。
Implements 指定要在包含该语句的类模块中实现的接口或类。
当我们熟悉上面的几个关键词后,再来分析类,就会发现类其实也挺简单。我们将从一个简单的类开始来正式的学习类的定义和使用方法。不过在此之前,我建议大家先去读读qee用兄写的一个帖子:VBA类:隐者的秘密,qee用兄在那个帖子中图文并茂,已经将类清晰的描绘出来,并亲自带领我们写了一个类。在此,让我们向qee用兄道声辛苦,并送上万分感激。好了,现在,我们来写一个非常简单的类,用它来实现两个数的四则运算。
- '类的名称定义为:四则运算
- Option Explicit
- Public Event OnError(ByVal Number As Long, ByVal Description As String, ByVal Source As String)
- Private lNum1 As Double, lNum2 As Double
- Public Property Let Number1(ByVal Number As Double)
- lNum1 = Number
- End Property
- Public Property Get Number1() As Double
- Number1 = lNum1
- End Property
- Public Property Let Number2(ByVal Number As Double)
- lNum2 = Number
- End Property
- Public Property Get Number2() As Double
- Number2 = lNum2
- End Property
- Public Property Get 和() As Double
- On Error GoTo Error01
-
- 和 = lNum1 + lNum2
- Exit Property
- Error01:
- RaiseEvent OnError(Err.Number, Err.Description, Err.Source)
- Err.Clear
- 和 = 0
- End Property
- Public Property Get 差() As Double
- On Error GoTo Error02
-
- 差 = lNum1 - lNum2
- Exit Property
- Error02:
- RaiseEvent OnError(Err.Number, Err.Description, Err.Source)
- Err.Clear
- 差 = 0
- End Property
- Public Property Get 积() As Double
- On Error GoTo Error03
-
- 积 = lNum1 * lNum2
- Exit Property
- Error03:
- RaiseEvent OnError(Err.Number, Err.Description, Err.Source)
- Err.Clear
- 积 = 0
- End Property
- Public Property Get 商() As Double
- On Error GoTo Error04
-
- 商 = lNum1 / lNum2
- Exit Property
- Error04:
- RaiseEvent OnError(Err.Number, Err.Description, Err.Source)
- Err.Clear
- 商 = 0
- End Property
- Public Sub ClearNumber()
- lNum1 = 0: lNum2 = 0
- End Sub
复制代码 然后,将下面的代码写到ThisWorkbook模块中:
- Option Explicit
- Private WithEvents Class1 As 四则运算
- Sub TestClass()
- If Class1 Is Nothing Then Set Class1 = New 四则运算
- Class1.ClearNumber
- Class1.Number1 = 9
- ' Class1.Number2 = 8
- Debug.Print Class1.和
- Debug.Print Class1.差
- Debug.Print Class1.积
- Debug.Print Class1.商
- End Sub
- Private Sub Class1_OnError(ByVal Number As Long, ByVal Description As String, ByVal Source As String)
- MsgBox "类中发生错误,错误代码:" & Number & " 错误提示:" & Description & " 错误源:" & Source
- End Sub
复制代码 这是一个非常简单的类的定义和应用(当然,实际编程时很少有人会为了四则运算而专门写个类。),虽然它很简单,不过它却几乎包含了类所有常见的特征:属性、方法、事件。运行TestClass后我们可以看到立即窗口中的输出,并接收到一个MsgBox的窗体,它提示了我们在类运行过程中产生了一个除数为零的错误,而这正是类中的OnError事件激发的。
虽然这个类本身没有什么实际的应用的意义,但是我们却可以用它来做一个模版,我们只要参照这个类,就可以很快的写出自己想要的类。
你现在是不是已经有了自己马上要编写一个类的冲动呢?那么我们开始准备动手吧,不过在你动手之前,我这里还要给你几点关于类的建议(这些都是笔者自身积累的经验或是前辈们的忠告。):
一、在正式编写一个类的前期,最好尽可能规定好所有要用到的接口与方法。一旦一个类被正式发行后(封装成DLL并被其它的工程引用),后期维护时尽量不要再去修改已有的接口函数及其参数,除非你打算将所有的工程全部重写;
二、为你类中的属性、方法、事件取个有意义的名字(特别是声明为Public方式的),如果允许的话,最好使用“工具”->“过程属性”为其添加一些描叙,说明它的意义或是调用方式,这样可以方便查看其属性并让客户(甚至是自己)能很快明白这个函数的用途;
三、只暴露必须的接口供外部调用,不要将一些可能仅在类内部使用属性和方法暴露给类外部;
四、不到万不得已,不要在类中定义Public方式的变量;
五、重新编译新版本的DLL时,最好是按“版本相同、版本兼容、版本不兼容”这个次序来选择编译后DLL版本兼容性,即:能用“版本相同”方式就不要用“版本兼容”,更不要用“版本不兼容”,因为“版本不兼容”方式就意味着你的旧工程在其重新编译前将无法使用这个DLL。一旦准备使用无兼容性版本,那么就要考虑如何减少部件的用户以后可能遇到的麻烦。如果以后的版本还要做一些可能破坏兼容性的修改,那么最好把这些修改集中在一次进行。计划对不兼容进行的修改时,把工程作为一个新的任务来对待,应该投入尽可能多的精力进行计划,就象在创建新的部件。创建不兼容版本,这要求三个步骤:改变工程名,改变文件名以及通过选定不兼容进行编译(注:这三个步骤最好是一定要做到,否则将会让我们在后面安装引用此类时会发生各种问题而导致我们无法使用类);
六、可能的话,不要忘了在类内部添加错误处理过程,尽管这些错误处理过程可能会损耗我们的运行时间,但它们能让我们的类更健壮;
七、类中不能定义Public方式的结构(Type),如果必需要这么做的话,可以使用一个类来代替Type;
八、在类的Class_Terminate()事件中释放被当前类所引用的类或是数据。当我们某个类的实例已经不再需要时,请将其使用 Set [I]Object[/I] = Nothing 的方式来销毁它;
九、……(留给大家填吧)……
第三篇 为什么要使用类
——类的作用及其重要性
现在,我们已经初步的认识了类,甚至我们已经可以自己开始写自己的类了。但是我们却还不是很清楚,类给我们带来了什么?我们为什么要用类?我们就先来看看类的好处吧。
1、代码重用;
2、降低程序的藕合度;
3、增强程序的可拓展性;
4、易修改性;
5、……
“藕合度”指的是程序模块间存在联系的紧密程度。如果一个程序各模块存在的联系太紧密时,就意味后期的修改将会非常复杂,甚至于有时为了修改某一个代码功能时可能要全部工程文件都修改一次,并且由于各模块间的函数与变量相互相交调用,严重的影响了程序的可读性。自然,我们不希望发生这种情况。而类正是我们规避这个情况发生的首选工具。因为类对于外部而言,我们所需要的仅仅是调用类的接口函数,而类内部的结构与运行我们并不需要去关心它。就像一个用来处理数据的类,我们只要约定好类的接口,就可以在外部直接获取或是修改我们想要的数据。但是数据存在哪,用什么方式或是结构组成我们根本就不需要了解,我们所要做的只是在外部直接调用就好了。这就意味着我们一旦约定好了接口,就可以同时开始开发前台与后台的数据处理块。而且,当我们需要修改数据的存储方式时,也会变得更加简单。比如:我们现有一个处理数据的类,它目前是使用文件来存储数据,现在我们需要把数据存储改为SQL数据库,这时我们要做的仅仅是在类内部修改要数据的处理过程,而对于整个工程而言,我们甚至不用做任何的修改就可以直接用了。这样对于大型工程而言,犹为重要,因为我们常常把类封装成DLL,这就意味我们仅仅只需要重新编译这个DLL,然后发布给用户升级就完成了这次修改,否则的话我们只能将整个工程全部修改后重新发布给用户才能使用。如果你经常使用VBA的话就一定会常常遇见Range这个类,Office每一次大版本升级,其内部的文件结构都会有变动,但是我们在VBA中对Range的操作却无需更改就可以直接使用了,这个正是因为Range类虽然内部改动了,但是它的接口还是一样,所以我们原有的VBA工程并不需要改动就可以直接使用了。
现在我们再来通过一个选中区域高亮显示的例子来看看类的其它几个特性吧:
- Option Explicit
- Private Type TypeRageLast
- Range As Range
- ColorIndex As Long
- End Type
- Private Const ColorIndex = 5&
- Private RangeLast() As TypeRageLast
- Private Sub Worksheet_SelectionChange(ByVal Target As Range)
- Dim I As Long
- Dim Range1 As Range
-
- On Error Resume Next
- If UBound(RangeLast) = -1 Then
- ReDim RangeLast(0)
- ElseIf UBound(RangeLast) Then
- For I = 1 To UBound(RangeLast)
- If RangeLast(I).Range.Interior.ColorIndex = ColorIndex Then
- RangeLast(I).Range.Interior.ColorIndex = RangeLast(I).ColorIndex
- End If
- Next I
- End If
-
- I = 0
- ReDim RangeLast(0)
-
- For Each Range1 In Target
- I = I + 1: ReDim Preserve RangeLast(I)
- RangeLast(I).ColorIndex = Range1.Interior.ColorIndex
- Set RangeLast(I).Range = Range1
- Next Range1
- Target.Interior.ColorIndex = ColorIndex
- End Sub
复制代码 这段代码的功能很简单(我们在这里并不去讨论它的功能),只是在工作表中,选中某个区域后就把该区域的背景色设置为并且恢复上次被选中区域的原有背景色。我们只要把上面的代码拷贝到我们需要的工作表的代码中就可以实现这个功能了。但是,当我在实际使用过程中就会发现一些新的问题:
1、如果我们的工作簿中有200个工作表需要这个功能,我们就要复制200次——晕倒……
2、复制完200次后,我们再在各个工作表中实际测试一下。就会发现新的问题:当我们在各个工作表之间切换焦点时,原工作表中最后一次被高亮的区域没有被还原!现在我们需要修改代码——在每一个工作表中Deactivate事件中加上相应处理代码。天啊,又得200次……又要晕了吧?
3、文档开始正式使用了,但是当我们新增一个工作表后发现:我们又得重新切换到VBA中再复制一次代码!如果这个文档是给客户使用的,难道我们还要要求客户每次增加工作表时自己去复制VBA代码?……这次彻底晕了吧?^_^
难道就没有更方便的方法吗?(下接二楼)
[ 本帖最后由 joforn 于 2009-12-15 17:16 编辑 ] |
评分
-
2
查看全部评分
-
|