Visual Basic 6面向对象编程总结

Visual Basic 6面向对象编程总结

1 年前

Visual Basic 6面向对象编程总结

前言

Visual basic 6是一个很强大很高级的语言,在面向对象和面向过程之间有很好的平衡,支持面向对象的一些基本特性,但绝对不会滥用面向对象中的概念。

事实上,面向对象并没有什么了不起。面向对象几乎没有解决任何算法的问题,只是解决了一点编程时程序代码的组织的思路和方法的问题,而且在编写几百行代码的小型程序时,使用结构体,如VB的自定义类型可以解决绝大多数问题,用不上面向对象。

但即使如此,VB6仍然提供了基本的面向对象的支持,并且有一定的实用性,但又没有搞到太复杂。

但是VB6的面向对象的特性往往受到忽视。主要原因应该是很少有书籍介绍VB6的面向对象编程相关的知识。尤其是计算机等级考试二级的教材,将其完全忽略,让业余人士误以为VB6的全部功能就是这点。这是一种典型的做法。而像是明日科技等出版的实用教材也宁可花很多功夫介绍VB的各类奇技淫巧,却几乎从来不提这块内容。尽管关于vb6的面向对象特性的知识全部都明明白白毫无隐瞒地写在MSDN98中文版中。

当然这些障碍是迷惑不了当年那些专业VB开发人士的。

但是时过境迁,由于微软抛弃并拒绝开源VB6,专业开发人员基本都远离了它。但VB6仍然是业余桌面编程爱好者的最佳玩具。原因在于:

  1. 没有什么语言的IDE如此轻便,简化版甚至不到10MB。 VS.NET 现在已经十几个GB了。
  2. 没有什么语言带有完整的官方中文文档,F1一键直达。所以Delphi比不了。
  3. VB6很好平衡了入门和深入。不了解面向对象也可以上手。
  4. 开发环境兼容性保持到WIN10。
  5. 关键字和标识符不区分大小写,减少了犯错的机会。打败一堆C系语言。
  6. 绝不使用稀奇古怪难以理解充满歧义,而且键盘上很不方便按到的陌生符号来故作高深,可读性超级高。继续打败C系语言。
  7. 既可以强类型又可以弱类型,一句option explicit搞定。非常灵活。拒绝弱类型语言的指控。
  8. 成对的代码有各自不同的说明符,不像C系语言都用大括号,嵌套多了都不知道这个右大括号前面对应的是个什么语句,还得干出在右大括号后面手动写上注释:前面是一个for循环这类傻事。非常有利于插入代码时的定位。
  9. 虽然关键字较长,但是可读性高,而且现在写代码谁不用自动完成?C系语言所谓的语法简洁已经没有任何实际意义。
  10. 有了codesmart等代码格式化插件,自动缩进格式整理一秒完成,不存在结构乱的问题。所以Python的语言级强制缩进已经没有任何意义,反而只能成为一种奇葩,使你无法享受用工具自动整理格式的快感。
  11. 原IDE的一些缺陷,如没有函数列表、自动缩进格式整理、没有鼠标滚轮支持等,已经全部被codesmart等插件完美搞定并得到很多增强。
  12. 垃圾回收。虽然从来不像Java等那样吹嘘。作为遵循了com规范的开发工具,这都是基本要求。
  13. 类有真正的属性过程,property get let set。这种特性连java、c++都没有,C#的get let都是从这里学的。
  14. 对桌面GUI程序进行方便的可视化开发,事件驱动。打败Python这些只能玩命令行的GUI弱鸡的语言,以及一些貌似可以做GUI但只能用switch过滤检查消息跳转对应处理过程的语言(这做法真的很古老)。
  15. 虽然缺少一些顶级的特性,如多线程、try……catch这样的错误处理,但是那些是很专业的开发者或者开发领域才会用到的。而且ActivX exe多进程以及on error也可以一定程度上替代。对于日常玩耍根本用不着。又不是拿VB6去搞现代企业级开发。
  16. 运行速度快,打败Python。
  17. 既可以解释执行也可以编译执行(类似JAVA虚拟机)。可以直接单步执行调试,不像编译语言那样要进入什么“调试模式”(其实也是编译了的,而且每一句后面都插入了断点才实现。)
  18. 保留goto语句,跳出多重循环很方便。保留GOTO不是错,自己别乱用不就行了。

当然VB6也有不便:

  1. 没有continue语句。跳过本次循环有些不方便。
  2. 语句换行比较麻烦,要自己添加空格和_。所以其实插入写成了多行的SQL并不方便。不知为什么还老用在数据库开发方面。
  3. 没法静态编译,不能做单exe文件的程序。
  4. 没法搞出不用注册的组件。如果你写了一个辅助工程比如一个activeX exe放到项目文件夹里面,并在工程里面引用。下次项目文件夹搬家,还得重新注册一下,否则报错。这会导致一些小问题。比如你拷贝项目文件夹了,但是新文件夹里面的工程引用的组件还是原来旧项目文件夹下面的那一个。

综上,VB6仍然是业余桌面编程爱好者的最佳玩具。为了让新来的爱好者能继续愉快地玩耍,本文较为全面地总结了VB6面向对象开发的一些关键特性,使新手爱好者对VB6的面向对象能力有客观的认识。如有缺失或错误欢迎补充。

类和类模块

VB6可以建立类。操作方法就是建立类模块。类模块和窗体模块、标准模块类似,每一个类模块代表了一个类。类模块(属性栏中)的名就是类名。并且和窗体模块、标准模块一样,有自己单独的代码编辑窗口。

类的所有成员:属性、方法、事件都在类模块自己的代码窗口中编写。

类的属性

类的属性有两种。1是直接定义在类中的public变量。可由外部直接访问。但这不是现在最流行的属性概念,而且已经被称为“类的字段”。

2是利用属性过程所定义的真正可以做到封装的属性。

这种属性可通过工具菜单-添加过程对话框,并选择“属性”来添加。其操作结果实际就是写入一对过程的定义语句。

这一对过程拥有相同的过程名,此即为属性名。但其前缀不同。一个为property get,一个为property let。其中前者有返回值,后者有传入参数。返回值的参数类型必须和传入参数最后一个类型相同。

当属性过程建立后,此属性成员也就建立了,并为代码提示功能所识别。

即可以用clsname.属性名的格式对这些属性进行引用。

对属性进行引用只有两个用途,即读取和写入。其中读取操作,如将属性值赋予某一变量,系统实际将运行property get中的代码。写入操作,如为属性赋予某值,实际将运行property let中的代码。因此类的属性虽然使用起来像是变量一般,但实际在类模块内部,并无以此属性名命名的变量。

如果两个过程内没有任何代码,引用类来对其进行设置或读取该属性的代码不会起任何作用。

为了使其像变量一样地工作,需要配合类内部的私有变量。比如在类中,定义一个private mMyProperty,在property get过程中,返回内部变量mMyProperty的值,在property let过程中,为内部变量mMyProperty赋值。

这种机制看似多此一举,实际上是起到了真正的“封装”的作用,可以利用在两个过程中添加其他代码来进行诸如赋值前的校验等配套操作。

并且,可以通过是否省略property get或property let过程,来控制该属性的可读、可写性。省略前者将使属性称为只写的。省略后者将使属性称为只读的。

属性的赋值过程,除了let过程外,其实还有set过程,在调用者使用“set cls.myp=xxx”的方式对属性赋值时,会自动调用set过程。所以实际上总共是有3个关联的过程。要实现可写的属性,let 和set过程至少要有一个。但亦可同时具备(应对变体类型)。

但是使用let还是set属性过程,和属性值的类型是否是对象类型没有直接关系。如果属性是对象类型,但是已约定引用者对其直接使用“=”赋值的话,那么仍然运行let过程中的代码,set过程可以省略。

VB6中有少数BUG,即使赋值时使用=,但模块内的property set过程必须存在,否则报错,但系统仍然调用let过程的代码。目前只在用户控件的调用中发现这个BUG。

在let和set过程中的参数,除了默认的以外,还可以自行增加,只要两边保持对应即可。这就方便地实现了所谓索引器的作用,甚至更强。因为索引器只有一个参数。

类的方法

定义在类内的public公共子程序或者函数将成为类的方法。private的则成为类的私有成员,不会暴露给调用者。这里我们不使用“外部调用者”的说法,仿佛公有成员只有外部调用者在调用。因为在类的内部,也可以调用公有成员。一般采用Me.membername的方式来调用。但是当然不管是否公有私有,类内的代码都可以直接用过程名调用他们。

类的事件

类的事件的定义和使用,相对复杂一些。

要定义事件,需要在类中使用“public event 事件名()”即可。事件可以带有参数,就像标准控件的事件带有参数一样。可以自定义参数的个数和类型。后面触发事件的时候,需要给参数赋值。

事件的使用,分为触发和处理两个部分。

事件的触发,使用raiseevent 事件名()。这个语句只能在定义该事件的类的内部的某个过程中使用。

而调用者如果想引发该类的某个事件,那么他可以调用这个类中包含raiseevet该事件的方法、属性过程即可。

这样就能触发事件。

但是仅仅触发事件,没有事件处理过程,那没有任何其他操作被执行。

事件的处理:

假定事件E是定义在类cls1中,引发代码也在cls1中。

现在设置cls1类型的对象变量A并实例化,那么执行A的某个包含raiseevent的方法E就可以引发事件。

但并不是通过在cls1里面写事件处理代码来处理时间的。

要处理这个事件,需要在调用模块中,比如窗体模块或者类模块中(标准模块不行),还要定义一个也是CLS1类型的模块级对象变量B,但是声明B的时候,在B的标识符前面加上withevents。

当这样定义之后,在VB的开发环境的代码窗口中,上方左侧对象列表就会出现B,而右方事件列表就会出现E,当你选择他们后,代码中就会自动添加private sub B_E()的过程。在这个过程里面,你就可以写事件处理代码。事件发生时,这个过程的代码会被系统自动调用,传入的参数值,就是raiseevent 方法那里传入的实参。

但是这仍然没有完。

然后要set B=A,即这两个对象变量要指向同一个对象。这样B_E()才会被真正调用。

这个做法等于是把A引发的事件处理委托给B这个对象变量在处理。

这里有两类变化的做法。

一是在设置B=A的同时,还可以设置其他的cls1类的变量B1=A,B2=A,B3=A等。这样这四者的事件处理代码,就可以分开编写,实现类似观察者模式的机制。但是这几份代码的执行先后顺序是无法确定的,虽然看起来是按照令他们=A时的那部分代码的先后顺序定的。这是微软也不能保证的。

二是,事件处理的过程也可以不委托给B对象变量。当你声明A变量的时候,如果也使用withevents关键字,那么也可以写A_E()的过程,由变量A自己来处理。但是任何作为事件处理的对象变量,都得是模块级的变量。

继承和多态

VB6没有完善的面向对象机制,不能用一个类继承另一个类,从而复用父类的方法。VB6事实上是采用了类继承的语法,但是所实现的本质上只是接口继承的功能。

如果类B要继承类A,那么只需要建立类B的类模块,并在第一行写上 implements A 即可。这就表示类B是继承了类A。一旦形成了这个关系,在类B内部就必须写上类A的过程、属性的实现(类A的字段在B中也要求以属性的形式来实现)。至少你得写出它的声明。即使没有内容,也要有这个壳子。不过幸运的是这个这些实现过程的列出,并不需要手动去写。只要B是A的子类,在B的代码窗口上方,左列表会列出A,选择后,又列表会列出A的所有需要B实现的成员。只要点选,就会自动添加或跳到相应的实现代码。这些成员过程的名称都遵循“父类名_成员名”的规则。这个列表中,凡是已实现的,其名称将以粗体字列出,凡是未实现的,其名称将以正常字体列出。所以只要把所有没有加粗的都点过,使其增加到代码编辑区来,这样的代码运行起来就不会报“成员没有实现”的错误。

当然除了实现父类的成员,子类仍然还可以增加自己的方法成员等。由于VB6的面向对象机制并不完整,什么抽象类、抽象方法、封闭方法等,那都是不存在的。

因为VB6的继承实际上是接口继承,所以父类的成员,不管父类是否有真实的实现代码,子类都不能省略自己的实现以直接复用父类的代码,而是必须全部自行实现,只是做到了成员的一种统一规范化约束。但是由于父类的定义又是使用的定义普通类的语法,所以VB6的父类又可以进行实例化,而不像真正的接口那样不能实例化。并且父类的成员如果有实际的实现代码,也可以用父类的实例来调用。

也有一种似乎被称为委托的方法,可以实现类似父类继承的效果。但那其实只是在子类里面定义一个内部的父类型的对象变量,并在子类的class_initialize()过程里面实例化它,并在子类继承的那些方法的过程里面,引用这个对象变量来调用父类方法而已。

同一个子类,可以继承多个接口。多个不同的子类,当然可以继承同一个父类接口,并且有各自迥异的实现过程。利用这些机制,可以实现多态。

多态

如果A是父类,拥有s方法,B和C是A的子类。那么,如果定义一个myobj对象变量,其类型为B,或者C,那么myobj只能使用B或者C自己的成员,不能使用myobj.s这样的写法。必须在myobj的定义中,声明为A父类的类型,才能使用myobj.s这样的写法。

但是虽然myobj被声明为A类型,但是仍然可以使用set myobj = new B 或者set myobj = new C的语句将其引用到子类B或C的对象上。这个时候再使用myobj.s实际调用的则是B或C中的实现代码。这样就实现了多态。

当myobj的类型申明为父类的时候,代码提示器会提示它可以使用s方法。因为这是它声明的类型所拥有的。但是它不会提示B类型或者C类型的成员。因为编辑时IDE不能判断它将来运行到这里到底是什么类型,只有它声明过的类型是可以肯定的。但是实际上,如果你知道此时myobj的强类型就是B或者C,即使代码没有提示,你也可以手动写上“myobj.B类型的某个方法”。只要你保证运行时myobj所引用的对象的类型确实有那个方法成员,程序就不会报错。

集合类

可以建立集合类来代替类的集合。因为事实上只有集合,并没有专用于类的集合。所以集合并不能控制其成员的类型。

建立集合类可以使用VB6的类生成器工具来创建。集合类中,包含一个私有的普通collection集合对象变量,并在类模块的initialize事件代码中进行初始化new,但集合类通过把自己的add、count等方法、属性过程交给这个私有的集合来处理,来模拟普通集合的操作,并可以通过额外的校验代码或者像例程那样,不提供所加对象参数等限制手段,来提高代码强健性。

唯一特殊的就是通过get newenum() 过程,读取私有集合对象的隐藏的[_NewEnum]成员,作为newenum的返回结果,并将newenum()的过程标识符设置为-4,以及隐藏该成员(在工具菜单下的过程属性窗口中设置),来支持foreach功能对这个集合类的访问。这个写法虽然有些复杂,但是用前述的类生成器生成集合类代码的时候,会自动加入这个过程的代码,只需要设置一下过程标识符和隐藏即可。如果你忘记怎么写了,就可以用类生成器生成一个临时的集合类,从里面复制这段代码并稍加改动和处理即可。

控件数组似乎并不是真正的数组,而是像是集合。并且VB6无法实现强类型声明数组控件,所以如果你要把控件数组作为参数向过程传递,那么形参的声明不能写具体类型,只能写object类型,并且形参不带数组的空括号。虽然形参的使用还是可以用.lbound和.ubound(当然没有代码提示,只能自己写)。但是可以传递引用控件对象的对象变量的数组,就是说要对控件数组的成员做一个浅拷贝,将其成员分别引用给另一个对象型变量的数组,然后将这个数组作为普通数组传递。被调用的过程的形参声明那边,则是像普通数组一样,可以声明和对象型变量的数组一样的强类型,形参名也要带着数组的空括号。这是一个变通的做法。

用户控件

除了单独的activeX控件工程外,用户控件和类模块一样是可以直接添加在工程里面的,不像activeX控件等需要另外建立一个工程。最后发布的时候也不用多带一个组件文件。用户控件也类似于一个类模块,只不过它带有窗体,可以在上面加别的控件。此外用户控件自身的属性比类模块多很多。不过直接在本工程添加的用户控件,不能单独发布。

用户控件的使用和类模块、窗体模块都类似,但是有很多需要注意的地方。

VB6这里的用户控件在编辑的时候(绘制其界面以及为其编写内部代码),和关闭了用户控件的编辑窗口,在主窗体中拖出调用的时候,对用户控件来说,它是处于两种不同的状态。可以说,当用户控件在被编辑的时候,它当然是“静止”的,但是当关闭用户控件的编辑窗口,在主窗体上拖出它的时候,它已经处于一种“运行”的状态了。此时它已经能够响应一些事件,比如它自己的initialize事件,并执行usercontrol_initialize()过程中的代码。(在这里,我们可以注意到这个事件过程名的前缀,是usercontrol,而非这个用户控件的名称,比如mycard等。从接口继承那里的规律来判断,大概可以认为每一个用户控件,都是继承了一个叫做usercontrol的父类的。而窗体中,窗体事件处理代码的前缀都叫form而非form1,可能也是这个原因)。

在用户控件的代码中,当然也可以用property get/let添加很多属性。但是当用户控件打开处于编辑状态时,此时的用户控件的属性面板中,并不会出现这些属性。而是当用户控件的编辑窗口被关闭,为主窗体所加载的时候,选中已加载的用户控件实例,才能在它的属性面板中看见这些属性。你可以修改这些属性。并且,如前所述,此刻的用户控件已经属于是“运行”状态了,所以你对这些属性值的修改,也必定会执行用户控件内相应的property get/set等过程代码。

但是此时修改的用户控件的属性,在程序运行的时候,又会全部消失。对于这个问题,则需要用属性包来解决。在用户控件的propertywrite事件中,用propertybag类型的变量propbag.writeproperty方法,保存控件的值,在propertyread事件中,用propertyread方法,读出保存的值,赋值给me的对应属性即可。

在使用用户控件的时候,关于属性还有一件值得注意的事情。那就是你会发现你无法直接使用他们的很多“基本”的属性,比如left、top这类。虽然在属性面板里面有,但是代码里面不能直接用。因为这些成员继承于VBcontrolExdender对象。所以要在代码中使用这些成员,要声明一个对象变量为VBcontrolExdender类型,然后用set=来引用到用户控件的实例。这样代码就会提示你可以使用left、top等成员。并且你仍然可以使用用户控件自己的那些方法,虽然没有代码提示。

用户控件也可以增加事件,和类模块一样。只要它定义并自己raiseevent就行。用户控件的实例,就像标准控件那样,可以在调用者的代码模块里面,写这个事件的处理代码。

用户控件中的子控件

用户控件上拖放的子控件,相对于使用用户控件的窗体来说,有如下特点:

1.子控件的事件可以触发。比如点到用户控件的子控件上,子控件的click事件会触发,对应的事件处理代码可以执行(当然这些是写在用户控件的代码里面的)。当然这是在整个程序的运行时,用户控件拖到主窗体后,主窗体还处在编辑时不会触发和执行。

2.但是子控件不能用代码直接访问。比如你的用户控件card上面有一个picturebox,那么不能用实例名card1.picturebox等方式直接访问它的属性、方法。必须把你打算要用到的那些属性方法,用这个用户控件自己的自行添加的属性、方法来包裹一次才行。所以也是挺麻烦的。

其他

IS操作符可以判断两个对象变量的引用是否是同一对象。不过type of 返回的是类名字符串,也作为IS操作符的操作数。

编辑于 2021-08-15 20:15