使用MVC设计模式在Access中实现数据的“查改增删”

在知乎上写了一篇文章,有兴趣的朋友可以通过这个链接访问。

使用MVC设计模式在Access中实现数据的“查改增删” – Jasoftiger的文章 – 知乎

因为知乎上不能上传示例文件,可以从这里下载:MVC Design Pattern in Access VBA

使用MVC设计模式在Access中实现数据的“查改增删”后记 – Jasoftiger的文章 – 知乎

MVC Design Pattern in Access VBA v2 彻底解耦MV

《老兵新传》与”公式相声”

最近在看群友介绍的好书:《老兵新传 Visual Basic核心编程及通用模块开发》——张宁编著,精读到第4章,很有感触。遂写一篇以留记忆。

首先这是一本好书,读起来很顺,内容的编排也很合理,在中文版的技术书籍中算是写得非常好的。以前看过一些中文技术类书籍,晦涩难懂,深感失望,所以从此只找英文版的来读。

读英文版的,刚开始英语确实是一个障碍,有很多英文单词不知道意思,就查字典,每次遇到生词,就查一次。好在现在网络发达,生词在线一搜就翻译出来了,耽误不了太多功夫。时间久了,发现查来查去,也就是那些单词。所以越到后来,字典查得也越少。一旦克服了语言的障碍,展现在你眼前的是一片新大陆。因为计算机毕竟是外国人发明的,编程语言也是,所以读那些原汁原味的书,你才知道其中的各种蹊跷。当然我也读过写得很糟糕的英文书,但是还是以好书居多。

话题好像跑偏了,拉回来。这本《老兵新传》的作者,应该是花了很多的心血在里面的。特别是前几章的内容,写得非常细致。从我之前写《VBA类模块》的经历来看,要把一个技术类的东西,写出来让不懂得人看懂,是很不容易的。这个过程需要足够的耐心,还需要有一点编剧的才能。作者这些都做到了。

我突然又联想到这两天的一个热门话题:《交大博士夫妻“公式相声”叫板郭德纲》。我看完了整个视频,平心而论,博士夫妻讲的这段内容,月亮的倒影,永动机这些,不是什么高深的内容,受过初等教育的普罗大众都能听懂。拿这2个点说事,有点言过其辞了。但是,他们最大的问题出在对他们衣服上的公式的解释上。

他们没有用普罗大众能理解的方式,对这2个公式做出解释。更准确的说,他们压根没打算让观众明白这些公式的含义。那他们为什么要把公式印在衣服上呢?唯一的答案,就只能是装逼了。

不解释这些公式就算了,博士男还一副老子天下第一的姿态,打算用蛮横不讲理的态度,逼迫郭德纲和观众信服他们的“公式相声”,这个就显得情商欠费了。犯了众怒,也就不奇怪了。

尽管这样,我还是宁愿相信他们的“公式相声”里面是有技术含量的。毕竟别人出了3本书,没读之前,还是要有所期待。

为什么要扯到这里来?我总是觉得,如果这两博士如果能够将技术应用这两件事情中的任何一件能做好,也不至于落到现在这般境地。比如,运用他们的“公式相声”的技术,说一段脍炙人口的相声。或者将他们研究出来的“公式”用平易近人的语言给观众解释清楚。这2方面,他们都做得很糟糕。

这种2头都很糟糕的例子,在国内的技术群或论坛里面非常常见。风气很不好。国外的论坛,完全是另外一个样子,你可能连装逼的机会都没有,因为在其他人看来,你的发的装逼帖子,没有给其他人带来任何价值,反倒是浪费了大家最宝贵的时间。

回到《老兵新传》这本书,话说2012年就出版了这本书,6年时间不到,就在各大购物购书网站上无法购买了。

而网络上随便一搜就能搜到该书的电子版。不知道作者该作何感想。

中国技术不如美国,中兴通讯被美国啪啪打脸的时候,我们除了吐槽之外,有做过任何一件有助益的事情吗?

P.S. 我看《老兵新传》的也是电子版,如原作者看到本文,欢迎联系提供账号,愿意付费。

Visual Basic 的秘密(翻译)

本文讲的是VB中的指针,是我看过的讲得最好的一篇文章。很底层的东西,有兴趣的可以看看,对学习Windows API编程很有帮助。若翻译有欠妥的地方,欢迎指正^_^


  • 原文网址:http://www.thevbzone.com/secrets.htm
  • 作者:Kevin Wilson
  • 目录:
    1. 介绍
    2. 在Visual Basic 中使用指针
    3. VarPtr,StrPtr和ObjPtr
    4. ByRef / ByVal
    5. AddressOf 和回调
    6. 访问“隐藏的”API

1 介绍:

Visual Basic被称为“快速应用程序开发(RAD)开发工具”,因为它旨在为你处理Windows“基础工作”,从而使你可以专注于重要的东西,如程序的功能和文档。

例如,你打开VB,新建一个项目,然后在项目中添加一个标准的“窗体”,按“F5”来执行程序,该窗体就被显示出来,我们操作起来非常简单,但是,你可能不知道,VB在背后帮我们做了很多事情。 VB需要调用“CreateWindow”来实际创建“窗体”,并为构成它界面的属性赋值。 然后,需要通过调用各种Win32 API来修改窗体上的字体,前景色,背景色,设备上下文(device context)等。 最后,VB需要用子类化的方式,将新创建的窗体挂钩(hook)到Windows消息流中,让窗体捕获到发送给它的Windows消息,最后使用“WindowProc”回调函数,处理每个Windows消息。 窗体的接口越复杂,窗体对象的创建和功能处理代码就越复杂。 而C和C ++程序员就没有这么轻松了,他们需要亲自编写代码,来创建所有的对象,处理消息流以及销毁对象(或者通过模板来生成代码)。

对于会正确使用VB开发工具的程序员来讲,Visual Basic能帮助你做一些类似上述“基础”的事情,是一个非常强大的功能。但是,与此同时,那些不太了解如何编程的人,也拥有了很大的能力。也正是这个原因,Visual Basic被C和C++程序员嘲笑。他们说,“任何人都可以用VB做开发,但只有真正的程序员才能使用C / C++做开发。我认为,聪明的程序员选择Visual Basic,因为VB在对象创建,消息处理,对象销毁过程中,可以帮你消除潜在的Bug。VB能提供更简单更快捷Windows事件处理,VB能为你提供更强大的界面功能,VB能让你更轻松的访问COM对象和第三方控件,VB更容易阅读,因为它非常接近阅读英语,而C/C++看上去却非常神秘。VB允许你轻松访问Win32 API (这使得程序员有驾驭Windows的强大功能的能力)。以及最最重要的是,Visual Basic可以通过组件,库,和其他用C/C++编写的代码来挂钩住(hook)C/C++的强大功能和速度。嘿。。。。。。C/C++程序员,现在怎么不继续吹牛逼了?^_^

事情就是这样。。。。。。即使是工作多年的VB程序员也没有意识到VB的真正力量,因为他们没有掌握(或意识到)VB提供的一些关键概念和功能。这些概念很少被大家知道,或者受重视的程度还远远不够,因为我将它们称之为“VB的秘密”。

2 在VB中使用指针:

我曾经在求职面试中被问到一个问题,现在我意识到这是一个有坑的问题。“Visual Basic 是否有或者使用‘指针’?”任何用过VB的人都会显而易见的回答“No”,在VB中,你看不到任何像C/C++中那样的指针的声明,这就是当时我认为面试官要问的点,而她对我的答案也表示认同。然而,正确的答案应该是“Yes”。

Visual Basic (就像几乎所有其他编程语言一样)确实使用指针,广泛的使用。不同的是,Visual Basic会尽可能的将它们隐藏起来,或者将它们称为不同的东西,以免给你带来负担。

接下来,我们谈谈,如何使用指针直接访问变量中的信息(VarPtr / StrPtr / ObjPtr),通过指针将信息传递给函数(ByRef / ByVal),取回和传递指向函数的指针(AddressOf)。

3 VarPtr, StrPtr 和 ObjPtr:

VB函数“VarPtr”(变量指针),“StrPtr”(字符串指针)和“ObjPtr”(对象指针)是无官方文档(undocumented)无技术支持的(unsupported)的函数,微软在VB5.0和VB6.0中引入。这些函数(跟随很多其他的函数一起)在VB.Net中不再使用。这些函数允许你获取VB变量在内存中的地址(指针),以及变量所指向的实际数据在内存中的地址。这些函数为什么作用这么大?因为,如果你知道数据的内存地址,你可以任意操控它,直接复制或者传递出去,而不需要借助VB的帮助。这样做,速度上更快,并且(在某些情况下)你可以做到VB本身做不到的事情。

下面是微软MSDN关于“VarPtr”的说明:


该函数可以用来获取一个变量或数组元素的地址。它使用变量名或数组元素作为参数,返回它的地址。然而,你需要注意的是,未锁定的动态数组可能会被Visual Basic重新分配地址,所以你当你使用VarPtr获取数组元素地址时,必须非常小心。

下面的示例获取变量的地址:

下面的示例获取某个数组的第4个元素的地址:

限制:VarPtr函数不能用来获取数组的地址。。。。。。


下面是微软MSDN关于“StrPtr”的说明:


在Visual Basic 中,字符串是以BSTR来存储的。如果你对一个字符串变量使用VarPtr,你将得到BSTR的地址,它是该字符串指针的指针。要获取字符串缓冲本身的地址,你需要使用StrPtr函数。该函数返回字符串第一个字符的地址。需要考虑到在Visual Basic中,字符串是以UNICODE来存储的。

要获取一个字符串的第一个字符的地址,请将该字符串变量传递给StrPtr函数。

示例:

当你需要传递一个指向UNICODE字符串指针给API时,你可以使用StrPtr函数。


下面是微软MSDN关于“ObjPtr”的说明:


ObjPtr函数使用一个对象变量名作为参数,获取该对象变量所引用的接口的地址。

一种试用该函数的情况,是当你需要处理集合对象的时候。相比使用Is操作符遍历集合对象而言,通过使用对象地址作为索引关键字,你可以获取更快的访问速度。在很多情况下,对象的地址是唯一可以信赖的键值。

示例:


注意在“VarPtr”说明的底部的“限制”,它说你不能使用VarPtr获取数组的地址。在某种程度上,说得没错。你不能将变量“MyArray”传递给它(因为VB将数组保存在一个叫做“SafeArray”的OLE对象中),但是,如果你获取了数据的第一个元素“MyArray(0)”的地址,你就有了整个数组的地址,因为数组元素在内存中是连续存储的(按照数字顺序从第一个元素到最后一个元素)。所以,如果某个Win32 API 或者C / C++ 函数需要一个指向某个字节数组的指针,像这样:

你可以像这样调用它:

注意:将坐标点(pointers)保存到动态数组中要小心,因为当动态数组重新分配内存地址后,或者动态数组改变了大小后,或者被重新定义(ReDim)后,实际数据将非常有可能被保存到一个新的内存地址。

关于VarPtr,StrPtr,ObjPtr的更多信息,可以参见以下链接:

http://support.microsoft.com/default.aspx?scid=kb;en-us;Q199824

http://msdn.microsoft.com/library/en-us/dnw32dev/html/ora_apiprog6_topic1.asp

http://msdn.microsoft.com/library/en-us/dnovba00/html/LightningStrings.asp

http://msdn.microsoft.com/library/en-us/dnovba01/html/Lightweight.asp

4 ByRef / ByVal

到目前为止,VB程序员在调用Win32 API(或任何C/C++导出函数)时遇到的最大的问题,就是参数的正确传递。在应该使用“ByVal”的地方使用了“ByRef”(或者反过来也一样),或者当函数期望传入一个指针时,你传入的却是一个值或者一个变量,这些都可能会导致系统崩溃。要想搞懂如何正确的传递参数,就要了解Windows程序如何调用堆栈的(calling stacks),以及主调程序与被调函数之间的内存分配。

首先,我们来讨论一下什么是“调用堆栈”,以及当传递参数给函数时,它是怎样受内存分配影响的。“调用堆栈”说白了,就是内存中的一段空间,那些传递给函数的变量和值,以及函数返回值都存储在这段内存空间中。它被叫做“堆栈”(stack)是因为参数值是一个挨着一个被存储在这段内存空间中,访问这些参数值时,也是按照这种假定来访问。正因为如此,从纵向角度从下到上来看,这些参数一个堆在另一个上面,组成了要给到函数的全部参数信息。当参数被添加到函数的调用堆栈上时,它被称为“压入”(push)到调用堆栈上面。当参数从函数的调用堆栈移除的时候,它被称之为从堆栈“弹出”(pop)。“堆栈”,“压入”,“弹出”这些都是汇编术语(是的,我们现在讨论得非常底层),如果你将程序或者DLL反编译到汇编语言,你会看到一些代码行上有“push”,“pop”等单词。

当Visual Basic调用Win32 API(或者任何导出的C/C++函数)时,它期望被调函数使用“标准调用惯例”(Standard Calling Convention)(_stdcall),这与C/C++默认的调用惯例(_cdecl)刚好不同。这就意味着,当函数被调用的时候,参数被从右到左传入到内存中(或者压入到堆栈上面),被调函数负责清理参数的内存(或者从堆栈弹出)。如果调用一个声明为任何非“标准调用惯例”_stdcall的导出函数,Visual Basic不知道如何处理堆栈和传出传入的参数,VB就会跳出信息说“错误的DLL调用惯例”(Bad DLL Calling Convention)。

调用参数时内存如何分配和收回?调用堆栈是什么?以及它们在Windows中是怎么工作的?想知道更多更深入的解释,我强烈的推荐一本Dan Appleman写的书:“Dan Appleman’s Win32 API Puzzle Book and Tutorial for Visual Basic Programmers”。

现在我们跳出底层的内存运作机制,回到VB上面来。调用函数时,给它传递参数的方式有2种,要么传递给它明确的值,要么传递给它一个指针,该指针指向内存中的某个存储了值的地址。当你传递像数字,大小,标志等简单信息时,你希望用值的方式(ByVal: By Value)传递信息,因为你想让函数获取你所传递的值,而不是这个值当前被存储在内存中的地址。现在当你想要传递更加复杂的数据的时候,比如一个数据类型,一组数组,或者一个对象引用,你需要传递一个引用(或指针)到被调函数,告诉它数据在内存中的地址是多少。这是通过指定ByRef(By Reference)关键字完成的。这样,被调函数到内存中找到这个地址,读取相关的数据。这里有一个例外,当你传递字符串(String)参数给Win32 API函数时(或者任何C/C++导出函数),请使用ByVal方式(除非你传递字符串数组,这种情况下使用ByRef,或者用ByVal传递字符串数组的第一个元素)。

所以到现在为止,你可以说:“我已经知道ByRef/ByVal这两种方式传递参数了”。是的,但是你意识到通过ByRef传递参数时,是在传递指针吗?如果你把这个概念再往前推进一步,你可以让函数接口变得更加通用:把通过引用传递(ByRef)改为通过值传递(ByVal),然后传递一个明文的指针。所以,你可以像这样声明函数:

如果你仔细想想的话,在声明函数和参数的时候,它可以给你各种选择。你不在局限于特定的变量类型。你可以将所有参数都变成“Long”型变量,然后将指针传递给任何函数参数(只要你小心翼翼的话)。比如说在传递自定义类型的时候遇到麻烦了,忘掉它,传递指针就好了。再比如在传递对象的时候遇到麻烦了,忘掉它,传递指针就好了。VB5.0中不允许将变量数组作为函数返回类型,忘掉它,返回一个长整型,指向数组的内存地址,然后用API函数CopyMemory将它拷贝到本地数组中。看出来我要干什么了吗?

联合ByRef和ByVal一起使用VarPtr,StrPtr和ObjPtr,可以让你传递任何格式的数据,如果你知道自己在做什么的话。

5 AddressOf 和回调

操作符“AddressOf”跟回调(call back)息息相关。“但什么是回调?”你问。回调是VB事件在Windows中的等价物。事实上,从底层来讲,当回调函数捕获了以Windows系统消息形式存在的原始事件之后,回调函数会触发VB事件。回调函数常见于Win32 API内部(和其他C/C++代码中),特别是当你的应用程序中需要监测用户和/或Windows的活动的时候。在VB中你不怎么看到回调函数,是因为VB通过“事件”(Events)处理消息和通知,这种方式比起使用回调函数更加简单和安全。

假如说,在你的项目中,Windows发送给窗体的每一个消息你都想收到通知,(很多消息也许你根本不会用到),当然还有一些由于其他API调用,可能发送到你的窗体的个性化消息。要想达到这个目的,你所要做的就是设置一个被Windows认可的回调函数(“WindowProc”),然后告诉Windows(通过“SetWindowLong” API函数)将它的所有与你的窗体有关的消息发送给你的回调函数,这样你就能够检查它们,对消息做出反应,将消息传递下去(通过“CallWindowProc” API函数)。所有这一切,就叫做“子类化”(Sub-Classing),功能非常强大(同时也是非常危险的)的技术,你可以用这个技术来个性化的重画你的窗体,菜单和内容(或你想对你的窗体干的任何事情)。

使用“AddressOf”有2个缺点:

1)你只能获取VB标准模块中的函数或子程序(公有或私有)的地址。没有办法能绕过这个限制。

2)它只能被作为函数或子程序参数列表的一部分被调用。绕过这个限制的办法如下:

你会注意到我们传递“AddressOf”和我们想得到地址(内存指针)的函数名到“GetProcAddress”函数,该函数只是简单的返回地址值,简单又非常有效。函数和子程序的地址不会变,所以你可以将想要调用的函数和子程序的地址保存下来,而不必在每次调用的时候都使用AddressOf。

“那我们来看看实际应用吧!”你说。ok!

如上所述,这里有一个“子类化”的示例:

下面是一个“列举”的示例,它是反馈Windows列表信息非常流行的方法(例如所有窗体列表,窗体上所有对象的列表,已安装的所有字体列表等):

6 访问“隐藏的”API

(未完待续。。。。。。)

VBA 类模块系列之十五——结语

VBA类模块系列文章终于写完了。第一次填完一个坑,先给自己一点掌声^_^。

本系列,从类的概念和思想谈起,逐一介绍了类对象的属性,方法和事件。当然也谈到了类中的常量和错误处理以及类对象的生命周期。作为一个入门级的系列,该谈的内容都差不多谈完了。

在介绍类的自定义事件时,为了示例的效果,引入了稍微复杂一点的“心脏起搏器”的做法,这里本质上,其实是属于类对象之间的关系范畴(一个类对象包含了另一个类对象)。有了这样一个有自主意识的类对象,我们可以有更多的想象空间,比如,做成一个谈话机器人。当然这个方向就有点跑偏了,大多数人使用Access VBA并不是要搞这个。

关于VB/VBA类模块,其实还有很多内容没有谈到,比如:集合类,类对象之间的关系,子类化问题,多态(多接口),接口继承等等。这些等将来有兴致,再开一个高级类模块的系列谈谈。

最后,向广大的群友们致谢。没有你们的支持和鼓励,意见和建议,按我的一贯作风,这个系列肯定是半途夭折^_^。特别鸣谢:tmtony(王站),河北-SQL Designer,滋阴壮阳冬瓜汤。

 

VBA 类模块系列之十四——在类中定义和触发自己的事件

作为一个有一定经验的Access VBA开发人员,你没少写过事件的响应程序。比如:Form_Load(),Command0_Click(),Text0_Enter()等等。总是使用别人定义好的事件,有没有觉得乏味和沮丧?

现在是时候解锁两个新技能了——在类中定义触发自己的事件(Event)。这两个技能,再加上上一篇介绍的WithEvents,这三项技能一旦合体,就会变形成一支笔,神笔马良的笔,凡是用这只笔创建的类对象,就立刻有了自主意识。

前面几篇一直要说给Person类创建一个生日提醒事件,终于吹过的牛逼要兑现了^_^,所以我们在Person类的头部写如下代码:

第8行就是我们定义的生日(Birthday)事件。是不是出奇的简单?^_^Public表示生日事件是公有的,事件是用来往外转播的,私有事件没有任何意义。所以,这里必须是Public,你可以试试将它改成Private,编译就会报错。Event是VBA的又一个关键字,专门用来定义事件。Birthday就是我们的事件名,后面有一对括号,我们可以在括号里面添加参数,当往外传播事件时,连同参数一起传递出去。我们先弄简单一点,不写任何参数。

生日事件定义好了,但是,在Person类中,我们该怎么来触发这个事件呢?

还记得上一篇给Person类安装的心脏起搏器吗?它每隔0.7秒就能获得一次脉冲,让她有机会思考一下今天是不是她的生日。如果是,就触发生日事件。我们修改一下Person类的Timer事件响应子程序如下:

第3行代码就是她的思考过程,比较一下当前日期(Date)的月和日与她的出生日期的月和日的值是否一致,如果一致,就执行第4行代码。

第4行代码就是触发生日事件的代码!RaiseEvent是与事件相关的第3个关键字(前2个分别是监听事件的WithEvents和定义事件的Event)RaiseEvent字面意思是“发起事件”。Birthday就是刚才我们定义的生日事件名称。

现在,我们在Person类中定义好了事件Birthday,也在Person类中,适当的地方,适当的时候,发起了这个事件。Person类(事件源 Event Source)中该做的事件都做完了,剩下的就是测试一下,看看事件监听者(Event Sink)能否监听到这个事件。第十二篇讲过,事件监听者必须是类对象,所以这个测试不能在模块1(标准模块)中来进行了。我们使用窗体4(窗体模块,也是一种类模块)来做测试。

上一篇我们在窗体4代码窗口的头部定义了一个objPerson对象变量,现在,我们还要监听objPerson对象指向的Person类对象的Birthday事件,所以,我们需要给objPerson添加WithEvents修饰符。如下代码所示:

有了WithEvents,就可以在窗体4的代码窗口的左上角的下拉框中,找到对象变量objPerson,然后为它的Birthday事件编写事件响应程序。如下代码所示:

事件监听者的代码也写好了,我们打开窗体4。等着一个消息窗口谈出来,告诉我们“今天是赵冰冰的生日”。

可是等了好久都没出现什么消息窗口。什么原因?是不是事件监听失败了?呃,不是的,因为今天不是她的生日,你要等到明年2月3日,才会弹出这个消息窗口。呃,要等半年多时间,有点太久了,大家都这么忙的,我们把“生日”,改成“生秒”。比如说,你是1995年2月3日8时30分25秒出生的,那么任何时点的第25秒,比如8点31分25秒,你的“生秒”就到了。所以每分钟你都能过一个“生秒”。我们修改Person类中“心脏起搏器的脉冲”响应代码:

也调整一下窗体4的代码如下:

然后打开窗口4,最多等待1分钟,就能看到弹出窗口:

我们完美的完成了生日事件的监听测试。

这个生日事件,有点不足之处,就是不知道她是多少岁的生日,叫我怎么为她选择数字蜡烛呢?

现在,让我们给事件加上参数。重新定义Birthday事件。

第6行代码,为生日事件添加了一个年龄参数lintAge。那么当你触发这个事件,向外部传播之前,需要给给这个参数赋值。

我们修改一下触发事件的代码:

这里直接使用现成的类属性Age作为实参向外传递。

然后我们同步修改一下窗体4中的事件监听响应代码如下:

再次打开窗体4,等待1分钟,弹出如下消息窗:

这就是带参数的事件的定义,触发,和响应。

总结一下。事件看起来神秘莫测,实际上,它的定义和触发都是非常简单的。在类中定义什么样的事件?在类的什么地方触发它?在什么时候触发它?这3个问题,是设计事件时,需要通盘考虑的主要问题。事件定义好以后,就可以在各个地方监听和响应事件。

最后,将我的Access文件贴在这里,供大家下载。VBA 类模块系列之十四——在类中定义和触发自己的事件

 

 

 

 

VBA 类模块系列之十三——给类对象安装心脏起搏器

原计划本篇应该谈谈如何定义自己的事件的,比如给我们创建的第一个类Person定义一个生日提醒事件。然而,我发现,我们的Person类对象从一出生(Set objperson = New Person)到最后死亡(Set objperson = Nothing),中间是没有呼吸和脉搏的。

你在说什么?什么是呼吸和脉搏?

上面的3句代码不是类对象的呼吸和脉搏吗?

呃,你说的没错,这3句是类对象的呼吸和脉搏,但它的心脏就只跳动了3下而已。因为外界就只刺激了它3下,这3句代码执行完,我们的类对象的心脏就停止了跳动。

一个人,虽然活着,却需要外界刺激才能反应,这个人其实跟植物人没什么两样。一个植物人,你怎么能期望她主动告诉你:“下周我过生日”呢?

所以,在给我们创建的第一个类Person定义事件之前,我们得先给她安装一个心脏起搏器,要让她有一定的主观能动性才行。

心脏起搏器是个什么东西?它具体长什么样,我也不知道,但是你可以捂住你的胸口感受一下自己的心跳,你就会知道,它是一个非常有规律的脉冲,比如0.7秒跳动一下。我们从哪里去给我们的类Person找一个每0.7秒跳动一下的东西?

作为一个有一定经验的Access VBA开发人员,不知道你使用过窗体的Timer事件没有?没有没关系,我们一起来创建一个窗体3,在窗体3的属性表的事件选项卡中,将“计时器间隔”设置为一个大于0的数(比如说2000),然后为“计时器触发事件”编写一段响应程序,那么,每隔2000毫秒(2秒)时间,这段响应程序就会自动执行一次。如下图所示:

你保存一下窗体3,然后打开它,进到IDE界面,你会看到立即窗口中,每隔2秒,就会有一行文字“每隔2秒,这里就自动执行一次”。

如果将2000改为700,就是心脏跳动的频率了。这就是我们要给Person类安装的心脏起搏器。

那怎么安装到我们的Person类中呢?我们先来分析一下我们的需求。我们的Person类需要每隔0.7秒获得一个脉冲,而窗体3,它每隔0.7秒就能发起一个Timer事件,所以,如果我们在Person类中能够监听窗体3的Timer事件的话,我们的目的就达到了。

所以,我们在Person类的头部,定义一个与窗体3一样类型的窗体变量mfrm,因为Person类还要监听窗体变量mfrm所指向的窗体3的Timer事件,所以要添加WithEvents修饰符:

到目前位置,只是定义好了窗体变量mfrm,它还需要指向窗体3。套用上一篇的方法,在Person类的实例化事件Class_Initialize()中(对应着上一篇的窗体2的Form_Load()事件),将mfrm指向窗体3(Set mfrm = Forms(“窗体3”))。

当然这样做是没有问题的,需要注意的一点是,代码执行前,先要打开窗体3,否则Forms(“窗体3”)就报错。

窗体3是Person类的心脏起搏器,现在你把它打开了,呃,人人都看得到Person类的心脏起搏器,一个人,她的心脏长在外面,吓人就算了,你发现她的心脏右上角还有一个关闭按钮,那就真的有点说不过去了。一不小心人把她的心脏给关闭了可咋整?(当然,应该是关不掉的,原因暂按不表)

所以我们这里,换一种方法,如下:

第5行代码,有没有让你惊掉下巴^_^?其实很好理解,Form_窗体3是一个类的名字,所以 New Form_窗体3 就是创建一个Form_窗体3的类的对象,让mfrm这个窗体变量指向这个类对象。于是窗体3就在内存中被创建,但是,它不会显示出来,除非你设置它的Visible属性等于True。使用这种方式打开的窗体3,不会出现在Forms()中,所以你引用Forms(“窗体3”)是会报错的。

如果你问我,我们在定义mfrm的时候,指定的类型是Access.Form,为什么我们不给mfrm指定“Form_窗体3”类型?如果你能有这种疑问,说明你是带着脑子在看我的文章^_^。这里也先按下不表。因为涉及到面向对象的多态的概念,以后再谈。

好了,内存中创建了窗体3对象,mfrm也指向了窗体3。现在我们可以用指向窗体3的mfrm对象变量,任意的调用窗体3的属性和方法了。又由于mfrm前面加了关键字WithEvents,Person类对象可以监听窗体3的任何事件了!为什么任意任何被我加粗了?我想强调的是一种自由,一种想干嘛就可以干嘛的自由。通过mfrm,窗体3现在是你的提线木偶,一切听你的指示,做上帝的感觉又来了,有木有?^_^

现在可以监听窗体3的Timer事件了吗?呃,要看情况。要看什么情况?我们知道窗体的Timer事件的触发,是需要条件的,什么条件?窗体的“计时器触发”属性和“计时器间隔”属性必须要设置正确,Timer事件才能正常被触发。我们的第一个图中,“计时器触发”属性的值是“[事件过程]”,“计时器间隔”属性的值是“2000”,都已经设置好了呀?

是的,你没有看错,上面的截图确实是这样设置好了的。但是,心脏这么重要的事情,你能交给别人去帮你设置这2个重要属性的值吗?如果万一某个人打开窗体3,把这2个属性值给改了,怎么办?所以要确保万无一失,我们使用木偶的线mfrm来设置窗体3的这2个属性:

这里,直接将计时器间隔(TimerInterval)2000的原始值,覆盖为700。

好了,现在可以为mfrm监听到的窗体3的Timer事件写事件响应代码了:

现在窗体3的Timer事件,有了2个监听者,一个是Person类对象,通过mfrm监听,另一个是窗体3本身,还记得窗体3中的 Form_Timer()代码吧?窗体3的Timer事件,只需要Person类对象来监听就好了,我们将窗体3中的Form_Timer()代码注释掉。

最后,在Person类的销毁事件中,将mfrm对象变量的指针释放掉:

好了,Person类对象的心脏起搏器安装完毕。我们在模块1中写如下测试代码:

第14到16行代码,写了一个无限循环的语句。目的是为了让Test中定义的objPerson对象一直存活下去。DoEvents是释放线程控制权的意思。运行Test代码,你会在立即窗口中看到如下的消息:

这段测试代码,你不干预,它会永远运行下去,所以,当你听厌了赵冰冰的呼喊声后,可以点停止按钮强行停止执行。

对代码有洁癖的人,是不会就此罢休的。所以,再创建一个窗体4,在窗体4的代码窗口写如下测试代码:

使用窗体4做类的代码测试的好处是,只要窗体4打开后不关闭,objPerson对象就一直存活在内存中,你就可以一直听到赵冰冰的心跳和呼喊声,直到关闭窗体4。^_^

这回的出生入死的记录就完整了^_^

最后,就不按惯例贴出所有代码了。因为有点长,窗体也比较多。直接将我的Access文件贴在这里,供大家下载。VBA 类模块系列之十三——给类对象安装心脏起搏器

下一篇应该不会跳票了,开讲自定义事件和触发自定义事件。敬请期待^_^

VBA 类模块系列之十二——揭开事件的真相以及使用WithEvents

上篇介绍到,当类的使用者在给类的属性赋值的时候,如果赋的值不符合类的设计者规定的范围,那么作为类的设计者,为了类的健壮性,必须让类主动抛出一个异常(运行时错误)给到类的使用者。

你对“类主动”3个字有何种感受?

还记得我们为我们的第一个类Person设计了姓名(Name),性别(Gender),出生日期(DOB)属性吗?这些属性,我们既可以读取属性值,又能重新赋予它们新的值。这个过程,是谁在主动?我们看到,给这些属性赋值,以及读取这些属性值的代码,都在模块1中。模块1是Person类的使用者。所以,这个过程,是类的使用者主动在调用属性,类的使用者还能主动调用类的方法(例如Speak)。

我为什么对“类主动”还是“类的使用者主动”这个问题这么有兴趣?谈过恋爱的人都知道,如果你心仪某个女孩子,你肯定先想方设法要到她的微信,然后发消息虚寒问暖(比如,你叫什么名字呀?赵冰冰。你是什么性别啊?女性。出生日期呢?X年X月。)如果对方都是这样,有一句答一句,从不主动向你发送任何消息,你肯定是垂头丧气的。突然有一天,她主动发来消息:“下周我过生日”。你估计会欣喜若狂一晚上。这就是主动发送消息较之被动回应的不同之处。

在《系列之二——什么是类》中,我们谈到了类的接口是由属性,方法,事件组成。属性和方法,都是类的被动元素,你什么时候问我,我就什么时候告诉你,你不问,我也懒得说。而事件(Events)却恰好相反,作为类的使用者,你冷不丁的会突然收到类发过来一句消息:“下周我过生日”。有了事件,这场恋爱谈起来就有意思了,否则,她跟充气娃娃有什么区别?^_^

接下来是要给我们的第一个类Person添加一个能主动发送“下周我过生日”消息的事件吗?呃,还没到时候。

本篇讲到现在,我们只是讲清了事件的真相之一:事件的传递方向(类 -> 类的使用者)。(而属性和方法的传递方向:类的使用者 ->类)。我们还需要了解其他几个真相。

先从大家最熟悉的开始:作为一个有一定经验的Access VBA开发人员,你肯定给某个窗体(窗体1)上的命令按钮(Command0)编写过单击事件响应(Click)代码,比如:

这里,我们清点一下:有2个对象:窗体1,命令按钮Command0,窗体1使用命令按钮Command0来接受用户鼠标的单击信息,从而执行一段预定的代码,完成一定的功能。命令按钮是Access的一个内置控件,它是一个类。窗体(窗体1)是命令按钮(Command0)类的使用者。命令按钮(Command0)这个类能向它的使用者(窗体1)发送很多消息事件(Events),比如,单击(Click),双击(DblClick),获得焦点(GotFocus)等。

稍等,你说获得焦点(GotFocus)这个事件我怎么这么陌生?我怎么从来没用过呢?你是怎么知道命令按钮还有这个事件呢?命令按钮还有哪些事件?呃,如果你有这样的问题,那我得给你补补Access VBA初级的内容了。

Alt+F11进到IDE界面,双击进入窗体1的代码模块,在代码模块窗口的左上角和右上角,分别都有一个下拉框,先选择左上角的下拉框,如图下图所示:

这个下拉框中列出了当前窗体类模块(Form_窗体1)中,可以接收到事件的对象。我们选择Command0。然后点开右上角的下拉框,如下图所示:

这个下拉框中,列出了Command0这个对象能够发送出来的消息事件(Events)。你不熟悉的“GotFocus”也在其中。

命令按钮是Access内置的控件,它也是一个类,但是我们看不到它的类代码,所以

  1. 我们不清楚命令按钮(Command0)是怎样将一个事件(比如Click)发送(Raise)出来
  2. 我们也不清楚窗体1是怎样接收到这个事件的

我们唯一清楚的是,可以为Command0的单击事件编写响应代码子程序(Private Sub Commaond0_Click()),当单击事件被Command0对象发送出来的时候,窗体1接收到这个事件,然后响应该事件的子程序自动被执行。

事件的发起方(比如Command0),我们称之为事件源(Event Source),事件的接收方(比如窗体1),我们称之为事件监听者(Event Sink)(Sink这个词的本意是下沉,沉没,很难翻译成容易理解的中文,所以我就意译为事件监听者)这就是事件的真相之二:事件包含两方:事件发起方(事件源),事件接收方(事件监听者)。

我们可以拿电台收音机来类比一下。事件源就好比一个电台,它向外发送电波,事件监听者就好比收音机,可以接收到电台发出来的电波,收听到音乐新闻等。一个事件源发送出来的电波,可以被很多台收音机接收到。这是一个生活常识,但是,事件源发送出来的事件,能被多个事件监听者接收到吗?

我直接给出答案:可以。这就是事件的真相之三:一个事件源发送的事件可以被多个事件监听者接收到。能演示一下吗?必须的。

我们新建一个窗体2,让窗体2也能接收到窗体1上的命令按钮Command0发送出来的单击(Click)事件。

为了让窗体2能建立与窗体1上的按钮Command0之间的联系,我们要在窗体2的头部定义一个命令按钮对象变量mCmd。然后想办法将这个对象变量指向Command0。

如何让mCmd指向Command0?我们知道,Access中所有被打开的窗体,都会被自动添加到一个叫做Forms的集合中。所以当窗体1被打开后,我们可以用Forms(“窗体1”)来引用到内存中的窗体1,然后用Forms(“窗体1”).Command0,或者Forms(“窗体1”).Controls(“Command0”)来引用到窗体1上的Command0。这件事最好是在窗体2加载的时候完成,所以有了下面的代码:

当然这段代码要想执行成功,必须得先将窗体1打开加载到内存才行。先不管错误处理代码。

现在我们有了一个指向窗体1上的命令按钮Command0的对象变量mCmd。我们就可以随心所欲的来调用Command0这个对象的任何属性和方法,但是必须由mCmd主动发起。

但是,Command0发送出来的事件,能窗体2中的mCmd接收到吗?

能不能我不知道,但是我知道,如果能,那么窗体2的代码模块窗口左上角的下拉框中,如果有mCmd这个对象变量,那么就说明mCmd可以接收到事件,就可以为它接收到的事件编写响应子程序。你点一下左下拉框看看,mCmd并不在其中。

这里我们要向大家介绍VBA中的一个新的关键字:WithEvents。(字面意思是携带事件)它是专门用来解决我们目前遇到的问题的:现在mCmd已经指向了一个命令按钮对象Command0,如果要向让mCmd能接收到Command0发送出来的事件,就可以在定义mCmd的时候,将WithEvents关键字添加到mCmd前面。我们修改mCmd的定义如下:

然后我们再来看看左下拉框中的选项,如下图所示:

在mCmd之前加了关键字“WithEvents”之后,左上下拉框中就出现了mCmd选项,说明我们可以为mCmd对象变量编写事件响应子程序了。我们在下拉框中选择mCmd,然后点开右下拉框,如下图所示:

右下拉框与窗体1中Command0的右下拉框列示的内容是一样的。说明mCmd现在可以接收它所指向的对象(Command0)发送出来的所有事件了。我们只对单击事件感兴趣,所以编码如下:

当然我们在窗体2卸载的时候,将mCmd指向Command0的指针清除。要养成主动释放指针的好习惯。

窗体2的代码写好了,我们来测试一下,看看窗体1上的命令按钮Command0发送的单击事件,是否也能被窗体2中的mCmd接收到。

先打开窗体1,然后打开窗体2。最后单击窗体1上的Command0按钮。显示的截图如下:

这样就验证了窗体2中的mCmd也接收到了Command0的单击事件!

窗体1和窗体2都可以接收到Command0的单击事件,让我们有一种错觉,似乎所有的模块都可以接收到事件。其实不是的,模块1(标准模块)就无法接收到事件。我们试一下看看先,在模块1中头部位置写如下代码:

我们编译一下跳出错误:

WithEvents被高亮选中,编译提示:仅在模块中有效。标准模块不是模块?这里提示翻译得不准确,应该是:仅在类模块和窗体模块(或报表模块)中有效。而类模块和窗体模块都是类模块。所以这里,我们从试错的角度给大家揭示事件的真相之四:事件接收方(监听者)必须是类对象

为什么事件的接收方(事件监听者)必须是类对象?

这个与事件传播到事件监听者(可能有多个)的底层实现有关。“传播”这个词是从结果上来描述的,在代码层面,只有一个词:“调用”。那么事件是如何从事件源传播到所有的事件监听者那里的?原来,事件源(Event Source)含有一个连接点容器(Connection Point Container),该容器中都是连接点对象(Connection Point Object),每一个连接点对象都指向一个事件监听者(Event Sink),当事件源对象中某个事件被触发(Raise),传播这个事件给每个事件监听者,就变成了遍历连接点容器中的所有对象,然后调用每个对象所指向的事件监听者的事件响应程序,于是,事件得以在全部事件监听者中传播。

了解了事件的底层实现机制,事件监听者(Event Sink)必须是类对象就很好理解了。因为事件源的连接点容器中的连接点对象,是指向事件监听者的,什么东西能被一个对象(变量)所指向?很简单,这个东西肯定是内存中的一个对象。所以事件监听者必须是类对象。WithEvents关键字只能出现在类模块中(包含窗体模块和报表模块)

最后总结一下本篇的要点:

  • 事件的真相之一:事件的传递方向(类 -> 类的使用者)(而属性和方法的调用方向:类的使用者 ->类)
  • 事件的真相之二:事件包含两方:事件发起方(事件源),事件接收方(事件监听者)。
  • 事件的真相之三:一个事件源发送的事件可以被多个事件监听者接收到。
  • 事件的真相之四:事件监听者必须是类对象。
  • 定义对象变量的时候,使用WithEvents修饰关键字,能接收对象变量指向的对象发送的事件。

作为一个有一定经验的Access VBA开发者,你早就知道如何为对象的事件编写事件响应程序。通过本篇的介绍,你应该对事件有更深入的了解,并且学会了如何接收其他类对象发送出来的事件(使用WithEvent关键字),那么关于事件,你就只剩下两块内容:在类中定义自己的事件,和在类中发起你定义的事件。下一篇将介绍该内容。

最后,按惯例,将最终全部代码贴给大家。

注意操作顺序:先打开窗体1,然后窗体2,最后点窗体1按钮Command0。

后记:有群友(河北-SQL Designer)建议将类的术语统一起来,与其他面向对象编程的语言相一致。非常感谢他的建议。我仔细斟酌之后,发现使用统一的术语,反而让意思更加晦涩难懂。所以就还是保持原样了。敬请谅解。

VBA 类模块系列之十一——类代码中的警察(二)

在《系列之九——类中的常量》中,我们为类定义了枚举类型的性别常量

然后在测试代码中为性别属性赋值的时候,IDE的智能提示跑了出来:

我当时拍着胸脯说,这回,类的使用者不会瞎给性别属性赋值了。好吧,我承认当时有点说大话了。因为当时闻到空气中兴奋的气味,所以不想扫大家的兴。

我们弄个极端的例子:某泰国的lady boy在给这个属性赋值的时候,一看提示出来的2个选项,没有一个是他(or 她?)能选的。心想,MD,这不是明摆的歧视我这个第3性嘛,我就给赋个值3,然后编译一下,居然编译通过了!

为什么能编译通过?还记得我说过,枚举常量用起来是非常简单的,简单到什么程度?不负责任的程度^_^。枚举类型,从本质上讲,是长整形(Long),你拿任何一个长整形的值赋给它,它都无条件接受。至于你在这个枚举类型下面,定义了多少个常量,它一概不管。

好吧,既然你知道了类中枚举常量的底线轻易的就可以被人突破,导致类的属性,被设置了非法的值,你说怎么办?既然是非法,当然得交给警察来管!

我们终于切入正题了。

我们将原来性别赋值属性子程序(Public Property Let Gender)中的代码全部删掉(相信你已经掌握了如何设计只赋值一次的属性),在其中按照上一篇的样式设置警察,警察局如下面代码所示:

  • 第2行,第8-14行,就是我们上一篇介绍的内容,不再赘述。
  • 第3行代码,很容易理解,如果属性被赋的值,既不是男,又不是女,那就是非法的值了!
  • 第5-7行,是合法的赋值。

现在问题就来了,第4行我们要写什么代码,才能让警察现身?上一篇是故意用一个0作为除数,本篇还要这么搞一下么?这样固然可以把警察召唤出来,但是有点为了目的,不择手段的感觉,咱们可都是有品味的人^_^。

我们这里的困境是,属性被赋了非法的值以后,怎么才能让警察知道?等等,什么是非法?这个法律是谁说了算?是你规定了性别只能是男和女,当然是你说了算。既然别人给性别赋了一个既不是男,又不是女的值,你就得跳出来说,这是非法的!而不是自己做一件非法的事(比如弄个0做为除数),以便故意把警察引出来。所以现在的问题是:你通过什么指控别人非法?

每当你穷尽脑汁还是一筹莫展的时候,就是学习新东西的时候了。话说,你对 “Err” 这个系统对象,了解么?作为一个有一定经验的VBA开发人员,你使用这个系统对象,多半都是在错误处理程序当中,当发生运行时错误了,你一般会去调用 Err.Description 来看看错误说明。这个对象还能用来指控别人非法!怎么指控?用Err对象的Raise方法(raise可以翻译理解为提起诉讼的意思)。

Err.Raise的第一个参数是错误代码(Number),可以理解为违反的是第几条法律。不要忘记了,性别必须是男或者女,这法律可是你定的,我C,有没有一种很爽的感觉?^_^,虽然这条法律是你说了算,但是你在给它编号的时候,要注意不要使用系统中已经被占用的错误编号。我们修改第4行代码如下:

第4行代码,给错误编号为vbObjectError+513,为什么弄这么一个奇怪的编号?因为vbObjectError+512之前的号码都被占用了。vbObjectError是一个系统常量。另外,我们设置了错误的来源(Source)和错误的描述(Description)。

代码运行到第4行,被宣布违反了你制定的法律以后,警察就把你拷起来带到了警察局,第13行。

问题又来了,在警察局你要做什么事?上一篇是用MsgBox检讨了错误,就给放了。这里你要做同样的事情吗?当然,这个决定权在你。你可以MsgBox弹出来告诉用户,它违反了你的什么法律,你还能在这里将这件事记下来,写入数据库中,留下永久的案底。你想怎样都可以,因为在编程的世界里,你是上帝。^_^

对于现在的情况,我的建议是,将错误上报给上级机关,然后,然后就没有然后了。这样有什么好处吗?呃,省事,我只负责立法和司法,确保我设计的类中没有违法乱纪的行为。行政处罚就交给使用我的类对象的上级主管单位吧,是做检讨撤案,还是写入档案,由类的使用者来决定。

我们修改第13行代码如下:

第13行我们还是用的Err.Raise来向调用这个子程序的程序提起违法诉讼。

现在我们整体梳理一遍这个警察故事二:首先我们设了一个警察和警察局,然后当性别被赋予违法的值的时候,我们提起了指控,代码被带到警察局,警察局直接将指控上报给了上级机关。

发现问题没有?如果警察只是把罪犯抓起来送给上级,和没有警察是一样的。因为上一篇讲过,如果你在县里不设置警察,代码在县里发生的运行时错误,会自动上报至市里的警察。所以我们可以把县里的所有警察和警察局都撤掉,代码如下:

世界一下子清净了不少^_^

我们修改一下类的测试代码如下:

F8逐步执行一下就会知道,虽然你将性别赋值为3,编译器没有检测出非法代码,运行的时候还是没逃出法律的制裁。

最后,按惯例,将最终全部代码贴给大家。

 

VBA 类模块系列之十一——类代码中的警察

好吧,我承认本篇的标题有点哗众取宠,不过这是我能想到的最佳的类比方式——以你最容易理解的方式——把我想谈的讲清楚。类代码中的警察就是类代码中的错误处理

我们先来谈谈标准模块或者窗体模块中的错误处理。

作为一个有一定VBA编程经验的你,应该写过错误处理代码。我们来举个例子,在Access中新建一个空白的窗体,然后拖一个按钮到这个窗体上,窗体和按钮的名字都用默认的就好了,保存以后进入到IDE界面,为按钮的单击事件写一段简单的代码如下:

这个子程序除去错误处理代码以后,非常简单,声明了2个变量,给变量a赋值为1,然后用1除以0的值,赋给变量b。大概没有人能写出比这还蠢的代码了。^_^ 之所以要这么干,就是故意让警察抓个现行。

警察在哪呢?第6行代码就是!第6行代码翻译成大白话就是:如果在接下来的代码(从第7行开始)中发现了违法的代码,就铐起来带到名叫“ErrHandler”警察局,“ErrHandler”警察局在哪里?第15行。为什么警察局的名字叫“ErrHandler”?呃,这个名字的意思是“错误处理”,其实取什么名字是你说了算,我一般取这个名字的警察局,你可以换成你喜欢的名字。

第10行代码是一个非常典型的运行时违法代码,或者叫运行时出错代码,计算机无法将0作为除数计算一个结果给你。

  • 为什么我这里要加一个“运行时”?因为第10行代码在编译的时候,不会报错,只有在运行的时候才会。
  • 为什么在编译的时候不会报错?呃,你可以把0想象成一个参数,编译的时候,这个参数的值还没有确定,只有在代码运行的时候,发现这个参数是0,然后作为除数,参与计算,就报错了。
  • 为什么我们不考虑编译时的违法代码?因为如果编译的时候发现了违法代码,比如使用了未定义的变量名,编译器会报错,你不去修改这些错误代码,你连运行都无法运行。
  • 还有别的运行时出错代码吗?呃,有的,多了去了。比如代码去打开一个不存在的文件,数组变量越界访问等等。

好了,关于运行时出错代码的十万个为什么到此结束,假定你都理解了。我们从头来捋一遍。

  • 我们放置了一个警察在第6行代码中,告诉它,如果发现运行时的出错代码,就铐起来带到“ErrHandler”警察局。
  • 当程序运行到第10行的时候,代码运行出错了
  • 所以程序就从第10行跳到了15行,警察局的位置,然后接着往下执行。
  • MsgBox向你检讨错误编号,错误描述,错误来源。
  • 接下来执行第21行代码,Resume ExitProcedure,这句代码的意思翻译过来就是说:好了,警察局问话结束,该交代的都交代了,撤销你的案底(Resume),然后去ExitProcedure报到把。
  • 然后程序跳到第11行接着往下执行
  • 第12行代码与第6行代码很像,也是放置了一个警察,但是这回不是让它铐起来带回警察局了,而是直接撤销案底(Resume),接着往下执行(Next)。然后如果在执行到第14行之前,遇到任何执行错误,通通就地无罪释放。
  • 最后到第14行,结束程序的执行。

我感觉我刚刚写了一篇侦探小说。^_^ 本来只想简单介绍一下背景的,结果一发不可收拾。。。。。。本篇就作为警察故事一吧。

既然想好了写个警察故事二,那在这个警察故事一中,在加点料。

关于错误处理代码,除了刚刚介绍的那些意外,还有一个也很重要的代码:

On Error GoTo 0

如果让人工智能来推断这句代码的意思,应该就是:设置一个警察,一旦发现运行时错误,就把它拷起来带到叫做0的警察局。哈哈,所以说人工智能还是很蠢的。这句代码真正的含义是:从这句代码开始,把警察撤走。

什么?把警察撤走了?那出了运行时错误,谁管?把一个小县城的警察局撤销了,这个小县城的违法犯罪你说归谁管?当然是上一级单位,市警察局管了。这就相当于,你的子程序代码中没有任何错误处理程序,如果运行时出错,就归调用你的子程序的程序(上级单位)的警察管了。

最后再提出一个问题,如果在警察局里面发生了运行时错误,会怎么样?你可以自己写代码验证一下你的直觉。^_^

好吧,希望你对错误处理代码有更深刻的理解,这样在类中的错误处理代码才更容易掌握。

VBA 类模块系列之十——类对象的生与死

在谈类对象的生死问题之前,我们先谈一下一般变量的生死问题。来看下面的代码:

这段代码声明了一个整型变量 a ,这个变量 a 就在计算机的内存中,占据了几个字节的空间,然后被赋值为100,然后在立即窗口中被打印出来。最后,变量 a 离开了它的作用域,它所占用的内存空间被系统自动回收。

任何程序的运行都需要占用一定的内存,内存是一种有限的资源,所以不能随意浪费。占用完,必须要返还给系统,以备其他程序使用。

对于一般变量来讲,我们不用考虑它占用的内存的释放问题,因为VB/VBA没有指针的概念(严格来讲,VB/VBA还是有指针的,只是非常少用,不像C语言指针那么普遍,以后有机会再给大家介绍),所以不存在其他变量也指向变量 a 所指向的内存地址。如果一段内存地址永远只被一个变量所指向,那事情就非常好办了,该变量何时离开它的作用域,它所指向的内存就何时被系统释放回收。所以对于一般变量,我们完全不用考虑其占用内存的释放问题。

但是对于类对象变量来讲,情况就稍微复杂一点了。我们来看下面的代码:

类对象objPerson从什么时候开始占据内存空间的?这个内存空间又是何时返还给系统的?我们把类对象开始占据内存空间看作是类对象的生,内存空间被释放看作是类对象的死

我们在第6篇,类对象的初始化的时候,谈到了类与生俱来的2个事件:实例化事件(Class_Initialize)销毁事件(Class_Terminate)。这2个事件,刚好对应着类对象的生与死。

类对象开始占据内存空间的时候,实例化事件就被触发,实例化事件子程序(如果你为实例化事件写了事件代码的话)中的代码就会自动执行。实例化事件子程序也是初始化类对象的最好的地方。

相反,销毁事件,是在最后一个指向该内存空间的对象变量被置为Nothing的时候,被触发。在销毁事件子程序中,我们主要是做一些对象销毁前的善后工作,比如释放类对象内部的一些资源等,这个要等到创建多个类之间的关系的时候才会谈到。

为了清楚的给大家演示类的出生和死亡,我们可以分别在实例化事件子程序和销毁事件子程序中,添加一条简单的语句:Debug.Print “我出生了!” 和 Debug.Print “我挂了!”,一旦这2个事件被触发,你在立即窗口就能清楚看到。我们修改Person类代码如下:

我们使用F8快捷键逐语句执行 Test3 中的代码,你就能清楚的知道,当你执行Set objperson = New Person的时候,类对象的实例化事件被触发。当你执行End Sub的时候,类对象的销毁事件被触发。

为何我们不使用Set objPerson=Nothing,也能让类对象的销毁事件被触法?因为在Test3中定义的objPerson在执行End Sub的时候,已经离开了objPerson的作用域,所以objPerson被自动置为了Nothing,而类对象只有一个对象变量objPerson指向它,当objPerson被置为Nothing的时候,类对象的销毁事件就被触发,执行其中的代码,立即窗口就出现“我挂了!”

我们在做面向对象编程的时候,为了安全起见,一般不要去依赖对象变量的作用域来自动销毁类对象。而是显式的写一条释放语句:Set objPerson = Nothing。我相信你在使用ADO对象的时候,写过很多Set rst = Nothing的语句。

本篇要讲的内容基本上谈完了,但是我回头看了一遍,除了一点概念,没有什么值得装逼的地方,这不符合我的一贯作风^_^。所以,下面教大家一点装逼的技能。前方高能!

我在Access 开发框架(翻译+改编)系列之三——集合、类和垃圾回收中有谈到过对象编程的一条公理:一个对象会一直驻留在内存中,直到最后一个指向它的对象变量被设置成 Nothing。类对象变量与一般的变量最大的不同,就在于:可以有多个类对象变量指向同一个内存空间。

既然这样,我们弄2个对象变量,让它们指向同一个类对象,然后我们释放其中一个对象变量,看看会发生什么情况。编写测试代码test4如下:

F8逐语句来运行Test4中的代码,看看类对象何时被创建,何时被销毁吧。装逼点在哪里?瞪大眼睛看第7行代码中的“Is”操作符!你应该写过 if obj is nothing then 之类的语句,obj is obj这种代码你应该是第一次看到,还不够你装逼的?^_^

最后,按惯例,将最终全部代码贴给大家。