开发新的VCL 组件 -2

开发新的VCL 组件 -2 

16.4.2 怎样实现标准事件
Delphi 自带的所有控件继承了大多数最常见的Windows 事件,这些就是标准事件。尽管所有这些
事件都嵌在标准控件中,但它们一般都是protected。当开发控件时,可以根据需要选择这些事件使它
们可以被应用程序员访问。
1.识别标准事件
有两种层次的标准事件:用于所有控件和只用于标准Windows 控件的标准事件。
(1)用于所有控件的标准事件
最基本的事件都定义在对象TControl 中。所有控件,无论是窗口控件、图形控件还是自定义控件
都继承了这些事件。下面事件都是用于所有控件:OnClick、OnDragDrop、OnEndDrag、OnMouseMove、
OnDblClick、OnDragOver、OnMouseDown 和OnMouseUp 等。
所有标准事件在TControl 中都定义了相应的protected 虚方法,名字与相应的事件名字是对应的。
例如OnClick 事件调用名为Click 的方法,OnEndDrag 事件调用名为DoEndDrag 的方法。
(2)标准窗口控件的标准方法
除了公用的所有控件都有的事件,标准窗口控件(VCL 应用程序中从TWinControl 派生,CLX 从
TWidgetControl 派生)具有下列事件:OnEnter、OnKeyDown、OnKeyPress、OnKeyUp、OnExit 等。
正如TControl 中的标准事件,窗口控件也有相应protected 虚方法。上面列出的就是响应正常击键
触发的标准键盘事件。
注意:为响应特殊的击键(如Alt 键),必须响应来自Windows 的WM_GETDLGCODE 或
CM_WANTSPECIALKEYS 消息。
第16 章 开发新的VCL 组件
·437·
2.使事件可见
TControl 和TWinControl(CLX 应用程序中是TWidgetControl)中的标准事件的声明是protected,
它们相应的方法也是protected。如果从这些抽象类中继承并且想使应用程序员在运行时或设计时能访
问它们,就需要将它们重声明为public 和published。
重声明属性而不描述它的实现,将继承相同的实现方法,只是改变了保护级别。因此可以利用已
经在TControl 中定义但是不可见的事件,通过定义它们为public 或published 而使它们显露出来。
例如创建一个组件并使它的OnClick 事件出现在运行时,可以增加下面的组件声明:
type
TMyControl=class(TCustomControl)
published
property OnClick; {使OnClick 在组件编辑器中可见}
end;
3.改变标准事件处理过程
如果想改变自定义组件响应某种事件的方法,可以重写事件处理代码并将其赋给事件。应用程序
员正是这么做的。但是当开发组件时,必须保证使用组件的程序员仍然可以使用这个事件。这也是将
连接每个标准事件的方法声明为protected 的原因。通过重载实现方法,能修改内部事件处理过程。通
过调用继承的方法,能维护事件处理过程,包括应用程序员的代码。
调用继承的方法的顺序是很重要的。通常的做法是,首先调用继承的方法,允许应用程序员的事
件处理过程代码在定制代码前执行(在某些情况下,定制代码可能不执行)。然而也有在调用继承的方
法之前执行自己的代码情况出现。例如如果继承的代码依赖于组件的状态而代码改变了组件的状态,
这时应该先作出改变,然后执行应用程序员关于该事件处理的代码。
下面是一个重载Click 事件的示例:
procedureTMyControl.Click;
begin
inheritedClick; {执行标准处理,包括调用事件处理过程及定制的代码}
end;
16.4.3 定义新事件
定义全新的事件是很少见的,只有组件的行为完全不同于所有其他事件时才需要定义新事件。
1.触发事件
定义新事件要遇到的第1 个关键问题是知道什么触发了事件(使用标准事件时不需要考虑由什么
触发事件)。
对某些事件答案是显然的。例如: 当用户按下鼠标的左键,Windows 给应用程序发送
WM_LBUTTONDOWN 消息,一个MouseDown 事件发生。接到消息后,一个组件调用它的MouseDown
方法,它依次调用OnMouseDown 事件处理过程代码。
但是有些事件却不是可以简单描述清楚的。例如当滚动条有一个OnChange 事件时,可在几种情
况下触发,包括按键、鼠标单击或其他控件变化。定义事件时,必须保证发生各种情况时能调用正确
的事件。
组件开发者拥有对自定义事件触发的完全控制,因此必须小心定义事件以便应用程序员能理解和
使用它们。
2.定义处理过程类型
定义事件之后就必须要定义事件如何被处理,这就意味着要决定事件处理过程的类型。在大多数
情况下,自定义的事件处理过程的类型是简单通知或与事件相关的类型,也有可能需要从事件处理代

·438·
码中返回信息。
(1)简单通知事件
通知事件只是说明特定的事件发生了,而没有描述在什么时候和什么地方发生的特定信息。
通知事件使用TNotifyEvent 类型,只带一个Sender 参数,该参数是TObject 类型的。所有通知事
件的处理过程都“知道”它是什么样的事件和事件发生在哪个组件。例如Click 事件是通知类型。当
编写Click 事件的处理过程时,所知道的就是发生了Click 事件和单击哪个组件。
通知事件是单向过程,没有反馈机制以避免一个通知被重复处理。
(2)事件特定处理过程
在某些情况下,只知道什么事件发生和发生在哪个组件是不够的。例如当发生了按键事件时,事
件处理过程应该要知道用户按了哪个键。在这种情况下,需要事件处理过程包含有关事件的必要信息
的参数。
如果事件产生是为了响应消息,那么传递给事件的参数最好是直接来自消息参数,而返回信息来
自事件处理过程。
因为所有事件处理过程都是过程,所以从事件处理过程中返回信息的惟一方法是通过var 参数。
组件可以用这些信息决定在应用程序员的事件处理过程执行后怎样处理事件。
例如所有的击键事件(OnKeyDown、OnKeyUp 和OnKeyPressed)都通过var 参数key 传递键值。
为了使应用程序看见包含在事件中的不同的键,事件处理过程可以改变key 变量值。在输入时强制使
字母转换为大写的方法就是一个例子。
3.声明事件
决定了事件处理过程的类型之后就要声明事件的方法指针和属性。为了让应用程序员易于理解事
件的功能,应当赋给事件一个有意义的名字,并且应该与其他组件中相似属性的名称保持一致。
Delphi 中所有标准事件的名称都以“On”开头。这只是一个惯例,编译器并不强制它。对象编辑
器是根据属性类型来决定属性是否是事件,所有的方法指针属性都被看作是事件,并出现在事件页中。
并且程序员一般都会按照以“On”开始的字母顺序查找事件。使用其他类型的名称可能会引起混淆。
注意:这个规则的主要例外是在某些事情发生之前和之后的事件名称以“Before”和“After”
开始
4.调用事件
一般说来,最好将调用集中在事件上。也就是说在组件中创建一个虚方法来调用应用程序员的事
件处理过程并提供所有的默认处理。
将所有的事件调用集中在一个地方,保证从该组件派生组件的人能够通过重载一个简单的方法来
定制事件处理过程,而不需要在所有的代码中查找调用事件的代码。
(1)空的事件处理代码必须是有效的
不能允许使空事件处理过程产生错误的情况出现,自定义组件的正常功能也不能依赖来自应用程
序事件处理过程的特定响应。实际上,空事件处理过程应当产生与无事件处理过程一样的结果。
组件不应当要求用户以特殊方式使用它们。既然一个空事件处理过程应当与无事件处理过程一样
动作,那么调用应用程序事件处理过程的代码应当像这样:
if Assigned(OnClick) then OnClick(Self);
?;{执行默认处理} 
而不应该有这样的代码: 
if Assigned(OnClick) then 
OnClick(Self) 
else 
第16 章 开发新的VCL 组件
·439·
?;{执行默认处理} 
(2)应用程序员能重载默认处理 
对于某些种类的事件,应用程序员可能想取代默认处理甚至删除所有的响应。为支持用户实现这
种功能,需要传递var 参数给事件处理过程,并在事件处理过程返回时检测某个值。 
这就是空事件处理过程与无事件处理过程有相同作用的规则。因为空事件处理过程不会改变任何
按引用调用的参数值,所以默认处理总是在调用空事件处理过程后发生。 
例如在处理KeyPress 事件时,应用程序员可以通过将var 参数key 的值设置为空字符(#0)来跳
过组件的默认处理,代码如下: 
if Assigned(OnkeyPress) then OnkeyPress(Selfkey); 
if key<>#0 then {执行默认处理}; 
实际的代码将与这稍有不同,因为它只处理窗口消息,但处理逻辑是相同的。在默认情况下,组
件先调用应用程序中赋予的事件处理过程,然后执行标准处理。如果应用程序的事件处理过程将key
设为空,则组件跳过默认处理。 
16.5 创建方法
组件方法是内建在一个类内部的过程和函数。通常,组件不应该包含很多方法,并且应该使应用
程序能够调用的方法数目最少。希望在方法中执行的特征通常封装在属性中更好,因为属性提供了一
个界面并且可以在设计时访问。 
虽然Delphi 对组件的方法可以做什么基本没有限制,但是提出了应该遵循的一些基本准则。 
16.5.1 避免互相依赖
开发组件的最终目标就是对应用程序员的约束最少。为了减少依赖性,不应该在组件中使用下面
的方法。 
*必须调用之后才能使用组件的方法。 
*必须按一定的顺序执行的方法。 
*将使组件进入一种状态或模式导致某些事件和方法无效的方法。 
例如当调用一个方法使组件进入一种调用其他方法无效的状态时,则第2 个方法在执行它的主代
码前应纠正这个状态,以便当组件处于一个“坏的”状态时,应用程序仍能调用它。最低程度,在应
用程序员调用无效方法时应抛出异常。换句话说,如果代码的某个部分的一个状态依赖于其他代码,
则保证即使应用程序员不正确使用代码也不会引起问题的责任在于组件开发者。至少当应用程序员没
有遵循这个依赖关系造成系统错误时,应该提出一个警告信息。 
16.5.2 命名方法的惯例
如果习惯于只是写给自己或一个小群体的程序员使用的代码,那么可能不需要考虑太多命名的事
情。但是方法命名清楚仍然是个好习惯,因为不熟悉组件代码的人(甚至不熟悉编码的人)也可能会
使用这个组件。 
Delphi 对方法的命名规则和参数没有限制。当然有一些惯例使应用程序员更容易使用这些方法。
必须记住组件本身就是为了可以让许多不同的人使用。 
下面是一些给方法命名的建议。 
*使名称具有描述性,使用有意义的动词。如PasteFromClipboard 的名称比简单的Paste 或PFC
提供了更多的信息。 
*函数的名称应能反映返回值的性质。虽然对于程序员来说,命名为X 的函数返回一个水平位置
可能是很明显的,但是将这个函数命名为GetHorizontalPosition 可能更容易理解。 
*保证方法确实是需要一个“方法”。一个准则是方法名中包含动词,如果发现一个方法名中没

·440·
有动词,则应该考虑方法是否应该变更为属性。
16.5.3 保护方法
类的所有部分,包括域、方法和属性,都有一个保护或“可见度”水平。选择一个组件方法的可
见度是比较简单的。
大部分组件中的方法应该是public 或protected 的。很少需要一个private 的方法,除非是组件特
定的方法,指明即使派生组件都不能访问它。
1.应该设置为public 的方法
应用程序员可以调用的所有方法应该声明为public。由于大部分方法是在事件处理过程中调用的,
因此方法应该避免绑定系统资源或使操作系统进入不能响应用户操作的状态。
注意:构造方法(Constructor)和析构方法(Destructor)必须是public 的。
2.应该设置为protected 的方法
组件实现的方法应该是protected 的,以使应用程序不能在错误的事件中调用。如果有应用程序不
能调用但是可以被派生类调用的方法,也应该声明为protected。
例如有一个方法依赖于预先设置的某个数据。如果这个方法是public 的,应用程序就有可能在设
置数据之前调用它。另一方面,使它成为proteted,就可以确保应用程序不能直接调用它。然后就可
以设置其他public 方法,确保在调用protected 方法之前设置数据。
属性实现方法应该声明为虚拟的protected 方法。这样声明的方法允许应用程序员重载属性的实现
方法,即可以增加它的功能或完全替换它。这样的属性是完全多态的。保持访问方法为protected,可
以确保应用程序员不会意外地调用它而修改一个属性。
3.抽象方法
有时Delphi 组件中的方法声明为抽象的(abstract)。在组件库中,抽象方法通常用于名字以Custom
开头的类中,例如TCustomGrid。这些类本身是抽象的,也就是说它们只用于派生子类。不应该创建
一个包含抽象成员的类的实例对象。调用抽象成员会导致EAbstractError 异常。
abstract 指令用于指明类的成员需要在子组件中定义和实现,它强制组件开发者在创建类的实例之
前在子类中重新声明抽象成员。
16.5.4 使方法变为虚方法
当希望调用同一方法能执行不同的代码时可以使方法成为虚方法。
如果打算开发给应用程序员直接使用的组件,可以将所有的方法都定义为非虚方法。另一方面,
如果是开发其他组件可以派生的抽象组件,则应该考虑将加入的方法变为虚方法,然后派生组件可以
重载继承的虚方法。
16.5.5 声明方法
组件中声明方法与在其他类中声明方法是一样的。在一个组件中声明方法的步骤如下。
*在组件的对象类型声明部分加入方法声明。
*在组件单元的实现部分实现方法。
16.6 在组件中使用图形
Windows 提供了一个强大的图形界面(Graphics Device Interface,GDI)用于绘制设备独立的图形。
但是GDI 对程序员提出了额外的要求,例如管理图形资源。
Delphi 接管了所有GDI 的单调工作,使程序员能够集中精力在创造性的工作中,而不是忙于查找
第16 章 开发新的VCL 组件
·441·
丢失的句柄或没有释放的资源。使用Windows API 可以在Delphi 应用程序中直接调用GDI 函数。但
是使用Delphi 封装的图形函数会更加简单而且速度快。
注意:GDI 函数都是Windows 相关的,不能用于CLX 应用程序。CLX 组件可以使用Qt 库。
16.6.1 VCL 的图形接口
Delphi 在几个层次封装了Windows GDI(CLX 应用程序中是Qt)。组件开发者最重要的工作是确
定组件中将图形显示在屏幕上的方法。直接调用GDI 函数时,需要有一个设备上下文的句柄,通过句
柄可以选择不同的绘画工具例如Pen、Brush 和Font。在绘制了图形之后,必须恢复设备上下文到使用
它之前的初始状态。
Delphi 提供了一个简单然而更完全的接口:组件的Canvas 属性。Canvas 保证它有一个合法的设
备上下文,并且在不使用时释放上下文。Canvas 有它自己的属性代表当前的Pen、Brush 和Font,并
接管了这些资源,因此程序员不需要关心创建、选择和释放Pen 之类的句柄,只需要告诉Canvas 应该
使用什么样的Pen,然后Canvas 负责其他的事情。
让Delphi 管理图形资源的一个好处是它可以对资源进行缓存,从而加快重复的操作。如果程序中
重复创建、使用和分配一个特定的Pen 工具,则每次使用时都需要重复这些步骤。因为Delphi 将这些
图形资源进行了缓存,很大的机会在重复使用这些资源的时候它仍在缓存中,所以不必再创建一个工
具,Delphi 将直接使用现存的Pen。
这样的一个例子是当一个应用程序有大量的表单包含上百个控件要打开时,每个控件可能有一个
或多个TFont 对象,这会导致存在成百上千个TFont 对象。而实际上应用程序中只是用2 或3 个Font
句柄,这就是Font 缓存的作用。
16.6.2 使用Canvas
Canvas 类在几个层次封装了图形控件,包括绘制线、形状和文本的高层函数、操作Canvas 绘制
能力的中层属性以及在类库水平提供的对Windows GDI 的低层访问。
总结Canvas 的功能,如表16-6 所示。
表16-6 Canvas 功能总结
层次 可用功能 实现工具
绘制线和形状 如MoveTo、LineTo、Rectangle 和Ellipse 方法
显示和测定文本 如TextOut、TextHeight、TextWidth 高 和TextRect 方法
填充区域 如FillRect 和FloodFill 方法
定制文本和图形 如Pen、Brush 和Font 属性
中 操作像素 如Pixels 属性
拷贝和合并图形 如Draw、StretchDraw、BrushCopy 和CopyRect 方法,CopyMode 属性
低 调用Windows GDI 函数 如Handle 属性
16.6.3 使用图形工作
Delphi 中对图形所作的大部分工作都限制于直接在组件和表单的Canvas 上绘制。Delphi 也提供了
处理独立图形(如位图、图元文件和图标等)的功能,包括自动管理调色板。
1.使用Picture、Graphic 和Canvas
在Delphi 中处理绘图有如下3 类:
*Canvas 代表在一个表单、图形控件、打印机或位图中绘制位图的画布。Canvas 永远是另外一个
类的属性,而不能独立存在。

·442·
*Graphics 代表了通常在一个文件或资源(如位图、图标或图元文件)中的一个图像。Delphi 定
义了TBitmap、TIcon 和TMetafile(仅在VCL 中)类,它们都是从TGraphic 派生的。当然也可以自
己派生图形类。通过定义这个所有图形的一个最小标准界面,TGraphic 给应用程序提供一个方便使用
不同种类图形的机制。
*Picture 是一个Graphic 的容器,这意味着它能包含任意的Graphic 类。也就是说,一个TPicture
类可以包含一个位图、图标、图元文件或用户定义的Graphic 类型,应用程序可以通过Picture 类按照
同样的方法访问它们。例如Image 控件有一个类型为TPicture 的Picture 属性,可以使控件显示许多种
Graphic 类型的图像。
在Delphi 中,一个Picture 肯定有一个Graphic,而一个Graphic 可以有一个Canvas(实际上具有
Canvas 的惟一标准Graphic 是TBitmap)。当处理图形时,通常只需要通过TPicture 使用Graphic 类。
如果需要访问Graphic 类本身的特性,可以引用Picture 的Graphic 属性。
2.装载和存储图形
Delphi 中所有的Picture 和Graphic 都可以从文件中装载和存储图形(或保存到不同的文件中)。
可以在任何时间装载和存储一个Picture 的图形。
注意:开发跨平台组件时,也可以从Qt MIME 源或对象流中装载和存储图形
调用Picture 的LoadFromFile 方法从一个文件中装载图形,调用Picture 的SaveToFile 方法保存图
形到一个文件中。LoadFromFile 和SaveToFile 只使用文件名作为参数。LoadFromFile 使用文件名的扩
展名以决定创建和装载什么样的图形。SaveToFile 按照Graphic 对象的类型决定保存文件的类型。
3.处理调色板
对VCL 和CLX 组件,当在一个基于调色板的设备(典型的,256 色video 模式)上运行时,Delphi
控件自动支持调色板的实现。也就是说,如果有一个拥有调色板的控件,可以使用从TControl 继承的
方法控制Windows 适应调色板。
大部分控件不需要调色板,但是包含名为“丰富颜色”的图形时(例如图形控件)可能需要与
Windows 和屏幕设备进行交互以保证控件的外观合适。Windows 把这个过程叫作实现调色板。
实现调色板是保证最前面的窗口使用它完全的调色板,后面的窗口尽可能多的使用它们的调色
板,然后将所有其他颜色映射到“实际”调色板最接近、可获得的颜色的过程。当窗口移到另一个前
面时,Windows 继续实现调色板。
注意:Delphi 本身不对创建和维护调色板提供特别的支持。如果有一个调色板句柄,Delphi
控件可以对它进行管理。
(1)为控件指定一个调色板
重载控件的GetPalette 方法,返回调色板句柄可以为一个控件指定一个调色板。为一个控件指定
调色板可以为应用程序做以下事情。
*告诉应用程序控件的调色板需要实现。
*分配使用的调色板以便实现。
(2)对调色板的变化作出反应
如果VCL 或CLX 控件通过重载GetPalette 指定了调色板,Delphi 将自动管理来自Windows 的调
色板消息。处理调色板消息的方法是PaletteChanged。
PaletteChanged 的主要角色是决定是在前台还是后台实现控件的调色板。Windows 通过使最前端
的窗口拥有前台的调色板,其他窗口在后台调色板解析,来实现这个调色板。Delphi 更进一步进行处
理,它实现了一个窗口内部按照Tab 顺序设置控件的调色板。当一个控件不是在Tab 顺序的第1 个但
是需要拥有前台调色板时,可能需要重载这个默认方法。
第16 章 开发新的VCL 组件
·443·
16.6.4 脱屏位图(Off-Screen Bitmap)
当绘制复杂的图形时,图形程序的一个通用技术是创建一个脱屏位图(Off-Screen Bitmap)。在位
图上绘制图形,然后从位图中拷贝完整的图像到最终目的屏幕。使用脱屏位图可以减少由于重复直接
在屏幕上绘制引起的闪烁。
Delphi 中的Bitmap 类,代表了源和文件中的“位图化”图形,也可以作为一个脱屏位图工作。
1.创建和管理脱屏位图
当创建复杂图形时,应该避免直接在Canvas 上绘制后直接显示在屏幕上。可以创建一个Bitmap
对象,在它的Canvas 上绘制,然后拷贝完全的图像到屏幕的Canvas。
在一个图形控件的Paint 方法中经常使用脱屏位图。下面是一个使用脱屏位图重载Paint 方法的模
式。由于使用临时对象,Bitmap 应该在一个try..finally 块中。
type
TFancyControl = class(TGraphicControl)
protected
procedure Paint; override; {重载Paint 方法}
end;
procedure TFancyControl.Paint;
var
Bitmap: TBitmap; {脱屏位图临时变量}
begin
Bitmap := TBitmap.Create; {构造位图对象}
try
{绘制位图代码}
{拷贝结果到控件的Canvas }
finally
Bitmap.Free; {销毁位图对象}
end;
end;
2.拷贝位图化的图形
Delphi 提供了将一个Canvas 中的图形拷贝到另一个Canvas 中的4 种方法。根据需要的不同效果,
可以调用不同的方法。
总结Canvas 对象中的图像拷贝方法,如表16-7 所示。
表16-7 Canvas 图像拷贝方法总结
需要实现的效果 实现方法
拷贝一个完整图形 Draw
拷贝并且重新设置图形大小 StretchDraw
拷贝Canvas 的一部分 CopyRect
伴随光栅操作拷贝位图 BrushCopy(VCL)
重复拷贝一个图形重叠到一个区域 TiledDraw(CLX)
16.6.5 对变化作出反应
所有的图形对象,包括Canvas 和它们拥有的对象(Pen、Brush 和Font)都内建了事件以对对象
的变化作出反应。通过使用这些事件,可以使组件(或使用它们的应用程序)通过重画它们的图形对

·444·
变化作出反应。
如果把图形对象作为组件的设计时界面而发布它们,则对图形对象中的变化作出反应是非常重要
的。保证组件的设计时的外观与在对象编辑器中设置的属性一致的惟一办法是对对象的变化作出反应。
为对图形对象的变化作出反应,应该给类的OnChange 事件赋予处理过程。
16.7 处理消息和系统通知
组件经常要对底层操作系统的通知作出反应。操作系统通知应用程序发生的事情,例如用户使用
鼠标和键盘进行的输入。一些控件也产生消息,例如用户行为的结果(如在一个列表框中选择一个项
目)。组件已经处理了大多数的通用消息,但是在创建组件的过程中有可能需要编写代码处理消息。
在VCL 应用程序中,通知(Notification)以消息(Message)的形式到达。这些消息能够来自不
同的源,包括Windows、VCL 组件已经定义的其他组件。在CLX 应用程序中,通知以信号(Signal)
和系统事件(System Events)的形式到达。本节主要讲述开发VCL 组件中的消息处理。
16.7.1 理解消息处理系统
所有的VCL 类内部都具有处理消息的机制,如调用消息处理方法或消息处理过程。消息处理过程
的基本流程是对象接收某种消息并派送它们,然后调用与接收的消息对应的方法。如果没有指定与消
息对应的方法,那就调用默认处理。
图16-3 所示为VCL 的消息处理系统。
MainWndProc WndProc Event Dispatch Handler
图16-3 VCL 的消息处理系统
VCL 定义了将所有Windows 消息(包括用户自定义消息)直接转换到特定对象方法调用的消息派
送系统。一般没有必要改变这种消息派送机制,只需要创建消息处理方法。
1.Windows 消息中包含的内容
Windows 消息是包含若干域的数据记录。记录中最重要的是一个整型值,该值标识消息。Windows
定义了大量的消息,库单元Messages 声明了所有消息的标识。消息中其他的有用信息包括两个参数域
和一个结果域。
两个参数分别是16 位和32 位的。Windows 代码总是以wParam 和lParam 来引用它们(分别代表
word 参数和long 参数)。通常每个参数包含不止一点信息,因此如果看到如lParamHi 之类的名字的引
用,就是指的long 参数的高位部分。
最初,Windows 程序员不得不记住或在Windows API 中查找包含的每一个参数。现在,微软公司
已经命名了这些参数。这样理解伴随这些消息的信息就更简单了。例如WM_KEYDOWN 消息的参数
被称为nVirtKey 和lKeyData,这就比wParam 和lParam 给出了更多的描述信息。
Delphi 为每种不同类型的消息定义了指定的记录类型,并且给了一个好记的名称。如鼠标消息在
long 参数中传递鼠标事件的x、y 坐标,一个在高位,一个在低位。使用鼠标消息记录时就不需要关心
哪个字是哪个坐标,因为引用这些参数可以通过名称Xpos 和Ypos 进行,而不是lParamLo 和lParamHi。
2.派送消息
当应用程序创建窗口时,在Windows 内核中注册了一个窗口过程。窗口过程是处理窗口消息的函
数。传统上,窗口过程包括了一个巨大的Case 语句,语句的每个入口是窗口要处理的每一条消息。记
住这里的“窗口”是在屏幕上显示的所有内容,包括每个窗口、控件等。当每次创建窗口时,必须建
立完整的窗口过程。
VCL 在如下3 方面简化了消息派送:
*每个组件都继承了完整的消息派送系统。
*派送系统具有默认处理,只需定义需要响应的消息处理方法。
第16 章 开发新的VCL 组件
·445·
*可以只修改消息处理的一部分,依靠继承的方法完成大多数处理。
这种消息派送系统的最大优点是用户能在任何时候安全地发送任何消息给任何组件。如果组件没
有为该消息定义处理方法,那默认处理方法会解决这个问题,通常是忽略它。
3.跟踪消息流
VCL 为应用程序中每种类型的组件都注册了名为MainWndProc 的方法作为窗口过程。
MainWndProc 包含了异常处理块,它将消息记录从Windows 传递到名为WndProc 的虚方法,并且通
过调用应用程序类的HandleException 方法处理异常。
MainWndProc 不是虚方法,没有包含任何特定消息的处理方法。定制过程在WndProc 中,因此每
个组件类型都能重载该方法以适合特定的需要。
WndProc 方法检查所有影响它们处理的特定条件,因此可以“捕捉”不要的消息。例如当组件被
拖动时,它忽略键盘事件,因此TWinControl 的WndProc 只在没有被拖动时传送键盘事件。最后
WndProc 调用Dispatch 方法,该方法是从TObject 继承来的非虚方法,它决定什么方法来处理消息。
Dispatch 使用消息记录的Msg 域来决定怎样派送特定消息。如果组件已经给该消息定义了处理方
法,则Dispatch 调用该方法,否则,Dispatch 调用默认处理方法。
16.7.2 改变消息处理方法
在改变组件的消息处理方法之前,必须先要考虑清楚真正想要做什么。VCL 将大多数的Windows
消息转换成组件编写者和组件用户都能处理的事件。通常说来,应当改变事件处理行为而不是改变消
息处理行为。
在VCL 组件中要改变消息处理行为,需要重载消息处理方法。在某些情况下,也可以捕获消息防
止组件处理该消息。
1.重载处理方法
要改变组件处理特定消息的方法,就需要重载那个消息的处理方法。如果组件不处理该消息,就
需要声明新的消息处理方法。
为了重载消息处理方法,要在组件中以相同的消息索引声明新的方法。不需要使用override 指令,
但必须使用Message 指令和相应的消息索引。
例如为了重载一个处理WM_PAINT 消息的方法,需要重声明WMPaint 方法:
type
TMyComponent=class(?)
procedureWMPaint(varMessage:TWMPaint);messageWM_PAINT;
end;
消息名称和单个var 参数的类型不一定与重载方法匹配,但是消息索引必须匹配。但是为了清晰,
最好是按照惯例给消息处理方法命名。
2.使用消息参数
在消息处理方法内部,组件可以访问消息记录的所有参数。因为传递的总是var 参数,消息处理
过程可以根据需要改变参数的值。消息的Result 域是惟一经常改变的参数:它由SendMessage 返回,。
因为消息处理方法的消息参数的类型随着被处理的消息的变化而变化,所以应当参考Windows 消
息文档中的参数的名字和含义。如果出于某种原因要使用旧风格的消息参数(WParam、LParam 等),
可以将Message 转换为通用类型TMessage(它使用那些参数名)。
3.捕获消息
在某种情况下,可能希望组件能忽略一些消息,就是说,阻止组件将该消息派送给它的处理方法。
为捕获消息,可以重载虚方法WndProc。
对VCL 组件来说,WndProc 方法将在消息传给Dispatch 方法前会屏蔽该消息。它依次决定哪一

·446·
个方法来处理消息。通过重载WndProc,组件得到了派送消息之前过滤它们的机会。
从TWinControl 派生的组件重载WndProc 的方法是这样的:
procedure TMyControl.WndProc(var Message: TMessage);
begin
{测试并决定是否继续处理的代码}
inherited WndProc(Message);
end;
重载WndProc 可以在两方面得到好处。
*它可以过滤部分消息而不是为每一个消息指定处理过程。
*它可以根本不分派消息,因此处理代码根本不会调用。
下面代码是TControl.WndProc 中的一部分:
procedure TControl.WndProc(var Message: TMessage);
begin
...
if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
if Dragging then {特别处理拖动}
DragMouseMsg(TWMMouse(Message))
else
... {正常处理其他消息}
end;
... {否则正常进行}
end;
16.7.3 创建新的消息处理方法
因为VCL 为大多数普通Windows 消息提供了处理方法,所以基本上只有定义新消息时,才需要
创建新的消息处理方法。
1.定义新消息
许多标准组件为了内部使用而定义了消息。一般是因为标准消息没有包含的广播信息和状态改变
的通知,可以在VCL 中定义新消息。
(1)声明消息标识
消息标识是整型常量。Windows 保存了小于1024 的消息用于自己使用,因此当声明新消息时应
当大于1024。
常量WM_USER 代表用于自定义消息的开始数字。当定义消息标识符时,应当基于WM_USER。
必须注意某些标准Windows 控件使用用户自定义范围的消息。包括ListBox、ComboBox、EditBox
和Button 组件。当从上述组件中派生一个组件,并希望定义新消息时,应当检查一下Message 单元
Windows 已经使用了哪些消息用于该控件。
定义消息的方法如下:
Const
WM_MYFIRSTMESSAGE=WM_USER+0;
WM_MYSECONDMESSAGE=WM_USER+1;
(2)声明消息记录类型
如果想给自定义消息的参数有含义的名称,就需要为该消息声明消息记录类型。消息记录是传给
消息处理方法的参数类型。如果不使用消息参数或者想使用旧风格参数(wParam,lParam 等),可以
使用默认的消息记录TMessage。
第16 章 开发新的VCL 组件
·447·
声明消息记录类型要遵循下列惯例。
*以消息名命名消息记录类型,并以T 开始。
*将记录中第1 个域命名为Msg,类型为TMsgPraram。
*随后的两个字节定义为word,以响应word 大小的参数,再接下来的两个字节定义为未使用或
者把4 个字节定义为与long 参数匹配。
*最后把域命名为Result,类型为Longint。
2.声明新的消息处理方法
有两种情况需要定义新的消息处理方法:
*组件需要处理没有被标准组件处理的Windows 消息。
*定义了组件使用的新消息。
声明消息处理方法的步骤如下:
*在组件声明中的protected 部分声明方法。
*将方法做成过程。
*以要处理的消息名命名方法,但不带下划线。
*传递一个命名为Message 的var 参数,类型为消息记录类型。
*编写用于该组件的特别处理代码。
*调用继承的消息方法。
3.发送消息
典型的应用程序发送消息以发送状态改变的通知或广播信息。组件能够向表单中的所有控件广播
消息、向一个特定控件(或应用程序本身)发送消息或者向自己发送消息。
发送一个Windows 消息有几个方法,使用哪种方法取决于为什么发送消息。
(1)向表单中的所有控件广播消息
当组件改变全局设置而影响表单中的所有控件或其他容器时,需要向控件发送消息以便它们正确
地更新自己。并不是所有控件需要对通知作出反应,但是广播消息可以通知所有控件如何反应以及允
许其他控件忽略消息。
向另一个控件中的所有控件广播消息可以使用Broadcast 方法。在广播消息之前,应该先填写想
要发送的消息记录。
var
Msg: TMessage;
begin
Msg.Msg := MY_MYCUSTOMMESSAGE;
Msg.WParam := 0;
Msg.LParam := Longint(Self);
Msg.Result := 0;
然后将这个消息记录传递给需要通知的所有控件的父控件。这可以是应用程序中的任意控件,例
如它可以是正在开发的控件的父控件:
Parent.Broadcast(Msg);
也可以是包含控件的表单:
GetParentForm(self).Broadcast(Msg);
或者是活动表单:
Screen.ActiveForm.Broadcast(Msg);
甚至可以是应用程序中的所有表单:
for I:= 0 to Screen.FormCount - 1 do

·448·
Screen.Forms[I].Broadcast(Msg);
(2)直接调用控件的消息处理方法
有时只有一个控件需要响应消息。如果知道哪个控件接收消息,则最简单直接发送消息的方法是
调用控件的Perform 方法。
直接调用控件的Perform 方法有两个原因。
*需要触发控件处理标准Windows 消息时的一样的反应。例如当表格控件接收到键盘消息时,它
创建一个编辑控件然后向那个编辑控件发送击键消息。
*所有控件都有消息处理能力,在知道需要通知的控件,但是不知道控件的类型时,可以发送消
息获得其特定的方法。如果控件有对发送的消息进行处理的方法,它就会正常响应,否则它会忽略发
送的消息并返回0。
调用Perform 方法不需要创建消息记录,只需要将消息标识符、WParam 和LParam 作为参数,
Perform 即可返回消息结果。
(3)使用Windows 消息队列发送消息
在一个多线程的应用程序中,不能只是调用Perform 方法,因为目标控件可能在另一个线程中。
然而通过使用Windows 消息队列,可以安全的与其他线程通信。消息处理永远在VCL 主线程发生,
但是可以使用来自应用程序其他线程的Windows 消息队列发送消息。调用SendMessage 是同步的。也
就是说,SendMessage 直到目标控件已经处理了消息才返回,即使它在另一个线程中。
使用Windows API 调用SendMessage,可以使用Windows 消息队列向控件发送消息。SendMessage
除了必须使用Windows 句柄标识目标控件之外,其参数与Perform 一样,因此应将:
MsgResult := TargetControl.Perform(MY_MYMESSAGE, 0, 0);
写为:
MsgResult := SendMessage(TargetControl.Handle, MYMESSAGE, 0, 0);
(4)发送一个不立即执行的消息
有时可能需要发送一个消息,但是不知道目标控件执行是否安全。例如发送消息的代码是从目标
控件的事件处理方法中调用的,就必须保证在控件执行新消息之前事件处理方法已经完成。可以将这
种情况当作不需要消息结果一样处理。
使用Windows API 调用PostMessage 可以向一个控件发送消息,并且等到它处理完其他消息后再
处理这个消息。PostMessage 的参数与SendMessage 是一样的。
16.8 使组件在设计阶段可用
使组件在设计时可用需要以下几个步骤:
*注册组件。
*为组件提供帮助文件。
*加入属性编辑器。
*加入组件编辑器。
*将组件编译到一个包中。
并不是每个组件都需要以上几步。例如,如果没有定义新属性或事件,就不需要提供对它们的帮
助。全部需要的步骤只是注册和编译。
一旦组件已经注册和编译到一个包中,它们就可以发布给其他程序员并安装到IDE 中。
16.8.1 注册组件
注册是在一个编译单元的基础上进行的,因此如果单个编译单元中包含几个组件,那么可以一次
注册所有的组件。
注册组件需要在单元中加入Register 过程。在Register 过程中可以注册组件并决定它们安装到组
第16 章 开发新的VCL 组件
·449·
件面板的哪一页中。
注意:使用IDE 菜单“Component”*“New Component”建立新组件,注册的代码将自动
添加。
1.声明Register 过程
注册必须在单元中编写一个过程,过程名字必须为Register。Register 过程必须在单元的interface
部分声明,并且与其他Delphi 过程不一样,它是大小写敏感的。
注意:虽然Delphi 是大小写不敏感的,但Register 是大小写敏感的,并且必须使用大写R
开始。
下面的代码显示了一个简单的创建和注册一个新组件的单元的模板。
unit MyBtns;
interface
type
{在这里声明新组件的类型}
procedure Register; {这必须在interface 部分出现}
implementation
{组件的实现部分在这里}
procedure Register;
begin
{注册组件的代码}
end;
end.
在Register 过程中,每个加入组件面板的组件都需要调用RegisterComponents。如果单元包含几
个组件,可以所有的组件一次注册。
2.编写Register 函数
在一个包含组件的单元的Register 过程中,必须注册需要加入到组件面板的所有组件。如果单元
包含几个组件,可以所有的组件一次注册。注册组件时,在每次组件面板的一页添加需要的组件时就
要调用RegisterComponents 过程一次。
(1)指定组件
Register 过程中,组件名称在一个集合中传递,它可以在调用RegisterComponents 时创建:
RegisterComponents(’Miscellaneous’, [TMyComponent]);
也可以一次注册几个组件到同一页中,或注册组件到不同的页中,示例代码如下:
procedure Register;
begin
RegisterComponents(’Miscellaneous’, [TFirst, TSecond]); {安装两个到这一页}
RegisterComponents(’Assorted’, [TThird]); {另一个到其他页}
RegisterComponents(LoadStr(srStandard), [TFourth]); {一个到标准页}
end;
(2)指定面板页
面板页的名称是字符串。如果指定的面板页名称不存在,Delphi 会使用这个名称创建一个新页。
Delphi 将标准的页名保存在一个字符串列表资源中,以便产品的国际版本可以使用本地语言命名页。
如果需要安装一个组件到一个标准页中,应该通过LoadStr 函数获得标准页名的字符串,将代表页的
字符资源的常量作为参数,例如srSystem 代表System 页。

·450·
(3)使用RegisterComponents 函数
在Register 过程内,调用RegisterComponents 注册在类集合中的组件。RegisterComponents 函数是
一个具有两个参数的函数:组件面板页名和组件类的集合。
设置页名参数为组件需要安装到组件面板的页名。如果名称已经存在,组件将加到该页中。如果
页不存在,Delphi 将创建一个新页。
在定义了一个或几个新组件的单元的Register 过程的实现部分调用RegisterComponents。定义组
件的单元必须编译到一个包中,并且包必须在加入到组件面板之前已经安装。
procedure Register;
begin
RegisterComponents(’System’, [TSystem1, TSystem2]); {加入到System 页}
RegisterComponents(’MyCustomPage’,[TCustom1, TCustom2]); {新建页}
end;
16.8.2 安装组件到组件面板
一个第三方组件可能以多种方式提供,它们安装的方法也是不一样的,下面就目前常见的各种形
式的组件的安装方法进行介绍。
1.常见组件提供的方式
*只提供一个.dcu 文件的组件。
.dcu 文件是编译好的单元文件,这样的组件不公布源码。一般来说,作者必须说明此组件适合
Delphi 的哪种版本,如果版本不对,在安装时就会出现错误。也正是因为没有源码,给使用者带来了
不便,那就是一旦Delphi 版本升级,此组件就不能再使用了,当然有的组件也给出了几种版本的.dcu
文件,用户可根据需要选择使用。
*只有.pas 文件或既有.pas 又有.dcu 文件的组件。
*有.dpk 文件的组件包。
带有.dpk 文件的组件包一般是由多个组件构成的,也就是说安装后会有多个组件供使用。
*带有.bpl 文件的组件包。
一般来说这也是由多种组件构成的组件包,它其实是一个动态链接库文件(DLL)。
*ActiveX 控件。
不管以何种形式提供,组件在安装到IDE 之前必须将它们编译为一个包。一个包可以包含一个或
多个组件以及定制的属性编辑器。
在安装组件(包)操作之前,最好将*.bpl 拷贝到System 目录中,将*.pas、*.dcu、*.dcr、*.dpk
拷贝到Delphi 的Lib 目录中再进行。
2.将组件编译到一个包
如果获得的组件已经是一个DPK 文件或BPL 文件,则它已经是一个包文件,可以忽略这一步。
安装一个组件到一个包中的步骤如下。
*从IDE 菜单中选择“Component”*“Install Component”,将会显示启动之后的安装组件对话
框,如图16-4 所示。
第16 章 开发新的VCL 组件
·451·
图16-4 安装组件对话框
*切换页面以选择安装新组件到现存的或新的包中。
*在Unit File name 输入框中输入包含新组件的.pas 文件名或.dcu 文件名,或单击“Browse”查找
单元文件。
*如果新组件不在显示的默认位置,则可以调整查找路径。
*输入需要安装的组件的包名或单击“Browse”按钮查找包名。
*如果组件安装到一个包中,可以选择给包加入有意义的描述。
*单击“OK”按钮,关闭Install Component 对话框。
上述步骤之后,IDE 将会将组件加入到一个新的包(*.dpk)或已经存在的包中。
3.将DPK 文件中的组件加入到组件面板中
在将组件加入到包中之后,将包中的组件加入到组件面板中的步骤如下。
*打开包文件,可以从IDE 菜单中选择“File”*“Open”或者“File”*“Open Project”打开
需要的.dpk 文件。
*打开之后将显示编辑包对话框,如图16-5 所示,Contains 节点下包含了DPK 文件中包含的所
有组件单元和其他单元。
图16-5 编辑包对话框
*单击“Compile”按钮可以对包中所包含的单元进行编译。
*单击“Install”按钮则将包中的组件安装到组件面板中。
本例中由于所有包中的组件都已经安装,所以“Install”按钮不可用。
注意:新安装的组件初始显示在组件面板的页中是由组件编写者指定的。可以在组件安装到
组件面板之后,使用IDE 菜单“Component”*“Configure Palette”中弹出的对话框
移动组件到其他页中。
4.安装BPL 文件提供的组件
这种组件包的安装方法如下:
*在IDE 菜单项中选择“Component”*“Install Packages”,显示安装包对话框,如图16-6 所示。

·452·
图16-6 安装包对话框
*在对话框中单击“Add”按钮,在弹出的文件对话框中找到相应的.bpl 文件后,再单击“OK”
按钮返回即可。
5.安装ActiveX 控件
要安装这类控件,需要先用regsvr32.exe 注册,然后从IDE 菜单中选择“Component”*“Import
ActiveX Control”项,显示导入ActiveX 控件对话框,如图16-7 所示。
图16-7 导入ActiveX 控件对话框
在对话框中,只有已经注册的ActiveX 控件才出现在列表中,选中其中一项然后单击“Install”按
钮就可以安装了。如果事先没有用regsvr32.exe 注册也可以单击“Add”按钮,找到OCX 文件即时注
册,注册后再进行安装即可。
6.删除已安装组件
组件的删除方法很简单,在如图16-6 所示的安装包对话框中,在列表中找到组件所在的BPL,单
击“Remove”按钮,就将组件从Delphi 中删除了。如果需要可以把.bpl 文件删掉。如果不删除.bpl 文
件,还可以按找安装BPL 组件的方法再次安装使用。
16.8.3 使组件的资源文件可用
组件编写者应将组件使用的所有的资源文件放在同一目录中。这些文件包括源代码文件(*.pas)、
第16 章 开发新的VCL 组件
·453·
以及附加的项目文件(*.dfm、*.xfm、*.res、*.rc 以及*.dcr)。
加入一个新组件的过程将会创建几个文件,这些文件自动放置在由IDE 环境选项(使用IDE 菜单
“Tools”*“Environment Options”,选择Library 页)指定的目录文件。*.lib 文件放置在DCP 输出目
录。如果是在一个现存的包中加入新组件,*.bpl 文件放置在BPL 输出目录。
16.8.4 为组件设置位图
每个组件都需要一个在组件面板上代表它自己的位图。如果不指定组件的位图,IDE 使用默认的
位图。因为组件位图只是在设计时是必须的,因此不会编译到组件的编译单元中。但是需要在一个与
单元同名的Windows 资源文件(*.dcr,Dynamic Component Resource,动态组件资源)中提供它们。
可以使用Delphi 的图像编辑器创建这个资源文件。
开发一个新组件时,可以为组件定义位图。创建一个新位图的步骤如下:
*从IDE 中选择“Tools|Image Editor”启动图像编辑器,如图16-8 所示。
图16-8 图像编辑器
*在图像编辑器菜单中,选择“File”*“New”*“Component Resource File (.dcr)”。
*在untitled1.dcr 窗口中,用鼠标右键单击“Contents”,选择“New”*“Bitmap”。
*在Bitmaps Properties 对话框中,将Width 和Height 都设置为24 像素,并选择颜色为VGA(16
colors),单击“OK”按钮。
*Bitmap 和Bitmap1 将会在Contents 下面显示,用鼠标右键单击Bitmap1,选择Rename,将位图
名称命名为与新组件的类名称一样(包括字母T)。例如当一个新的类名为TmyNewButton 时,位图应
命名为TMYNEWBUTTON。
注意:必须全部用大写字母命名,而不论在New Component 对话框中是如何拼写类名的。
*双击TMYNEWBUTTON,显示一个包含空位图的窗口。
*使用图像编辑器设计图标。
*在图像编辑器菜单中选择“File”*“Save As”保存结果,并且将资源文件(*.dcr 或*.res)命
名与声明组件类的单元名称一样。例如命名资源文件名为MyNewButton.dcr。
*从IDE 中选择“Component”*“New Component”,按照使用组件向导创建一个新组件的方法
设置位图。必须保证MyNewButton.pas 和MyNewButton.dcr 资源文件在同一个目录下。
*名称为TMyNewButton 的组件源文件或单元MyNewButton.pas 默认为在LIB 目录下。单击
“Browse”按钮,可以查找已经生成的组件单元的新位置。

·454·
注意:如果使用*.res 文件而不是使用*.dcr 文件,必须加一个组件资源的引用以绑定资源。
例如*.res 文件是MyNewButton.res,则必须确保MyNewButton.pas 和MyNewButton.res 在同一个
目录,并且在MyNewButton.pas 中加入下面代码:
{*R *.res}
*从IDE 中选择“Component”*“Install Component”安装组件到一个新的或已经存在的包,单
击“OK”按钮。
此时,新的包已经建立并且安装,位图代表了显示在组件面板页中的新组件。
16.8.5 为组件提供帮助
当在窗体中选择一个组件或在对象编辑器中选择事件或属性时,能够按F1 得到有关这一项的帮
助。如果创建了相应的帮助文件,应用程序员能得到有关组件的相应文档。
因为Delphi 使用了特殊的帮助引擎支持跨多个帮助文件处理主题搜索,所以能提供关于自定义组
件的一个很小的帮助文件描述组件。应用程序员不需要额外的步骤就能找到文档。新建的帮助也成了
Delphi 帮助系统的一部分。
可以使用任何可以创建Windows 帮助文件(RTF 格式)的工具创建帮助的源文件。Delphi 中提供
了Microsoft Help Workshop,它可以编译帮助文件并提供在线帮助的指导。
为使新组件的帮助同库中其他组件的帮助一起工作,要遵循下列约定。
*每个组件有一个帮助主题。
*每个组件应该包含二级导航标题。
*组件中声明的每个属性、方法和事件都应该有一个帮助主题。
关于制作帮助文件的信息请参考制作工具的文档。
16.8.6 增加属性编辑器
对象编辑器提供所有类型属性的默认编辑器。Delphi 也支持通过编写和注册属性编辑器(property
editor)的方法为属性设计自己的编辑器。可以注册专门为自定义组件的属性设计的编辑器,也可设计
用于所有某类型的属性。
属性编辑器至少应该有两种工作方式:显示并且允许应用程序员将当前值作为文本字符串编辑,
或者显示一个对话框进行编辑。
1.派生一个属性编辑器对象
VCL 和CLX 都定义了几种属性编辑器。它们都是从TPropertyEditor 派生的。当创建属性编辑器
时,可以直接从TPropertyEditor 中继承或从如表16-8 所示的属性编辑器的类型中任一属性编辑器中继
承。DesignEditors 单元中的类VCL 和CLX 都可以使用。一些提供特殊对话框的属性编辑器类只能用
于VCL 或者CLX 中,它们相应地在VCLEditors 和CLXEditors 单元中定义。
注意:一个属性编辑器的最低要求是必须从TBasePropertyEditor 派生并支持IProperty 接口。
TPropertyEditor 只是提供了IProperty 接口的一个实现。
属性编辑器的类型如表16-8 所示。但是不完全,表中列出的属性编辑器是广泛使用的用户定义属
性的属性编辑器。VCLEditors 和CLXEditors 单元也定义了一些非常特殊的属性编辑器,如提供给组
件名称之类具有惟一性的属性使用的编辑器。
表16-8 属性编辑器的类型
类 可以编辑的属性
TOrdinalProperty 所有有序的属性(整数、字符、枚举)编辑器都从该类派生
TIntegerProperty 所有整型,包括子界类型
第16 章 开发新的VCL 组件
·455·
续表
类 可以编辑的属性
TCharProperty 字符类型或字符子集
TEnumProperty 所有枚举类型
TFloatProperty 所有浮点数
TStringProperty 字符串
TSetElementProperty 集合中的独立元素,显示为布尔值
TSetProperty 所有的集合,并不直接编辑集合类型,但是能展开成一列集合元素属性
TClassProperty 类,显示类名,并允许类属性的展开
TMethodPropevty 方法指针,主要指事件
TComponentProperty 相同表单中的组件,不能编辑组件的属性,但能指向兼容类型的组件
TColorProperty 组件颜色。显示颜色常量或十六进制数。下拉框显示颜色常量,双击打开颜色选择对话框
TFontNameProperty 字体名称,下拉框显示当前安装的所有字体
TFontProperty 字体,允许展开字体的属性或弹出字体对话框
2.将属性作为文本编辑
属性编辑器的SetValue 方法将应用程序员在对象编辑器中输入的字符串转换为合适的类型,并设置
属性的值。如果字符串不能转换为属性的值,SetValue 将显示异常并且不使用错误的值。要将字符串值
读入属性,可以重载属性编辑器的SetValue 方法。SetValue 在调用Set 方法之前会转换字符并验证值。
3.将属性作为一个整体来编辑
Delphi 支持以对话框的方式可视化地编辑属性。这种情况常用于对象类型属性的编辑。一个典型
的例子是Font 属性,它允许打开Font 对话框来一次选择字体的所有属性。提供整体属性编辑对话框,
要重载属性编辑器类的Edit 方法。
Edit 方法也使用编写GetValue 和SetValue 方法时使用的Get 和Set 方法。实际上,Edit 方法即调
用Get 方法也调用Set 方法。因为编辑器是与类型相关的,通常不需要将属性值转换为字符串。
当应用程序员单击属性后面的“...”按钮,或双击值列,对象编辑器调用属性编辑器的Edit 方法。
实现Edit 方法,需执行下列步骤:
*创建属性使用的编辑器。
*使用Get 方法读出当前值并赋值给属性。
*当选择新值时,使用Set 方法将新值赋值给属性。
*销毁编辑器。
4.设定编辑器属性
属性编辑器必须告诉对象编辑器如何采用合适的显示工具。例如属性编辑器需要知道属性是否有
子属性,或者是否能显示可能取值的列表。设定编辑器的属性通常重载属性编辑器的GetAttributes 方
法。GetAttributes 返回TPropertyAttributes 类型的集合。TPropertyAttributes 的可能取值如表16-9 所示。
表16-9 属性编辑器特征标志
标志 含义 相关方法
paValuelist 编辑器能给予一组枚举值 GetValues
paSubPropertie 属性有能显示的子属性 GetPropertises
paDialog 编辑器能显示编辑对话框以完整地编辑属性 Edit
PaMultiSelect 当用户选择多于一个组件时,属性应能显示 N/A
paAutoUpdate 每次改变后更新组件而不用等待确认 SetValue
paSortList 对象编辑器应对值表进行排序 N/A
paReadOnly 不能修改属性值 N/A

·456·
续表
标志 含义 相关方法
paRevertable
能回复对象编辑器上下文菜单继承的菜单项。菜单项告诉属性编辑器放
弃当前的属性值并返回以前设定的默认或标准值
N/A
paFullWidthName 值不需要显示。对象编辑器使用空白代替值 N/A
paVolatileSubProperties 在任何时候属性值变化时,对象编辑器都从子属性中重新取值 GetProperties
paReference
值是一个引用。当联合使用paSubProperties 时,引用的对象应作为这个
属性的子属性显示
GetComponentValue
5.注册属性编辑器
创建的属性编辑器必须在Delphi 中注册。注册属性编辑器就是将某种类型的属性与一个特定的属
性编辑器联系。可以注册一个编辑器与所有同一类型的属性相联系,也可以只是与组件的一个特定类
型的特定属性相联系。
注册属性编辑器是调用RegisterPropertyEditor 过程完成的。该过程接受4 个参数:
*要编辑的属性类型信息指针。这总是通过调用TypeInfo 函数得到,如TypeInfo(TMyComponent)。
*编辑器应用的组件类型,如果该参数为nil 则编辑器应用于所给类型的所有属性。
*属性名,该参数只有在前一参数指定了组件的特定类型时才可用。在这种情况下,可以指定在
这个编辑器应用的组件类型的特定属性名称。
*使用该属性编辑器的属性类型。
(1)属性类型
在IDE 中,对象编辑器能选择性地隐藏或显示基于类型的属性。新组件的属性能够通过在类型中
注册来使用这个特性, 只要在注册组件的同时调用RegisterPropertyInCategory 或
RegisterPropertiesInCategory 即可。使用RegisterPropertyInCategory 注册单个属性, 使用
RegisterPropertiesInCategory 一次注册多个组件。这些函数都在DesignIntf 单元定义。
注意:当一个组件的一些属性注册时,并不强制注册其他的属性。没有显示注册的属性都隐
含在TMiscellaneousCategory 类型中。默认类型的对象编辑器都可以隐藏或显示这些
属性。
(2)一次注册一个属性
使用RegisterPropertyInCategory 函数一次注册一个属性并将它与一个属性类型相联系。有4 种形
式的RegisterPropertyInCategory 方法。每种形式提供一个不同的识别属性的标准。
第1 种形式通过属性名称识别属性。下面这一行代码注册一个关于组件的可视显示的属性,并通
过它的名字“AutoSize”识别属性。
RegisterPropertyInCategory(’Visual’, ’AutoSize’);
第2 种形式与第1 种类似,区别在于它限制一个给定类型的组件的属性。下面是将一个TMyButton
类中名称为“HelpContext”的属性注册到“Help and Hints”类型的例子。
RegisterPropertyInCategory(’Help and Hints’, TMyButton, ’HelpContext’);
第3 种形式通过类型而不是名称识别属性。下面是一个通过类型(Integer)注册属性的例子。
RegisterPropertyInCategory(’Visual’, TypeInfo(Integer));
第4 种形式使用属性的类型和名称共同识别属性。下面是一个根据类型(TBitmap)和名称(Pattern)
注册属性的例子。
RegisterPropertyInCategory(’Visual’, TypeInfo(TBitmap), ’Pattern’);
(3)一次注册多个属性
使用RegisterPropertiesInCategory 函数可以一次注册多个属性并将它们与一个属性类型联系起来。
RegisterPropertiesInCategory 有3 种形式的声明,每个形式提供一个不同的识别属性的标准。
第16 章 开发新的VCL 组件
·457·
第1 种形式是基于属性名称或类型识别属性。列表传递一个常量数组。下面的例子中,所有名称
为“Text”或属于TEdit 类型的属性都注册到“Localizable”类型中。
RegisterPropertiesInCategory(’Localizable’, [’Text’, TEdit]);
第2 种形式允许限制注册的属性属于一个特定的组件。需要注册的属性列表只包含名称,没有类
型。例如下面的代码将所有组件的属性注册到“Help and Hints”类型中。
RegisterPropertiesInCategory(’Help and Hints’, TComponent, [’HelpContext’, ’Hint’, ’ParentShowHint’,
’ShowHint’]);
第3 种形式允许限制注册的属性为特定类型的属性。与第2 种形式一起使用,注册的属性列表可
以只包含名称。
RegisterPropertiesInCategory(’Localizable’, TypeInfo(String), [’Text’, ’Caption’]);
(4)指定属性类型
当在一个类型中注册属性时,可以使用任何字符串作为类型的名称。如果名称以前没有使用,对
象编辑器产生一个新的属性类型。然而也可以将属性注册到内建的类型中。Delphi 内建的属性类型如
表16-10 所示。
表16-10 Delphi 内建属性类型表
类型 目的
Action 关于运行时行为的属性,如TEdit 的Enabled 和Hint 属性
Database 关于数据库操作的属性,如TQuery 的DatabaseName 和SQL 属性
Drag, Drop, and Docking 关于拖放操作的属性,如TImage 的DragCursor 和DragKind 属性
Help and Hints 使用在线帮助的属性,如TMemo 的HelpContext 和Hint 属性
Layout 控件设计时的显示特性的属性,如TDBEdit 的Top 和Left 属性
Legacy 已经不用的操作的属性,如TComboBox 的Ctl3D 和ParentCtl3D 属性
Linkage 联系或连接其他组件的属性,如TDataSource 的DataSet 属性
Locale 关于本地化的属性,如TMainMenu 的BiDiMode 和ParentBiDiMode 属性
Localizable 应用程序的本地版本中可能需要修改的属性。许多字符属性(Caption)都在这类中,也包
括决定控件大小和位置的属性
Visual 控件运行时外观的属性,如TScrollBox 的Align 和Visible 属性
Input 数据输入的属性(不一定与数据库操作有关),如TEdit 的Enabled 和ReadOnly 属性
Miscellaneous 不能放在一个类型中或不需要分类的属性( 以及没有注册到一个类别的属性), 如
TSpeedButton 的AllowAllUp 和Name 属性
(5)使用IsPropertyInCategory 函数
应用程序可以查询已经注册的属性以决定是否给定的一个属性已经注册到一个特定的类别中。在
进行应用程序的本地化时特别有用。IsPropertyInCategory 定义了两种形式,允许两种判断是否一个属
性在一个类中的标准。
第1 种形式比较组件的类型和属性名称。下面的命令行中,如果属性属于TCustomEdit 的后代,
名称为“Text”并且分类在“Localizable”中,则函数IsPropertyInCategory 返回True。
IsItThere := IsPropertyInCategory(’Localizable’, TCustomEdit, ’Text’);
第2 种形式比较属性的名称和类名称。下面的命令行中,如果属性是属于TCustomEdit 的后代,
名称为“Text”并且分类在“Localizable”中,则函数IsPropertyInCategory 返回True。
IsItThere := IsPropertyInCategory(’Localizable’, ’TCustomEdit’, ’Text’);
16.8.7 加入组件编辑器
当组件在设计器中被双击,或者在组件上单击鼠标右键的上下文菜单中加入了命令时,组件编辑
器就会被弹出。它们也可以按定制的格式将组件拷贝到Windows 剪贴板中。
如果没有给组件设计组件编辑器,Delphi 使用默认的组件编辑器。默认的组件编辑器由

·458·
TDefaultEditor 实现。TDefaultEditor 并不在组件的上下文菜单中添加项目。当组件被双击时,
TDefaultEditor 查找组件的属性并产生它发现的第1 个事件处理器。
要添加项目到上下文菜单中、改变双击组件时的行为或加入新的剪贴板格式, 可以从
TComponentEditor 派生新类并进行注册。在重载方法中,可以使用TComponentEditor 的Component
属性访问被编辑的组件。
1.在上下文菜单中加入项目
当鼠标右键单击组件时,组件编辑器的GetVerbCount 和GetVerb 方法被调用以建立上下文菜单。
可以重载这个方法以在上下文菜单中加入命令。
(1)指定菜单项
重载GetVerbCount 方法可以返回加入到上下文菜单中的命令的数目。重载GetVerb 方法可以返回
这些命令的显示字符串。当重载GetVerb 时,在字符串中加入“&”可以使紧接的字符显示下划线并
作为选择菜单项的快捷键。如果一个命令会产生对话框,必须确保它后面以省略号“...”结束。GetVerb
只有一个参数指明命令的索引。
下面的代码重载了GetVerbCount 和GetVerb 方法,并在上下文菜单中加入了两条命令。
function TMyEditor.GetVerbCount: Integer;
begin
Result := 2;
end;
function TMyEditor.GetVerb(Index: Integer): String;
begin
case Index of
0: Result := ’&DoThis ...’;
1: Result := ’Do&That’;
end;
end;
注意:必须保证GetVerb 方法返回GetVerbCount 指明的每一个可能的索引
(2)实现命令
当在设计器中GetVerb 提供的命令被选择时,就会调用ExecuteVerb 方法。在GetVerb 方法中提供
的每一个命令,都必须在ExecuteVerb 中有一个实现。可以使用编辑器的Component 属性访问被编辑
的组件。
例如下面的ExecuteVerb 方法实现了上述例子中的GetVerb 方法:
procedure TMyEditor.ExecuteVerb(Index: Integer);
var
MySpecialDialog: TMyDialog;
begin
case Index of
0: begin
MyDialog := TMySpecialDialog.Create(Application); {初始化对话框}
if MySpecialDialog.Execute then; {如果在对话框中单击OK}
MyComponent.FThisProperty := MySpecialDialog.ReturnValue; {使用值}
MySpecialDialog.Free; {销毁编辑器}
end;
1: That; {调用That 方法}
第16 章 开发新的VCL 组件
·459·
end;
end;
2.改变双击行为
当组件被双击时,组件编辑器的Edit 方法被调用。默认时,Edit 执行上下文菜单的第1 条命令。
因此在上述的例子中,双击组件将执行DoThis 命令。
执行第1 条命令通常是比较好的,但是可能需要改变这个行为。下面就是几种需要改变默认行为
的情况。
*想提供其他的行为。
*没有在上下文菜单中加入命令。
*双击组件时想显示联合几个命令的对话框。
重载Edit 方法可以指定当组件双击时的新行为。例如当双击组件时,下面的Edit 方法产生一个字
体对话框。
procedure TMyEditor.Edit;
var
FontDlg: TFontDialog;
begin
FontDlg := TFontDialog.Create(Application);
try
if FontDlg.Execute then
MyComponent.FFont.Assign(FontDlg.Font);
finally
FontDlg.Free
end;
end;
如果需要在双击组件时显示一个事件处理代码的编辑器, 要使用TDefaultEditor 而不是
TComponentEditor 作为组件编辑器的基类。然后不是重载Edit 方法, 而是重载protected 的
TDefaultEditor.EditProperty 方法。EditProperty 扫描组件所有的事件处理代码,并执行它发现的第1 段
代码。可以改变它而查找一个特定的事件。例如:
procedure TMyEditor.EditProperty(PropertyEditor: TPropertyEditor;
Continue, FreeEditor: Boolean)
begin
if (PropertyEditor.ClassName = ’TMethodProperty’) and
(PropertyEditor.GetName = ’OnSpecialEvent’) then
// DefaultEditor.EditProperty(PropertyEditor, Continue, FreeEditor);
end;
3.添加剪贴板格式
组件在IDE 中被选择并且应用程序员选择Copy 时,组件默认为被拷贝为Delphi 的内部格式。它
可以粘贴到其他表单或数据模块中。通过重载Copy 方法也可以将它以其他的格式拷贝到剪贴板中。
例如下面的Copy 方法允许一个TImage 组件拷贝它的图形到剪贴板中。Delphi IDE 会忽略这个图
形,但是可以粘贴到其他应用程序。代码如下:
procedure TMyComponent.Copy;
var
MyFormat : Word;
AData,APalette : THandle;

·460·
begin
TImage(Component).Picture.Bitmap.SaveToClipBoardFormat(MyFormat, AData, APalette);
ClipBoard.SetAsHandle(MyFormat, AData);
end;
4.注册组件编辑器
定义了组件编辑器之后,可以注册它和一个特定的组件一起工作。当该组件在表单设计器中被选
中时,就会创建一个注册的组件编辑器。
为了建立组件编辑器和组件类之间的联系, 可以调用RegisterComponentEditor 。
RegisterComponentEditor 使用该编辑器组件类的名称和定义组件编辑器类的名称作为参数。例如下面
的语句注册了一个TMyEditor 的组件编辑器类与所有类型为TMyComponent 的组件一起工作:
RegisterComponentEditor(TMyComponent, TMyEditor);
注册组件时,应该在Register 中调用RegisterComponentEditor。例如新组件名称为TMyComponent,
它的组件编辑器为TMyEditor,并且在同一个单元中实现,则下面的代码注册了组件和组件编辑器。
procedure Register;
begin
RegisterComponents(’Miscellaneous’, [TMyComponent);
RegisterComponentEditor(classes[0], TMyEditor);
end;
16.9 修改现存的组件:制作非自动换行的Memo组件
开发新组件的一个简单的途径就是从一个几乎完成了全部功能只是需要少量修改的组件中派生。
下面将介绍创建一个修改普通的Memo 组件宾使它默认不自动换行的新组件的例子。
标准Memo 组件的WordWrap 属性默认为True。如果频繁使用不需要自动换行的Memo,就可以
创建一个默认的不自动换行的Memo 组件。
16.9.1 创建并注册组件
创建组件的步骤是首先创建一个单元,然后派生一个组件类,最后注册并安装到组件面板。
本例按照下面的步骤创建一个组件。
*创建组件单元Memos。
*从TMemo 派生一个新组件类型,叫作TWrapMemo。
*在组件面板的Samples 页中注册TWrapMemo。
最后的结果如下:
unit Memos;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, StdCtrls;
type
TWrapMemo = class(TMemo)
end;
procedure Register;
implementation
procedure Register;
begin
第16 章 开发新的VCL 组件
·461·
RegisterComponents(’Samples’, [TWrapMemo]);
end;
end.
如果现在编译和安装该组件,它和它的父组件TMemo 是一样的。
16.9.2 修改组件类
一旦创建一个新的组件类,可以使用任何办法修改它。在本例中,只需要修改Memo 组件的一个
属性的初始值。这只需要对组件类进行两个小的改变。
*重载构造方法。
*指定新的默认属性值。
构造方法实际上需要设置属性值。默认值告诉Delphi 什么值存储在表单文件中。Delphi 只保存与
默认值不同的值,因此上面两步都是非常重要的。
1.重载构造方法
当设计时将组件放在表单中,或运行时应用程序创建组件时,组件的构造方法都会设置属性值。
当组件从表单文件中装载时,应用程序将设置在设计时改变的所有属性。
注意:当重载构造方法时,新的构造方法必须在所有代码之前调用继承的构造方法。
例如新的组件需要重载从TMemo 继承的构造方法以设置WordWrap 属性值为False。为了达到这
个目的,首先在构造方法中加入override 声明,然后在单元的实现部分编写新的构造方法。
type
TWrapMemo = class(TMemo)
public {构造方法必须是public }
constructor Create(AOwner: TComponent); override; {这个声明都是一样的}
end;
...
constructor TWrapMemo.Create(AOwner: TComponent); {这个在单元的implementation 部分}
begin
inherited Create(AOwner); {必须在第1 条语句!}
WordWrap := False; {设置新的值}
end;
这时可以在组件面板上安装组件,并将它加到一个表单中。WordWrap 属性已经初始化为False。
如果改变一个属性的初始值,应该将这个值指定为默认值。如果不在构造方法中设置默认值,
Delphi 将不能存储和恢复属性值。
2.指定新的属性默认值
当Delphi 在一个表单文件中存储一个表单的描述时,它只存储与默认值不同的属性值,这可以使
表单文件较小并且装载速度快。如果创建一个属性或改变了默认值,则最好更新属性声明以包含新的
默认值。
改变属性的默认值,需要重新声明属性并加上default 指令以及新的默认值。不需要重新完整地声
明属性,只需要名称和默认值。
在本例中,可以重新在对象声明的published 部分中声明WordWrap 属性默认值为False:
type
TWrapMemo = class(TMemo)
...
published

·462·
property WordWrap default False;
end;
指定默认值并不影响组件工作。不必在组件的构造方法中初始化值。重新声明默认值保证Delphi
知道什么时候向表单文件写入WordWrap 值。
16.10 开发图形组件:制作Shape图形组件
图形组件是一类比较简单的组件。因为纯图形控件不需要获得焦点,它没有也不需要窗口句柄。
可以使用鼠标操作控制图形控件,但没有键盘界面。
本例中的图形组件是TShape。Shape 组件位于组件面板的Additional 标签页。创建的图形组件与
标准的Shape 组件相同,为了避免名称重复,所以称它为TSampleShape 组件。TSampleShape 组件根
据设定可以在窗口中显示圆形、椭圆、正方形、矩形等,但不能接收键盘的输入。
16.10.1 创建和注册组件
本例中按如下步骤创建和注册组件。
*创建名为Shapes 的组件单元。
*从TGraphicControl 派生新组件,命名为TSampleShape。
*在组件面板Samples 页中注册TSampleShape。
通过以上步骤可以产生代码如下:
unit Shapes;
interface
uses SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls, Forms;
type
TSampleShape = class(TGraphicControl)
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponent(’Samples’, [TSampleShape]);
end;
end.
16.10.2 发布继承的属性
派生了组件之后,就需要决定在父类的protected 部分声明的哪些属性和事件在新组件中能被用户
访问。TGraphicControl 已经发布了所有作为图形控件的属性,因此只需发布响应鼠标和拖放事件的属
性和事件。
发布继承属性和事件都需要在类声明的published 部分重新声明。在Shape 控件中,需要发布3 个
鼠标事件、3 个拖放事件和两个拖放属性。
type
TSampleShape = class(TGraphicControl)
published
property DragCursor; {拖放属性}
property DragMode;
property OnDragDrop; {拖放事件}
property OnDragOver;
第16 章 开发新的VCL 组件
·463·
property OnEndDrag;
property OnMouseDown; {鼠标事件}
property OnMouseMove;
property OnMouseUp;
end;
SampleShape 现在已经提供了与用户交互需要的鼠标和拖放的能力。

posted @ 2013-08-26 14:36  Wishmeluck  阅读(185)  评论(0编辑  收藏  举报