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