Access 开发框架(翻译+改编)系列之八——从属对象

框架的最终目的,是提供一个明确定义的地方,将对象的属性和行为可以放置在这里,用于将来的项目开发。控件是一个主要 … 继续阅读“Access 开发框架(翻译+改编)系列之八——从属对象”

框架的最终目的,是提供一个明确定义的地方,将对象的属性和行为可以放置在这里,用于将来的项目开发。控件是一个主要的例子,因为它们没有内建类(built-in class),而我们想扩展它们的功能。于是,我们为每个控件类型创建了类,这样我们就能扩展这个控件的功能。

为了这个目的,在本系列中,我们将为已创建的组合框控件类添加一些新的功能。这个新功能就是从属对象重新查询(dependent object re-query)

从属对象重新查询的意思就是,我们某个类对象创建一个方法,当该类对象的值有变化时,任何从属于该类对象的对象的值,都会执行重新查询,获得更新。在本系列中,这个类对象就是组合框控件类。当用户在组合框控件中选择了一个新的值,任何从属于这个组合框控件的对象会自动的重新查询,基于组合框控件的值,更新自己的筛选数据。

这样的行为通常是非常有用的,同时也能向我们展示,如果我们想让框架做更多的事情的话,我们应该如何在其中添加新的事物,从而扩展我们的框架。

从属对象

从属对象在Access应用程序中是很普遍的。举个例子,有一个窗体,上面有2个组合框,名字分别为cboCompany和cboEmployees。当用户选择cboCompany的值后,cboEmployees需要重新查询自己的可选数据,只显示公司为组合框控件cboCompany中的值的员工数据。

从属对象的功能在通常情况下是怎样实现的呢?一般是为从属对象创建一个查询,这个查询为该从属对象提供数据,在该查询中,有一个字段,该字段引用了被从属的对象的值。换句话说,cboEmployees的“row source”属性(译者注:我用的英文版本Access,该属性对应的中文名称可能是“行来源”)中的查询会设置类似于“Like form!frmMyForm!cboCompany”这样的筛选条件。控件cboEmployees是从属于(依赖于)控件cboCompany的。仅仅这样做好设置以后,当你修改cboCompany的值以后,你会发现cboEmployee的值并未立即发生更新,你必须手动刷新(或通过代码)窗体,从属的组合框控件才会更新。

为了在我们的框架中实现这种功能。我们可以简单的直接在控件类中添加一个从属对象集,然后为每一个控件类添加方法,以便可被添加到这个集合中,当控件类对象销毁之前,将其从集合中移除。当然我们也会添加一个重新查询的方法,遍历集合中的所有对象,调用每个对象的重新查询方法。注意,这隐含着一个意思,每个可从属于其他控件的控件必须有一个公用的重新查询方法。

本章相关代码示例文件8.1 从属对象

从属对象代码

从属对象的代码大概是这个样子的:

在每个控件类的开始部分(为了方便阅读,剥离了所有的错误处理代码)

Private mcolDepObj As Collection

在每个控件类初始化事件中,将集合对象实例化。

Private Sub Class_Initialize()
    Set mcolDepObj = New Collection
End Sub

在每个控件类的Term()方法中写入清除代码

Public Sub Term()
    Set mcolDepObj = Nothing
End Sub

最后,公用函数或方法来处理从属对象的相关事情:

'将从属对象集合中的所有对象清除
Private Function ColEmpty()
    While mcolDepObj.Count > 0
        mcolDepObj.remove 1
    Wend
End Function

'遍历从属对象集合中的每个对象,调用各自的Requery方法
Public Function Requery()
    Dim obj As Object
    For Each obj In mcolDepObj
        obj.Requery
    Next obj
End Function

'一次添加一批对象到从属对象集合
Public Function AddDepObjs(ParamArray lDepObjsArr() As Variant)
    Dim obj As Variant
    For Each obj In lDepObjsArr
        mcolDepObj.Add obj, obj.Name
    Next obj
End Function

'一次添加一个对象到从属对象集合
Public Function Add(Item As Variant, Key As Variant)
    mcolDepObj.Add Item, Key
    Requery
End Function

'删除一个对象
Public Function Remove(Index As Variant)
    mcolDepObj.Remove Index
End Function

'返回一个计数
Public Function Count()
    Count = mcolDepObj.Count
End Function

'返回一个特定的元素
Public Function Item(Index As Variant)
    Item = mcolDepObj.Item(Index)
End Function

用这种方法,当然,不会有任何问题。但是,如果我们从类的角度,来思考这个问题的话,我们完全有更好的方案。我们可以创建一个从属对象类,将相关的代码都放置其中,然后我们只需要在每个控件类中添加一个从属类对象。

使用一个专用的类来处理这个问题的最大的好处,就是我们可以把所有类似的代码都放在一个地方——类中,然后在我们需要的地方,定义一个clsDepObj对象,然后调用类的方法。我们不再需要在每个控件中都重复去维护那一堆相似的代码。

每个控件类代码

创建了从属对象类clsDepObj,在控件类中,我们不再需要上述代码了,我们只需要这些:

在每个控件类的开始部分(为了方便阅读,剥离了所有的错误处理代码)

'定义从属对象
Private mclsDepObj As clsDepObj

在每个控件类初始化事件中,将从属类对象实例化。

Private Sub Class_Initialize()
    Set mclsDepObj = New clsDepObj
End Sub

在每个控件类的Term()方法中写入清除代码

Public Sub Term()
    mclsDepObj.Term
    Set mclsDepObj = Nothing
End Sub

然后创建一个属性来获取从属类对象的指针(句柄/引用)

Public Property Get clsDepObj() As clsDepObj
    Set clsDepObj = mclsDepObj
End Property

你不得不承认,相比将整个从属类的代码都添加在到每一个控件类中来说,在每个控件类中只用添加这些行,要来的更加清晰和容易。更不必说,如果我们想添加一些其他的从属对象功能到框架中来的话,我们只需要在一个地方添加即可,而不必在每个控件类中都做一遍。

有一件事情我们必须要记住,在每个包含从属对象的控件类中,要添加一个重新查询的方法。显而易见,如果这个控件类有一个数据源,那么我们需要重新查询这个控件,也需要调用从属对象类的重新查询方法。

'重新查询控件和从属集合
Public Function Requery()
    mcbo.Requery
    mclsDepObj.Requery
End Function

最后,当组合框的AfterUpdate(更新后)事件被触发后,我们想重新查询新类的从属对象集合中所有的对象。我们通过调用mclsDepObj的重新查询方法就可以了。WithEvents!你将会爱上它。

Private Sub mcbo_AfterUpdate()
    mclsDepObj.Requery
End Sub

现在,我们有了这个新的类和它的功能,我们怎样告诉框架来使用它呢?答案就在窗体类的初始化的地方——窗体的Open事件。

窗体的内建类

我修改了frmPeople11窗体,添加了2个组合框控件在窗体的头部,cboCompany和cboEmployee,分别包含公司和和这些公司的员工。窗体的代码大概是这样子的:

Option Compare Database
Option Explicit

我们的自定义窗体类dclsFrm

Private fdclsFrm As dclsFrm

以及窗体的打开事件

Private Sub Form_Open(Cancel As Integer)
    Set fdclsFrm = New dclsFrm
    fdclsFrm.Init Me, Me
    With fdclsFrm.Children
        .Item("cboCompany").clsDepObj.Add .Item("cboEmployee"), cboEmployee.Name
        .Item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name
        .Item("cboCompany").Requery
    End With
End Sub

我知道这看起来非常复杂,但是我们可以一步一步来看,我会告诉你每一步是在干什么。Set语句和Init语句跟以前是一样的。我们将指向窗体内建类的指针(Me)和指向窗体本身的指针(Me)传递给fdclsFrm对象。非常凑巧的是,对窗体来讲,Me既代表了窗体内建类,也代表了物理窗体,所以出现2个Me,看起来有点奇怪。

    Set fdclsFrm = New dclsFrm
    fdclsFrm.Init Me, Me

接下来,我们用一个With语句来加快我们获取dclsFrm对象内部内容的速度

    With fdclsFrm.Children

dclsFrm有一个Children属性,可以返回类的集合指针。有了这个,我们现在可以引用集合的属性和方法。我们使用.Item()方法从集合中返回一些东西。

        .Item("cboCompany").clsDepObj.Add .Item("cboEmployee"), cboEmployee.Name

还记得吗?在dclsFrm类中,我们有一个控件扫描方法,可以发现窗体上的所有控件,然后为每个控件创建一个对象,然后将该对象的指针保存在集合colChildren中。还记得我们使用什么作为主键吗?是的,我们使用了控件的名字。这样一来,.Item(“cboCompany”)告诉类对象在集合colChildren中返回名叫“cboCompany”的对象,换句话说,就是控件cboCompany的控件类对象。

这样的话,.Item(“CboCompany”)就是指向组合框类对象的指针,与下述语句是一样的:

dim ldclsCbo as dclsCtlComboBox
set dclsCbo=.Item("cboCompany")

既然我们有了指向组合框控件类对象的指针,我们就可以调用这个类对象的任何方法或属性了,这就有了下面的语句:

        .Item("cboCompany").clsDepObj.Add

代码的意思就是,对组合框控件cboCompany的控件类对象来讲,调用它的从属类方法clsDepObj,返回指向它的从属类对象的指针,然后调用从属类对象的Add方法,准备添加一个从属类对象。

记住,句点符号指示那个类的属性或方法,这样一层一层的引用,有点像魔术师的盒子,大盒子放着中盒子,中盒子中放着小盒子,一直到我们最终要引用的对象。

fdclsFrm.Children.Item(“class name”).clsDepObj.Add

顺便说一下,我们可以在这行上面设置一个断点,然后单步调试这段代码,观察各种类的方法的调用情况,返回的指针指向下一个对象,然后调用下一个对象的方法,返回下下一个对象等,一直到最后,我们钻入到最后的Add方法。

这非常有趣!

好了,让我们回到主题。

    With fdclsFrm.Children
        .Item("cboCompany").clsDepObj.Add .Item("cboEmployee"), cboEmployee.Name
        .Item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name
        .Item("cboCompany").Requery
    End With

我们已经告诉dclsFrm要添加一些东西到clsDepObj。“一些东西”就是组合框控件cboEmployee的类对象。

        .Item("cboCompany").clsDepObj.Add .Item("cboEmployee"), cboEmployee.Name

类似的,我让窗体本身也从属与组合框控件cboCompany,所以我们也需要告诉cboCompany,dclsFrm也是一个从属对象。

        .Item("cboCompany").clsDepObj.Add fdclsFrm, Me.Name

最后,我们还需要重新查询cboCompany,以便它能重新查询它的从属对象。

        .Item("cboCompany").Requery

引用窗体的Children集合对象,找到为cboCompany创建的对象,适用其clsDepObj的方法获取从属对象的引用,使用从属对象的Add方法,添加一个从属对象到从属对象集中。被添加的对象是cboEmployee的类对象和窗体本身,fdclsFrm。一旦我们将这两个项目添加到从属对象集中,重新查询整个结构。

我非常清楚,当你第一看到这些东西的时候,你肯定是眼花缭乱的。但是,请相信我,一旦你使用了类,方法,属性和集合一段时间后,这会变成你的第二天性。

总结

通过添加一个新的,只有一个集合变量,和几个公用方法的小的类,我们创建了一个操控从属对象的方法。一旦类创建好,每个需要从属对象功能的控件类,只需要添加几行代码就可以了。很有可能,最大的难点,是在窗体打开事件中,需要找到正确的控件,引用到它的从属对象,然后添加该控件的从属对象。这两个从属对象分别是cboEmployee和窗体本身。

最后的结果就是,当窗体被打开以后,没有任何记录显示出来,因为公司还没有选择,而窗体的绑定数据源中,公司作为了筛选条件。一旦我们选择了一个公司,公司组合框控件就有了值,这个值就可以用来筛选窗体和组合框控件cboEmployee。因为cboCompany会调用它的从属对象clsDepObj的重新查询方法Requery,窗体的类会重新查询窗体本身,cboEmployee的控件类会重新查询cboEmployee。顺便说一下,如果cboEmployee也被编码了,拥有自己的从属对象,cboEmployee的类对象也会重新查询它所有的从属对象,整个过程,就想波纹一样,自动的一环套一环的执行下去。

类是非常强大的工具,允许我们将功能封装在一个地方,将所有的实现必要的行为相关的变量和代码封装在一个位置,如果有必要的话,我们只需要到这个地方修改或添加新的功能。你可以将这个独立的类放到你自己的框架中,只需要相对少的功夫,就能为你自己的组合框和其他控件添加从属对象功能。从属对象处理是非常小而琐碎的功能,但是想象一下,如果类拥有十几个属性和方法会有会怎样?封装性和可移植行成为了学习使用类的首要原因。

译者注:本章介绍的从属对象非常不好理解,要搞清楚三点,一是,谁是从属对象(cboEmployee,fdclsFrm),谁是被从属对象(cboComanpy),被从属对象的变化会导致从属对象的变化。第二点是要知道如何设置窗体的绑定数据,以及组合框控件的绑定数据源,当然Access比较基础的知识。最后一点是,要了解,如果没有本章介绍的从属对象,那些在绑定的数据源中引用了被从属控件值的窗体,在被从属控件值更新后,会自动获得更新吗?答案当然是否定的。

最后,我做了这个示例文件8.1 从属对象,在该示例文件中,有3个窗体:frmPeople10,frmPeople11,frmPeople12,分别演示了未应用任何从属对象类的情况,为cboCompany添加了从属对象,为cboCompany和cboEmployee都添加了从属对象的情况。读者可以分别测试一下,看看效果差别在哪里。

后记:翻译到此章的时候,一直萦绕在我脑中的一个疑问有了一个答案。还是那个比较恼人的对象卸载问题。按照Colby的类模板的要求,对象在卸载自己的时候,首先卸载自己colChildren中的子对象,卸载子对象前,先执行子对象的Term()方法。问题来了:很显然,本章介绍的clsDepObj中的集合mcolDepObj中的对象,肯定都不是由其父对象dclsCtlComboBox所实例化,当卸载clsDepObj的时候,也需要执行它的集合中的子对象的Term()方法吗?答案当然是否定的。那么什么时候执行子对象的Term()方法?什么时候不用执行呢?

大家可以注意到,我在类模板的最后,添加了下面一个私有方法。什么时候执行Term(),由子对象的父对象是否是当前对象来决定。因为每个对象的父对象只有一个,就是实例化自己的对象。

'该方法将类对象中的子集对象清除掉,若子对象的父对象是自己的时候,执行Term
Private Sub RemoveChildren()
    Dim obj As Object
    
    If mcolChildren Is Nothing Then Exit Sub
    With mcolChildren
        While .Count > 0
            If .Item(.Count).Parent Is Me Then .Item(.Count).Term
            .remove .Count
        Wend
    End With
    Set mcolChildren = Nothing
    
End Sub

相应的,重写后的每个类中,都有类似的代码。这样就解决了,什么时候该释放类,什么时候只是清除其指针的问题。

Colby自始至终都未曾论述过这个问题,相反,他创建了一个叫做实例堆的类clsInstanceStack(后续系列中会谈到),这个类用通俗的话来讲,就是对象的生死簿。主要用来做故障查询和除错用的。个人觉得,如果不把问题的根源找到,而只是在出现问题的时候,再来调试查错,还是于事无补的。

对象由谁实例化的,谁就是这个对象的父对象,这个对象的销毁也必须由其父对象来完成,其他的类对象,如果在其子集中引用了该类对象,那么只能在其子集中清除引用指针就好了。总结成一句话,谁创造的,谁才能毁灭

Access 开发框架(翻译+改编)系列之七——类模板

终于,我们到了该要面对类模板的时候。为了让类提供故障排查工具,和调试帮助的功能,我们需要为类添加统一的接口(I … 继续阅读“Access 开发框架(翻译+改编)系列之七——类模板”

终于,我们到了该要面对类模板的时候。为了让类提供故障排查工具,和调试帮助的功能,我们需要为类添加统一的接口(Interface)。如果你记得系列之三——集合、类和垃圾回收中,我讨论过类的一些问题,特别是那些引用窗体和控件的类。除非非常小心仔细,我们可能会在卸载类的时候遇到麻烦,从而导致内存泄露,甚至Access都无法关闭。现在是时候好好讨论这些问题了。

这里,请大家保持更多的耐心,我真的不想因为要讨论的这些问题和代码,失去任何读者。但是我知道你确实需要一点时间来适应这些内容。

我将讨论一个叫模板的类(Template class), 我们以后创建的任何新的类都将复制这个模板类,所有的必要的接口结构都在这里,然后粘贴到新的地方,作为建立新类的基础,并在该基础上添加新类特有的部分。你可以在示例文件7.1类模板中看到模板类的所有代码。

模块注释和开头部分(Header)

我们要做的第一件事情,是为类添加一个标准的头部注释,我将一些常见的信息放在里面,比如类的作者、创建日期、版权信息等。这个区域用来为这个类做注释,解释类的一些行为,这个类的是用来做什么的,以及一些其他的你想放在此处的说明。帮助你自己一周之后再回来看到这个类时,能很快想起来你做了什么。

Option Compare Database
Option Explicit

'.============================================================================
'.Copyright 2017 Jasoftiger 版权所有.
'.QQ        :185437290
'.QQ群      :113700839
'.============================================================================
'.请不要删除以上注释。模块中所有其他注释可以被删除。
'.----------------------------------------------------------------------------
'.描述      :
'.
'.作者      : Jasoftiger
'.创建日期  : 2017-05-18
'.修改历史  :
'.
'.评论      :
'
'.----------------------------------------------------------------------------
'.附加注释
'.
'.----------------------------------------------------------------------------
'.用法说明:
'

在这之后,我为常量和变量添加了标准的代码块结构,以及一些预定义的常量。

'.----------------------------------------------------------------------------
'以下常量和变量用于实现框架接口
'*+框架常量声明
Private Const mcblnDebugPrint As Boolean = False
Private Const mcstrModuleName As String = "clsTemplate"
'*-框架常量声明

常量mcblnDebugPrint是一个开关,用来决定是否将本模块中的调试语句打印到立即窗口。为此,我们将调用一个名为assDebugPrint的函数,而不是直接使用Debug.Print语句。我们将这个常量传递给这个函数,函数根据常量的值来决定是否将调试语句打印到立即窗口。如果将常量mcblnDebugPrint设置为True,那么本模块中的所有调试语句都将打印到立即窗口;如果设置为False的话,就不会打印。

另外一个常量mcstrModuleName将类模块在数据库窗口,模块项下的名字明文存储起来。(译者注:获取类名还有另外一种简便的方式TypeName(Me),不知道原作者为何不用这种方式,当然使用前“Me”必须已经被实例化了。

接下来是类的变量声明:

'*+框架变量声明
'指向创建该类实例的对象 (该类的父类)
Private mobjParent As Object

'子集合用来存储该类实例引用的所有子对象(子类实例),这些子对象需要被实例化和销毁
'通常这些用于控件,比如TAB控件,或者记录选择控件,但是他们也可额能是商业规则类等
Private mcolChildren As Collection

'通过模块名和随机数形成
Public mstrInstanceName As String

'简单的名字,用于关键字等
Public mstrName As String
'*-框架变量声明

此刻,我们也需要讨论一下,我们用来“自动”创建和销毁类的一些过程,毕竟,这个模板类的整个开头部分就是用来做这个的。你将在这儿添加自己的变量,但是到目前为止,你看到的内容都是框架的接口。

类很少单独存在于框架之中。类经常使用子类,子类也经常有它们自己的子类。窗体类dclsFrm就是一个很好的例子,dclsFrm可以使用组合框类dclsCbo,组合框类dclsCbo可能会使用从属类dclsDepObj(下一系列将介绍)。非常明显,窗体类可能会通过控件扫描函数,实例化一打控件类,这些指向控件类对象的指针被保存在集合中。一个组合框可能有一个或多个从属类对象,以便基于组合框的值,筛选自己的数据,如此一来,组合框控件就有了从属类dclsDepObj,该从属类对象保存的指针指向那些从属于该组合框的控件。

所有这些使用类的类,祈求有一个统一的接口界面。这样,每一个类都把指向自己父类的指针保存到mobjParent对象变量中。

'*+框架变量声明
'指向创建该类实例的对象 (该类的父类)
Private mobjParent As Object

dclsFrm是dclsCbo对象的父对象。dclsCbo是dclsDepObj的父对象。窗体将“Me”传递给dclsFrm,dclsFrm将“Me”传递给dclsCbo,DclsCbo将“Me”传递给dclsDepObj等等。“Me”在这三种情况下都是指向当前类的指针。“Me”在窗体里,指的就是窗体的内建类(built-in class),“Me”在dclsFrm里,指的就是dclsFrm的实例。

通过向子类传递一个指向父对象的指针,任何子类都能获得它需要的父对象的任何属性和方法。实际上,为父对象添加的一些属性和方法,可能就是用于子类调用的。

此外,每个类都有一个子集合,我们命名它为mcolChildren。mcolChildren保存了指向子对象的指针。

'子集合用来存储该类实例引用的所有子对象(子类实例),这些子对象需要被实例化和销毁
'通常这些用于控件,比如TAB控件,或者记录选择控件,但是他们也可额能是商业规则类等
Private mcolChildren As Collection

类的实例名mstrInstranceName是根据模块名mcstrModuleName和其他信息组成。我最后选择了“线性命名”的方法,也就是说,每个类使用它的父类的名字加上它自己的名字。这种方法使得我们非常容易的知道任何一个给定的类名具体代表什么。

'通过模块名和随机数形成
Public mstrInstanceName As String

最后,还有一个简单的名字变量

'简单的名字,用于关键字等
Public mstrName As String
'*-框架变量声明

当类被保存在它的父对象的子集合中时,这个简单的名字变量将通常作为关键字来使用。换句话来说,类总是被存储在它的父对象的子集合中,这样,当父对象被卸载清除的时候,父对象就能清除掉它所有的子对象。集合中用作关键字的,通常是象控件名那样简单的名字。我们这样做是为了方便使用一些可用的信息(比如控件名)来索引到这对象。

 

常量和变量声明

我喜欢将开头部分的内容组织起来,让所有常量都在一起,然后是所有的变量,再然后是类可能需要抛出的自定义的事件。如果一个类变得非常复杂之后,将功能性的信息片段组合在一起,这样做应该会更方便查找一些。总的来说,这是关于组织和有条理的编写代码的方法选择,只要你按照一定的规则去编码,总会比那些没有规则的编码方式要好得多。

'.----------------------------------------------------------------------------
'以下常量和变量用于实现类的功能
'*+自定义常量声明
'
'*-自定义常量声明


'*+自定义变量声明
'
'Private WithEvents mfrm As Form     '传入的窗体指针
'*-自定义变量声明


'
'在此处定义类将抛出的任何事件
'*+自定义事件声明
'Public Event MyEvent(status As Integer)
'*-自定义事件声明

 

类的实例化和卸载

接下来,我将类的开头部分定义的所有对象的实例化语句都放入了类的初始化事件Initialize中。Initialize是类的两个固有事件之一(另一个事件是Terminate)。当Set语句实例化类,Initialize事件就被触发了,类就开始在内存中加载自己。

'.----------------------------------------------------------------------------
'以下函数和子程序用于实现框架
'*+ 类的实例化/卸载接口
Private Sub class_Initialize()
On Error GoTo ErrorHandler

    assDebugPrint "Initialize " & mcstrModuleName, DebugPrint
    Set mcolChildren = New Collection
    
ExitProcedure:
On Error Resume Next

Exit Sub
ErrorHandler:
    MsgBox Err.Description, , "Error in Sub clsTemplate.Class_Initialize"
    Resume ExitProcedure
    Resume 0    '用于调试
End Sub

Init()通常是类被调用的第一个方法,我们在这里将类的父对象传递进来,换句话说,将哪个对象实例化了该类,那么该对象就是该类的父对象,将父对象传递进来后,类对象与父类对象的链接就形成了。

'实例化类
Public Sub Init(ByRef robjParent As Object)

    Set mobjParent = robjParent
    '如果父对象有一个子集,那就将自己放在其中
    assDebugPrint "Init " & mstrInstanceName, DebugPrint
    
End Sub

当指向类的最后一个指针被设置为Nothing时,类的卸载事件Terminate就被触发了。

Private Sub Class_Terminate()
On Error Resume Next

    assDebugPrint "Terminate " & mcstrModuleName, DebugPrint
    Term
    
End Sub

Term()是类的一个方法,它通常是被父对象(实例化类的对象)的清除代码所调用。例如,dclsFrm为每个控件实例化了一个控件类(dclsCtlTextBox),将指向这些控件类的指针保存在子集(mcolChildren)中。当窗体关闭的时候,dclsFrm将遍历这个子集,调用每个控件类的Term方法,然后将控件类的指针从子集中删除。

同时,我也在类的Terminate事件中调用Term方法。任何情况下,当最后一个指向类对象的指针被设置为Nothing的时候,该类对象将自动卸载。这样,理论上来讲,只用将指向类的指针设置为Nothing,类的Terminate事件就会被触发,然后事件代码中的Term方法就被执行了。

这看起来有点“重复工作”了。是的,一点没错。我之所以这样做,是因为在一些情况下,我们很有可能为类对象设置了一个指向其自身引用(Set mobjParent=Me),这样一来,在类对象内部,就永远有一个指向该类对象的指针。如果我在其他类中也有一个指向该类对象的指针,然后我将这个指针设置为Nothing,然后,发现了吗?这个类对象根本不会被释放,因为还存在最后一个指向它的指针,位于它的开头部分(mobjParent)。然而,如果我调用Term方法,我就将它所有保存的指针清除掉了,指向自身的指针也被清除掉了,这样,当我将外部的指向该类对象的指针设置为Nothing的时候,类对象就能正常的被卸载了!

这种情况虽然只是偶尔出现,但是只要有出现的可能,为了保险起见,还是调用类的Term()方法,然后设置类对象的指针为Nothing,触发类的Terminate事件,Term()被再次调用。我们可以设置一个静态变量来帮我们记录Term()方法是否已经被执行,这样我们就不用多次运行。

'清楚掉所有的类指针
Public Sub Term()
On Error Resume Next

    Static blnRan As Boolean
    If blnRan Then Exit Sub
    blnRan = True
    assDebugPrint "Term() " & mcstrModuleName, DebugPrint
    Set mobjParent = Nothing
    Set mcolChildren = Nothing
    
End Sub
'*- 类的实例化/卸载接口

译者注:原作者对“重复工作”的解释,存在疑点。如果一个类对象的mobjParent指向其自身,则必须“显式”调用Term方法,才能将该指针清除,Terminate事件中的Term代码是执行不了的,因为类对象的最后一个指针并未清除。对“重复工作”合理的解释,应该是:在不存在环形引用的情况下,如果类的使用者未按照约定的方式去销毁类对象,也即并未先执行Term,而直接Set类对象的最后一个指针为Nothing,类对象的Terminate事件被触发,Term被调用,清除类对象的子集中的子对象。若不在Terminate事件中调用Term,会导致子对象无法被清除。

 

标准属性和方法

我们已经讨论了开头部分和类的实例化和卸载接口,接下来,我们要讨论标准的属性和方法。显然,我们需要让其他类(或者其他开发者)获得开头部分的数据。模块名ModuleName,实例名InstanceName,以及普通的名字Name,这些都需要作为类的属性,给到类的使用者读取。为了在编码时,智能感知窗口能将这些属性组织在一起,我将Name放在了属性名的前面。

'获取类模块的名字
Property Get NameModule() As String
    NameModule = mcstrModuleName
End Property

Public Property Get Name() As String
    Name = mstrName
End Property

Public Property Get NameInstance() As String
    NameInstance = mstrInstanceName
End Property
Public Property Let NameInstance(strName As String)
    mstrInstanceName = strName
End Property

最后,我们还需要有获取父对象和子集的属性。

'*+父对象,子集合外部接口
'获取父对象指针
Public Property Get parent() As Object
    Set parent = mobjParent
End Property

'获取子集对象
Public Property Get Chilren() As Collection
    Set Chilren = mcolChildren
End Property
'*-父对象,子集合外部接口

以上就是框架类模板的全部代码。框架中的每一个类都包含上述所有代码,这样才能给我们一个非常标准的编程界面,我称之为框架接口。这些与框架相关的代码与类要实现的功能几乎是没有任何关系的,它存在的唯一目的,就是让我们以一个标准的方式来创建和销毁类对象。

如果一个类不能被卸载掉,在某种情况下,会导致Access程序无法关闭退出,从这一点上来讲,框架接口设计所发挥的作用就变得非常关键。

类的功能代码

最后是类的功能代码部分。我觉得用以下的结构去安排,会更加有组织一点。我喜欢将相似的事物放到一起,当然,每个人都有自己喜欢的方式,这取决于各自的想法。是否参照,取决于你自己。

'.----------------------------------------------------------------------------
'以下函数沉没开头部分声明的WithEvents对象的事件
'*+ Form WithEvent 接口
'*- Form WithEvent 接口

'.----------------------------------------------------------------------------
'以下函数/子程序用于实现类的功能
'*+私有类函数,子程序
'*-私有类函数,子程序
'*+公有类方法,属性
'*-公有类方法,属性

将框架类接口应用到已有的类上

我将会按照上面介绍的框架接口的标准,修改以前系列中创建的所有的类。我们需要花一点时间来适应类中的这些框架接口相关的代码。

总结

框架变得更加复杂了,框架中有很多类,类会实例化其他的类,在子集合中创建自己的子对象,子对象又可能会实例化子子对象,形成一条对象链。类对象销毁自己的时候,先会销毁自己的子集合中的子类对象,子类对象被销毁前,会先销毁子子类对象,子子类对象销毁自己前,会。。。。。。你看懂了吗?类对象销毁时,销毁的是一整条对象链。为了让类对象按照这个逻辑来创建和销毁自己,我们必须有一个系统上的安排,让它来帮助我们自动做到这一点,否则,我们将陷入混乱不堪的局面。本系列所讲的所有内容,就是尝试去标准化类的创建和销毁,以一种我们可以依靠的相同方式,不管我们实例化的是什么类。

Access 开发框架(翻译+改编)系列之六——更多的类

在上个系列中,我们讨论到创建一个框架类和文本框类。当文本框获得焦点时,文本框类仅仅把它的背景色改变了一下,当它 … 继续阅读“Access 开发框架(翻译+改编)系列之六——更多的类”

在上个系列中,我们讨论到创建一个框架类和文本框类。当文本框获得焦点时,文本框类仅仅把它的背景色改变了一下,当它失去焦点时,再改回去。窗体类也只是扫描窗体的控件集合,为任何我们创建了类的控件实例化一个对象,目前,我们只是对文本框创建了类。

今天我将为我们的框架添加更多的类。首先我要添加一个组合框类。组合框类也只是根据其获得或失去焦点时,改变其自身的背景颜色。我知道这有点无聊,呵呵,但是它能清晰的向我们展示,窗体的控件扫描程序确实为我们实例化了这个类,控件类确实被加载到了内存,而且控件的一些指定的事件能被类接收到。相信我,为了理解类是如何工作的,WithEvents是如何工作的,类可以怎样使用,多个类如何一起协同,我们已经朝这个目标走了很长一段路了。

类dclsCbo

组合框类的开始部分看起来跟文本框类很像。实际上,我直接拷贝了文本框类的代码,然后用查找替换功能将文本框替换成组合框,这样我们得到了一个新的类:

'from www.Jasoftiger.com by Jasoftiger @ May 2,2017
Option Compare Database
Option Explicit

'定义一个带有事件的组合框
Private WithEvents mcbo As ComboBox

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

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

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

d类的头部定义了私有的,带有事件的组合框控件对象变量,创建了一个字符常量,一个长整形常量,保存颜色代码,以及一个长整形变量来保存原始的背景色。

'每个类的 Init 函数初始化这个类,传入指向某个具体控件的指针
Public Sub Init(ByRef lcbo As ComboBox)

    '将传入的指针保存到类的私有变量中
    Set mcbo = lcbo
    
    '将控件的 OnEnter 属性设置为 [Event Procedure]
    mcbo.OnEnter = mcstrEventProcedure

    '为 OnExit 做相同的设置
    mcbo.OnExit = mcstrEventProcedure
    
End Sub

例程Init将参数传入的某个具体的组合框的指针保存在类的私有对象变量mcbo中。同时设置好组合框的OnEnter和OnExit属性。

'每个类的 Term 方法清除所有指向类中的对象的指针
Public Sub Term()

    '将指向控件的指针设置成Nothing
    Set mcbo = Nothing
    
End Sub

Term 方法用来释放或清除指向组合框控件的指针。

'这里有组合框的 OnEnter 和 OnExit 事件的事件处理代码
Private Sub mcbo_Enter()

    '当组合框获得焦点时,将原来的背景色保存下来
    mlngBackColorOrig = mcbo.BackColor
    
    '将组合框的背景颜色设置成我们预先定义的颜色
    mcbo.BackColor = mclngBackColor
    
End Sub

Private Sub mcbo_Exit(Cancel As Integer)

    '当组合框失去焦点时,还原其背景颜色
    mcbo.BackColor = mlngBackColorOrig
    
End Sub

最后组合框的Enter事件将控件的原始背景色保存下来,然后改变控件的背景色。OnExit事件将背景色还原。

所有代码基本上与文本框控件一模一样。

窗体类dclsFrm的修改

窗体类dclsFrm的修改只有一点,就是在控件扫描例程中,添加一个Case语句,让其能对组合框控件实例化一个对象。

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
                    '实例化一个dclsCtlComboBox控件,将其保存在集合对象中,以其名字作为键值
                    mcolClasses.Add New dclsCtlComboBox, .Name
                    '执行该dclsCtlComboBox控件的Init方法,将控件传递给对象
                    mcolClasses(.Name).Init ctl
'                Case acCheckBox
'
'                Case acListBox
'
'                Case Else
                
            End Select
        End With
    Next ctl
End Sub

注意到在case acComboBox语句下,我们实例化了一个新的类dclsCtlComboBox,并将其添加到集合对象中,使用控件名作为关键字,以便后续引用。通过这种方式,我们可以节省中间变量的定义,获得简洁的代码。

现在,我们已经创建了一个新的类,用来处理任何我们想要的组合框的功能。我们也在dclsFrm类中添加了2行代码,使其能与新的类一起工作。我们可以继续为列表框、复选框、单选框等等控件创建类,但是通过文本框和组合框的演示,已经足够向我们展示框架系统的强大能力了。

想看到新组合框类dclsComboBox的功能,我们可以打开窗体frmPeople7,用Tab键遍历窗体的每个控件。组合框现在也能像文本框一样,根据焦点情况自动变换颜色了。

上述示例代码可以在该下载文件中看到6.1更多类dclsCtlComboBox示例

计时类dclsTimer

在这里我们要引入一个计时类,主要是想计算一下刚才的窗体打开花了多长时间,特别是想知道控件扫描花了多久。

计时类完整的展示了类的优点:复用性和封装性。实际代码我相信是从ADH(译者:估计是Access Developer Handbook)中提取出来的,但是它是一个单一的实例,也就是说,它只有几个函数和一个存储时间的变量,一次只能对一件事情计算时间。我们把它转换成了一个类,封装了它的代码,用文档记录下了它的工作原理 。它非常典型的向我们展示,一个类并不需要多复杂就能做非常有用的事情。

'from www.Jasoftiger.com by Jasoftiger @ May 3,2017
Option Compare Database
Option Explicit

'声明API函数,得到当前系统时间,以微秒为单位
Private Declare Function apiGetTime Lib "winmm.dll" Alias "timeGetTime" () As Long

'用于保存当前时间值,长整型
Private lngStartTime As Long

在计时类的头部,我声明了一个函数,用来调用Windows 的API函数,获取当前系统时间,以微秒为单位,返回结果以长整形来表示。另外我也定义了一个长整形变量,用来存储开始时间。

'首先运行StartTimer,获得当前时间值
Public Sub StartTimer()
    lngStartTime = apiGetTime()
End Sub

'函数返回间隔事件值,微秒为单位
Public Function EndTimer() As Long
    EndTimer = apiGetTime() - lngStartTime
End Function

类中只有2个方法,StartTime用来保存计时开始时间,EndTimer返回结束时间与开始时间的时间差。要记住返回的结果是以微秒为单位的。

最后,我在类dclsFrm中使用这个计时类来为我们做一点计时工作。

'from www.Jasoftiger.com by Jasoftiger @ May 3,2017
Option Compare Database
Option Explicit

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

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

'定义一个计时类对象变量
Private mclsTimer As clsTimer

我在类dclsFrm的头部定义了一个计时类对象变量mclsTimer,以便在类的内部其他任何地方可以引用它。

Private Sub Class_Initialize()

    '创建集合变量的实例
    Set mcolClasses = New Collection
    
    '创建计时类的实例
    Set mclsTimer = New clsTimer
    
    '开始计时
    mclsTimer.StartTimer
    
End Sub

在类的初始化事件Initialize中,我实例化了计时类,然后开始计时。

Public Sub Term()
    'Term会被执行2次,第二次不再执行
    If Not mfrm Is Nothing Then
        '在立即窗口打印窗体打开的时间
        Debug.Print mfrm.Name & "打开的时间是" & mclsTimer.EndTimer & "微秒"
        
        '销毁计时类对象变量
        Set mclsTimer = Nothing
        
        '销毁集对象中的所有对象后,再销毁集合对象本身
        ClsDestroy
        
        '释放指向实际窗体的指针,解除环形引用
        Set mfrm = Nothing
    End If
End Sub

在Term方法中,我在立即窗口打印出计时类对象的EndTimer函数值,这样就将窗体打开的时间显示出来了。

现在我们将使用另外一个计时类的实例,来为控件扫描计算耗时。我们针对FindControls例程做如下修改:

Private Sub FindControls()
    Dim ctl As Access.Control
    
    Dim lclsTimer As clsTimer
    Set lclsTimer = New clsTimer
    lclsTimer.StartTimer
    
    '遍历窗体的所有控件
    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
                    '实例化一个dclsCtlComboBox控件,将其保存在集合对象中,以其名字作为键值
                    mcolClasses.Add New dclsCtlComboBox, .Name
                    '执行该dclsCtlComboBox控件的Init方法,将控件传递给对象
                    mcolClasses(.Name).Init ctl
'                Case acCheckBox
'
'                Case acListBox
'
'                Case Else
                
            End Select
        End With
    Next ctl
    
    Debug.Print mfrm.Name & "的控件扫描耗时" & lclsTimer.EndTimer & "微秒"
    Set lclsTimer = Nothing
End Sub

我们在控件扫描例程的头部定义一个计时类对象变量lclsTimer,然后开始计时。我们在例程退出前,结束计时,将耗时打印到立即窗口。然后销毁对象。

我在我的2.5GHz AMD开发电脑上运行这个窗体,控件扫描程序耗时1微秒完成扫描工作(译者注:我的电脑耗时0微秒)。在这么短的时间内,控件扫描程序加载了8个控件。我的立即窗口显示:

frmPeople7的控件扫描耗时1微秒

在我们创建越来越多的各种控件类的时,这个计时类对我们将会非常有用。当窗体加载它的所有的类的时候,我们想要监控其所耗时间,如果加载时间一下子跳到很久,我们能马上察觉,然后调查原因。

上述示例代码可以在该下载文件中看到6.1更多类clsTimer示例

总结

在该系列中,我们添加了一个新的控件类,来处理组合框的功能,然后我们又添加了一个计时类。这些类再一次向我们演示了创建一个类以及使用一个类是非常简单的。dclsFrm做了修改,允许控件扫描程序,为所有的组合框控件实例化一个dclsComboBox对象。我们也设置了2个计时类控件,一个在类的头部,对整个类内部可见,用来为窗体的打开时间计时;另一个在控件扫描例程中,用来计算加载所有的控件类对象呢所耗时间。

我们现在有了4个类,它们相互协同,创建了一个小小的“系统”,能让窗体为其上的控件实例化对象,控件能拥有Access本来没有赋予的功能。(译者注:如果仅仅只是为Access已有的类添加新的功能,可以在网络上搜索关键词“子类化(Sub-Classing)”了解更多的信息。)计时类可以告诉我们任何例程运行耗时的情况。