Delphi的程序单元、结构、基础知识(转)
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Delphi的程序单元、结构、基础知识(转)
1 Object Passal的程序结构很特殊,与其它语⾔如C++,Object Windows等结构都不同。
⼀个Delphi程序由多个称为单元的源代码模块组成。
使⽤单元可以把⼀个⼤型程序分成多个逻辑相关的模块,并⽤来创建在不同程序中使⽤的程序库。
28.1 Program单元
3 Program单元就是Delphi中的项⽬⽂件。
4 Program单元是⼀个特殊的单元,类似于C语⾔中的Main程序,即为应⽤程序的主程序。
⼀个程序可以有多个单元组成,也可以只有⼀个Program单元组成,例如前⾯我们介绍过的DOS窗⼝程序就只有⼀个Program单元组成。
下⾯是⼀个典型的 5 uses Forms,
6 Unit1 in'Unit1.pas'{Form1};
7{$R *.RES}
8begin
9 Application.Initialize;
10 Application.CreateForm(TForm1, Form1);
11 Application.Run;
12end
13(1)程序⾸部指定程序名、以及参数等。
14(2)Uses语句定义程序⽤到的所有单元。
15 标识符为单元的名字,各单元之间⽤逗好(,)隔开,最后结束⽤分号(;)。
16 注意:每个程序总是⾃动包含System单元,Program单元的Uses部分不能显式指定。
System单元⽤于实现⼀些低级的运⾏时间程序的⽀持,如⽂件输⼊输出(I/O)、字符串操作、浮点运算、动态内存分配等。
另外,Delphi在发⾏时提供了许 17 Uses部分列出单元的顺序决定它们初始化的顺序,并影响编译器定位标识符的顺序。
如果两个单元定义了⼀个相同名字的类型,编译器将总是使⽤前⾯那个单元的类型。
18(3)程序块由保留字Begin和End括起来的⼀段代码组成,⽤于对程序的初始化。
198.2 UNIT单元
20 UNIT单元相当于C语⾔的⼦程序。
基本上Delphi每个窗体都⼀个对应的单元。
当你为应⽤程序创建窗体时,你就创建了⼀个与该窗体相联系的新单元。
然⽽,单元也可以独⽴于窗体⽽存在。
例如,⼀个单元可以只包含数学运算程序,⽽不需要 21 ⼀个单元可以由多个程序共享。
单元的磁盘⽂件名后缀为.pas。
228.2.1单元结构
23 不管单元是否与窗体相关,单元的基本结构都是⼀样的。
UNIT单元由单元⾸部、接⼝部分(interface part)、实现部分(implementation part)、可选择的初始化部分(initialization part)、结束部分(finalization part)、end.组成。
248.2.2单元⾸部
25 单元的⾸部⽤保留字Unit开始,后跟单元名。
单元名必须遵循标识符的所有⼀般原则(不能以数字开头等)。
下⾯的单元名将是有效的:
26 Unit Rsgl;
278.2.3接⼝部分(Interface)
28 在单元名之后是接⼝部分。
接⼝部分⽤于声明变量、类型、过程和函数等。
在接⼝部分声明的变量、类型以及过程、函数等是其它使⽤该单元的程序或单元等都可见的。
接⼝部分⽤保留字Interface标明开始,⽤implemention标明结束。
接⼝部 29 ⼀个单元的接⼝部分还作为该单元说明⽂件的起点。
虽然接⼝部分没有告诉你⼦程序做什么或变量如何使⽤,但它正确告诉了你的变量、类型、过程、函数等的名字及其调⽤⽅法。
30 接⼝部分本⾝⼜可以由⼏个可选的部分组成,分别是单元的USES语句、常量声明部分、类型声明部分、变量声明部分、过程和函数声明部分。
其中常量声明、类型声明、变量声明、过程和函数声明部分⽤于声明其它使⽤该单元的单元可以 31 ⽽USES语句列出该单元要⽤到的标准单元和其它单元,⽤于把外部的已定义的常量、类型、变量、过程或函数引⼊到本单元中使⽤。
USES语句紧跟在Interface之后。
328.2.4实现部分
33 单元的第⼆部分,称为实现部分(Implementation),主要⽤于定义接⼝部分声明过的过程、函数等的代码。
实现部分⽤保留字implementation标明,总是紧随接⼝部分之后。
34 实现部分也可以⽤USES语句列出该单元要⽤到的标准单元和其它单元等。
如上⾯的uses MDIEdit;语句。
实际上,实现部分也可以声明变量、数据类型、过程及函数等。
35 但是,在实现部分定义的变量、类型、过程、函数等只能由本单元⾃⼰使⽤(private declarations),使⽤该单元的其它单元或程序不可见的。
私有定义可以隐藏单元的细节。
368.2.5 USES⼦句
37 USES⼦句⽤于访问其它单元。
例如,如果你要让程序来效仿⼀个电传打字机,可以在USES包含WinCRT,因为WinCrt含有进⾏这个仿效所需要的程序。
38 USES WinCRT;
39 Delphi提供了许多预先定义的单元,你可以在程序中直接使⽤。
实际上,当你将⼀个新构件放⼊设计的窗体时,DElphi会⾃动将该构件的单元放⼊USES⼦句中。
例如,如果你将Color Grid放⼊窗体,则单元ColorGrd就附加在窗体单元的USES 40 要使⽤USES⼦句包含单元中的程序,只要在单元名后加上程序名即可。
例如,如果要在Unit2中访问Unit1中的ComputePayMent函数。
41 USES⼦句可以放在接⼝部分(保留字Interface之后),也可放在实现部分(保留字Implementation之后),但是USES⼦句必须出现在它所有⼦程序、数据类型或变量被使⽤之前。
42 USES⼦句放在实现部分可以隐藏单元的内部细节,同时可以避免循环引⽤发⽣。
438.2.6初始化部分(Initialization)
44 初始化部分是单元的可选部分,主要⽤于对单元数据的初始化,⼀般很少⽤到。
45 该部分总是在其它程序代码之前运⾏。
如果⼀个程序包含多个单元,则在程序的其它部分运⾏之前,每个单元的初始化代码都会先调⽤,其顺序是单元显⽰在Uses语句的顺序。
468.2.7完成部分(Finalization)
47 完成部分(Finalization)是初始化过程的反过程,只要单元有初始化部分,才会有完成部分。
完成部分对应在Delphi1.0中ExitProc和AddEXitProc函数。
48 完成部分在程序关闭时运⾏。
任何在单元初始化时获得的资源如分配内存、打开⽂件等通常都在完成部分释放。
单元完成部分的执⾏顺序与初始化部分相反。
例如假如程序以A、B、C的顺序初始化单元,则完成过程的顺序是C、B、A。
49 ⼀旦单元的初始化部分开始执⾏,就必须保证程序关闭时对应的完成部分执⾏。
完成部分必须能够处理不完全初始的数据,因为如果产⽣异常,初始化代码可能不能完全执⾏。
501.构造
51 构造⽤建⽴对象,并对对象进⾏初始化。
通常,当调⽤构造时,构造类似⼀个函数,返回⼀个新分配的并初始化了的类类型实例。
52 构造跟⼀般的⽅法不同的是,⼀般的⽅法只能在对象实例中引⽤,⽽构造既可以由⼀个对象实例引⽤,也可以直接由类来引⽤。
当⽤类来引⽤类的构造时,实际上程序做了以下⼯作:
53 (1)⾸先在堆中开辟⼀块区域⽤于存贮对象。
54 (2)然后对这块区域缺省初始化。
初始化,包括有序类型的字段清零,指针类型和类类型的字段设置为Nil,字符串类型的字段清为空等。
55 (3)执⾏构造中⽤户指定的动作。
56 (4)返回⼀个新分配的并初始化了的类类型实例。
返回值的类型必须就是类的类型。
57 当你⽤在对象实例中引⽤类的构造时,构造类似⼀个普通的过程⽅法。
这意味着⼀个新对象还没有被分配和初始化,调⽤构造不返回⼀个对象实例。
相反,构造只对⼀个指定的对象实例操作,只执⾏⽤户在构造语句中指定的操作。
58 例如,在创建⼀个新的对象时,尽管还没有对象实例存在,仍然可以调⽤类的构造,程序⽰例如下:
59type
60 TShape = class(TGraphicControl)
61 private
62 FPen: TPen;
63 FBrush: TBrush;
64 procedure PenChanged(Sender: TObject);
65 procedure BrushChanged(Sender: TObject);
66 public
67 constructor Create(Owner: TComponent);
68 override;
69 destructor Destroy;
70 override; ...
71end;
72 constructor TShape.Create(Owner: TComponent);
73begin
74 inherited Create(Owner);{ Initialize inherited parts }
75 Width := 65;{ Change inherited properties }
76 Height := 65;
77 FPen := TPen.Create;{ Initialize new fields }
78 FPen.OnChange := PenChanged;
79 FBrush := TBrush.Create;
80 FBrush.OnChange := BrushChanged;
81 end;
82 构造的第⼀⾏是Inherited Create(Owner),其中Inherited是保留字,Create是祖先类的构造名,事实上⼤多数构造都是这么写的。
这句话的意思是⾸先调⽤祖先类的构造来初始化祖先类的字段,接下来的代码才是初始化派⽣类的字段,当然也可 83 如果在⽤类来引⽤构造的过程中发⽣了异常,程序将⾃动调⽤析构来删除还没有完全创建好的对象实例。
效果类似在构造中嵌⼊了⼀个try協inally语句,例如:
84 try
85 ...{ User defined actions }
86 except{ On any exception }
87 Destroy;{ Destroy unfinished object }
88 raise;{ Re-raise exception }
89 end;
90 构造也可以声明为虚拟的,当构造由类来引⽤时,虚拟的构造跟静态的构造没有什么区别。
当构造由对象实例来引⽤时,构造就具有多态性。
可以使⽤不同的构造来初始化对象实例。
912.析构
92 析构的作⽤跟构造正相反,它⽤于删除对象并指定删除对象时的动作,通常是释放对象所占⽤的堆和先前占⽤的其它资源。
构造的定义中,第⼀句通常是调⽤祖先类的构造,⽽析构正相反,通常是最后⼀句调⽤祖先类的析构,程序⽰例如下: 93 destructor TShape.Destroy;
94 begin
95 FBrush.Free;
96 FPen.Free;
97 inherited Destroy;
98 end;
99 上例中,析构⾸先释放了刷⼦和笔的句然后调⽤祖先类的析构。
100 析构可以被声明为虚拟的,这样派⽣类就可以重载它的定义,甚⾄由多个析构的版本存在。
事实上,Delphi中的所有类都是从TObject继承下来的,TObject的析构名为Destroy,它就是⼀个虚拟的⽆参数的析构,这样,所有的类都可以重载De 101 前⾯提到,当⽤类来引⽤构造时,如果发⽣运⾏期异常,程序将⾃动调⽤析构来删除还没有完全创建好的对象。
由于构造将执⾏缺省的初始化动作,可能把指针类型和类类型的字段清为空,这就要求析构在对这样字段操作以前要判断这些字段 102procedure TObject.Free;
103begin
104if Self <> nil then Destroy;
105end;
106 也即Free⽅法在调⽤Destroy前会⾃动判断指针是否为Nil。
如果改⽤FBrush.Destroy和FPen.Destroy,当这些指针为Nil时将产⽣异常导致程序中⽌。
1077.2.3⽅法指令字
108 声明⽅法的语法规则中,method directives为⽅法的指令字。
109 从语法⽰意图中可以看出,⽅法按指令字分⼜可分为三种,分别是虚拟、动态、消息⽅法,它们分别是⽅法名后⽤Virtual,Dynamic,Message保留字指定。
也可以不加⽅法指令字,这种情况下声明的⽅法是静态的(static)。
110 另外,从语法⽰意图中可以看出,⼀个⽅法也可以像函数那样,指定参数的传递的⽅式,也即⽅法的调⽤约定。
⼀个⽅法调⽤约定与通常的过程和函数相同,请参看本书关于过程和函数的部分。
111 1.静态⽅法
112 缺省情况,所有的⽅法都是静态的,除⾮你为⽅法提供了其它指令字。
静态⽅法类似于通常的过程和函数,编译器在编译时就已指定了输出该⽅法的对象实例。
静态⽅法的主要优点是调⽤的速度快。
113 当从⼀个类派⽣⼀个类时,静态⽅法不会改变。
如果你定义⼀个包含静态⽅法的类,然后派⽣⼀个新类,则被派⽣的类在同⼀地址共享基类的静态⽅法,也即你不能重载静态⽅法。
如果你在派⽣类定义⼀个与祖先类相同名的静态⽅法,派⽣类 114type TFirstComponent = class(TComponent)
115procedure Move; procedure Flash;
116end;
117 TSecondComponent = class(TFirstComponent)
118procedure Move;{该MOVE不同于祖先类的MOVE}
119function Flash(HowOften: Integer): Integer;{该Flash不同于祖先类的Flash}
120end;
121 上⾯代码中,第⼀个类定义了两个静态⽅法,第⼆个类定义了于祖先类同名的两个静态⽅法,第⼆个类的两个静态⽅法将替换第⼀个类的两个静态⽅法。
1222.虚拟⽅法
123 虚拟⽅法⽐静态⽅法更灵活、更复杂。
虚拟⽅法的地址不是在编译时确定的,⽽是程序在运⾏期根据调⽤这个虚拟⽅法的对象实例来决定的,这种⽅法⼜为滞后联编。
虚拟⽅法在对象虚拟⽅法表(VMT表)中占有⼀个索引号。
124 VMT表保存类类型的所有虚拟⽅法的地址。
当你从⼀个类派⽣⼀个新类时,派⽣类创建它⾃⼰的VMT,该VMT包括了祖先类的VMT,同时加上⾃⼰定义的虚拟⽅法的地址虚拟⽅法可以在派⽣类中重新被定义,但祖先类中仍然可以被调⽤。
例 125type TFirstComponent = class(TCustomControl)
126 procedure Move;{ static method }
127 procedure Flash; virtual;{ virtual method }
128 procedure Beep; dynamic;{ dynamic virtual method }
129 end;
130 TSecondComponent = class(TFirstComponent)
131 procedure Move;{ declares new method }
132 procedure Flash;
133 override;{ overrides inherited method }
134 procedure Beep; override;{ overrides inherited method }
135end;
136 上例中,祖先类TFirstComponentw中⽅法Flash声明为虚拟的,派⽣类TSecondComponent重载了⽅法Flash。
声明派⽣类的Flash 时,后⾯加了⼀个Override指令字,表⽰被声明的⽅法是重载基类中的同名的虚拟或动态⽅法。
137 注意:重载的⽅法必须与祖先类中被继承的⽅法在参数个数,参数和顺序,数据类型上完全匹配,如果是函数的话,还要求函数的返回类型⼀致。
138 要重载祖先类中的⽅法,必须使⽤Override批⽰字,如果不加这个指令字,⽽在派⽣类中声明了于祖先类同名的⽅法,则新声明的⽅法将隐藏被继承的⽅法。
1393.动态⽅法
140 所谓动态⽅法,⾮常类似于虚拟⽅法,当把⼀个基类中的某个⽅法声明为动态⽅法时,派⽣类可以重载它,如上例的Beep。
不同的是,被声明为动态的⽅法不是放在类的虚拟⽅法表中,⽽是由编译器给它⼀个索引号(⼀般不直接⽤到这个索 141 从功能上讲,虚拟⽅法和动态⽅法⼏乎完全相同,只不过虚拟⽅法在调⽤速度上较快,但类型对象占⽤空间⼤,⽽动态⽅法在调⽤速度上稍慢⽽对象占⽤空间⼩。
如果⼀个⽅法经常需要调⽤,或该⽅法的执⾏时间要求短,则在虚拟和动态之间 1424.消息句柄⽅法
143 在⽅法定义时加上⼀个message指令字,就可以定义⼀个消息句柄⽅法。
消息句柄⽅法主要⽤于响应并处理某个特定的事件。
144 把⼀个⽅法声明为消息句柄的⽰例如下:
145type
146 TTextBox = class(TCustomControl)
147 private
148 procedure WMChar(var Message: TWMChar); message WM_CHAR;
149 ...
150 end;
151 上例中声明了⼀个名叫TTextBox的类类型,其中还声明了⼀个过程WMPaint,只有⼀个变量参数Message,过程的⾸部后⽤保留字Message表⽰这是个消息句柄,后跟⼀个常量WM_PAINT表⽰消息句柄要响应的事件。
152 Object Pascal规定消息句柄⽅法必须是⼀个过程,并且带有⼀个唯⼀的变量参数。
message保留字后必须跟随⼀个范围在1到49151的整型常量,以指定消息的ID号。
注意,当为⼀个VCL控制定义⼀个消息句柄⽅法时,整型常量必须是Windo 153 注意:消息句柄不能使⽤Cdecl调⽤约定,也不能⽤Virtual,Dynamic,Override或Abstract等指令字。
154 在消息句柄中,你还可以调⽤缺省的消息句柄,例如上例中,你声明了⼀个处理WM_PAINT消息的⽅法,事实上Delphi提供了处理这个消息的缺省的句柄,不过句柄的名称可能与你声明的⽅法名称不⼀样,也就是说你未必知道缺省句柄,那怎 155procedure TTextBox.WMChar(var Message: TWMChar); message WM_CHAR;
156begin
157 Inherited
158 ...
159end;
160 上例中,消息句柄⾸先调⽤WM_PAINT消息的缺省句柄,然后再执⾏⾃⼰的代码。
使⽤Inherited保留字总是能⾃动找到对应于指定消息的缺省句柄(如果有的话)。
161 使⽤Inherited保留字还有个好处,就是如果Delphi没有提供处理该消息的缺省句柄,程序就会⾃动调⽤TObject的DefaultHandler⽅法,这是个能对所有消息进⾏基本处理的缺省句柄。
1627.2.4抽象⽅法
163 从图7.7的⽅法指令字语法规则可知,可以在⽅法的调⽤约定之后加⼀个Abstract,以进⼀步指明该⽅法是否是抽象的。
所谓抽象⽅法,⾸先必须是虚拟的或动态的,其次它只有声明⽽没有定义,只能在派⽣类中定义它(重载)。
因此定义⼀个 164 抽象⽅法在C++中称为纯虚函数,⾄少含有⼀个纯虚函数的类称为抽象类,抽象类不能建⽴对象实例。
165 声明⼀个抽象⽅法是⽤Abstract指令字,例如:
166type
167 TFigure = class
168procedure Draw; virtual; abstract;
169 ...
170end;
171 上例中声明了⼀个抽象⽅法,注意,Virtual或Dynamic指令字必须写在Abstract指令字之前。
在派⽣类中重载抽象⽅法,跟重载普通的虚拟或动态⽅法相似,不同的是在重载的⽅法定义中不能使⽤Inherited保留字,因为基类中抽象⽅法本来就 1727.2.5重载⽅法与重定义⽅法
173 在⼦类中重载⼀个滞后联编的对象⽅法,需要使⽤保留字override。
然⽽,值得注意的是,只有在祖先类中定义对象⽅法为虚拟后,才能进⾏重载。
否则,对于静态对象⽅法,没有办法激活滞后联编,只有改变祖先类的代码。
174 规则⾮常简单:定义为静态的对象⽅法会在每个⼦类中保持静态,除⾮⽤⼀个同名的新虚拟⽅法隐藏它,被定义为虚拟的⽅法在每个⼦类中保持滞后联编。
这是⽆法改变的,因为编译器会为滞后联编⽅法建⽴不同的代码。
175 为重新定义静态对象⽅法,⽤户只需向⼦类添加该对象⽅法,它的参数可以与原来⽅法的参数相同或不同,⽽不需要其它特殊的标志。
重载虚拟⽅法,必须指定相同的参数并使⽤保留字override。
例如:
176type
177 AClass=Class
178 procedure One;virtual;
179 procedure Two;{static method}
180 end;
181 BClass=Clas(Aclass)
182 procedure One;override;
183 procedure Two;
184end;
185 重载对象⽅法有两种典型的⽅法。
⼀种是⽤新版本替代祖先类的⽅法,另⼀种是向现有⽅法添加代码。
这可以通过使⽤保留字inherited(继承)调⽤祖先类中相同的⽅法来实现。
例如:
186procedure Bclass.One;
187begin //new code
188 ...?
189 //call inherited procedure Bclass
190 inherited One;
191end;
192 在Delphi,对象可以有多个同名的⽅法,这些⽅法被称为重新定义的⽅法(overload),并⽤保留字Overload标识。
各同名的⽅法必须能够根据参数中不同的类型信息予以区分。
例如:
193 constructor Create(AOwner: TComponent); overload; override;
194 constructor Create(AOwner: TComponent; Text: string); overload;
195 如果要重新定义⼀个虚拟的⽅法,在继承类中必须使⽤reintroduce指令字。
例如:
196type
197 T1 = class(TObject)
198 procedure Test(I: Integer); overload; virtual;
199 end;
200 T2 = class(T1)
201 procedure Test(S: string); reintroduce; overload;
202 end;
203 ...
204 SomeObject := T2.Create;
205 SomeObject.Test('Hello!'); // calls T2.Test
206 SomeObject.Test(7); // calls T1.Test
207 在同⼀个类⾥,不同同时公布(publish)具有同名的重定义⽅法。
例如:
208type
209 TSomeClass = class
210 published
211 function Func(P: Integer): Integer;
212 function Func(P: Boolean): Integer // error
213 ...
2147.3类的特性
215 特性有点类似于字段,因为特性也是类的数据,不过跟字段不同的是,特性还封装了读写特性的⽅法。
特性可能是Delphi程序员接触得最多的名词之⼀,因为操纵Delphi的构件主要是通过读写和修改构件的特性来实现的,例如要改变窗⼝的标 216 Delphi的特性还有个显著特点就是,特性本⾝还可以是类类型,例如Font特性就是TFont类型的类。
2177.3.1声明特性
218 要声明特性,必须说明三件事情:特性名、特性的数据类型、读写特性值的⽅法。
Object Pascal使⽤保留字Property声明特性。
219 特性的声明由保留字Property,特性标识符,可选的特性接⼝(Property Interface)和特性限定符(Property Specifier)构成。
220 特性接⼝指定特性的数据类型,参数和索引号。
⼀个特性可以是除⽂件类型外的任何数据类型。
221 在声明特性时,必须指定特性的名字、特性的数据类型以及读写特性的⽅法。
通常是把特性的值放在⼀个字段中,然后⽤Read和Write指定的⽅法去读或写字段的值。
程序⽰例如下:
222type TYourComponent = class(TComponent)
223 private
224 FCount: Integer; { used for internal storage }
225 procedure SetCount (Value: Integer); { write method }
226 public
227 property Count: Integer read FCount write SetCount;
228end;
229 上例中声明了⼀个TYourComponent类型的类,声明了⼀个字段FCount,它的数据类型是Integer,还声明了⽅法过程SetCount,最后声明了⼀个特性Count,它的数据类型跟字段FCount的数据类型相同,并且指定特性的值从字段Fcountt中读取 230 特性的声明似乎⽐较复杂,但要在程序中要访问特性却是很简单的,例如假设创建了 TYourComponent类型的对象AObject,⼀个Integer型变量AInteger,程序可以这么写:
231 AInteger:=Aobject.Count;
232 Aobject.Count:=5;
233 实际上,编译器根据声明中的Read⼦句和Write⼦句⾃动把上述语句分别转换成:
234 Ainteger:=Aobject.Fcount;
235 Aobject.SetCount(5);
236 顺便说⼀下,跟访问字段和⽅法⼀样,要访问特性也需要加对象限定符,当然如果使⽤With语句则可简化。
237 跟字段不同的是,特性不能作为变量参数来传递,也不能⽤@来引⽤特性的地址。
2387.3.2特性限定符
239 特性限定符可以有四类,分别是Read,Write,Stored和Default。
其中Read和Write限定符⽤于指定访问特性的⽅法或字段。
240 注意:Read和Write限定符指定的⽅法或字段只能在类的Private部分声明,也就是说它们是私有的(关于Private的概念将在后⾯介绍),这样能保证对特性的访问不会⼲扰到这些⽅法的实现,也能防⽌不⼩⼼破坏数据结构。
熟悉C++的程序 241 1.Read限定符
242 Read限定符⽤于指定读取特性的⽅法或字段,通常是⼀个不带参数的函数,返回的类型就是特性的类型,并且函数名通常以“Get”加特性名组成,例如⼀个读取Caption特性的⽅法通常命名为GetCaption。
243 从语法上讲,可以没有Read限定符,这时候我们称特性是“只写”的,不过这种情况较为少见。
244 2.Write限定符
245 Write限定符⽤于指定修改特性的⽅法,通常是⼀个与特性同类型的过程,这个参数⽤于传递特性新的值,并且过程名通常以“Set”加特性名组成,例如修改Caption特性的⽅法通常命名为SetCaption。
246 在Write限定符指定的⽅法的定义中,通常⾸先是把传递过来的值跟原先的值⽐较,如果两者不同,就把传递过来的特性值保存在⼀个字段中,然后再对特性的修改作出相应的反应。
这样当下次读取特性值时,读取的总是最新的值。
如果两者相 247 从语法上讲,可以没有Write限定符,这时候特性就是“只读”的。
只读的特性在Delphi中是常见的,只读的特性不能被修改。
248 3.Stored限定符
249 Stored限定符⽤于指定⼀个布尔表达式,通过这个布尔表达式的值来控制特性的存贮⾏为,注意,这个限定符只适⽤于⾮数组的特性(关于数组特性将在后⾯介绍)。
250 Stored限定符指定的布尔表达式可以是⼀个布尔常量,或布尔类型的字段,也可以是返回布尔值的函数。
当表达式的值为False时,不把特性当前的值存到Form⽂件中(扩展名为DFM),如果表达式的值为True,就⾸先把特性的当前值跟Def 251 含有Stored限定符的特性声明⽰例如下:
252 TSampleComponent = class(TComponent)
253 protected
254 function StoreIt: Boolean;
255 public { normally not stored }
256 property Important: Integer stored True;{ always stored }
257 published { normally stored always }
258 property Unimportant: Integer stored False;{ never stored }
259 property Sometimes: Integer stored StoreIt;{ storage depends on function value }
260end;
261 上例中,TSampleComponent类类型包括三个特性,⼀个总是Stored,⼀个总是不Stored,第三个的Stored取决于布尔类型⽅法StoreIt的值。
262 4.Default和NoDefult限定符
263 Default限定符⽤于指定特性的缺省值,在Delphi的Object Inspector中,可能已发现所有特性都有⼀个缺省值,例如把⼀个TButton元件放到Form上时,它的AllowAllUp特性缺省是False,Down特性的缺省值是False,这些缺省值都是通过De 264 TStatusBar = class(TPanel)
265 public
266 constructor Create(AOwner: TComponent); override; { override to set new default }
267 published
268 property Align default alBottom; { redeclare with new default value }
269end;
270 ...
271 constructor TStatusBar.Create(AOwner: TComponent);
272begin
273 inherited Create(AOwner); { perform inherited initialization }
274 Align := alBottom; { assign new default value for Align }
275end;
276 上例中,TStatusBar类类型包括Align特性,指定了缺省值为alBottom,TStatusBar类类型在实现部分构造定义中,也设置了缺省值。
277 注意:Default限定符只适⽤于数据类型为有序类型或集合类型的特性,Default后必须跟⼀个常量,常量的类型必须与特性的类型⼀致。
278 如果特性声明时没有Default限定符(也可能是不能有Default限定符),表⽰特性没有缺省值,相当于⽤NoDefault限定符(NoDefault限定符只是强调⼀下特性没有缺省值,其效果跟什么也不写是⼀样的)。
2797.3.3数组特性
280 所谓数组特性,就是说特性是个数组,它是由多个同类型的值组成的,其中每个值都有⼀个索引号,不过跟⼀般的数组不同的是,⼀般的数组是⾃定义类型,可以把数组作为⼀个整体参与运算如赋值或传递等,⽽对数组特性来说,⼀次只能访 281type
282 TDemoComponent = class(TComponent)
283 private
284 function GetNumberName(Index: Integer): string;
285 public
286 property NumberName[Index: Integer]: string read GetNumberName;
287end;
288 ...
289function TDemoComponent.GetNumberName(Index: Integer): string;
290begin
291 Result := 'Unknown';
292 case Index of
293 -MaxInt..-1: Result := 'Negative';
294 0 : Result := 'Zero';
295 1..100 : Result := 'Small';
296 101..MaxInt: Result := 'Large';
297 end;
298end;
299 上例中,声明了⼀个数组特性NumberName,它的元素类型是String,索引变量是Index,索引变量的类型是Integer。
上例中还同时声明了Read⼦句。
从上⾯的例⼦中可以看出,声明⼀个数组特性的索引变量,跟声明⼀个过程或函数的参数类 300 对于数组特性来说,可以使⽤Read和Write限定符,但Read和Write限定符只能指定⽅法⽽不能是字段,并且Object Pascal规定,Read限定符指定的⽅法必须是⼀个函数,函数的参数必须在数量和类型上与索引变量⼀⼀对应,其返回类型与数 301 访问数组特性中的元素跟访问⼀般数组中的元素⼀样,也是⽤特性名加索引号。
3027.3.4特性重载
303 所谓特性重载,就是在祖先类中声明的特性,可以在派⽣类中重新声明,包括改变特性的可见性(关于类成员的可见性将在后⾯详细介绍),重新指定访问⽅法和存贮限定符以及缺省限定符等。
304 最简单的重载,就是在派⽣类中这么写:
305 Property 特性名:
306 这种重载通常⽤于只改变特性的可见性,其它什么也不改变,例如特性在祖先类中是在Protected部分声明,现在把它移到Published部分声明。
307 特性重载的原则是,派⽣类中只能改变或增加限定符,但不能删除限定符,请看下⾯的程序⽰例:
308type
309 TBase = class
310 ...
311 protected
312 property Size: Integer read FSize;
313 property Text: string read GetText write SetText;
314 property Color: TColor read FColor write SetColor stored False;
315 ...
316 end;
317type TDerived = class(TBase)
318 ...
319 protected
320 property Size write SetSize; published property Text;
321 property Color stored True default clBlue;
322 ...
323 end;
324 对于祖先类中的Size特性,增加了Write限定符,对于祖先类中的Text特性,改在Published部分声明,对于祖先类中的Color特性,⾸先是改在Published部分声明,其次是改变了Stored限定符中的表达式,从False改为True,并且增加了⼀个D 3257.4类成员的可见性
326 ⾯向对象编程的重要特征之⼀就是类成员可以具有不同的可见性,在Object Pascal中,是通过这么⼏个保留字来设置成员的可见性的:Published,Public,Protected,Private,Automated。
如
327 TBASE = class
328 private
329 FMinValue: Longint;
330 FMaxValue: Longint;
331 procedure SetMinValue(Value: Longint);
332 procedure SetMaxValue(Value: Longint);
333 function GetPercentDone: Longint;
334 protected
335 procedure Paint; override;
336 public
337 constructor Create(AOwner: TComponent); override;
338 procedure AddProgress(Value: Longint);
339 property PercentDone: Longint read GetPercentDone;
340 published
341 property MinValue: Longint read FMinValue write SetMinValue default 0;
342 property MaxValue: Longint read FMaxValue write SetMaxValue default 100; property Progress: Longint read FCurValue write SetProgress
343end;
344 上例中,FMinValue、FMaxValue、FCurValue等字段是在Private部分声明的,表⽰它们是私有的,Public部分声明的⼏个⽅法是公共的。
345 再请看下⾯的例⼦:
346 TBASE = class
347 FMinValue: Longint;
348 FMaxValue: Longint;
349 private
350 procedure SetMinValue(Value: Longint);
351 procedure SetMaxValue(Value: Longint);
352 function GetPercentDone: Longint;
353 protected
354 procedure Paint; override;
355 public
356 constructor Create(AOwner: TComponent); override;
357 procedure AddProgress(Value: Longint);
358 property PercentDone: Longint read GetPercentDone;
359 published
360 property MinValue: Longint read FMinValue write SetMinValue default 0;
361 property MaxValue: Longint read FMaxValue write SetMaxValue default 100;
362 property Progress: Longint read FCurValue write SetProgress;
363end;
364 上例中,FminValue,FmaxValue,FCurValue这三个字段紧接着类类型⾸部,前⾯没有任何描述可见性的保留字,那么它们属于哪⼀类的可见性呢? ObjectPascal规定,当类是在{$M+}状态编译或者继承的是⽤{$M+}状态编译的基类,其可见 3657.4.1 Private
366 在Private部分声明的成员是私有的,它们只能被同⼀个类中的⽅法访问,相当于C语⾔中的内部变量,对于其它类包括它的派⽣类,Private部分声明的成员是不可见的,这就是⾯向对象编程中的数据保护机制,程序员不必知道类实现的细节 3677.4.2 Public
368 在Public声明的成员是公共的,也就是说,它们虽然在某个类中声明的。
但其它类的实例也可以引⽤,相当于C语⾔中的外部变量,例如,假设应⽤程序由两个Form构成,相应的单元是Unit1和Unit2,如果希望Unit2能共享Unit1中的整型变量 369 注意:⾯向对象的编程思想其特征之⼀就是隐藏复杂性,除⾮必须把某个成员在不同类之间共享,⼀般来说尽量不要把成员声明在类的Public部分,以防⽌程序意外地不正确地修改了数据。
3707.4.3 Published
371 在Published部分声明的成员,其可见性与在Public部分声明的成员可见性是⼀样的,它们都是公共的,即这些成员可以被其它类的实例引⽤,Published和Public的区别在于成员的运⾏期类型信息不同。
⼀个Published元素或对象⽅法不但能在 372 注意:只有当编译开关$N的状态为$M+时或者基类是⽤$M+编译时,类的声明中才能有Published部分,换句话说,编译开关$M⽤于控制运⾏期类型信息的⽣成。
3737.4.4 Protected
374 Protected与Private有些类似。
在Protected部分声明的成员是私有的(受保护的),不同的是在Protected部分声明的成员在它的派⽣类中可见的,并且成为派⽣类中的私有成员。
375 在Protected部分声明的成员通常是⽅法,这样既可以在派⽣类中访问这些⽅法,⼜不必知道⽅法实现的细节。
3767.4.5 Automated
377 C++的程序员可能对这个保留字⽐较陌⽣,在Automated部分声明的成员类似于在Public部分声明的成员,它们都是公共的,唯⼀的区别在于在Automated部分声明的⽅法和特性将⽣成OLE⾃动化操作的类型信息。
378 注意:Automated只适⽤于基类是TAuto0bject的类声明中,在Automated部分声明的⽅法,其参数和返回类型(如果是函数的话)必须是可⾃动操作的。
在Automated部分声明的特性其类型包括数组特性的参数类型也必须是可⾃动操作的,否 379 在Automated部分声明的⽅法只能采⽤Register调⽤约定,⽅法可以是虚拟的但不能是动态的。
在Automated部分声明的特性只能带Read和Write限定符,不能有其它限定符如Index、Stored、Default、NoDefault等,Read和Write指定的只能是 380 在Automated部分声明的⽅法或特性分配⼀个识别号(ID),如果不带DispId限定符,编译器⾃动给⽅法或特性分配⼀个相异的Id,如果带DispId限定符,注意Id不能重复。
3817.5类类型的兼容性
382 ⼀个类类型类与它的任何祖先类型兼容。
因此,在程序执⾏时,⼀个类类型变量既可以引⽤那个类型本⾝的实例,也可以引⽤任何继承类的实例。
例如下⾯的⼀段代码:
383type
384 TScreenThing = Class
385 X,Y:Longint;
386 procedure Draw;
387end;
388 T3DScreenThing = Class(TScreenThing)
389 Z:Longint;
390end;
391procedure ResetScreenThing(T:TScreenThing);
392begin
393 T. X:=0;
394 U. Y:=0;
395 V. Draw;
396end;
397procedure Reset3DScreenThing(T:T3DScreenThing);
398begin
399 T. X:=0;
400 T.Y:=0;
401 T.Z:=0;
402 U.Draw;
403end;
404var
405 Q:TScreenThing; R:T3DScreenThin;
406begin
407 {...}
408 ResetScreenThing(Q);
409 ResetScreenThing(R); {this work}
410 Reset3DScreenThing(Q); { but this does not}
411 在上⾯,过程ResetScreenThing定义时使⽤TScreenThing类型的参数,但可以使⽤TScreenThing类型和T3DScreenThing类型参数,因为T3DScreenThing类型是TScreenThing类型的继承类。
⽽Reset3DScreenThing使⽤TScreenThing类型的 4127.6 VCL类结构
413 我们介绍过的Delphi的VCL构件都是使⽤类类型定义的对象。
在Delphi中,所有的类都是从⼀个共同的类TObject继承下来的,TObject类的声明在System单元中,它定义了⼀些操纵类的最基本的⽅法,是Delphi所有类的缺省祖先类。
使⽤Vie 414 TObject类是⼀切构件类和对象的基类,位于继承关系的最顶层。
TPersistent类是TObject类的下⼀级继承者,它是⼀个抽象类,主要为它的继承者提供对流的读写能⼒。
415 TComponent类是TPersistent类的下⼀级继承者,它是VCL中所有构件的祖先类。
TComponent类定义了构件最基本的特性、⽅法和事件。
尽管TComponent类是VCL中所有构件的基类,但直接继承下来的却只有⼏个⾮可视的构件,如TTime构 416 TWinControl和TGraphicControl类都是TControl类的⼦类。
TWinControl的⼦类主要是⽤于窗⼝控制(如按钮、对话框、列表框、组合框等控制),它们实际上也是窗⼝,有⾃⼰的句柄,占⽤Windows资源,并且可以与⽤户交互。
⽽TGraphicC 417 实际上,整数类型可以分为基本整数类型(Fundamental type)和⼀般整数类型(generic type)。
⼀般整数类型(generic type)包括Integer和Cardinal两种。
在实际编程时,请尽量区分这两种,因为底层CPU和操作系统对结果进⾏了优化。
418 表6-2列出了Integer和Cardinal的取值范围及存储格式。
419 表6-2⼀般整数类型的取值范围及存储格式
420数据类型 取值范围 存储格式
421 Integer ?147483648..2147483647 32位有符号
422 Cardina l0..4294967295 32位⽆符号
423 基本整数类型包括Shortint、Smallint、Longint、Int64、Byte、Word和Longword。
表6-3列出了它们的取值范围及存储格式。
424 表6-3基本整数类型的取值范围及存储格式
425数据类型 取值范围 存储格式
426 Shortint -128..127 signed 8-bit
427 Smallint -12768..32767 signed 16-bit
428 Longint -2147483648..2147483647 signed 32-bit
429 Int64 -2^63..2^63? signed 64-bit
430 Byte 0..255 unsigned 8-bit
431 Word 0..65535 unsigned 16-bit
432 Longword 0..4294967295 unsigned 32-bit
433 ⼀般整数类型的实际范围和存储格式随着Object Pascal的不同实现⽽不同,但通常根据当前CPU和操作系统来采取最佳的操作⽅式。
434 ⼀般整数类型是最常⽤的整数类型,可以充分利⽤CPU和操作系统的特性,因此程序中应当尽量使⽤⼀般整数类型。
基本整数类型独⽴于操作系统和CPU,只有当应⽤程序的数据范围或存储格式特别需要时,才使⽤基本整数类型。
435 通常情况下,整数的算术运算结果为Integer类型,等价于32位的Longint类型。
只有当操作数存在 Int64类型时,才返回Int64类型的值。
因此,下⾯的代码将产⽣错误的结果:
436var I: Integer; J: Int64;
437 ...
438 I := High(Integer);
439 J := I + 1;
440 在这种情况下,要取得⼀个Int64的值,必须进⾏类型转换:
441 J := Int64(I) + 1;
442 注意:绝⼤多数例程在遇到Int64时都把它转换为32位。
但例程High,Low,Succ,Pred,Inc,Dec,IntToStr和IntToHex则完全⽀持Int64参数。
Round,Trunc,StrToInt64,和StrToInt64Def函数可以返回Int64类型的结果。
443(2)字符类型(Char)
444 字符类型中Char类型设计来只存储⼀个字符。
⼀个字符占⼀个字节,因此Char数据类型可以存储256个不同的字符,其对应的整数为0到255。
445 除了Char数据类型外,Delphi还提供了Char类型的扩展,即AnsiChar和WideChar型。
表6-4是字符数据类型的列表。
446 表6-4字符整数类型
447字符类型 占⽤字节数 存储内容
448 AnsiChar 1 存储⼀个Ansi字符。
449 WideChar 2 存储⼀个UniCode字符。
450 Char 1 ⽬前,对应AnsiChar。
但Delphi将来的版本可能对应于WideChar。
451 Ansi字符集是扩展的ASCII字符集,仍然占⼀个字节。
⽬前,Char对应AnsiChar,但Borland公司在将来的Delphi版本中可能使Char对应WideChar。
452 WideChar⽤来⽀持泛字符集(Unicode)。
Unicode字符占⽤两个字节,可以有65536种不同的取值,可以表达现代计算机中使⽤的世界上所有的字符,包括图形符号和⽤于出版业的特殊符号等。
453 UniCode字符集的前256个字符对应着ANSI字符。
如果你把⼀个AnsiChar字符放到WideChar字符类型的变量中,WideChar字符类型变量的⾼字节将全部置为0,AnsiChar字符存放到WideChar字符类型的变量的低字节中。
454 注意:Windows NT全⾯⽀持Unicode字符号集,但Windows 95却不同。
如果你希望书写的程序同时能在两种系统上运⾏,必须使⽤SizeOf()函数,以确定字符占多少字节。
455(3)布尔类型(Boolean)
456 Boolean数据类型的变量只存储⼀个逻辑值,例如True或False。
共有4种Boolean数据类型,见表6-5。
457表6-5布尔类型
458类型 说明
459 Boolean 占1个字节
460 ByteBool 占1个字节
461 WordBool 占2个字节
462 LongBool 占4个字节
463 Delphi提供多种Boolean数据类型的⽬的是为了兼容,因为在某些情况下,Windows需要⽤⼀个字(2个字节)或双字(4个字节)来表⽰⼀个布尔值。
464(4)枚举型
465 所谓枚举类型,就是⽤⼀组数量有限的标识符来表⽰⼀组连续的整数常数,在类型定义时就列出该类型可能具有的值。
枚举类型是⼀种⽤户⾃定义的简单数据类型。
在类型定义时就列出该类型可能具有的值。
下⾯是枚举类型定义的⼀些例⼦: 466 TDays=(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday);
467 TPrimaryColor=(Red,Yelloow,Blue); TDepartment=(Finance,Personnel,Engineering,Marketing,MIS); TDog=(Poodle,GoldenRetriever,Dachshund,NorwegianElkhound,Beagle);
468 枚举类型定义中的每个值都对应⼀个整数,整数值由该值在类型定义表中的位置决定,通常类型定义的第⼀个数对应的整数值为0。
例如,在TDay类型定义中Monday对应值为0、Tuesday值为1,等等。
如果你把DayOfWeek定义为Integer,通过 469 下⾯是声明⼀个枚举类型的语法(图6.2)。
470 其中标识符列表中的标识符之间⽤逗号隔开,它列出该类型可能具有的值。
471下⾯是声明⼀个枚举类型变量的举例:
472 var DayOfWeek:TDays;
473 Hue:TPrimaryColor;
474 Department:TDepartment;
475 Dogbreed:TDog;
476 也可以把类型声明和变量声明合⼆为⼀,例如:
477var DayOfWeek:(Monday,YuesDay,Wednesday,Thursday,Friday,Saturday,Sunday);
478 在声明枚举类型和枚举变量时,请注意以下⼏点:
4791)枚举的元素只能是标识符,标识符的命名必须符合 Pascal关于标识符的规定,例如下⾯的声明就是错误的:
480type TDays=(0,1,2,3,4,5,6,7);
4812)同⼀个枚举元素不能出现在多个枚举类型中。
例如下⾯的声明就是错误的:
482type TColors1=(Red,Blue,Green,White,Black);
483 TColors2=(Yellow,Lime,Silver,Green);
484 两个类型变量中都有Green元素,是不允许的。
4853)不能直接⽤枚举类型中的元素参加运算,例如,下⾯的语句就是错误的:
486 X:=Red+Blue;
487 但是,可以⽤某个枚举类型中的元素对枚举变量赋值,例如,下⾯的语句:
488 DayOfWeek:=Monday;。