DELPHI基础教程
第十九章 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; ; |