Access 开发框架(翻译+改编)系列之五——监管类

在系列之四中,我们讨论了为什么要使用类,也看到了文本框背景色自动变化的示例,从繁琐的方法:运行窗体本地代码(f … 继续阅读“Access 开发框架(翻译+改编)系列之五——监管类”

在系列之四中,我们讨论了为什么要使用类,也看到了文本框背景色自动变化的示例,从繁琐的方法:运行窗体本地代码(frmPeople1);到更简洁的方法:为文本框创建控件类。我们看到控件类在窗体的头部被定义,然后直接初始化(frmPeople2),或者使用类工厂(class factory)函数初始化(frmPeople3/4)。为了进一步简化窗体代码,现在让我们来了解一下我称之为“监管类”(supervisor class)的类。

监管类是指加载和管理其他类的类。

注意到没有?系列之四中,窗体的内建类(built in class)为创建这些控件类做了很多工作。当我们添加新的控件类,比如组合框,复选框等,情况只会变得更加糟糕,如果每个控件类型都有50个实例的话,事情会变得不可想象的复杂。我们在frmPeople4中看到的所有代码都不得不在任何你想实现类似功能的窗体中不断重复出现。我们需要创建一个我们自己的窗体类,将这些创建控件类的代码都放在里面。然后我们只需要在每个窗体中创建和销毁这个监管类。本质上来讲,我们所做的,不过是将窗体中的代码转移到一个类中,用名为Init()和Term()的方法,来创建和销毁我们的新窗体类。

好了,新类dclsFrm(窗体类)的代码可以在示例文件中看到5.1监管类示例

监管类 dclsFrm 代码

'from www.Jasoftiger.com by Jasoftiger @ Apr 26,2017
Option Compare Database
Option Explicit

'定义一个集合变量,用来存储控件类的实例
Private mcolClasses As Collection

'定义自己的窗体
Private mfrm As Form

现在我们应该对类的头部有点熟悉了。在这里,我们定义了集合变量mcolClasses和窗体变量mfrm,集合变量用来存储控件类的实例,比如dclsCtlTextBox。mfrm用来保存指向实际窗体的指针。

Private Sub Class_Initialize()

    '创建集合变量的实例
    Set mcolClasses = New Collection
    
End Sub

Public Sub Init(lfrm As Form)

    '将传入的指针保存到类的私有变量中
    Set mfrm = lfrm
    
    '遍历窗体上的所有控件,找到控件后,实例化该控件,控件对象保存在集合变量中
    FindControls
    
End Sub


所有的类都有Initialize事件,当类被实例化的时候,该事件会自动执行。我使用Initialize事件来初始化类要用到的所有对象。mcolClasses集合对象,是该类用来存放所有控件对象的容器,在使用之前,必须将其实例化。但是,Initialize事件是不能带有任何参数的,而类初始化的时候,需要传入一些参数才能配置好它的功能。所以我使用了名为Init()方法。Init()将传入的指向窗体的指针保存在本地变量中。现在我们有了一个指向窗体的变量,所以我们可以调用FindControls来遍历窗体的控件集,然后加载控件类。

Private Sub FindControls()
    Dim ctl As Access.Control
    
    '遍历窗体的所有控件
    For Each ctl In mfrm.Controls
        With ctl
            Select Case .ControlType
                Case acTextBox      '找到TextBox控件
                    '实例化一个dclsCtlTextBox控件,将其保存在集合对象中,以其名字作为键值
                    mcolClasses.Add New dclsCtlTextBox, .Name
                    '执行该dclsCtlTextBox控件的Init方法,将控件传递给对象
                    mcolClasses(.Name).Init ctl
'                Case acComboBox
'
'                Case acCheckBox
'
'                Case acListBox
'
'                Case Else
                
            End Select
        End With
    Next ctl
End Sub

在上一系列中,我们使用的是ClassFactory来为控件创建我们的类对象,在这里的FindControls中,我们进一步简化了代码。这个子例程遍历窗体上的所有控件。它检查每个控件的控件类型,然后根据控件类型,为每个控件实例化一个相应的对象(目前我们只为文本框创建了类,所以这里我们只为文本框控件实例化对象。我们很快就会为组合框,复选框,列表框等更多的控件创建类。)这个例程也将相应的为这些控件类型实例化对象。

Private Sub Class_Terminate()
    Term
End Sub

Public Sub Term()

    '销毁集对象中的所有对象后,再销毁集合对象本身
    ClsDestroy
    
    '释放指向实际窗体的指针,解除环形引用
    Set mfrm = Nothing
    
End Sub

Initialize事件是在类被实例化的时候自动运行,对应的,Terminate事件会在类对象被销毁的时候自动运行。在Terminate事件中,直接调用Term例程。Term例程先通过例程ClsDestroy销毁集合对象mcolClasses中的所有对象,然后释放窗体指针。

译者注:在这里,为什么不直接将Term例程中的代码写到Terminate事件中呢?根源在于环形引用问题。对于环形引用情况来讲,必须先将类中参与环形引用的对象(mfrm),释放其指向类外部对象(一个真实的窗体)的指针,变成单向引用后,类(dclsFrm)对象才能被顺利释放掉,Terminate事件才会被执行。

我们可以将一个类中使用到的所有对象分成2种,第1种是参与环形引用的对象,第2种才是普通的非环形引用的对象。只有第2种才能放在Terminate事件中。而第1种要在解除环形引用后,类才能被正确销毁,Terminate事件才会发生

在类的实际设计中,区分这2种情况需要花费更多的精力,为了简便起见,选择了不去区分,全部视为第1种情况。在释放类(dclsFrm)对象之前,首先运行Term方法(注意到Term方法为Public没有?),先将类中使用的对象全部销毁,然后再销毁类对象本身。

这样一来,Terminate事件好像就没有了存在的必要了。在这里还是保留它,在Terminate事件中调用Term方法,这样做一是为了统一起见,二是为了防止用户在使用类时,忘记了调用Term方法,而直接释放了类对象。毕竟重复释放类中使用的(包含的)对象并不是一件坏事。

'销毁集对象中的所有对象后,再销毁集合对象本身
Private Sub ClsDestroy()
    Dim obj As Object
    
    '因为Term()会被执行2次,第二次执行时,mcolClasses已被设置为nothing,不需要再次清空
    If Not mcolClasses Is Nothing Then
        For Each obj In mcolClasses
            obj.Term
            Set obj = Nothing
        Next obj
        Set mcolClasses = Nothing
    End If
End Sub

类的私有例程ClsDestory遍历类的集合变量mcolClasses中的所有的控件类实例,首先运行该对象的Term方法,然后将其销毁。最后将集合变量本身mcolClasses也销毁了。

窗体的内建类(built in class)代码

dclsFrm这个类应该如何使用呢?现在,我们来看看实际窗体中的代码。(参见frmPeople55.1监管类示例

'from www.Jasoftiger.com by Jasoftiger @ Apr 29,2017
Option Compare Database
Option Explicit

Private fdclsFrm As dclsFrm

Private Sub Form_Open(Cancel As Integer)
    Set fdclsFrm = New dclsFrm
    fdclsFrm.Init Me
End Sub

Private Sub Form_Close()
    fdclsFrm.Term
    Set fdclsFrm = Nothing
End Sub

在窗体的头部,我们定义了一个类型为dclsFrm的变量fdclsFrm。然后在窗体的Open事件中,将其实例化,最后将窗体本身(Me,Me只能在类中使用,指代该类的实例本身。)作为参数传递给dclsFrm类的Init方法。最后在我们关闭窗体的时候,先调用dcldFrm类的Term方法,然后销毁fdclsFrm对象。

如果你记得上一系列中窗体内建类中处理控件的代码(在frmPeople4中),你会发现,这个窗体(frmPeople5)中的代码大大的减少了。而且更有意思的是,如果我们在窗体上再继续增加100个文本框,我们一行代码都不需要增加!现在,你可以在窗体内建类中写专门适用于当前应用程序相关的功能代码,而不再将适用所应用程序的通用代码放置其中。

dclsFrm是框架(Framework)中为数不多的几个监管类之一。我们会在后续的系列中介绍其他的几个监管类。

我们现在有了一个我们可以称之为迷你型的框架。我们有了一个窗体类,我们可以使用它来将更多的窗体功能挂上去。我们有了一个控件类dctlTextBox,它给我们演示了控件自动扫描和加载的概念,我们也可以添加更多的功能到这个类上,而不仅仅只是背景色的自动变化。更加重要的是,我们需要让框架(framework)的基础被创建出来,而这便是另一个叫做clsFramework的监管类要做的事情了。

译者注:其实frmPeople5中的代码还有进一步简化的可能性!Form_Close事件代码完全可以移到类dclsFrm中去处理。我们只需要在类dclsFrm的头部定义窗体变量mfrm时添加一个关键字 WithEvents即可。由于本系列主要讲监管类,具体不再展开讲了。可以参见示例5.25.2监管类dclsFrm示例

Access 开发框架(翻译+改编)系列之四——类是什么?为何要使用类?

类是什么? 类是 VB / VBA 中的一个模块,它是一个特殊的模块。类用于 Hold 住代码和数据,这些代码 … 继续阅读“Access 开发框架(翻译+改编)系列之四——类是什么?为何要使用类?”

类是什么?

类是 VB / VBA 中的一个模块,它是一个特殊的模块。类用于 Hold 住代码和数据,这些代码和数据用于描述某个对象,实现该对象的行为和属性。让类区别于 VB / VBA 中其他模块的是,类是完整加载于内存中的,而且可以加载不止一次。实际上,每实例化一个对象的时候,类就会被加载一次。

在设计类的时候,使用数据范式模型是很有用的。表是对象的模型,在设计表的时候,最好不要将 2 个及以上的对象建模到 1 张表里面。同样的道理,类也应该只对单一对象建模。我们应该避免让类 Hold 住或实现 2 个及以上的不同的对象。假设你有一个“银行”类,那么这个类应该使用诸如“账户”,“客户”,“支票”,“存款”等银行相关的类,这些类中,每一个都只需要存储它所建模的对象的数据,也只需要放置它所建模的对象行为的实现代码。如果一个类既描述银行,同时又描述账户,客户等,那注定乱成一团。

所以,如果一个类描述了一张支票,那么它肯定有支票号码属性,签发日期属性,金额属性,签名属性等。直到被加载到内存,这个类才可以描述任何能与它相匹配的支票。在这个类加载到内存,这些属性都赋值数据之前,它是不能描述任何东西的。这张支票号码是 1234,签发日期是 2000 年 1 月 1 日,金额 $123.45,是由史密斯先生签发。这个类的实例才描述了一张特定的支票。如果有一堆支票,你需要用类来管理它们,那么你需要为每一张支票都加载一个类的实例。

类通常都会在内存中加载多次,用来描述某个对象的多个实例。然而这并不是必须。我们也可以设计只加载一次的类,在这种情况下,开发者主要看中了类的封装特性。将所有的代码和变量都集中放在一个地方,类被实例化成对象后,这个对象知道如何来做一些事情。举例来说,我的框架有一个框架类。这个类只加载一次。这个类知道如何从表中读取加载参数,配置自己的属性,它也知道如何加载其他的服务类,例如 MD5 加密类,它也知道如何计算应用程序加载的对象的个数。这样,在标准模块中定义一个变量,运行一个函数,这个函数将我的框架类的实例赋给这个变量,然后初始化这个类的实例,一环套一环的事件发生,让其他的类也加载到内存,我的应用程序被设置好,准备开始工作了。当这些做完后,我可以调用我的框架类的方法,为我提供服务:压缩 / 解压缩文件,加密 / 解密文件,以及更一般的充当框架管理的角色。

总的来说,类是一种特殊的模块,这个模块中存储的是代码和数据,这些代码和数据是用来描述某个对象的行为和属性,每个加载到内存中的实例都对应着一个对象,这些对象可以用来帮我们做一些事情。

为何要使用类?

我已经简单的描述了类,现在,让我们来使用它。首先,每一个窗体都有(或都可以有)一个内建类(Built in class),当加载这个窗体时,这个内建类的一个实例就被加载到内存。这个内建类中有一些事件,例如打开事件(Open),关闭事件(Close)和更新后事件(AfterUpdate)。它也能被用来为窗体本身设置属性。对任何类来讲,ME 这个关键字是用来指代这个类的当前实例。换句话讲,如果你想在代码中设置窗体的标题属性,可以使用代码 Me.Caption=”Some text” 。因为“窗体后面的代码” 是驻留在窗体的类中,任何在窗体中写过代码的 Access 开发者就已经使用过类了。

类是窗体的一部分,这个事实非常美好的一点在于,你可以导出这个窗体到另一个数据库,窗体模块(类)中的代码也被跟着一起导出了。不幸的是,这也导致了一些维护上的困难,如果你在窗体模块(类)中写了非常复杂的代码,在另一个窗体中也需要相同的代码的话。很多开发者只是简单的复制和粘贴这些代码到另一个窗体中就完事了。如果代码有 Bug 怎么办?你需要打开 2 个窗体,清除这个 Bug。如果你将这个窗体导出到了另外一个项目中怎么办?你需要打开 2 个项目,修改代码清除 Bug。

认识到这是一个问题后,一些开发者开发了库(Libraries),将这些代码保存到他们自己的库中。这种方式运行得非常好(这也是我的做法)。现在,如果一个文本框的 OnEnter 事件需要调用一个函数,这个函数在库中,两个窗体都调用这个相同的函数。如果这个函数有 Bug,在库中修复这个 Bug,任何使用这个函数的地方就都没问题了。这种方法比之前好多了。

然而我们仍然有事件本身的问题。为了让复选框的 OnEnter 事件调用一个方法,这个事件会执行一段代码。对 99% 的 Access 程序员来讲,这段代码都会放在复选框所在窗体的窗体模块(类)中。我并不是说这种方式有什么不对,但是当一个大的窗体上有 10 个 标签页,80 个控件在其上时,每个控件触发 3 或 4 个不同的事件,再加上半打窗体本身的事件,再加上 30 个一般的执行调用函数,再加上……你懂了吧?现在这个窗体就像噩梦一样,非常难以维护。在海量的事件代码中,翻上翻下的来查找修改一点东西。

事实上,其他 Office 程序 (Word,Excel,Access,Powerpoint)的主要问题之一就是控件没有自己的类,与窗体不一样。因此,窗体类被当做了控件类。还记得之前说过的吗?类应该只为一个对象建模。我们现在已经遇到了一个类既为窗体建模,又为各种不同的控件建模。嗯……

下述示例演示了,当用户在控件中移动时,如何改变活动文本框的背景颜色。打开 frmPeople1 窗体,按 Tab 键观察一下活动光标。(点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Option Compare Database
Option Explicit

Private Const mclngBackColor As Long = 16777088         '颜色代码,用来设置文本框背景色
Private mlngBackColorOrigFName As Long
Private mlngBackColorOrigLName As Long

Private Sub txtFName_Enter()
    mlngBackColorOrigFName = txtFName.BackColor  '当进入文本框,保存原始的背景色
    txtFName.BackColor = mclngBackColor     '将背景色设置成我们喜欢的颜色
End Sub

Private Sub txtFName_Exit(Cancel As Integer)
    txtFName.BackColor = mlngBackColorOrigFName     '退出文本框时,还原背景色
End Sub

Private Sub txtLName_Enter()
    mlngBackColorOrigLName = txtLName.BackColor  '当进入文本框,保存原始的背景色
    txtLName.BackColor = mclngBackColor     '将背景色设置成我们喜欢的颜色
End Sub

Private Sub txtLName_Exit(Cancel As Integer)
    txtLName.BackColor = mlngBackColorOrigLName     '退出文本框时,还原背景色
End Sub

将这段代码放到一个窗体模块中,添加 2 个文本框,命名为 txtFName 和 txtLName 。打开这个窗体,当光标进入到这 2 个文本框中时(OnEnter 事件发生),文本框的背景颜色变成了淡蓝色。当光标退出这 2 个文本框时 (OnExit 事件发生),文本框的背景颜色还原成原来的颜色。感觉很酷吧?但是来看看我们遇到了什么吧。我们需要在窗体顶部为每一个我们想使用该效果的控件定义一个变量,来保存旧的背景色。如果只有一两个控件需要使用该效果,那还算好。但是如果有 20 个或 40 个控件需要使用该效果,那该怎么办?更进一步来讲,我们不仅需要变量来存储这些东西,我们还需要为每一个控件设置 OnEnter 和 OnExit 这 2 个事件,乘以 20 或 40  个控件,那就是40 或 80 个事件代码,够痛苦吧?

幸运的是,我们可以创建类来为控件建模,比如一个文本框。我将选用这种简单的控件作为示例,以方便你理解它的工作原理,之后我会为其添加故障排查相关的代码。

以下是类 dclsCtlTextBox 中的代码。它代表着文本框的类,很简单也相当有用。(点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Option Compare Database
Option Explicit

'定义一个带有事件的文本框
Private WithEvents mtxt As TextBox

'一个常量被定义为[Event Procedure]
Private Const mcstrEventProcedure="[Event Procedure]"

'漂亮的蓝色颜色代号,将会被设置成文本框的背景色
Private Cosnt mclngBackColor As Long=16777088

'保存文本框原来背景颜色的地方
Private mlngBackColorOrig As Long

注意到没有?我们有一个保存原背景色的变量;有一个常量用来保存我们选择的颜色,当文本框获得焦点之后,文本框的背景色会被设置成它;还有一个保存指向某个具体的文本框的指针的变量,而且被定义成能响应事件(Withevents)!这告诉程序这个类将会响应这个控件的事件。

'每个类的 Init 函数初始化这个类,传入指向某个具体控件的指针
Sub Init(ByRef ltxt as TextBox)
    '将传入的指针保存到类的私有变量中
    set mtxt=ltxt

    '将控件的 OnEnter 属性设置为 [Event Procedure]
    mtxt.OnEnter = mcstrEventProcedure

    '为 OnExit 做相同的设置
    mtxt.OnExit = mcstrEventProcedure

End Sub

接下来,我们有一个 Init 子程序,传入一个指向某个具体文本框的指针,我们将这个指针保存在类的头部定义的私有变量中。同时我们也将这个变量的 OnEnter 和 OnExit 属性设置成字符串 [Event Procedure]。

解释:将窗体或者控件的任何一个事件的事件属性设置成字符串 “[Event Procedure]”,会导致该窗体或控件在发生该事件时,为这个事件做广播,通知所有指向该窗体或控件的 WithEvents 变量执行该事件代码。因为这个原因,在类的 Init() 方法中,我们总是会将我们想要处理的事件的事件属性设置成 [Event Procedure],这样,当对象发生这个事件时,类中的事件代码就能够被执行。

'每个类的 Term 方法清除所有指向类中的对象的指针
Sub Term()
    '将指向控件的指针设置成Nothing
    Set mtxt=Nothing
End Sub

'这里有文本框的 OnEnter 和 OnExit 事件的事件处理代码
Private Sub mtxt_Enter()
    '当文本框获得焦点时,将原来的背景色保存下来
    mlngBackColorOrig = mtxt.BackColor
    '将文本框的背景颜色设置成我们预先定义的颜色
    mtxt.BackColor = mclngBackColor
End Sub

Private Sub mtxt_Exit(Cancel As Integer)
    '当文本框失去焦点时,还原其背景颜色
    mtxt.BackColor = mlngBackColorOrig
End Sub

Term 方法用来释放或清除指向文本框控件的指针。最后文本框的OnEnter和OnExit事件的事件代码处理文本框的背景颜色变化和还原。

设计好了 dclsTextBox 类之后,我们就可以开始为每个我们想控制的文本框做类的初始化了。我将使用我的示例数据库中的代码来处理4个文本框。这些代码保存在窗体 frmPeople2中。(点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Option Compare Database
Option Explicit

Private fdclsCtlTextBoxFName As dclsCtlTextBox
Private fdclsCtlTextBoxLName As dclsCtlTextBox
Private fdclsCtlTextBoxWeight As dclsCtlTextBox
Private fdclsCtlTextBoxHeight As dclsCtlTextBox

我们定义了类的 4 个变量,每个用一个不同的名字。

Private Sub Form_Open(Cancel As Integer)
    Set fdclsCtlTextBoxFName = New dclsCtlTextBox
    fdclsCtlTextBoxFName.Init txtFName
    
    Set fdclsCtlTextBoxLName = New dclsCtlTextBox
    fdclsCtlTextBoxLName.Init txtLName
    
    Set fdclsCtlTextBoxWeight = New dclsCtlTextBox
    fdclsCtlTextBoxWeight.Init txtWeight
    
    Set fdclsCtlTextBoxHeight = New dclsCtlTextBox
    fdclsCtlTextBoxHeight.Init txtHeight

End Sub

窗体的 Open 事件中,为这些类变量做初始化。

Private Sub Form_Close()
    fdclsCtlTextBoxFName.Term
    Set fdclsCtlTextBoxFName = Nothing
    
    fdclsCtlTextBoxLName.Term
    Set fdclsCtlTextBoxLName = Nothing
    
    fdclsCtlTextBoxWeight.Term
    Set fdclsCtlTextBoxWeight = Nothing
    
    fdclsCtlTextBoxHeight.Term
    Set fdclsCtlTextBoxHeight = Nothing

End Sub

窗体的Close 事件帮我们做清扫工作。

就是这样!注意到没有?在窗体中,你找不到任何文本框控件的事件处理程序!原因当然就在于文本框类 dclsCtlTextBox 可以响应这些事件,我们没必要在窗体中再去响应了。(译者注:当然你完全可以在窗体中为该事件写事件代码,事件发生时,dclsCtlTextBox 中的事件代码会首先获得执行,然后程序会去执行窗体中的事件代码,也就是说,控件或窗体的事件完全可以在多个地方都获得响应和执行。

我听到你已经开始抱怨了:“确实很酷,但是做初始化和善后清除工作的代码还是有不少啊!”你说的没错,使用这些繁琐的代码只是方便你理解,一旦你理解类的使用方法,我可以开始介绍一些技巧,简化我们的代码,如 frmPeople3 所示。(点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Option Compare Database
Option Explicit

Private fdclsCtlTextBoxFName As dclsCtlTextBox
Private fdclsCtlTextBoxLName As dclsCtlTextBox
Private fdclsCtlTextBoxWeight As dclsCtlTextBox
Private fdclsCtlTextBoxHeight As dclsCtlTextBox

Private Sub Form_Open(Cancel As Integer)
    ClassFactory fdclsCtlTextBoxFName, txtFName
    ClassFactory fdclsCtlTextBoxLName, txtLName
    ClassFactory fdclsCtlTextBoxWeight, txtWeight
    ClassFactory fdclsCtlTextBoxHeight, txtHeight

End Sub

Private Sub Form_Close()
    ClsDestroy fdclsCtlTextBoxFName
    ClsDestroy fdclsCtlTextBoxLName
    ClsDestroy fdclsCtlTextBoxWeight
    ClsDestroy fdclsCtlTextBoxHeight

End Sub

Function ClassFactory(ByRef ldclsCtlTextBox As dclsCtlTextBox, ByRef ltxt As TextBox)
    Set ldclsCtlTextBox = New dclsCtlTextBox
    ldclsCtlTextBox.Init ltxt
End Function

Function ClsDestroy(ByRef ldclsCtlTextBox As dclsCtlTextBox)
    ldclsCtlTextBox.Term
    Set ldclsCtlTextBox = Nothing
End Function

窗体 frmPeople3 中使用了类工厂(ClassFactory)函数和类清除(ClsDestroy)函数。

如果使用集合(Collection)去保存我们的控件类变量,我们就不再需要为每个控件都去定义一个类变量了,代码就更加的简洁了!如 frmPeople4 所示。(点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Option Compare Database
Option Explicit

Private colClasses As Collection

Private Sub Form_Open(Cancel As Integer)
    Set colClasses = New Collection
    ClassFactory txtFName
    ClassFactory txtLName
    ClassFactory txtWeight
    ClassFactory txtHeight

End Sub


Private Sub Form_Close()
    ClsDestroy
End Sub


Function ClassFactory(ByRef ltxt As TextBox)
    Dim ldclsCtlTextBox As dclsCtlTextBox
    
    Set ldclsCtlTextBox = New dclsCtlTextBox
    ldclsCtlTextBox.Init ltxt
    
    colClasses.Add ldclsCtlTextBox, ltxt.Name
    
End Function


Function ClsDestroy()
    Dim obj As Object
    
    For Each obj In colClasses
        obj.Term
    Next obj
    
    Set colClasses = Nothing
    
End Function

是不是更加的简洁了?我们可以创建一个窗体类来帮助我们做这些事情!!这正是后面章节要做的事情。

译注:如果说前3篇讲得都是很抽象概念的话,那么从本篇开始往后,你将会非常具体的了解 Access 开发框架的每一个细小的部分,从最基础的部分开始,到一个完整的框架构成。

点击该连接:Access框架研究系列之4 控件类示例下载本章所有示例代码文件

Access 开发框架(翻译+改编)系列之三——集合、类和垃圾回收

Access 的垃圾回收(garbage collection)跟它的健壮性相比差得太多。当我们开始使用更高级 … 继续阅读“Access 开发框架(翻译+改编)系列之三——集合、类和垃圾回收”

Access 的垃圾回收(garbage collection)跟它的健壮性相比差得太多。当我们开始使用更高级的结构,比如类(class)和集合(collection)的时候,我们可能会遇到对象没有从内存中释放的问题。如果你保存了一个指向任何对象,例如窗体,控件或自定义类的指针,并且你永远不释放这个指针,然后你关闭 Access,表面上看,你好像关闭了,但实际上,内存并没有被释放出来还给操作系统。这会导致内存泄露,以及一些古怪的问题,甚至会阻止 Access 的关闭。

假定你使用一个类,这个类可以初始化其他的类。类A实例化后赋给变量 clsA。clsA 实例化其他 10 个类,将这 10 个类的对象变量保存在集合(collection)中。当 clsA 被置为 nothing 后,一般会认为 Access 会自动的清除掉这个类中的所有变量,包含集合中的对象变量,指向记录集的变量,指向窗体和控件的变量等。不幸的是,实际情况并不总是这样。当它不是这样时,你要确保它能被清除。(翻译这段的时候,我特意在 Access 2010上做了一点测试,实际情况似乎总是能正常被清除。有兴趣的可以下载这个文件3.1 通过类创建一个窗体的10个实例,打开该文件后,可以运行宏“CreateObjA”, 实例化 clsA,clsA 在初始化的时候会创建10个Form1,将其保存在它的私有集合变量mcol中。你可以看到10个Form1的拷贝被创建并打开了。最后运行宏“ReleaseObjA”,简单的将 ObjA 置为Nothing。你会发现ObjA的10个Form1对象全部被释放,没有发生内存泄露。)

举例来讲,在我的框架中,我一直都使用类来为窗体和控件定制一些通用的特性。每个窗体的头部都定义了一个窗体类 dclsFrm 的公有变量,然后在该窗体的 OnOpen 事件中被创建和初始化,dclsFrm 初始化时,传递给它一个指向当前窗体内建类(built in class)的对象变量(Me),dclsFrm 将这个指向当前窗体内建类的指针保存在 dclsFrm 类的私有变量中,这个私有变量是在类 dclsFrm 的头部定义的。

这样做的的一个好处就是,当在函数中打开某个窗体的时候,它可以让这个窗体保持打开状态,直到这个函数退出才会被关闭。它也允许 dclsFrm 根据需要直接引用控件和窗体的属性。

注意到没有?这里有问题!问题就出在这是一个环形引用。窗体有一个指向类的对象变量,类也有一个指向窗体的变量。必须采取一点措施将类和窗体中的对象变量重置,以便让类和窗体能正确的关闭和释放内存。对象编程的一条公理就是:一个对象会一直驻留在内存中,直到最后一个指向它的对象变量被设置成 Nothing。(这里我也做了一个示例文件:3.2 环形引用无法卸载的示例下载打开该文件后,运行宏“CreateFrom1”后,代码会实例化一个Form1,然后在Form1的打开时,将Form1的头部定义的类变量实例化,将指向当前Form1的指针“Me”传递给 dclsFrm 的init 方法,该方法将该指针保存在类 dclsFrm 的私有变量mfrm中。这样就形成了一个环形引用。然后运行宏“ReleaseForm1”,你会发现Form1并没有关闭。如果将函数“ReleaseForm1”中的第一句代码取消注释,你重新运行整个环节后,你会发现Form1可以正常关闭了。)

此外,dclsFrm 有一个窗体扫描函数,可以遍历窗体的控件集合,为它扫描到的每一个控件实例化一个类。当它扫描完时,如果某个窗体上有10个控件,那么我就加载了11个类 – dclsFrm 和 10 个控件类。我将指向这些控件的指针保存在 colChildren 集合中,并以控件名作为索引关键字。这样,我就能用控件名通过遍历这个集合找到任何我想控制的控件。

现在,当窗体需要关闭时,我需要清空这个加载的类,关闭里面的所有内容并释放其占用的内存。我通过一个叫做 Term() 的方法函数来做到这一点。这个函数遍历 Children 集合,调用每个子类的 Term 方法,然后将指向这个子类的指针从 Children 集合中移除。

如你所见,如果每个类都有一个类似的结构,任何类卸载前,先调用它的 Term 方法,这个Term 方法遍历并卸载它的 Children 集合中的每一个对象。这样,一个类可能有10个子类,每个子类又可能有10个子子类,每个子子类……不管你继续往下走多深……既然调用父类的 Term 方法会首先调用子类的 Term , 调用子类的 Term 会调用子子类的 Term 方法,一直到最底层的类被卸载,然后这个链接会像涟漪一样返回到最初的父类。调用父类的 Term 方法最终将会有效的卸载掉其所包含的所有子类。

因为这个原因,我们势必要严格定义类的结构,让它有效提供加载和卸载的功能。它必须是永远的,自动的发生。

是的。说的没错。

事情不是这样的。就像上面提到的环形引用,或者一个指向控件的对象变量不能被卸载,导致 Access 不能将其父窗体关闭,所以这个类永远也卸载不掉,卸载链条的涟漪永远不会发生。

混乱发生了。我们需要帮助我们解决故障的工具。在这一点上,我要感谢Shamil Salakhedinov 的工具和想法。在我刚开始学习使用类的时候,我从他那里受益颇丰。

当我开始使用类的时候,我遇到的首要问题之一就是如何跟踪记载类的加载和卸载。加载一个类,当你用完之后,而不卸载它,会导致内存泄露,类所占用的内存不会还给操作系统。有时 Access 会在关闭时将内存释放出来,偶尔它会导致 Access 无法正确的关闭,这不仅会导致内存泄露,也会导致一些其他的问题。这些鬼魂一样的 Access 实例在 Windows 98 下是看不见的,据我所知,Access 2000 之前的版本中,这些实例是无法被关闭的。Access 2000 以及之后的版本,你可以使用任务管理器来关闭它。这两种方法都是丑陋的。因此,我们跟踪记录类的加载和卸载是非常关键的。

在我当前的框架中,我使用一个长整形变量,每个类在加载时,将这个长整形变量递增1。这是我的第一个工具,从 Shamil 那里拿过来的示例代码。

Private mlngObjCounter As Long      '系统中所有类的实例的计数

Public Sub IncObjCounter()
    mlngObjCounter = mlngObjCounter + 1
End Sub

Public Sub DecObjCounter()
    mlngObjCounter = mlngObjCounter - 1
End Sub

这段代码很有用,至少它告诉你加载的对象的总数。我决定我真的想知道加载对象的名字。为了做到这一点,我添加了一个集合,在其中,我添加了类的名字,当类卸载的时候,将类的名字在集合中移除掉。

Private mlngObjCounter As Long      '系统中所有类的实例的计数
Public mcolObjNames As Collection

Public Sub IncObjCounter(strObjName As String)
    mlngObjCounter = mlngObjCounter + 1
    mcolObjNames.Add strObjName
End Sub

Public Sub DecObjCounter(strObjName As String)
    mlngObjCounter = mlngObjCounter - 1
    mcolObjNames.Remove strObjName
End Sub

然后我可以用一个函数将集合中的名字全部读书来:

Public Function ObjNames() As String
On Error GoTo Err_ObjNames

    Dim strName As Variant
    Dim str As String
    For Each strName In mcolObjNames
        If Len(str) > 0 Then
            str = str & "; " & vbCrLf & strName
        Else
            str = strName
        End If
    Next strName
    ObjNames = str

End Function

这段代码工作非常好,我在系统中用了它很长一段时间。

对于新的框架,我决定将一个实际指向这个对象的指针也保存下来,而不仅仅只是保存对象的名字。这个类调用一个函数,在对象加载的时候,该函数将一个指向该对象的指针保存在集合中,另一个函数在该对象卸载的时候将它的指针移除掉。这样做了之后,我就有了一个单独的地方,在这个地方我可以查看和控制任何对象。我们要处理的问题之一是“指向这个类的指针存储在哪里”?换句话讲,类可能在窗体代码中被加载,那么指针就存储在窗体的头部变量中;或者类在某函数的一段代码中被加载,那么指针就存储在这个函数的当前变量中;亦或类在另一个类中被加载,那么指针就这个类的集合变量中。如果你想查看这个类,执行一个方法,检查一个属性等,你如何得到一个指向该类的指针呢?通过将指向所有加载类的指针存储在一个集合中,我们可以:

  • 通过集合的Count属性,可以获得所有加载的类的总数
  • 通过遍历集合,可以获取其中的对象名字列表
  • 通过指向类的指针,可以根据需要来控制其对象

因为这些,我已经决定使用这种方法。

免责声明:非常重要的是,你要意识到,类不会真的从内存中卸载,除非最后一个指向类的指针被设置为 nothing。这样,如果你实例化一个类,将指向这个类的指针存储在集合中,同时也在窗体头部(举例来说)的变量存储这个指针。这个类是不会卸载的,除非你在集合和窗体中删除掉了指向这个类的指针。如果你的代码在别的地方也保存了指向这个类的指针,这个类也是不会卸载的,除非这3个指针都被设置为nothing。我的类的 init() 和 term() 方法将指针保存在对象跟踪记录类的集合中,同时指针也被保存在当前类的的父类的 child 集合中(详见后续系列),当类卸载的时候,它会将这2个地方的指针清除掉。然而,如果你的代码在另外的地方保存了指向这个类对象的某个变量,我也不知道它,无法将其清除,那么这个类将一直无法卸载,直到你将你的指针设置成nothing。

一旦到了我们要使用这些功能的时候,我将创建一个类 clsInstanceStack 为框架提供对象追踪记录的服务。

(本篇讨论的是建立一个框架必须要解决的对象释放的问题,看不懂也没有关系。从下一篇开始往后,将从最基础的类的介绍开始,详细讨论创建一整套框架的每一个细节问题。有兴趣了解的请留言。另外网站也开通了QQ登陆功能。免去了注册的烦恼^_^)