分享

VBA类之一(初识类)

 郗peng 2021-05-28

第一章 开头篇
                                ——认识类



       Visual Basic是基于对象的编程(注:本文所有的代码和讨论将都以VB为基础模型,不过我会尽量使用一些大家在VBA中常见的例子来做说明的。),所以我们常见的一些东西其实都与类有关。不需惊讶,是的,类其实离我们很近,它们正和我们天天相处。可以夸张的说,如果离开了类,我们的VB(VBA)就无用武之地了。
        我们还是先来看一条非常简单的语句,让我们亲身体会一下类与我们的距离:

  1.  

  2. '例一

  3. Sub Hello()

  4.   Range('A1').Value = 'Hello,world'

  5. 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又是一个类呢。呵呵,不着急,我们还是看看下面的这个例子,或许你就很快就会明白了。

  1.  

  2. '例二

  3. Sub Hello()

  4.   Dim Range1 As Range             '引用一个Range类

  5.   Set Range1 = Range('A1')       '将类实例化

  6.   Range1.Value = 'Hello,world'   '这时,Range1又是一个对象了

  7.   Set Range1 = Nothing                '销毁一个类的实例

  8. End Sub

复制代码

       或许你现在已经发现了,我们引用了一个Range类,并将之实例化后修改了它的属性。而在例一中,我们只不过是将这一切都以隐藏起来,直接对一个对象Range('A1')修改它的属性,但Range('A1')这个对象正是引用了Ragne这个类,才具有了Range类的属性'Value'。类本身并不直接为我们做什么,但是,它却又一直默默的隐藏在幕后规化着我们的动作。是的,这就是类,它往往都是通过对象的方式展现在我们的面前,让我们无时无刻与之交流,却又常常在不经意间忽视了它的存在。现在再回到例一去看,就会很容易的发现,ActiveSheet实际引用了Worksheet类、ActiveWorkbook引用了Workbook类,而Application则引用了和它同名的Application类(这也正是我们会经常被混淆一个概念,一个对象可以和被它所引用的类同名),原来我们在短短一个赋值的语句中,已经在与这么多的类打交道。
        类就像是我们呼吸的空气,我们一直深在其中,却又常常忘却了它的存在。那我们又要怎样来区分类和对象呢?其实它们经常成对的出现在我们面前,只是一个看得见摸得着的,一个却深藏不露。我们可以这样去理解类与对象:类是一个概念或是一种定义,每个类拥有其自己的特征和行为方式,而对象就是某个类的实例。就像'人','人'是一个定义,你一看到'人'就会想到它是直立行走、有头、五官……等各种特征,但当'人'被具体到你、我、或是某一个人的时候,它就是一个对象了。
        既然类与我们和程序息息相关,那么我们更应该去好好的了解它了。让我们准备好行囊,到神秘的类的领地去好好浏览一番吧。


第二篇 走近类
                                ——类的基本写法及注意项



        如果说上一篇让我们看到类隐藏在神秘面纱后的面孔,那么现在我们将要做的就是真正的看清楚它。不过,在此之前我还是要让大家再次熟悉一下面的几个关键词:

Public                外部,可供类或是模块自身外调用;
Private               内部,只能由类或是模块内部直接调用;
Friend                友元,只能由工程内调用(即:在当前工程内部调用时相当于Public,而对外部工程相当于Private。)。
SubFunction  过程(不返回值)和函数(返回值),在类中我们可将其视为类的方法;
Property Get      返回属性的值;
Property Let      设置属性的值;
Property Set      设置对象属性的值(即:该属性含有对象引用)。
        [注:Property往往是成双出现在类中的某一个属性上,甚至可能三者同时出现,当一个类中某个属性只有Property Get时代表这一属性为只读属性,比如在VB工程中Form类的hWnd属性。]
Event           定义用户自定义的事件
        [注:Event可以像声明过程的参数一样来声明事件的参数,但有以下不同:事件不能有带命名参数、Optional 参数、或者 ParamArray参数。事件没有返回值。];
RaiseEvent      引发在一个类、窗体、或者文档中的模块级中声明的一个事件。
Implements      指定要在包含该语句的类模块中实现的接口或类。


       当我们熟悉上面的几个关键词后,再来分析类,就会发现类其实也挺简单。我们将从一个简单的类开始来正式的学习类的定义和使用方法。不过在此之前,我建议大家先去读读qee用兄写的一个帖子:VBA类:隐者的秘密qee用兄在那个帖子中图文并茂,已经将类清晰的描绘出来,并亲自带领我们写了一个类。在此,让我们向qee用兄道声辛苦,并送上万分感激。好了,现在,我们来写一个非常简单的类,用它来实现两个数的四则运算。

  1.  

  2. '类的名称定义为:四则运算

  3. Option Explicit

  4.  

  5. Public Event OnError(ByVal Number As Long, ByVal Description As String, ByVal Source As String)

  6.  

  7. Private lNum1 As Double, lNum2 As Double

  8.  

  9. Public Property Let Number1(ByVal Number As Double)

  10.   lNum1 = Number

  11. End Property

  12.  

  13. Public Property Get Number1() As Double

  14.   Number1 = lNum1

  15. End Property

  16.  

  17. Public Property Let Number2(ByVal Number As Double)

  18.   lNum2 = Number

  19. End Property

  20.  

  21. Public Property Get Number2() As Double

  22.   Number2 = lNum2

  23. End Property

  24.  

  25. Public Property Get 和() As Double

  26.  

  27.   On Error GoTo Error01

  28.   

  29.   和 = lNum1 + lNum2

  30.   Exit Property

  31. Error01:

  32.   RaiseEvent OnError(Err.Number, Err.Description, Err.Source)

  33.   Err.Clear

  34.   和 = 0

  35. End Property

  36.  

  37. Public Property Get 差() As Double

  38.  

  39.   On Error GoTo Error02

  40.   

  41.   差 = lNum1 - lNum2

  42.   Exit Property

  43. Error02:

  44.   RaiseEvent OnError(Err.Number, Err.Description, Err.Source)

  45.   Err.Clear

  46.   差 = 0

  47. End Property

  48.  

  49. Public Property Get 积() As Double

  50.  

  51.   On Error GoTo Error03

  52.   

  53.   积 = lNum1 * lNum2

  54.   Exit Property

  55. Error03:

  56.   RaiseEvent OnError(Err.Number, Err.Description, Err.Source)

  57.   Err.Clear

  58.   积 = 0

  59. End Property

  60.  

  61. Public Property Get 商() As Double

  62.  

  63.   On Error GoTo Error04

  64.   

  65.   商 = lNum1 / lNum2

  66.   Exit Property

  67. Error04:

  68.   RaiseEvent OnError(Err.Number, Err.Description, Err.Source)

  69.   Err.Clear

  70.   商 = 0

  71. End Property

  72.  

  73. Public Sub ClearNumber()

  74.   lNum1 = 0: lNum2 = 0

  75. End Sub

复制代码

       然后,将下面的代码写到ThisWorkbook模块中:

  1.  

  2. Option Explicit

  3.  

  4. Private WithEvents Class1 As 四则运算

  5.  

  6. Sub TestClass()

  7.   If Class1 Is Nothing Then Set Class1 = New 四则运算

  8.   Class1.ClearNumber

  9.   Class1.Number1 = 9

  10. '  Class1.Number2 = 8

  11.   Debug.Print Class1.和

  12.   Debug.Print Class1.差

  13.   Debug.Print Class1.积

  14.   Debug.Print Class1.商

  15. End Sub

  16.  

  17. Private Sub Class1_OnError(ByVal Number As Long, ByVal Description As String, ByVal Source As String)

  18.   MsgBox '类中发生错误,错误代码:' & Number & ' 错误提示:' & Description & ' 错误源:' & Source

  19. 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工程并不需要改动就可以直接使用了。
现在我们再来通过一个选中区域高亮显示的例子来看看类的其它几个特性吧:

  1.  

  2. Option Explicit

  3.  

  4. Private Type TypeRageLast

  5.   Range       As Range

  6.   ColorIndex  As Long

  7. End Type

  8.  

  9. Private Const ColorIndex = 5&

  10. Private RangeLast() As TypeRageLast

  11.  

  12. Private Sub Worksheet_SelectionChange(ByVal Target As Range)

  13.   Dim I As Long

  14.   Dim Range1 As Range

  15.   

  16.   On Error Resume Next

  17.   If UBound(RangeLast) = -1 Then

  18.     ReDim RangeLast(0)

  19.   ElseIf UBound(RangeLast) Then

  20.     For I = 1 To UBound(RangeLast)

  21.       If RangeLast(I).Range.Interior.ColorIndex = ColorIndex Then

  22.         RangeLast(I).Range.Interior.ColorIndex = RangeLast(I).ColorIndex

  23.       End If

  24.     Next I

  25.   End If

  26.   

  27.   I = 0

  28.   ReDim RangeLast(0)

  29.   

  30.   For Each Range1 In Target

  31.     I = I + 1: ReDim Preserve RangeLast(I)

  32.     RangeLast(I).ColorIndex = Range1.Interior.ColorIndex

  33.     Set RangeLast(I).Range = Range1    

  34.   Next Range1

  35.   Target.Interior.ColorIndex = ColorIndex

  36. End Sub

复制代码

这段代码的功能很简单(我们在这里并不去讨论它的功能),只是在工作表中,选中某个区域后就把该区域的背景色设置为并且恢复上次被选中区域的原有背景色。我们只要把上面的代码拷贝到我们需要的工作表的代码中就可以实现这个功能了。但是,当我在实际使用过程中就会发现一些新的问题:
       1、如果我们的工作簿中有200个工作表需要这个功能,我们就要复制200次——晕倒……
       2、复制完200次后,我们再在各个工作表中实际测试一下。就会发现新的问题:当我们在各个工作表之间切换焦点时,原工作表中最后一次被高亮的区域没有被还原!现在我们需要修改代码——在每一个工作表中Deactivate事件中加上相应处理代码。天啊,又得200次……又要晕了吧?
       3、文档开始正式使用了,但是当我们新增一个工作表后发现:我们又得重新切换到VBA中再复制一次代码!如果这个文档是给客户使用的,难道我们还要要求客户每次增加工作表时自己去复制VBA代码?……这次彻底晕了吧?^_^
       难道就没有更方便的方法吗?

       当然有了。我们就用类来试试吧,我们先建一个clsSelHeightColor类:

  1.  

  2. '类名:clsSelHeightColor

  3. Option Explicit

  4.  

  5. Private Type TypeRageLast

  6.   Range       As Range

  7.   ColorIndex  As Long

  8. End Type

  9.  

  10. Private Const ColorIndex = 5&

  11. Private WithEvents mSheet As Worksheet

  12. Private WithEvents Workbook  As Workbook

  13. Private RangeLast() As TypeRageLast

  14.  

  15. Private Sub mSheet_Activate()

  16.   Dim I As Long

  17.   Dim Range1 As Range

  18.   

  19.   On Error Resume Next

  20.   I = 0

  21.   ReDim RangeLast(0)

  22.   

  23.   For Each Range1 In Selection

  24.     I = I + 1: ReDim Preserve RangeLast(I)

  25.     RangeLast(I).ColorIndex = Range1.Interior.ColorIndex

  26.     Set RangeLast(I).Range = Range1

  27.     Range1.Interior.ColorIndex = ColorIndex

  28.   Next Range1

  29. End Sub

  30.  

  31. Private Sub mSheet_Deactivate()

  32.   mSheet_SelectionChange Nothing

  33. End Sub

  34.  

  35. Private Sub mSheet_SelectionChange(ByVal Target As Range)

  36.   Dim I As Long

  37.   Dim Range1 As Range

  38.   

  39.   On Error Resume Next

  40.   

  41.   If UBound(RangeLast) Then

  42.     For I = 1 To UBound(RangeLast)

  43.       If RangeLast(I).Range.Interior.ColorIndex = ColorIndex Then

  44.         RangeLast(I).Range.Interior.ColorIndex = RangeLast(I).ColorIndex

  45.       End If

  46.     Next I

  47.   End If

  48.   

  49.   I = 0

  50.   ReDim RangeLast(0)

  51.   

  52.   For Each Range1 In Target

  53.     I = I + 1: ReDim Preserve RangeLast(I)

  54.     RangeLast(I).ColorIndex = Range1.Interior.ColorIndex

  55.     Set RangeLast(I).Range = Range1

  56.   Next Range1

  57.   Target.Interior.ColorIndex = ColorIndex

  58. End Sub

  59.  

  60. Public Property Set Sheet(ByVal mSheet1 As Worksheet)

  61.   If Not (mSheet Is Nothing) Then mSheet_SelectionChange Nothing

  62.   Set mSheet = mSheet1

  63.   Call mSheet_Activate

  64. End Property

  65.  

  66. Public Property Get Sheet() As Worksheet

  67.   Set Sheet = mSheet

  68. End Property

  69.  

  70. Private Sub Class_Initialize()

  71.   ReDim RangeLast(0)

  72. End Sub

  73.  

  74. Private Sub Class_Terminate()

  75.   mSheet_SelectionChange Nothing

  76.   Erase RangeLast

  77. End Sub

复制代码

然后再把下面的代码复制到ThisWorkbook中的代码框中:

  1.  

  2. Option Explicit

  3.  

  4. Dim mSheets As Collection

  5.  

  6. Private Sub Workbook_BeforeClose(Cancel As Boolean)

  7.   Set mSheets = Nothing

  8. End Sub

  9.  

  10. Private Sub Workbook_NewSheet(ByVal Sh As Object)

  11.   Dim clsSheet As New clsSelHeightColor

  12.   

  13.   Set clsSheet.Sheet = Sh

  14.   mSheets.Add clsSheet

  15.   Set clsSheet = Nothing

  16. End Sub

  17.  

  18. Private Sub WorkBook_Open()

  19.   Dim clsSheet As clsSelHeightColor

  20.   Dim Sheet As Worksheet

  21.   

  22.   On Error Resume Next

  23.   

  24.   Set mSheets = New Collection

  25.   For Each Sheet In Sheets

  26.       Set clsSheet = New clsSelHeightColor

  27.       Set clsSheet.Sheet = Sheet

  28.       mSheets.Add clsSheet

  29.       Set clsSheet = Nothing

  30.   Next

  31. End Sub

复制代码

保存后重开工作簿测试……嘿嘿,全部搞定!而且当我们要增加的Deactivate事件中的代码仅仅只需类中添加一次,只有一行代码(上面的类中已经添加好了),方便吧?当我们新增工作表时,也不再需要去复制一次代码了,一行代码都不用写了就已经实现了这个功能。当其它工作簿也需要这个代码时,我们只要在ThisWorkbook代码中增加相应的代码,并导入这个类就行了,比一个一个表去复制代码方便多——一想到相同的代码要在每个工作表中复制一次就头晕了。
       在此,我们对类的功能和特性有了一定的了解,不过,还有更多的东西在等待我们去发现。当下面向对象编程如此风行,类在其中的功劳是无与伦比的。
       现在,还犹豫什么?快去和类做一次“0距离接触”吧……

       在此,此文也终于要作一个小小的终结了。大家也跟随我初步对类作了点了解,也算是对类有了个第一印象吧。当然,类的高级应用和复杂型的类我们并没有去了解,或许哪天你会引领我去浏览一番?
       我也谢谢大家浪费不少时间来看完这个帖子,如果对我有意见或是觉得我在胡说八道要拍我砖头的,请下手留点情——我很怕疼,有意见扔扔鸡蛋或是RMB就好了。如果觉得看完对你有所帮助的话就帮顶下吧,或是你看完有什么问题的话也可以直接问,只要我会的我一定回(太难的问题还请恕我无能为力)。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多