Access 历史版本特性变化列表(长期维护及时更新)

Access从92年到现在已经发布了12个版本。我们可以沿着各个版本特性的变化,感受一下Access的发展脉络。内容来源于网络,如有纰漏,欢迎在评论中指出。

时间

版本

版本号

变化

1992.11

Access 1.0

1.0

  • 第一个运行在Windows 3.0上的关系型数据库
  • 开始挑战当时DOS操作系统上的数据库管理系统霸主Borland公司的dBaseParadox

1993.05

Access 1.1

1.1

  • Access添加了Access Basic编程语言

1994.04

Access 2.0

2.0

  • 开始支持Windows 3.1,
  • 最小内存需求4M

1995.08

Access 95

7.0

  • Access被添加到微软办公软件Office 95套件中
  • 支持ActiveX控件
  • VBA取代了Access Basic
  • 需要运行在Windows 95

1997.01

Access 97

8.0

  • 加入了超链接特性

1999.06

Access 2000

9.0

  • 使用新的Jet数据库引擎
  • 开始拥有记录锁定功能
  • 引入ADOUnicode字符集编码,ADP
  • VBAVB6共享相同编程界面IDE

2001.05

Access 2002

10.0

  • 开始支持XML

2003.11

Access 2003

11.0

  • 使用Windows XP主题
  • 引入数字签名
  • 显式信任运行VBA代码

2007.01

Access 2007

12.0

  • 引入ACCDB数据库格式
  • 更安全的加密数据
  • 功能区用户界面
  • 选项卡式对象
  • 报表视图
  • 数据表中汇总行
  • 简化了的筛选功能
  • 分割式窗体
  • 导出为PDF
  • 日期选择器
  • 按钮图表支持
  • 支持搜索功能的导航窗格
  • 附件和多数值字段数据类型
  • 窗体对象锚定
  • Web浏览器控件
  • 图像控件
  • 目录级信任

2010.07

Access 2010

14.0

  • 引入Access 64位版本
  • 使用SharePoint ListAccess Web 应用程序(AWA)
  • 数据宏

2013.01

Access 2013

15.0

  • 可以创建使用SQL ServerAccess Web 应用程序(AWA)
  • 桌面Access数据库不再支持
    • ADP
    • 数据透视表
    • 数据透视图
    • 智能标记
    • 链接到dBase
    • Visual Source Code 集成
    • 升迁向导
    • 安装包向导
  • 文本和备注数据类型改名为短文本和长文本
  • 需要运行在Windows 7及更高版本操作系统上

2015.09

Access 2016

16.0

  • 引入Office助手告诉我”
  • 新的主题
  • 导出链接数据源信息到Excel
  • 更现代化的模板
  • 更大的“显示表”对话框
  • AWA可以用于SharePoint户内客户
  • 需要运行在Windows 7 SP1 及更高版本上

Access 与 Excel 之对比(翻译)

译者的话:

企业数据库全局战略中的Access(翻译)》发布之后,获得了大量的阅读点击。今天再翻译一篇Luke Chung的文章,相信你读完以后,不再对该类问题感到困惑。

翻译用了 1 周的业余时间,有误之处,欢迎批评指正。原文可参见这个链接


在Excel与Access之间抉择

Microsoft Office的高级用户经常问我们,为什么要使用Access?什么时候应该使用Access,而不是Excel?特别是当他们Excel用起来非常顺手的时候。以下是我们的观点。

给信息工作者赋能

我们认为,这不是二选一的问题。它们各自都有各自的优势,并且天然的相辅相成。了解它们的差异,并在不同的情况下,选择适合的一个的人,可以为他们自己以及所属的企业带来竞争优势。

Microsoft Office产品为个人(微软称之为信息工作者)赋能,使其可以独立完成任务。这样,你就可以充分利用你对工作内容的理解,为要做的事情弄一个解决方案。这要比你将问题提交到”IT专业人士”那里,去寻求帮助而高效得多。因为你需要让他了解你的技术需求,而他很可能对你的业务背景一无所知。当他还没有搞清楚状况,就去创建技术解决方案,结果可想而知。

Microsoft Excel的优点

Excel的学习曲线非常短,所以使用Excel很容易,而且生产效率很高。需要IT人员创建Excel的情况很少,信息工作者们可以自己做。

Excel可以轻松的存储数据,执行数值计算,格式化单元格,调整布局,生成结果或报告分享给他人。还有一些高级功能,例如,分类汇总,数据透视表,数据透视图,分析工具包,以及许多模板。这些高级功能使得Excel能够轻松完成各种任务。它甚至可以与SQL Server的分析服务(商业智能)集成,获取数据后,调整一下布局,字体,颜色等,得到你想要的报表。

Microsoft Excel的缺点

不幸的是,Excel的灵活性是有代价的。虽然创建公式,引用单元格,复制粘贴数据,以及将多个工作表和工作簿链接在一起都很容易,但随着工作变得越来越复杂,Excel数据变得越来越难管理。诚然,Excel是创建一次性分析的理想选择,但是随着时间的推移,数据会不断增长,业务会不断演化,Excel会变得问题多多。当新的行和列被添加进来后,汇总区域和公式可能需要修改或新增,数据和公式如果更新不一致,会导致错误的结果和决定。

Excel面临的挑战在于,随着时间推移,数据量不断增加,要准确的维护它们,是非常有难度的。

Microsoft Access的优点

Excel专家一般很难理解Access提供的那些Excel并不具备的功能。Access有以下几个特性:

  • 通过多个表让数据结构化和规范化
  • 可扩展性:可以自由的增加更多的数据记录
  • 数据和参照完整性
  • 查询和报表
  • 通过宏和VBA代码模块自动化

表结构和验证

使用Access,你可以很容易做到,将信息存储在一个地方,而在多个地方引用它。例如,你可以将客户信息(可能有客户姓名,地址,电话号码,电子邮件等)保存在客户表中。而这些信息可能会在其他的地方(可能是查询,窗体,报表等)被引用。如果客户的信息发生变化了,新的信息会在所有被引用的地方自动更新。设计表的时候,你会加入一些约束限制,例如设定字段为数值型,日期型,或者文本型等,从而获得比电子表格更高质量的数据。而设定字段类型仅仅只是一个开始。

Access中的记录可以自由的增减

Access与Excel最大的区别就是,在Access中,记录的增减是自由的。只要设计得当,新的数据记录可以随时不断的添加进来,而不需要填加任何新的字段(列)。所有的查询,窗体和报表会照常工作,而无需任何调整。当然,你可能会使用不同的筛选条件,但出来的结果一定是一致的。新的数据添加进来后,不需要重新测试或调整单元格公式。这样每年,每季度,每月,每周或每日,你都能生成准确的报告。

数据和参照完整性

有句话说“垃圾进,垃圾出”(garbage in – garbage out)。Access为规避这一点提供了许多工具,来保证数据质量。在Access表中,你可以很容易将查阅列表和验证规则应用于单个字段(列)和记录上。在窗体上做数据输入的时候,你还可以添加额外的规则来响应用户的选择和事件。Access还提供表间的参照完整性,以确保数据在多个表中有一致的定义。

查询和报表

你可以使用Access的查询和报表来切分数据,并以详细或者汇总的形式来呈现它,而不用去考虑数据是如何存储的和排序的。它提供了大量的功能和灵活性来分析和呈现结果。使用分组功能时,聚合信息可以随着数据的变动而自动增减。

通过宏与VBA模块进行自动化

在Access宏或者VBA模块中,你可以使用Docmd.TransferSpreadsheet命令将数据从Access表或查询中导出到Excel(使用acExport选项)。

使用acImport选项,TransferSpreadsheet命令还可以将Excel中的数据导入到Access表。

实际上,如果使用Office VBA自动化,你可以在Access中打开一个Excel文件,并将数据写入到指定的单元格,自动的更新Excel中的数据。

Microsoft Access的缺点

Access最大的缺点就是,做为一个数据库,它需要你掌握更多的知识和技能才能使用。搞清楚如何定义规范化的表,将他们连接在一起,并结构化好数据,使其易于编辑,查询和生成报表,这些内容对于初学者来说是很有挑战性的。但是这是设计所有关系型数据库,都需要面对的问题,一旦掌握了,你就可以将其应用于其他数据库。

构建数据库,创建查询和设计报表布局的学习曲线可能看起来相当艰巨。这肯定要比在Excel单元格中敲入数据要复杂得多。另外,很可能让你感到十分沮丧的是,在Access中,你无法轻松的复制和粘贴单元格区域,你也无法在Access的报表结构中做任何特别的调整。(例如,想通过特殊字体或备注来突出显示某个值或者某一行)虽然你可以利用Access的VBA代码模块来做定制化,但是相对于Excel所见即所得(WYSIWYG)的设计来说,你要花费更多的精力。此外,一些数据分析功能,比如power pivot,在Access中是没有的。

结论

Excel可以非常灵活的生成个性化的报表,并且可以在任何地方自由的设定格式或添加批注。Access回报给你的则是,一旦设计完毕投入使用,它就能提供长期的数据准确性和一致性,毕其功于一役。对于一次性的分析来讲,使用Access当然就是用牛刀杀鸡,但如果数据需要长期维护,报表需要定时生成的话,使用Excel往往会遇到困难。大多数企业都有许多“相似”的Excel表,相互之间只存在一点点细微的差异,但是随着时间的推移,很快它们就变得不一致。精心设计的Access数据库就不存在这种难以管理的挑战。也就是说,Access和Excel都有其优点和缺点。

最佳实践就是将Excel和Access两者的优点结合起来,形成一个混合解决方案,将Access中的数据导出或复制到Excel。Access数据库的数据参照完整性,严格定义的数据以及有质量保证的数据输出,再结合适宜临时分析的Excel,可以让你充分利用两者的优势。而使用自动化流程,可以让数据在Excel与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 从属对象

从属对象代码

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

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

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

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

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

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

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

每个控件类代码

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

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

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

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

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

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

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

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

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

窗体的内建类

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

我们的自定义窗体类dclsFrm

以及窗体的打开事件

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

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

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

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

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

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

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

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

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

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

这非常有趣!

好了,让我们回到主题。

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

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

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

引用窗体的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(),由子对象的父对象是否是当前对象来决定。因为每个对象的父对象只有一个,就是实例化自己的对象。

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

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

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

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

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

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

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

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

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

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

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

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

接下来是类的变量声明:

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

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

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

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

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

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

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

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

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

 

常量和变量声明

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

 

类的实例化和卸载

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

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

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

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

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

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

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

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

 

标准属性和方法

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

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

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

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

类的功能代码

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

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

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

总结

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

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

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

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

类dclsCbo

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

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

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

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

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

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

窗体类dclsFrm的修改

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

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

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

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

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

计时类dclsTimer

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

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

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

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

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

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

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

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

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

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

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

frmPeople7的控件扫描耗时1微秒

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

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

总结

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

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

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

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

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

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

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

监管类 dclsFrm 代码

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

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

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

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方法,而直接释放了类对象。毕竟重复释放类中使用的(包含的)对象并不是一件坏事。

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

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

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

在窗体的头部,我们定义了一个类型为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 住代码和数据,这些代码和数据用于描述某个对象,实现该对象的行为和属性。让类区别于 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 控件类示例下载本章所有示例代码文件

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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 那里拿过来的示例代码。

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

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

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

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

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

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

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

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

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

Access 类模块和开发框架(翻译+改编)系列之二——简介

当你使用任何编程语言编写一个应用程序的时候,在你编写的应用程序中,总会存在某些代码片段,它们除了能作用于当前编写的应用程序外,还能用于其他的应用程序上。

软件行业从诞生的第一天开始,主要关注的问题就是“代码重用”。所以程序员一直被教导,去设计“可重用的代码”,例如:

  • 函数(Functions)去替换程序中重复出现的代码块
  • (Libraries)将常用的函数打包起来,供程序员调用
  • 面向对象编程(OOP)将代码和数据捆绑在对象(Objects)中,供程序员引用
  • 对象库(Libraries of Object)
  • 框架(Frameworks,亦或平台

很多软件开发工程师从没到达过框架的层次,所以他们不懂框架是什么,也不知道它和有什么不同。

仅仅只是存放代码,函数,对象,可能还有全局变量(但很少)的容器。而框架其本身就是一个应用程序,它是一个结构,可以用来为开发者创建程序的基础部分,这样,开发者就不必一遍又一遍的,为他的每一个程序重复创建。

想象一下你是摩天写字楼的建筑商。每一栋建筑都需要一定的服务。你需要停车场,电力,给水排,电梯,空调,供暖,你还可以继续列举。再假想一下,你有一个停车场“模块”,你修改一下“停车数量”参数,然后“扑通”一声把它丢在地上,这栋写字楼的停车场就建好了。继续想象一下,你用相同的方式将剩下的电力,给水排等服务都建好了。全套繁重的写字楼设计和建筑工作就这样完成了。更进一步来讲,想象你有全套的摩天写字楼结构,你只要简单的修改一下“停车数量”,高度,宽度,建筑的层数,电力系统的安培数,电梯的个数,电梯的高度,等等。你瞬间就有了一栋高层建筑,可以开始准备招商了。

这就是框架(Framework),但要以编程的角度。

有成吨的“服务”可供你的应用程序使用的话,那是再好不过了。这些服务就是那些“模块”,当你需要它的时候就创建它,当你用完它之后就拆掉它。这当然是可行的,这也是许多程序员做事情的方式,然而,这种方式需要手动的地方非常多。

我们假定某个开发者需要一个服务,比如将所有的系统错误记录下来。这种功能最典型的实现方式是用,这意味着,你需要用这个类创建(实例化)一个对象,做完工作后,再将它卸载掉。或者,你可以让框架帮忙负责创建和卸载。你只需要调用框架的一个叫 “LogInit” 的方法,传递一个路径参数,指示你想把这个错误保存在什么地方。然后任何时候,系统发生错误,调用 “Log” 方法,传递系统的错误信息参数,系统错误就被记录下来。框架负责初始化这个 Log 类,存储路径信息,如果路径不存在则创建这个路径,当框架关闭时,卸载这个 Log 类。

再举一个稍微复杂一点的实际应用中的例子:为窗体装载控件类。我使用一个绑定窗体,我有这个窗体的类,同时也有组合框的类,文本框的类等等。组合框的类“懂得”如何处理“不在列表中”事件。如果我预先指定一个列表窗体,组合框的“不在列表中”事件会告诉用户他输入的数据不在列表中,是否想将其添加进去。如果用户说是的,类就以模式的方式打开指定的窗体。通过这个窗体,用户可以添加新的数据项进来。当用户关闭列表窗体后,组合框的类重新查询数据源,在列表窗体中新添加的数据项就可供选择了。

窗体类“懂得”如何去扫描窗体寻找放置其上的组合框控件。如果找到,框架会为每一个组合框都创建一个组合框类的实例。开发者只需要在窗体里面设置好代码,创建窗体类。窗体类会自动的去扫描和加载控件。当初始化完成,(在窗体类的 Init() 这一行之后),开发者可以为组合框指定相关的列表窗体。

所有这些功能都是框架要做的事情。对象类知道如何做事情。框架会记录对象的加载和卸载,在可能的时候自动清除掉对象。各种功能各司其职,因为框架会让这一切确保发生。

译者的话:

这篇文章主要介绍什么是框架以及与库的区别。用摩天写字楼的建设做类比也足够形象。

文中举的第二个例子,相当于我们谈论的控件子类化。将组合框的“不在列表中”事件的处理代码,从其所在窗体转移到组合框的子类中。组合框的子类由窗体的子类负责实例化。

关于该例子,后文中将会放出源码示例供大家参考。

通过子类化,你可以为 Access 的一些对象添加不存在的功能。子类化也许可以作为 VBA 的一个单独的话题拿出来讨论。

P.S.

我估计 Colby 的文章写于 2007 版 Access 出来之前,因为这之后,组合框的属性中就多了一条“列表编辑窗体”属性,开发者可以在此处直接指定一个窗体去负责处理不在列表中的事件发生时使用哪个窗体去编辑列表。

 

Access 类模块和开发框架(翻译+改编)系列之一——前言

在夜深人静的时候,我常常想,如果若干年后我不再从事 Access 开发,我所拥有的开发经验中,哪些是最值得与大家分享的?一个函数?一个算法?一个用户界面设计?一个经典项目的全部代码?都不是!

我想,没有什么比这篇文章更能代表我最想分享的内容了。它代表着编程的核心思想在Access 中的最佳实践

老实讲,这个思想不是我首创的。2008 年的某一天,忘记了是何缘故,访问到了这个网站:www.colbyconsulting.com 。里面有一个系列文章,标题就是《Access Class & Framework》。我当时还不是一个专职的 Access 开发人员,也只是偶尔用 Access 开发一些专供自己偷懒用的工具。看到这篇文章以后,我如获至宝。迅速就将所有的文档和示例文件全部下载保存下来了(事后证明这是多么明智的举动)。一两年以后,这个 Access 博客网站就变成了一个卖山地自行车的网站,Colby 放弃做 Access 咨询了?到目前为止,访问这个网站显示“因信息安全问题被屏蔽”。我后来一直在google Colby的信息,始终无法找到他。

Colby 的文章没有给你一个现成的开发框架(Framework),相反,他循循善诱,一步一步的给你娓娓道来,让你了解这个开发框架的核心思想是怎样的,如何在 Access 中去实现。我根据文章的指导,结合他给出的示例代码,创建了自己的 Access 开发框架。目前在工作中,也在使用我自己创建的开发框架。但是由于精力的缘故,一直没有对自己的开发框架进行更进一步的功能提升。例如可以将 “Access 用户界面 v1.0” 集成到开发框架中,更多的功能也能够添加到开发框架之中,好多事情都没来得及做。

现在,我准备一边编译(编辑+翻译)他的文章,一边向国内的 Access 爱好者们分享这一编程思想,同时也促成自己重温这些思想,继续提升自己的开发框架的功能。

以上。

P.S.

我将 Colby 说的 Framework 一词翻译成了“开发框架”,其实我本来是想将其翻译成“开发平台”的。后来担心与国内的 2 大 Access 论坛 / 网站上的“开发平台” 相冲突,就改成“开发框架”了。我私下认为如果你将 Framework 理解成“开发平台”也是合适的。

国内有 2 大 Access 论坛 / 网站,上面都有“Access 开发平台” 供免费 / 收费下载,都做得非常优秀。我有下载免费的版本,因为看不到核心的代码,所以不敢肯定这 2 个“开发平台” 都遵循了 Colby 文章中的思想。在仔细研究了其核心平台文件的类列表信息后,我只敢确认其中之一应该不是遵循 Colby 思想的产物,而是使用“库”(Library)来达到代码重用的目的,与“开发框架”(Framework)代码重用的方式,却差了一个级别。