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)”了解更多的信息。)计时类可以告诉我们任何例程运行耗时的情况。

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示例