若不是因为你

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

第一章

第二章 

2-1

VCL Framework设计之初便设定了数个目标:

使用单一的继承构架以避免陷入C++多重继承的问题,同时这也有助于简化Delphi编译器的开发工作

VCL Framework 必须不限于16位或32位平台

VCL Framework 必须提供开放的组件架构,以允许程序员开发自定义组件

VCL Framework 必须进化成可在设计时期即提供功能的Framework

VCL Framework 必须使用PME(Property-Event-Mothed)模型

VCL Framework 必须使用面向对象的技术来设计和实现

VCL Framework 必须完善地封装和分派窗口消息

 

 

2-3-1

TObject = class

constructor Create;

destructor Destroy; virtual; 

end; 

析构函数Destroy声明为虚拟方法是因为在TObject的派生类中可能会分配额外的资源,因此派生类可以改写(override)TObject的析构函数。当派生类对象释放时先释放它自己分配的资源,再调用TObject的析构函数来释放TObject为对象分配的资源。如果TObject的析构函数不声明成虚拟方法,那么派生类的析构函数便会覆盖TObject的析构函数,如此一来只有派生类分配的内存会被释放,由TObject为对象分配的资源则可能没有释放,这就造成了内存/资源泄漏(leak)的问题。

 

Object Pascal的对象模型在这行程序代码之后进行了许多的工作,包括了分配内存、设定字段变量数据结构以及设定执行框架等工作

TMyObject.AllocateMemory;

TMyObject.InitializeSpecialFields;

Obj := TMyObject.SetupExecFrame; 

 在分配了对象原始内存之后,Object Pascal的对象模型会先初始化所有的内存内容为0:

FillChar(Instance^, InstanceSize, 0)

在分配和初始化内存之后的动作就是为类中声明的特别字段进行初始化的工作,也许读者会问为什么?不是已经使用FillChar把所有的字段变量内容初始化为0了吗?没错,不过这是属于一般的字段变量,对于一些特别的字段变量,Object Pascal对象模型必须为这些字段变量进行初始化数据结构的设定工作。例如对于接口变量必须设定引用计数值为0,对于动态数组则必须初始化内存区块等。例如声明了Variant类型的字段变量,那么Object Pascal的对象模式便会为其进行特别初始化的工作,至于虚拟方法则会进入VMT之中 

 

2-3-2

在Object Pascal的对象模型为对象分配了内存之后,还有另外一个极为重要的工作,那就是为对象创建执行框架(Execution Frame)。执行框架的目的在于把对象模型分配的内存内容为转变成活生生生存在于内存的对象,要把内存内容转变为对象模型就牵涉到初始化内存的内容为对象声明的字段变量、方法,为对象设定正确的VMT并且串联起正确的继承架构

面向对象程序语言和传统程序语言不一样的地方就是在分配内存之后执行的对象模型设定的加值工作,这些动作也就是让原始内存形成对象雏形的关键原因 

 

2-4

Object Pascal对象模型提供的基础服务包含:

对象创建服务 —— 提供创建对象的机制

对象释放服务 —— 提供对象释放的机制

对象识别服务 —— 提供对象判断,识别的机制

对象信息服务 —— 提供程序代码存取对象信息的服务

对象消息分派服务 —— 提供Object Pascal分派消息的服务,和VCL封装窗口消息密切的关系

 

TObject的NewInstance是虚拟方法,代表派生类可以改写(override)它,NewInstance的功能即是为对象分配内存,并且调用InitInstance方法为对象设定对象支持的接口。NewInstance的返回值是TObject,代表调用了NewInstance之后Object Pascal的对象模型已经在内存形成了TObject的尸体(instance),不过此时内存中的TObject仍然无法使用,因为接着需要设定对象的执行框架 

InitInstance方法的功能是为对象设定类支持的接口

 

一般使用Create方法创建对象时经过了3个过程,首先调用TObject的NewInstance分配内存以及调用InitInstance设定类支持的接口以及初始化工作,最后TObject会调用CreateClass为对象进行执行框架的设定。 

 

 类型转换(Type Casting)实际上就是让编译器产生调整执行框架的工作,让对象变量能够正确地存取到其执行框架之中的特性、方法和事件等。

 

 2-4-3对象信息服务

MethodName似乎只会对从TComponent继承下来的类的published方法才有作用

 

 类方法(Class Method),亦称静态方法(Static Method),类范围内的方法,可直接使用类名称来调用,而无须经由类对象调用

对象方法(Object Method),类一般定义的方法,必须经由此类的对象来调用

虚拟方法(Virtual Method),提供派生类(derived class)改写(override)父类定义的方法的机制

重载方法(Overload Method),定义同名,但是具有不同参数原型的方法,主要是使用了让程序员定义多个构造函数,或是接受不同参数,但是拥有相同名称的函数。一旦使用了重载方法之后,那么编译器便会以对象变量声明的类型作为绑定的依据,而不是执行时期以对象变量的实际内容为准则。

动态方法(Dynamic Method),类似于虚拟方法,但是可以大幅减少VMT的大小,不过效率稍为比虚拟方法缓慢一点

事件处理函数(Event Handler),组件阶层VCL对象的特别方法,可结合图形用户界面提供反应外界触发事件的能力 

 

2-6 VCL对象的释放服务

基本上Object Pascal对于对象的分配机制是使用堆分配(Heap Allocation), 那么当然就有释放对象服务了。而不像C/C++一样可以同时使用堆分配以及栈分配(Stack Allocation)。

var  aObj: TBase; 只是定义了TBase类型的一个对象指针(对象引用),并没有实际地分配TBase或是TBase的派生类的物理内存。程序员必须调用对象创建服务相关方法才会让对象在内存中实际成形。

如果TObject的析构函数Destroy不声明称虚拟方法的话,那么会发生下面的设计问题:

派生类将无法分配自己的资源

TObject可能无法正确释放由TObject分配的资源

Method 'Destroy' hides virtual method of base type 'TObject' —— 这代表程序仍然可以通过编译并且能够执行。但是Delphi的编译器做了最后一层的保护,因为Delphi编译器仍然会产生调用TObject.Destroy的机器码而不会调用派生类的析构函数,因此造成了TObject可以正确释放分配的资源,而派生类则没有正确释放的情况。 

 

TObject释放资源分为两个步骤,第一个步骤是释放Delphi类分配的特别数据类型变量的空间,第二个步骤则是释放Delphi类占据的内存空间。 

对于派生类所分配的资源,程序员只需要遵守:

1,改写Destroy虚拟析构函数

2,在改写的Destroy虚拟析构函数中先释放派生类分配的资源

3,最后使用inherited关键字调用父类的虚拟析构函数 

 

 2-7 类和对象的Metadata-VMT(Virtual Method Table)

Metadata是描述其它数据信息的数据。例如描述一个数据表(Table)中的字段名称、字段格式和字段大小等信息的数据就是数据表的Metadata,通常也称为Database Schema。而在面向对象程序语言中描述类和对象的Metadata就储存在称为VMT的数据结构中。

由于类方法是由所有的类对象共享,而字段变量却是每一个类对象各自拥有,因此把所有类的定义产生在一起是不恰当的,更何况操作系统可能也不允许把数据和程序代码编译在相同的地址区域中。

把编译器产生的内容分门别类,把每一个对象各自拥有的资源产生在对象的内存之中,并且把类共享的资源产生在另外一块独立的内存之中,最后再于对象内存中插入指向共享资源的指针,那么各自独立的对象就能够通过这个内嵌指针存取到共享资源,例如类字段或者类方法 

编译器可以设计成把一般程序/函数产生在特定的内存地址(根据程序/函数在类中声明的顺序),同时为了能够处理面向对象中继承和多态的要求,编译器可以为每一个定义的类创建一个拥有特定数据结构的表格,这个表格就称为VMT。在这个VMT表格中将产生指到类中每一个虚拟方法的指针,通过这个指针可以调用虚拟方法。此外在这个表格中也将会产生一个指向另外一个产生的动态方法表格的指针,通过这个动态方法表格指针可以存取到所有类中定义的动态方法,这个储存着共享资源的数据结构VMT也就是Object Pascal中的Self指向的目的地。当然为解决继承的问题,在这个VMT表格中也将产生指向这个类的父类的VMT表格,以便允许编译器实现出多态的功能,最后编译器会在每一个对象独立的内存中插入一个指到VMT表格的指针,以便让每一个对象实体能够经由这个指针存取到类定义的虚拟方法,动态方法以及存取到父类的VMT表格。

 

第三章 面向对象程序语言和Framework

 

3-1 面向对象程序语言和VCL Framework

面向对象的三项核心技术:继承(Inheritance)、封装(Encapsulation)和多态(Polymorphism)。 

 

3-2 Framework使用面向对象程序语言的设计手法

抽象法,使用抽象类来定义父代服务类,然后再开发派生类来改写(override)父代抽象类以提供实现服务。抽象类有一些问题,第一是Object Pascal允许程序员创建抽象类对象,这会导致执行期错误,虽然创建抽象类对象在语义上有问题,但是在语法上却是合法的,为了避免产生问题,VCL Framework并不喜欢抽象类。第二是Object Pascal可以使用接口来取代抽象类,而且使用接口设计比较符合现代面向对象趋势,抽象类已逐渐被接口设计取代。第三,如果是混合抽象类以及一些实现方法,VCL Framework通常都倾向使用Palce Holder方法。

Place Holder,是指父类的一些虚拟方法被实现为空白的函数而不声明为抽象方法。 

逐渐增加法,是指父类提供了基础的实现,再交由派生类提供更多的实现。

三明治手法,是指派生类在改写父类的方法时,会在使用关键字inherited调用类的实现之前先加入一些派生类的程序代码,再调用类的实现方法,最后则再加入派生类的实现。

覆写类实现法,在面向对象的多态应用中也有一种情形是特定的派生类完全不使用父类提供的实现,因此决定完全覆写父类的实现而不是改写父类的实现,这种设计手法便称为覆写父类实现法。

BootStrap设计法,是指父类会定义各种服务方法,但是这些服务都需要特定的标地,例如Window Handle或是Window的Device Context Handle。父类在实现服务方法时都会使用特定的标地,但是这个特定的标地却只由派生类来提供,并不由父类提供。这种让特定的标地延迟到派生类才提供的设计便称为BootStrap设计法,这也就是说使用这种设计的类并不能且不应该创建父类对象,而只能创建派生类对象来执行。 

 

3-3-1 VCL  Framework的核心组件构架

 TComponent类必须提供下面的基础服务以便让派生类能够不再重复撰写这些服务程序代码:

作为基础的根组件类

可同时扮演Container组件和单一组件的功能

基础组件管理功能

基础组件互动通知功能(Notification)

同时提供可视化和非可视化组件架构基础 

 

3-4 这还不够,让它成为Windows控件吧

如果在TComponent类之下先设计一个控制类,这个控制类具备基本的控制服务,例如处理鼠标的服务,负责处理控制事件的服务以及处理光标(Cursor)服务等。让这个控制类成为其它具体控件类的父类,那么封装Windows控件的类或是其它VCL Framework自定义的控件类就可以从这个控制类继承下来并且自动拥有处理鼠标,光标和事件的基本功能,如此一来我们便可以让TComponent和封装Windows控件的派生类经由控制类分离,解决了紧耦合(Tight Coupling)的问题,也让Framework的控件类不成为Windows平台专属的类而提供能够封装其它控件的可能性。

 

3-4-1 TControl

TControl类提供的基础服务

控件基本信息

处理鼠标基本服务

控件使用的资源

分派消息基本服务

绘制控件区域基本服务

和父类对象互动的服务 

 

3-4-2 封装Windows控件的TWinControl类

Double Buffer是计算机图形处理常用的技术之一,在DOS时代便有许多游戏软件使用Double Buffer来增加游戏反应速度。所谓Double Buffer是指当游戏或是图形软件需要显示下一个画面时,软件先在内存中分配一块大小和需要变动画面相同的区域,先在此区域中画完要显示的内容,然后再一次切换画完的内容到画面中。如此一来画面显示的速度可大幅增加,减少画面因为重画而造成闪烁的情形,这种使用另外一块内存预先绘制下一画面再使用类似Memory Move的汇编语言切换的技术便成为Double Buffer。


从TComponent、TControl和TWinControl这三个类的生命和实现看到了类从通用形式逐渐具体化而成为封装Windows控件的类,VCL Framework工程师经由在每一个派生类中加入具体的功能而形成封装特定功能的类。TComponent提供基础组件服务,TControl加入处理鼠标事件等功能而形成“准组件”类,再到TWinControl直接为封装Windows控件而加入的属性和方法。从这三个类我们可以学习到如何在我们的应用系统中建构类架构,那就是从通用类,到准功能类,最后再开花结构形成提供特定服务的末端类。 

 

第四章 VCL FrameWork和窗口消息

 

4-1 窗口消息和VCL FrameWork

封装窗口消息核心面对的问题:传统窗口是以窗口数据结构、回调函数、WinAPI等为基础的C/C++语言机制作为处理的目标,然而Object Pascal却是一个面向对象的程序语言,Object Pascal的数据类型和C/C++也不尽相同,更何况VCL FrameWork准备以组件架构来封装窗口消息以及消息的处理、分派的流程。

 

 4-2 VCL的窗口消息封装机制

 仔细观察传统Windows程序,我们可以知道Windows应用程序向Windows操作系统注册的回调函数正是消息分派枢纽,当Windows操作系统中有事件发生时,Windows操作系统经由调用Windows应用程序注册的回调函数而获得处理此事件的机会。因此VCL FrameWork想结合面向对象实现封装窗口消息的功能,回调函数正是最好介入的地方。

 

其实要让Windows操作系统调用到VCL Framework提供的封装组件而不是一般的回调函数并不困难。整个运算逻辑可以使用如下的想法来表达:

找到消息分派的目的VCL组件

经由虚拟方法机制调用目的VCL组件处理窗口消息的方法 

VCL Framework就是使用这些想法来完成分派窗口消息的机制,当VCL Framework调用Windows API的RegisterClass注册窗口类时会使用VCL Framework内部提供的回调函数。在这个回调函数中会建立一个“调用通道(Tunnel)”,让Windows操作系统能够顺利地调用到VCL组件提供的窗口处理方法

 

4-2-1 从窗口回调函数到面向对象的类方法
在传统的Windows程序设计中,Windows操作系统是调用一般的回调函数的,而所谓一般的回调函数就是指C语言的函数类型。但是在面向对象程序语言中当程序代码调用对象的方法时,除了目标方法接受的参数之外,调用者(Caller)还需要传递一个额外的隐藏参数,那就是Object Pascal语言的Self或是C++语言中的this,也正是因为这个原因,在对象方法中才能够使用Self来进行存取对象本身的服务,因为VCL Framework要解决的问题就是如何从Windows操作系统调用到对象的方法。也就是说如何把Windows操作系统要调用的一般的C函数类型转换成可调用的VCL Framework中面向对象的方法?

这很简单,我们可以先撰写一个使用Object Pascal语法但是符合C函数类型的窗口回调函数让Windows操作系统调用,然后在这正常的回调函数中先找到目的VCL对象,再主动把Self推入栈中,再推入对象方法的参数,最后再调用对象方法即可让调用回调函数改变成调用对象方法,这不就可以完美地解决传统Windows回调函数进入面向对象程序语言世界问题了吗?不过在Delphi应用程序中,Borland为了提供VCL Framework分派窗口消息的执行效率,并不是把Self推入栈,而是把Self推入EAX缓存器(Register)中,以便使用Register的调用惯例以最快的执行速度来分派窗口消息。 

 

4-3-4 TObject分派消息的原理和流程

在TObject的消息分派服务中,虚拟方法Dispatch是真正负责到特定VCL组件的事件处理函数的核心方法。 VCL窗口消息封装机制把窗口消息分派到VCL组件的消息处理函数WndProc之后,真正让窗口消息和VCL组件的事件处理函数串联起来的枢纽就是TObject的消息分派服务虚拟方法Dispatch。Dispatch虚拟方法会根据触发的消息种类来决定如何分派这个消息。

Dispatch虚拟方法使用两种方式来分派消息,第一种是分派能够被VCL组件事件处理函数处理的消息,对于这种消息Dispatch会在目标VCL组件的动态方法数据表中搜寻拥有相同消息ID的动态方法,找到之后就直接调用找到的VCL事件处理函数来处理触发的消息。第二种是如果Dispatch虚拟方法对于触发的消息不需要分派,那么Dispatch虚拟方法就会调用同属TObject消息分派服务之中的DefaultHandler虚拟方法来处理,而VCL Framework已经提供了默认的DefaultHandler实现方法,程序员可以直接使用,当然也可以在VCL组件中重载DefaultHandler虚拟方法由程序员自己实现处理Dispatch不分派的消息。另外一个执行DefaultHandler虚拟方法的情形是当Dispatch无法在VCL组件的动态方法表格中找到能够处理消息的事件处理函数,那么Dispatch也会调用DefaultHandler虚拟方法来处理这类尚未经处理的消息。 

 

4-3-5 VCL消息分派架构

基本上VCL Framework中封装窗口消息的机制是经由InitWndProc取代TForm注册的窗口回调函数,因此Windows操作系统在产生了窗口消息之后实际上会调用InitWndProc函数,经由InitWndProc进行一些转换后再把控制权交给StdWndProc函数。

StdWndProc接着会根据处理此消息的VCL组件,调用VCL组件重载的窗口消息处理函数。当执行权分派到了VCL组件的窗口消息处理函数之后,VCL Framework便会经由面向对象的集成和虚拟方法机制让消息处理权流动到TObject的Dispatch虚拟方法。最后TObject.Dispatch会正确地把执行权交给VCL组件的事件处理函数来处理窗口消息。 

 

4-4 Delphi窗口应用程控者:TApplication

每一个用Delphi开发的Windows应用程序都是由TApplication开始形成生命的

 

4-4-1 TApplication对象的创建

在Controls程序单元被加载时它的initialization程序区块会被自动执行,而在initialization程序区块我们可以看到它调用了InitControls函数,TApplication对象在此函数中被创建

 

4-4-2 TApplication和秘密窗口

TApplication是从TComponent继承下来的,TApplication并不是VCL Framework中代表窗口的控件,而是VCL Framework中的一般的组件类,因此就类的定义上来说TApplication和窗口没有直接的关系,但是我们再仔细观察TApplication类却可以发现TApplication定义了一个字段变量FHandle,FHandle的类型是HWnd,而HWnd类型正是VCL Framework中代表窗口Handle值的类型。TApplication类会在内部创建一个秘密窗口。

这个窗口是Delphi的Windows应用程序的主窗口,Delphi应用程序的主窗体事实上是这个秘密窗口的子窗口。应此这个秘密窗口会以一个消息循环接受窗口消息并且加以分派和处理。TApplication类封装了创建秘密窗口和消息循环的程序代码。所有的事情都发生在全局对象Application对象被创建之时。

MakeObjectInstance主要的功能是接受一个类的方法为参数,这个参数事实上就是类使用来处理窗口消息的函数。由于窗口回调函数必须是一般的函数而不是类方法,而且窗口回调函数的参数有严格的要求,因此MakeObjectInstance函数的功能就在于进行一些“神奇的”工作让Windows操作系统能够正确地调用到类方法作为回调函数。

为什么窗口回调函数不可以是类方法呢?这是因为一般的函数只是普通的指针地址,在32位的Windows操作系统中是一个32位的指针地址,占据了四个字节,而类方法则同时包含了方法的指针以及类指针地址, 在32位的Windows操作系统中占据了八个字节。因此Windows操作系统无法调用以类方法为回调函数的使用方式,这也是为什么VCL Framework需要使用MakeObjectInstance方法在这两者之间进行巧妙转换的原因。

 

 

 

 

 

 

 

 

posted on 2012-04-11 10:12  若不是因为你  阅读(1190)  评论(0编辑  收藏  举报