第十九章 Delphi自定义部件开发(三)
3. 创建新的消息处理方法
因为Delphi只为大多数普通Windows消息提供了处理方法,所以当你定义自己的消息时,就要创建新的消息处理方法。
用户自定义消息的过程包括两个方面:
定义自己的消息
声明新的消息处理方法
⑴ 定义自己的消息
许多标准部件为了内部使用定义了消息。定义消息的最一般的动因是广播信息和状态改变的通知。
定义消息过程分两步:
声明消息标识符
声明消息记录类型
① 声明消息标识
消息标识是整型大小的常量。Windows保存了小于1024的消息用于自己使用,因此当声明自己的消息时,你应当大于1024。
常量WM_USER代表用于自定义消息的开始数字。当定义消息标准时,你应当基于WM_USER。
某些标准Windows控制使用用户自定义范围的消息,包括ListBox、ComboBox、EditBox和Button。如果从上述部件中继承了一个部件,在定义新的消息时,应当检查一下Message单元是否有消息用于该控制。
定义消息的方法如下:
Const
WM_MYFIRSTMESSAGE=WM_USER+0;
WM_MYSECONDMESSAGE=WM_USER+1;
② 声明消息记录类型
如果你想给予自定义消息的参数有含义的名字,就要为该消息声明消息记录类型。消息记录是传给消息处理方法的参数的类型。如果不使用消息参数或者想使用旧风格参数,可以使用缺省的消息记录。
声明消息记录类型要遵循下列规则
以消息名命名消息记录类型,以T打头
将记录中第一个域命名为Msg,类型为TMsgPraram
将接着的两个字节定义为word 以响应word大小的参数
将接着的四个字节与long参数匹配
将最后的域命名为Result,类型为Longint
下面是TWMMouse的定义
type
TWMMouse=record
Msg: TMsgParam; { 第一个是消息ID }
Keys: Word; { wParam }
case Integer of { 定义lParam的两种方式 }
o: (
Xpos: Integer; { 或者以x,y座标 }
Ypos: Integer);
1: (
Pos : TPoint; { 或者作为单个点 }
Result: Longint; ) { 最后是Result域 }
end;
TWMMouse使用变长记录定义了相同参数的不同名字集。
⑵ 声明新的消息处理方法
有两类环境需要你定义新的消息处理方法:
自定义新部件需要处理没有被标准部件处理的Windows消息
已定义了自定义部件使用的新消息
声明消息处理方法的办法如下:
在部件声明中的protected部分声明方法
将方法做成过程
以要处理的消息名命名方法 但不带下划线
传递一个命名为Message的var参数,类型为消息记录类型
编写用于该部件的特别处理代码
调用继承的消息方法
下面是用于用户自定义消息CM_CHANGECOLOR的消息处理代码:
type
TMyComponent=class(TControl)
…
protected
procedure CMChangeColor(var Message:TMessage);
message CM_CHANGECOLOR;
end:
procedure TMyComponent.CMChangeColor(var Message: TMessage);
begin
color := Message lParam;
inherited;
end;
19.2.2.4 注册部件
编写部件及其属性、方法和事件只是部件创建过程的一部分。尽管部件具有这些特征就可用,但部件真正功能强大的是在设计时操作它们的能力。
使部件在设计时可用需要经过如下几步:
用Delphi注册部件
增加选择板位图
提供有关属性和事件的帮助
存贮和读取属性
1. 用Delphi注册部件
为了让Delphi识别自定义部件,并将它们放置于Component Palette上,你必须注册每一个部件。
注册一个部件要在部件所在单元里加入Register方法,这包括两个方面的内容:
声明注册过程
实现注册过程
一旦安装了注册过程,就可以将部件安装在选择板上。
注册过程要在部件所在单元中写一个过程,该过程必须以Register命名。Register必须出现在库单元的interface部分,这样Delphi就能定位它。在Register过程中,可以为每个部件调用过程RegisterComponents。
下面的代码演示了建立和注册部件的概略方法:
unit MyBtns;
interface
type
… { 声明自定义部件 }
procedure Register;
Implementation
procedure Register;
begin
… { 注册部件 }
end;
end.
在Register过程中,必须注册每一个要加入Component Palette的部件,如果库单元包含若干部件,就要将它们一次性注册。
注册一个部件时,为部件调用RegisterComponents过程。RegisterComponents告诉Delphi两件有关所注册的部件的事::
要注册部件所在的Component Palette的页名
要安装的部件的名字
选择板的页名是个字符串。如果你所给名字的页不存在,Delphi就用该名字创建新的页。
下面的Register过程注册了一个名为TMyComponent的部件,并将其放在名为“Miscellaneous”的Component Palette页上。
procedure Register;
begin
RegisterComponents('Miscellaneous', [TFirst, TSecond]);
end;
也可以在相同的页上,或者在不同的页上,一次注册多个部件:
procedure Register;
begin
RegisterComponents('Miscellaneous', [TFirst, TSecond]);
RegisterComponents('Assorted', [TThird]);
end;
2. 增加Component Palette上的位图
每个部件都需要一个位图来在Component Palette上代表它。如果安装时没有描述自己的位图,则Delphi会自动套用缺省位图。
因为选择板位图只有在设计时需要,所以没有必要将它们编译进库单元。而是将它们提供在与库单名相同的Windows资源文件中,扩展名为.DCR。用Delphi的位图编辑器来生成资源文件,每个位图边长24个象素。
为每个要安装的库单元提供一个选择板位图文件,在每个文件中为每个要注册的部件提供一个位图。位图图象名与部件名相同,将文件放在与库单元相同的目录中,这样在安装部件时Dephi就能发现位图。
例如,如果你在ToolBox单元中创建一个名为TMyControl的部件,就需要建立名为TOOLBOX.DCR的资源文件,文件中包含名为TMyControl的位图。
3. 提供有关属性和事件的帮助
当在窗体中选择一个部件或在Object Inspector中选择事件或属性时,能够按F1得到有关这一项的帮助。如果创建了相应的Help文件的话,自定义部件的用户能得到有关你的部件的相应的文档。
因为Delph使用了特殊的Help引擎支持跨多个Help文件处理主题搜索,所以你能提供关于自定义部件的小的Help文件,用户不需要额外的步骤就能找到你的文档。你的Help成了Delphi Help系统的一部分。
要给用户提供帮助,要理解下列两方面:
Delphi怎样处理HELP请求
将HELP插入Delphi
⑴ Delphi怎样处理HELP请求
Delphi基于关键词查询HELP请求。就是说,当用户在窗体设计窗口的已选部件上按F1键时,Delpdi将部件的名字转换成一个关键词,然后调用Windows Help引擎查找那个关键词的帮助主题。关键词是Windows Help系统的标准部分。实际上 ,WinHelp使用Help中的关键词产生Search对话框中的列表。因为用于上下文敏感搜索中的关键词不是实际供用户读的,所以要输入关键词的替代词。
例如,一个查找名为TSomething的部件的详细信息的用户可能打开WinHelp的Search对话框并输入TSomething。但不会使用用于窗体设计窗口的上下文查找的替代形式class-TSomething。因此,这个特殊的关键词Class-TSomething对用户是不可见的,以免弄乱了搜索列表。
⑵ 将Help插入Delphi
Delphi提供了创建和插入Windows Help文件的工具,包括Windows Help编译器HC.EXE。为自定义部件建立Help文件的机制与建立任何Help文件没什么不同,但需要遵循一些约定以与库中其它Help兼容。
保持兼容性的方法如下:
建立Help文件
增加特殊的注脚
建立关键词文件
插入Help索引
当你为自定义部件建立完Help,有下列几个文件:
编译过的Help(.HLP)文件
Help关键词(.KWF)文件
一个或多个Help源文件(.RTF)
Help工程文件(.HLJ)
编译过的Help文件和关键词文件应当与库单元在同一目录。
① 建立Help文件
你可以使用任何的工具创建Windows Help文件。Delphi的多文件搜索引擎,可以包含任何数目的Help文件的要素。在编译的Help文件之外,你应当拥有RTF源文件,这样才能生成关键词文件。
为使自定义部件的Help同库中其它部件一起工作,要遵循下列约定:
每个部件有占一页的帮助
部件帮助页应当给出部件目的的简单描述,然后列出最终用户可用的属性、事件和方法的描述。应用开发者通过在窗体上选择部件并按F1访问这一页。
部件帮助页应当有一个用于关键词搜索的“K”脚注,脚注中包含部件名。例如,TMemo的关键词脚注读作"TMemo Component"
部件增加和修改的每一个属性,事件和方法应当有一页帮助
属性、事件或方法的帮助页应当指出该项用于哪个部件,显示声明语法和描述它的使用方法。
属性、事件或方法的帮助页应当有一个用于关键词搜索的“K”脚注,该脚注中包含该项的名字和种类。例如,属性Top的关键词脚注为“Top property”。
Help文件的每一页也需要用于多文件索引搜索的特殊脚注。
② 增加特殊脚注
Delphi需要特殊的搜索关键词以区别用于部件的帮助页和其它项目。你应当为每一项提供标准的关键词搜索项。但你也需要用于Delphi的特殊脚注。
要为来自Object Inspector窗口或代码编辑器F1的搜索增加关键词,就得为Help文件帮助页增加"B"脚注。
“B”脚注与用于标准WinHelp关键词搜索的“K”脚注很相象,但它们只用于Delphi搜索引擎。下表列出怎样为每种部件帮助页建立“B”脚注:
表19.7 部件帮助页搜索注脚
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
帮助页类型 "B"脚注内容 示 例
──────────────────────────────────
主部件页 'class_'+部件类型名 class_TMemd
一般属性或事件页 'prop_'+属性名 prop_WordWrap
'event_'+事件名 event_OnChange
部件特有的属性 'prop_'+部件类型名 prop_TMemoWordWrap
或事件页 +属性名
'event_'+部件类型名 event_TMemoOnChange
+事件名
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
区别一般帮助页和部件特有的帮助页是很重要的。一般帮助页应用于所有部件上的特定属性和事件。例如Left属性是所有部件中的标识。因此,它用字符串Prop-Left进行搜索。而Borde-style依赖于所属的部件,因此,BorderStyle属性拥有自己的帮助页。例如,TEdit有BorderStyle属性的帮助页,搜索字符串为Prop_TEditBorderStyle。
③ 建立关键词文件
建立和编译了Help文件,并且增加了脚注之后,还要生成独立的关键词文件,这样Delphi才能将它们插入主题搜索的索引。
从Help资源文件RTF创建关键词文件的方法如下:
在DOS提示行下,进入包含RTF文件的目录
运行关键词文件产生程序——KWGEN.EXE,后跟Help工程文件,如KWGEN SPECIAL.HPJ。当KWGEN运行完毕后,就有了与Help工程文件相同的关键词文件,但以.KWF为扩展名
将关键词文件放在编译完的库单元和Help文件相同的目录
当你在Component Palette上安装部件时,希望关键词插入Delphi Help系统的搜索索引。
④ 插入Help索引
以自定义部件建立关键词文件后,要将关键词插入Delphi的Help索引。
将关键词文件插入Detphi Help索引的方法如下:
将关键词文件放在与编译完的库单元和Heph文件相同的目录中
运行HELPINST程序
HELPINST运行完后,Delphi的Help索引文件(.HDX)包含自定义部件帮助页的关键词。
⑶ 存储和装入属性
Delphi将窗体及其拥有的部件存储在窗体文件(.DFM)中,DFM文件用二进制表示窗体的属性和它的部件。当Delphi用户将自定义部件加入窗体中时,自定义部件应当具有存储它们的属性的能力。同样,当被调入Delphi或应用程序时,部件必须能从DFM文件中恢复它们。
在大多数时候,不需要做任何使部件读写DFM文件的事。存储和装入都是继承的祖先部件的行为的一部分。然而在某些情况下,你可能想改变部件存储和装入时初始化的方法。因此,应当理解下述的机制:
存储和装入机制
描述缺省值
决定存储什么
装入后的初始化
① 存储和装入机制
当应用开发者设计窗体时,Delphi将窗体的描述存储在DFM文件中。当用户运行程序时,它读取这些描述。
窗体的描述包含了一系列的窗体属性和窗体中部件的相似描述。每一个部件,包括窗体本身,负责存储和装入自身的描述。
在缺省情况下,当存储时,部件将所有public和published属性的不同于缺省值的值以声明的顺序写入。当装入时,部件首先构造自己,并将所有属性设为缺省值;然后,读存储的、非缺省的属性值。
这种缺省机制,满足了大多数部件的需要,而又不需部件编写者的任何工作。然而自己定义存储和装入过程以适合自定义部件需要的方法也有几种。
② 描述缺省值。
Delphi部件只存储那些属性值不同于缺省值的属性。如果你不描述,Delphi假设属性没有缺省值,这意味着部件总是存储属性。
一个属性的值没被构造函数设置,则被假设为零值。为了描述一个缺省值,在属性声明后面加default指令和新的缺省值。
你也能在重声明属性时描述缺省值。实际上,重声明属性的一个原因是指定不同的缺省值。只描述缺省值,那么在对象创建时并不会自动地给属性赋值,还需要在部件的Create方法中赋所需的值。
下面的代码用Align属性演示了描述缺省值的过程.
type
TStatusBar=class(TPanel)
public
constructor Create(Aowner: TComponent); override; { 覆盖以设置新值 }
published
property Align default alBottom; { 重新声明缺省值 }
end;
constructor TStatusBar.Create(Aowner: TComponent);
begin
inherited Create(Aowner); { 执行继承的初始化过程 }
Align := alBottom; { 为Align赋新的缺省值 }
end;
③ 决定存储什么
用户也可以控制Delphi是否存储部件的每一个属性。缺省情况下,在对象的published部分声明的所有属性都被存储。然而,可以选择不存储所给的属性,或者设计一个函数在运行时决定是否存储属性。
控制Delphi是否存储属性的方法是在属性声明后面加stored指令,后跟True或False,或者是布尔方法名。你可以给任何属性的声明或重声明加stored表达式。下面的代码显示了部件声明三种新属性。一个属性是总是要存储,一个是不存,第三个则决定于布尔方法的值:
type
TSampleCompiment = class(TComponent)
protected
function storeIt: Boolean;
public { 正常情况下在不存 }
property Important: Integer stored True; { 总是存储 }
published { 正常情况下保存 }
property UnImportant: Integer stored False; { 不存 }
property Sometimes: Integer stored StoreIt; { 存储依赖于函数值 }
end;
④ 载入后的初始化
在部件从存储的描述中读取所有的属性后,它调用名为Loaded的虚方法,这提供了按需要执行任何初始化的机会。调用Loaded是在窗体和它的控制显示之前,因此,不需要担心初始化会带来屏幕闪烁。
在部件载入属性时初始化它,要覆盖Loaded方法。
在Loaded方法中,要做的第一件事是调用继承的Loaded方法。这使得在你的部件执行初始化之前,任何继承的属性都已初始化。
下面的代码来自于TDatabase部件。在装入后,TDatabase试图重建在它存储时已打开的连接,并描述在连接发生异常时如何处理。
procedure TDatabase.Loaded
begin
inherited Loaded; { 总是先调用继承的方法 }
Modified; { 设置内部标志 }
try
if FStreamedConnected then Open; { 重建联接 }
except
if csDesigning in ComponentState then { 在设计时 }
Application.HandleException(self) { 让Delphi处理异常 }
else raise; { 否 则 }
end;
end;
19.3 Delphi部件编程实例
19.3.1 创建数据库相关的日历控制-TDBCalendar
当处理数据库联接时,将控制和数据直接相联是很重要的。就是说,应用程序可以建立控制与数据库之间的链。Delphi包括了数据相关的标签、编辑框、列表框和栅格。用户可以使自己的控制与数据相关。
数据相关有若干等级。最简单的是只读数据相关或数据浏览,以及反映数据库当前状态的能力。比较复杂的是数据相关的编辑,也即用户可以在控制上操作数据库中的数据。
在本部分中将示例最简单的情况,即创建联接数据库的单个字段的只读控制。本例中将使用Component Palette的Samples页中的TCalendar部件。
创建数据相关的日历控制包括下列几步:
创建和注册部件
使控制只读
增加数据联接(Data Link)
响应数据改变
19.3.1. 1创建和注册部件
每个部件的创建都从相同的方式开始,在本例中将遵循下列过程:
将部件库单元命名为DBCal
从TCalendar继承一个新部件,名为TDBCalendar
在Component Palette的Samples页中注册TDBCalendar
下面就是创建的代码:
unit DBCal;
interface
uses SysUtils, WinTypes, WinProc, Messages, Classes, Graphics, Controls,
Forms, Grids, Calendar;
type
TDBCalendar=class(TCalendar)
end;
procedure Register;
implementation
procedure Register;
begin
RegisterComponents(Samples,[TDBabendar]);
end;
end.
19.3.1.2 使控制只读
因为这个数据日历以只读方式响应数据,所以用户不能在控制中改变数据并指望它们反映到数据库中。
使日历只读包含下列两步:
增加只读属性
允许所需的更新
1. 增加只读属性
给日历控制增加只读选项是直接过程。通过增加属性,可以提供在设计时使控制只读的方法,当属性值被设为True,将使控制中所有元素不可被选。
⑴ 增加属性声明和保存值的private域:
type
TDBCalendar=class(TClendar)
private
FReadOnly: Boolean;
public
constructor Create (Aowner: TComponent); override;
published
property ReadOnly: Boolean read FReadOnly write FReadOnly default True;
end;
constructor TDBCalendar.Create(Aowner: TComponent);
begin
inherited Create(AOwner);
FReadOnly := True;
end;
⑵ 覆盖SelectCell方法,使得当控制是只读时,不允许选择:
function TDBCalendar.SelectCell(ACol, Arow: Longint): Boolean;
begin
if FReadOnly then
Result := False
else
Result := inherited SelectCell(Acol,ARow);
end;
还要在TDBcalendar的声明中声明SelectCell。
如果现在将Calendar加入窗体,会发现部件完全忽略鼠标和击键事件,而且当改变日期时,也不能改变选择的位置。下面将使控制响应更新。
2. 允许所需的更新
只读日历使用SelectCell方法实现各种改变,包括设置Row和Col的值。当日期改变时,UpdateCalendar方法设置Row和Col的值,但因为SelectCell不允许你改变,即使日期改变了,选择仍留在原处。
可以给日历增加一个Boolean标志,当标志为True时允许改变:
type
TDBCalendar=class(TCalendar)
private
Fupdating: Boolean;
protected
function SelectCell(Acol, Arow: Longint); Boolean; override;
public
procedure UpdateCalendar; override;
end;
function TDBCalendar.SelectCell(ACol, ARow: Longint): Boolean;
begin
if (not FUpdating) and FReadOnly then
Result := False { 如果更新则允许选择 }
else
Result := inherited SelectCell(ACol, ARow); { 否则调用继承的方法 }
end;
procedure UpdateCalendar;
begin
FUpdating := True; { 将标志设为允许更新 }
try
inherited UpdateCalendar; { 象通常一样更新 }
finally
FUpdating := False; { 总是清除标志 }
end;
end;
现在日历仍旧不允许用户修改,但当改变日期属性时能正确反映改变;目前已有了一个真正只读控制,下一步是增加数据浏览能力。
3. 增加数据联接
控制和数据库的联接是由一个名为DataLink的对象处理。Delphi提供了几种类型的Datalink。将控制与数据库单个域相联的DataLink对象是TFieldDatalink。Delphi也提供了与整个表相联的DataLink。
一个数据相关控制拥有DataLink对象,就是说,控制负责创建和析构DataLink。
要建立作为拥有对象的Datalink,要执行下列三步:
声明对象域
声明访问属性
初始化DataLink
⑴ 声明对象域
每个部件要为其拥有对象声明一个对象域。因此,日历对象DataLink 声明TFieldDataLink类型的域。
日历部件中DataLink的声明如下:
type
TDBCalendar = class(TSampleCalendar)
private
FDataLink: TFieldDataLink;
…
end;
⑵ 声明访问属性
每一个数据相关控制有一个DataSource属性,该属性描述应用程序给控制提供数据的数据源。而且,访问单个域的数据库还需要一个DataField 属性描述数据源中的域。
下面是DataSource和DataField的声明和它们的实现方法:
type
TDBCalendar = class(TSampleCalendar)
private { 属性的实现方法是 }
function GetDataField: string; { 返回数据库字段的名字 }
function GetDataSource: TDataSource; { 返回数据源(Data source)的引用 }
procedure SetDataField(const Value: string); { 给数据库字段名赋值 }
procedure SetDataSource(Value: TDataSource); { 给数据源赋值 }
published { 使属性在设计时可用 }
property DataField: string read GetDataField write SetDataField;
property DataSource: TDataSource read GetDataSource write SetDataSource;
end;
……
function TDBCalendar.GetDataField: string;
begin
Result := FDataLink.FieldName;
end;
function TDBCalendar.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TDBCalendar.SetDataField(const Value: string);
begin
FDataLink.FieldName := Value;
end;
procedure TDBCalendar.SetDataSource(Value: TDataSource);
begin
FDataLink.DataSource := Value;
end;
现在,就建立了日历和DataLink的链,此外还有一个更重要的步骤。你必须在日历构建时创建DataLink对象,在日历析构时,撤消DataLink对象。
⑶ 初始化DataLink
在数据相关控制在其存在的期间要不停地访问DataLink对象,因此,必须在其构建函数中创建DataLink创建并且在析构时,撤消DataLink对象,因此要覆盖日历的Create和Destroy方法。
type
TDBCalendar=class(TCalendar)
public
constructor Create(Aowna: TComponent); override;
destructor Destroy; override;
end;
constructor TDBCalendar Create (Aowner: TComponent);
begin
inherited Create(AOwner);
FReadOnly := True;
FDataLink := TFieldDataLink.Create;
end;
destructor TDBCalendar Destroy;
begin
FDataLink.Free;
inherited Destroy;
end;
现在,部件已拥有完整的DataLink,但部件还不知从相联的域中读取什么数据。
19.3.1.4 响应数据变化
一旦控制拥有了数据联接(DataLink)和描述数据源和数据域的属性。就需在数据记录改变时响应域中数据的变化。
DataLink对象都有个名为OnDataChange的事件。当数据源指示数据发生变化时,DataLink对象调用任何OnDataChange所联接的事件处理过程。
要在数据改变时更新数据,就需要给DataLink对象的OnDataChange事件增加事件处理过程。
下面声明了DataChange方法,并将其赋给DataLink对象的OnDataChange事件:
type
TDBCalendar=class(TCalendar)
private
procedure Datachange(Sender: TObject);
end;
constructor TDBCalendar Create(AOwner:TComponent);
begin
inherited Create(AOwner);
FReadOnly := True;
FDataLink := TFieldDataLink.Create;
FDataLink.OnDataChange := DataChange;
end;
destructor TDBcalendar.Destroy;
begin
FDataLink.OnDataChange := nil;
FDataLink.Free;
inherited Destroy
end;
procedure TDBCalendar.DataChange(Sender: TObject);
begin
if FDataLink.Filed=nil then
CalendarDate := 0;
else
CalendarDate := FDataLink.Field.AsDate;
end;
;