第一部分 简介
1.1 此份文件的目的为何?
这份文件的目的是为了解答有关撰写 Delphi元件时常见或文件上找不到的问题。我曾经花了一段很长的时间来了解探索 TDataLink 类别,这让我觉得应该将撰写元件时常遇到的问题及经验心得写下来,分享给大家。不过我并不能保证写在这份文件里头的解答完全正确。如果你对其中的任何问题有更好的解决方法,或认为有什麽资讯适合放在这份文件里的话,请告知作者。有任何错误或缺漏也欢迎指正。
除了再加上更多的问题及解答外,我试著再补充两个部分:
进阶程式设计师喜爱的工具:这也许跟元件设计没有直接的关系但至少它们跟 Delphi有关系。
值得参考的文件刊物:由於空间的关系,这份文件不能放置太多的范例程式,因此参考其它文件是十分需要的。这不是一份教材式的文件,我不会做太多条理式的说明,但会试著将最具有参考价值的文献列出。
如果你有任何意见或建"",欢迎来信告诉我。
--------------------------------------------------------------------------------
第二部份 ""合环境
2.1 在""合环境中如何找出元件所产生的问题?
我发现唯一能找出问题的方法只有:
在 Delphi ""合环境的 Tools|Options 对话框的 Library 页中将『Compile with debug info』选项打勾。
选 Component|Rebuild Library 重新编译元件库。
从 Turbo Debugger 中执行 Delphi。
选File|Change Dir移至包含元件程式码的目录下。
如果你的元件发生GPF时就可以检视堆叠然後得知到底是哪些发生问题了。
2.2 如何检视 Delphi 所产生的组合语言码?
Glen Boyd 的回答:
开启登录编辑程式(REGEDIT.EXE),接著到『HKEY_CURRENT_USER\Software\Borland\Delphi\2.0\Debugging』下新增一个字串机码『EnableCPU』,将它的字串值设为『1』。此後Delphi""合环境的View选单下就会多一个『CPU』选项,它会开启一个视窗来检视目前程式指令的记忆体及组合语言。你可以在侦错时利用单步追踪或其它方法来观察它。
2.3 我可以在执行时期动态建立元件,但在设计时期就会发生错误。为什麽?
你的元件必须继承自TComponent类别或其衍生类别。
你的元件建构函式及灭构函式宣告必须看起来像这样:
constructor Create(AOwner: TComponent); override;
destructor Destroy; override ;
所有在published区段宣告的栏位型态必须是ordinal、single、double、extended 、comp、currency、string、small set(译注:指元素编号不超过0..31这个范围的集合;平常的集合可容许的范围为0..255)、method pointer或class其中一种。如果你宣告了其它型态的栏位,Delphi编译器并不会检查出错误。然而当你使用这个元件时依然会得到一个GPF。
如果你想让TMyComponent元件可以在设计时期操作,注意下面的宣告会引发十分严重的问题:
type TComplex = record
RealPart: Double;
ComplexPart: Double;
end;
class TMyComponent = Class(TComponent)
private
F1: TComplex;
published
property P1: TComplex read F1 write F1;
end;
2.4 如何撰写一个无法放置到表格上的元件?
Ray Lischner 的回答:
如果你不想让使用者将元件拉曳至表格上的话,使用 RegisterNoIcon 及 RegisterClass 程序来注册元件。
2.5 在程式码编辑器中快速切换程式区段最简单的方法是什麽?
Ray Konopka 的回答:
在探索 VCL 原始程式码时,强烈建""你最好熟悉程式码编辑器里的书签功能。使用方法很简单:Ctrl-Shift-N,N 是从 0 至 9 的数字,用来设定一个书签。此後就可以使用 Ctrl-N 来跳跃至书签处。(译注:使用这项功能真的可以节省你许多来回卷动程式及找寻函式的时间,别迟疑了,快学吧!)
2.6 如何使我的元件在按下滑鼠右键时出现快速功能选单?
你必须要建立一个元件编辑器。元件编辑器决定了元件在设计时期时对滑鼠键的反应及动作,你可以为元件定义它自己的快速功能选单。
建立元件编辑器的步骤大致如下:
从 TComponentEditor 类别继承一个新的类别。
改写类别的 GetVerbCount、GetVerb及 ExecuteVerb方法。
在 Register 程序中使用 RegisterComponentEditor 程序来注册此元件编辑器。
有关元件编辑器这个主题在『Developing Delphi Components』这本书中有详尽的解说及资讯。
2.7 为什麽元件在设计时期会出现『I/O 103』的错误?
你可能在元件中使用了Writeln这个程序。
2.8 为什麽元件编辑器不会将元件属性的变动储存起来?
我发现有时自制的元件编辑器不会将元件属性储存起来。设计时期一切正常,但是储存起来再重新读入後就有问题了。
原因是你很可能忘了在元件编辑器中呼叫此方法:
Designer.Modified;
如此一来Delphi才会知道你的元件编辑器更改过属性值了。
--------------------------------------------------------------------------------
第三部分在元件中使用其它元件
3.1 如何在元件中加入卷轴元件并让它在设计时期能动作?
你的卷轴元件类别必须处理 CM_DESIGNHITTEST 元件讯息才行。
TMyScrollBar = class (TScrollBar)
procedure CMDesignHitTest
(var Message: TCMDesignHitTest); message CM_DESIGNHITTEST;
end;
procedure TMyScrollBar.CMDesignHitTest( var Message: TCMDesignHitTest);
begin
Message.Result := 1;
end;
你的元件必须以以下方法建立卷轴:
TMyScrollBar.Create(nil);
而不是
TMyScrollBar.Create(Self);
3.2 如何建立Windows95式样的卷轴?
你必须设定卷轴的页面大小。你可以用以下的程式码来做:
procedure SetPageSize(ScrollBar: TScrollBar; PageSize: Integer);
var
ScrollInfo: TScrollInfo;
begin
ScrollInfo.cbSize := Sizeof (ScrollInfo);
ScrollInfo.fMask := SIF_PAGE;
ScrollInfo.nPage := PageSize;
SetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo, True);
end;
要取得目前页面大小可用如下方法:
function GetpageSize (ScrollBar: TScrollBar): Integer;
var
ScrollInfo: TScrollInfo;
begin
if HandleAllocated then
begin
ScrollInfo.cbSize := SizeOf (ScrollInfo);
ScrollInfo.fMask := SIF_PAGE;
GetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo);
Result := ScrollInfo.nPage;
end;
end;
--------------------------------------------------------------------------------
第四部分 Bound Controls
4.1 哪里可以找得到有关 TDataLink 类别的说明文件?
我可以大胆地说全世界有关 TDataLink 的说明文件只有一份,就在这儿:
属性 (Property) 介绍
property Active: Boolean(唯读)
当此 DataLink 连结至一个已开启的 DataSource 时会传回 True。当 Active 状态改变时会 触发ActiveChanged方法。
property ActiveRecord: Integer(可读写)
用来设定或取得 DataLink 缓冲区中目前所指向的记录代码,代码的范围是 0 .. BufferCount - 1。使用它来设定记录代码时必须小心不要超过这个范围,否则可能导致不可预期的错误。
property BufferCount: Integer(可读写)
DataLink 拥有一个资料缓冲区。而 BufferCount 属性即用来设定或取得缓冲区大小,缓冲区大小决定了一个dataset同时可以显视的资料记录笔数。对大部分的资料感知元件来说,BufferCount 的值是 1;但对 TDataGrid 来说,BufferCount 代表它的可视列数目。
property DataSet: TDataSet(唯读)
传回此 DataLink 所连结的 DataSet。其实就是 DataSource.DataSet。
property DataSource: TDataSource(可读写)
传回此DataLink所连结的DataSource。
property DataSourceFixed: Boolean(可读写)
这个属性可用来防止 DataSource 属性被更改。如果此属性设为 True,当我们试著改变 DataSource 属性时会引发一个例外。
property Editing: Boolean(唯读)
如果 DataLink 正处於编辑状态则传回 True。
property ReadOnly: Boolean(可读写)
设定 DataLink 是否为唯读状态。这个属性并不会影响所连结的 DataSet。在唯读状态下这个 DataLink 无法进入编辑状态。
property RecordCount: Integer(唯读)
传回DataSet的资料记录数目。
方法 (Method) 介绍
function Edit: Boolean;
让所连结的DataSet进入编辑状态。传回值: 成功传回 True ,失败传回 False
procedure UpdateRecord;
我们不直接呼叫这个方法,它是提供其它程式来呼叫的。这个方法只有设定一个旗帜然後呼叫 UpdateData 方法。
虚拟方法 ( Virtual Method )
要让 TDataLink 物件与元件沟通必须改写下列这些方法:
procedure ActiveChanged
当连结的 DataSource 开启状态改变时会呼叫此方法。使用 Active 属性可以得知目前是否为开启状态。
procedure CheckBrowseMode
资料库有任何改变之後都会先呼叫这个方法。
procedure DataSetChanged;
当下列任一事件发生时都会呼叫此方法:
移至DataSet的开头
移至DataSet的结尾
在DataSet中插入或新增资料
删除DataSet的资料
取消DataSet的编辑
更新记录
如果不想改写这个方法只要在其中呼叫:
RecordChanged(nil);
procedure DataSetScrolled(Distance: Integer)
每当目前记录变更时会呼叫此方法。Distance 参数代表缓冲区欲卷动的行数。(其值范围皆在 -1 .. 1 之间)。使用 ActiveRecord 属性可以取得缓冲区中目前所指向的记录。我们无法强制让 DataLink 的缓冲区卷动。
procedure FocusControl(Field: TFieldRef)
与TField.FocusControl方法相同。
procedure EditingChanged
当 DataLink 的编辑状态改变时会呼叫此方法。使用 Editing 属性可以得知DataLink 是否 正处於编辑状态。
procedure LayoutChanged
当 DataSet 的 Layout 改变时会呼叫此方法(例如新增一个column)。
procedure RecordChanged(Field: TField)
当下列任一事件发生时都会呼叫此方法:
目前记录进入编辑状态
目前记录内容更动
procedure UpdateData
在一笔记录被更新以前会呼叫此方法。你可以呼叫 Abort 程序来防止资料库更新。
4.2 如何得知一个 dataset 中有几笔记录?
TDateSet 的 RecNo 属性可以传回资料记录的数目,但很不幸地它只适用於 dBase 及 Paradox 的资料表格。若想得知目前资料记录的编号,可以从 TDataLink 类别衍生一个新的类别,然後进行下 列步骤:
改写 DataSetScrolled 方法以取得目前记录是否被卷动。
改写 DataSetChanged 方法来确认目前记录是否跳至最前面或最後面了。
接著你可以将这个新类别的物件连结到TDataSource物件上然後就可以随时得知目前的记录编号了。
--------------------------------------------------------------------------------
第五部分 VCL
5.1 使用""合环境除错时如何追踪检视 VCL元件的程式码?
将你想要追踪的 VCL 原始程式单元拷贝至存放专案的目录中并重新编译元件库,此後你就可以在那些 VCL单元中追踪检视程式码了。
5.2 我的元件参考到其它元件,如何得到参考元件被消灭的讯息?
Max Nilson 的回答:
TComponent 类别提供了 Notification 方法。当一个元件被移除时我们可以利用这个方法得到消息以进行适当的反应。你可以参考『Component Writer's Guide』内有关 Notification 及FreeNotification 这两个方法的说明。
当你的元件参考到另一个元件,例如,你的元件中有一个 TDataSource 型态的属性。那 你必须改写此元件的 Notification 方法,在其中检查被移除的元件是否就是本身所参考的元件。预设情况下,当元件被移除时,所有其它在同一个表格上的元件才会收到消息,如果参考元件位於另一个表格上时,你的元件无法得知这件事情。Delphi 2.0 推出了TDataModule,参考元件位於另一个表格上的机会大幅增加,所以你应该利用 FreeNotification 方法来确定当参考元件移除时,你一定可以得到消息。
如果你不改写 Notification 方法来处理参考元件被移除的讯息,这会让 Delphi""合环境陷入十分不稳定的状态。它可能不会立刻当掉,但你也不能再正常地继续其它工作了。
下面是一个范例,当你的元件参考其它元件时,千万记得要做以下的处理:
TMyComponent = class (TComponent)
private
FDataSource: TDataSource;
procedure SetDataSource(Value: TDataSource);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
published
property DataSource: TDataSource read FDataSource write SetDataSource;
end;
procedure TMyComponent.SetDataSource(Value: TDataSource);
begin
if Value FDataSource then
begin
FDataSource := Value;
// 告诉参考元件说,当它被移除时记得通知我一声。
if FDataSource nil then FDataSource.FreeNotification(Self)
end;
end;
procedure TMyComponent.Notification(AComponent: TComponent; Operation:TOperation);
begin
inherited Notification(AComponent, Operation);
// 如果被移除的正是参考元件,把FDataSource栏位清除。
if (Operation = opRemove) and (AComponent = FDataSource) then
FDataSource := nil
end;
5.3 什麽是元件讯息?
元件讯息是什麽?它十分类似Windows的视窗讯息,只有一点不同:元件讯息只适用於 VCL 元件;而视窗讯息可以用在系统内所有具有 window handle 的控制项或视窗。如果你有一个具有 Font 属性的元件(例如TLabel元件),当我们更改它的 Font 属性时并没有送出视窗讯息(译注:TLabel 元件不是视窗控制项,根本也没有视窗 Handle可以让我们传送视窗讯息),但是控制项仍然知道字型改变了所以要重画自己,为什麽?因为我们有元件讯息。
元件讯息不可以由虚拟方法来处理,这可能是设计 VCL 时的考量,大概是因为不想让虚 拟方法表格(Virtual Method Table)过於庞大的原因。
『Secrets of Delphi 2.0』这本书对於所有的元件讯息有十分详尽的解说。
接下来我们列出一些比较常见的元件讯息及它们的作用。标示著『Notification Only』 的讯息表示送出这个讯息只是为了通知元件某件消息而己,并不传入任何参数而且也不需要传回值。
CM_ACTIVATE (Notification Only)
当表格成为焦点视窗时会传给本身这个讯息。
CM_CTL3DCHANGED (Notification Only)
当控制项的Ctl3D属性更改时会传给本身这个讯息。
CM_DESIGNHITTEST 参数:TCMDesignHitTest 传回值:0或1
在设计时期当滑鼠移到元件上头时,""合环境会送给此元件这个讯息。此讯息的目的用来决定元件在设计时期是否要处理滑鼠讯息。如果传回值是 1,""合环境就让元件自行处理滑鼠讯息;若传回值是 0,则""合环境会帮你处理滑鼠讯息。如果传回值永远是 1,那麽元件的快速功能选单则永远不会出现;如果元件不处理这个讯息或永远传回 0,那此元件在设计时期将无法对滑鼠讯息做任何反应。
CM_FONTCHANGED (Notification Only)
控制项的字型改变後送给本身此讯息。
CM_FONTCHANGE (Notification Only)
当控制项收到WM_FONTCHANGE视窗讯息时会送给本身这个讯息。
CM_PARENTCTL3DCHANGED (Notification Only)
当元件父控制项的Ctl3D属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTCOLORCHANGED (Notification Only)
当元件父控制项的 Color 属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTFONTCHANGED (Notification Only)
当元件父控制项的Font属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTSHOWHINTCHANGED (Notification Only)
当元件父控制项的ShowHint属性改变或设定新的父控制项时会收到此讯息。
CM_WININICHANGE 参数:TWMWinIniChange 传回值:无
当控制项收到WM_WININICHANGE视窗讯息时会送给本身这个讯息。
5.4 我的元件得到输入焦点後仍不能接受键盘讯息,为什麽?
如果你的元件有 DragMode 属性而且将它设成 dmAutomatic 时,很有可能让你的元件以为它正被拖曳但实际上并没有的情况。在 Controls 单元中有一个区域变数 DragControl 指 向目前正被拖曳的元件。你遇到的情况很可能就是明明没有拖曳的动作但是DragControl 变数却指向你的元件。在 TWinControl 的 WndProc 方法中,当 DragControl 变数指向元件本身时,会忽略所有键盘讯息,这就是原因了!
--------------------------------------------------------------------------------
第六部分 其它资讯
6.1 有哪些书介绍或讲解如何撰写元件?
有关撰写元件的『标准』参考书籍:
『Developing Delphi Components』 作者:Ray Konopka 出版:Coriolis Group
下面这本书并不专注於元件写作,但里面提到许多元件撰写者不可不知的资讯:
『Secrets of Delphi 2』 作者:Ray Lischner 出版:Waite Group
另外一本元件撰写的好书,它有许多在『Developing Delphi Components』里找不到的资讯:
『Programming Delphi Custom Components』 作者:Fred Bulback 出版:M Books
6.2 有哪些Web站台可以取得撰写元件的资讯?
全世界最大的 Delphi Web 站台『Delphi SuperPage』
(译注:亚洲地区使用者可以就近到位於日本的 Mirror Site )
我在下面这些站台中找到许多元件的原始程式码:
Temple of Delphi
Delphi Free Stuff
(译注:『Your Delphi Programming Resource』""理元件也十分用心! )
(译注:台湾地区目前维持最好的 Delphi 站台是『32 Bit Delphi 深度历险』及其 Mirror Site)
你也可以使用一些搜寻引擎来寻找有关 Delphi 的站台:
Yahoo
Alta Vista
(译注:Excite 搜寻引擎也别错过罗!)
--------------------------------------------------------------------------------
第七部分 元件的储存与载入
7.1 如何将包含其它物件的物件一起存入 DFM 档?
我试过许多方法,包括改写元件的 DefineProperties及 WriteComponents方法,但都还是失败了。所以我只能说要解决这个问题的话只有使用 Delphi 原本的方法一途。
将包含其它物件的物件一起储存起来的步骤大致如下:
确定你要储存的所有物件都是从 TComponent 类别衍生下来的。
将所有需要储存的变数宣告在 published 区段。
在元件的 Register 程序中呼叫 RegisterComponents程序来注册所有你要储存起来的类别。
每个拥有子控制项的类别必须改写 GetChildren 方法以储存每个子控制项。(在 Delphi 1.0 中你必须改写 WriteComponents 方法并且为每个子控制项呼叫 WriteComponent方法)。
将物件载入的方法用了点小技巧。你必须改写元件的 GetChildOwner 及 GetChildParent 方法,否则 Delphi会将所有物件的拥有者都设定为元件所在的表格。(在Delphi 1.0 中你必须改写ReadState方法)。
7.2 如何得知元件是否正从资料流中读出?
当元件正从资料流中读出时,它的 ComponentState 属性会包含csLoading 旗帜。
constructor TMyClass.Create(AOwner: TComponent);
begin
if csLoading in AOwner.ComponentState then
begin ... end
else
begin ... end;
end;
7.3 如何确定元件的属性是否被正确地储存?
有许多很简单的方法可以验证属性是否被正确地储存在档案里:
在""合环境中用滑鼠右键点选表格然後选择『View as Text』。然而万一 DFM 档 有任何错误的话,你将什麽也看不到。
开个 DOS 视窗,利用 Delphi 所附的『Convert』程式将 DFM 档转成文字格式。
Stefan Hoffmeister 指出复制或剪下元件後,到任何一个文书编辑器(如记事本)中贴上,你就可以看到此元件的文字表示。你甚至可以编辑这些文字表示後再将它贴回 Delphi""合环境的表格上。
--------------------------------------------------------------------------------
第八部分 Delphi 的工具
8.1 有没有Delphi版本的 YACC 及 LEX?
有。Albert Graef 这位仁兄写了 Turbo Pascal 版本的 YACC 及 LEX,也可以让 Delphi 使用。
你可以在 ftp://ftp.simtel.net/pub/simtelnet/msdos/turbopas 下取得 tply30a1.zip 及 tply30a2.zip 这两个档案,其中还包含这两个工具的原始程式哦!
8.2 如何秀出 JPEG 格式图形档?
Jacques Nomssi Nzali 将 Independent JPEG Group 所发展的免费 JPEG 程式库改写成 Pascal 版本。你可以从下取得:
PASJPG10.ZIP
Independent JPEG Group 的免费 JPEG 函式库 rev 6a 之 Pascal 版本 (1.0 版)。
--------------------------------------------------------------------------------
第九部分 基本程式设计技巧
9.1 如何建立不定数目的物件阵列?
最简单的方法是使用 TList 类别。我发现从 TList 衍生一个新类别很有用处。接下来的程式码示范如何为一个特定型态撰写一个特别的 TList 类别,并且加进基本的错误检查。
TListOfMyObject = class (TList)
private
function GetItems(Index: Ordinal): TMyObject;
public
property Items[Index: Ordinal]: TMyObject read GetItems;
procedure Add(AObject: TMyObject);
end;
function TListOfMyObject.GetItems (Index: Ordinal): TMyObject;
begin
if Index = Count then
raise Exception.CreateFmt('Index(%d) outside range 1..%d', [Index, Count-1]);
Result := inherited Items[Index];
end;
procedure TListOfMyObject.Add (AObject: TmyObject);
begin
inherited Add(AObject);
end;
9.2 Delphi 2.0的 WinCrt单元到哪去了?
Delphi 2.0并没有 WinCrt单元。 先别伤心,这是因为我们可以用其它方法来取代它。在 Project|Options 的 Linker 页次中将『Generate console application』选项打开,你就可以像以前使用 WinCrt 单元一样地写程式了!
9.3 自制元件时该从哪个类别继承?
VCL 中有一些『自订』类别,而且有许多控制项是直接由这些『自订』类别继承下来的。例如 TMemo 直接继承自 TCustomMemo类别。这些自订类别写好了所有该控制项所拥有的功能,只是没有将属性公开出来而己。大部分情形下,你应该从那些自订类别继承而不是控制项类别。
如果你要从头撰写自己的元件,那麽从 TCustomControl 类别继承是个不错的主意。撰 写出来的元件会具有 Window Handle 且可以接受输入焦点。
另外根据你的需要也可以从这些类别继承:
TGraphicControl:视觉元件,但是没有window handle,也不能接受输入焦点。
TComponent:不可视元件,你没办法在执行时期看到它。
TWinControl:将已存在的视窗元件包装起来,如Windows标准控制项或VBX元件。
--------------------------------------------------------------------------------
第十部分 进阶程式设计技巧
10.1 Delphi 有与 C++ 一样的 I/O Stream 类别吗?
答案可以说有也可以说没有。Delphi允许你建立自己的『文字档驱动程式』,它可以让你使用Delphi 标准的 I/O 函式库来处理非标准的 I/O,如处理 UNIX 格式的文字档或处理 Socket 所取得的资料。虽然没有像 C++ 的 I/O Stream 类别那麽强大但应该也足够一般用途使用了。
建立『文字档驱动程式』的方法在『Object Pascal Language Guide』中有明述。此 外你也可以参考 VCL 的 Printer 单元。
Delphi有 TStream 类别,不过是设计用来将物件写入资料流的,不像 C++ 的 I/O Stream 类别那麽具有弹性。
10.2 如何取得列举型态变数的文字表示?
使用 TypInfo单元中的 GetEnumName 函式:
type
TMyType = (Value1, Value2);
var
TypeValue: TMyType;
begin
Writeln (GetEnumName(TypeInfo(TMyType), Ord(TypeValue));
end;
TypInfo单元中还有许多与型别资讯有关的函式。
『Secrets of Delphi 2.0』这本书有许多关於TypInfo单元的资讯,值得参考。
--------------------------------------------------------------------------------
第十一部分 元件虚拟方法
11.1 如何得知元件的window handle是何时建立的?
控制项的 window handle 是在 CreateWnd 方法中建立的。如果你想要在建立 window handle 後接著做某些动作那麽你应该改写 CreateWnd 方法:
procedure TMyClass.CreateWnd;
begin
// 现在还没取得 window handle
inherited CreateWnd;
// 呼叫 inherited 以取得 window handle
// 在这里撰写你想要执行的动作
end;
11.2 如何得知是否表格上所有元件都已载入完成?
Loaded 方法是在载入完成後接著被呼叫的。
procedure TMyClass.Loaded;
begin
inherited Loaded;
// 将ComponentState中的 csLoading 状态清除
// 在这里撰写你想要执行的动作
end;
11.3 在哪里绘制元件最适合?
你应该拦截 WM_PAINT 视窗讯息然後利用 Canvas 来绘制元件。然而 VCL 己经帮你拦 截好了,你只须改写元件的 Paint 方法即可。
procedure TMyClass.Paint;
begin
// 如果你的元件是己存在的元件继承下来的,那麽必须在这里呼叫 inherited Paint
inherited Paint ;
// 在这里撰写你想要执行的动作
end;
11.4 如何改变元件的视窗式样?
CreateParams方法用来设定元件的视窗式样及其它必须传递至 CreateWindowEx API 的 参数。要改变元件的视窗式样,例如增加或拿掉元件的垂直卷轴只要改写 CreateParams 方法:
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
if IWantAScrollBar then
Params.Style := Params.Style or WS_VSCROLL
else
Params.Style := Params.Style and not WS_VSCROLL;
end;
--------------------------------------------------------------------------------
第十二部分 Windows API
12.1 元件卷动时闪动的很厉害,如何克服这种情况?
要卷动元件本身最简单的方法就是改变它的座标然後重画元件,但是这方法会导致元件闪动的很厉害。
比较好的方法是呼叫 ScrollWindow 或 ScrollWindowEx Windows API。
闪动的另一个原因可能来自於 WM_PAINT 及 WM_ERASEBKGND。你可以试著拦截 WM_ERASEBKGND 及 WM_PAINT 讯息然後自己处理绘图动作,包括绘制背景的动作,或许可以改善闪动的情况。
12.2 如何重新启动Windows?
使用 ExitWindowsEx Windows API。
12.3 如何快速大量地更改元件资料?
在进行大量资料更改前後,利用 WM_SETREDRAW 讯息来控制你的元件暂时不要重画,这不但可以使资料设定速度增快也防止元件闪烁的情况。
--------------------------------------------------------------------------------
第十三部分 控制项边框
13.1 为什麽我的元件的 Ctl3D 属性设为 True 之後,它依然没有 3D 的边框呢?
如果 ControlStyle 属性内没有包含 csFramed 旗帜那麽 Ctl3D 属性就会没有作用。在元件 的建构函式内加上:
ControlStyle := ControlStyle + [csFramed];
13.2 如何实作 BorderStyle 属性?
在控制项设定有没有边框之後要重新建立 window handle:
FBorderStyle: TBorderStyle;
procedure SetBorderStyle(Style: TBorderStyle);
property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle;
procedure CreateParams(var Params: TCreateParams); override;
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
if FBorderStyle = bsSingle then
Params.Style := Params.Style or WS_BORDER
else
Params.Style := Params.Style and not WS_BORDER;
end;
procedure TMyControl.SetBorderStyle(Style: TBorderStyle);
begin
if Style FBorderStyle then
begin
FBorderStyle := Style;
// 重新建立window handle
RecreateWnd;
end;
end;
--------------------------------------------------------------------------------
第十四部分 控制项式样
14.1 当元件重绘时如何防止闪动的情况?
如果元件的 ComponentStyle 属性没有包含 csOpaque 旗帜的话,呼叫 Invalidate方法时 会导致元件的背景先被擦掉再重绘。如果你在 Paint 方法中绘制背景,那你应该在元件的建构函式中加上:
ComponentStyle := ComponentStyle + [csOpaque];
Max Nilson的回答:
引起闪动另一个原因可能是 WM_ERASEBKGND 讯息的处理。当 VCL 控制项收到一个 WM_ERASEBKGND 讯息时,它会将元件的背景擦掉然後设定成预设的颜色。如果你的元件衍生自 TWinControl,而且元件的颜色与背景颜色不同(例如图形),每次重画以前都会将元件先清成背景颜色再重绘,这就是造成闪动的原因了!
解决的方法不难,你必须告诉 Windows 你要自行解决『所有的』绘图动作。不过有一个前提是,你一定要确定你的 Paint 方法将""个元件都画过,如果你漏了什麽地方忘了画,那个部分的资料会由乱数组成,你能想见这情况吗?使用这个方法可以加速你的元件绘制动作(稍微快一点点),因为少了一个填满背景颜色的动作。
type
TMyComponent = class (TWinControl)
...
protected
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
...
end;
procedure TBMyComponent.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
// 不要重绘背景,这会造成元件闪动
Message.Result := 0
end;
--------------------------------------------------------------------------------
第十五部分 视窗讯息
15.1 为什麽我的元件得不到方向键的讯息?
你必须拦截 WM_GETDLGCODE 才能处理方向键的讯息,在 WM_GETDLGCODE 的讯息处理 者中传回 DLGC_WANTARROWS。如果你不这样做,那方向键的功用就只能用来移动视窗焦点而己。
Max Nilson 的回答:
想要你的元件能够处理方向键,你必须要拦截 CM_WANTSPECIALKEY 元件讯息。 CM_WANTSPECIALKEY 元件讯息提供你比拦截 WM_GETDLGCODE 视窗讯息更容易且灵活的判断方法来决定是否需要某些特殊键的讯息。当控制项收到任何一个特殊键时就会送出CM_WANTSPECIALKEY 元件讯息给控制项。
特殊键包括:VK_TAB、VK_LEFT、VK_RIGHT、VK_UP、VK_DOWN、VK_RETURN、VK_EXECUTE 、VK_ESCAPE 及 VK_CANCEL。如果讯息传回值是非零值,这个键就会被送至 KeyPress 方法以供处理,否则这个键的讯息会被送至元件的父控制项,以预设方式来处理。
一个简单的范例:
type
TMyComponent = class (TWinControl)
...
protected
procedure CMWantSpecialKey(var Message: TCMWantSpecialKey); message CM_WANTSPECIALKEY;
...
end;
procedure TMyComponent.CMWantSpecialKey(var Message: TCMWantSpecialKey); begin
inherited;
// 我们只想处理向左方向键,其它的特殊键都给 Windows 处理
if Message.CharCode = VK_LEFT then
Message.Result := 1;
end;
CM_WANTSPECIALKEY 元件讯息比 WM_GETDLGCODE 讯息更具有弹性的地方在这儿。我们甚至可以根据是按下的是哪个特殊键才决定是否处理这个键。例如,你的控制项有三张影像,你可以让使用者利用左右方向键来回检视它们,如果翻到最後一张影像再按向右键时,焦点就让它离开元件,剩下的全部都让 Delphi 来处理。
15.2 有没有与 Visual Basic『DoEvents』同样功能的函式?
有。Application.ProcessMessages方法。
1.1 此份文件的目的为何?
这份文件的目的是为了解答有关撰写 Delphi元件时常见或文件上找不到的问题。我曾经花了一段很长的时间来了解探索 TDataLink 类别,这让我觉得应该将撰写元件时常遇到的问题及经验心得写下来,分享给大家。不过我并不能保证写在这份文件里头的解答完全正确。如果你对其中的任何问题有更好的解决方法,或认为有什麽资讯适合放在这份文件里的话,请告知作者。有任何错误或缺漏也欢迎指正。
除了再加上更多的问题及解答外,我试著再补充两个部分:
进阶程式设计师喜爱的工具:这也许跟元件设计没有直接的关系但至少它们跟 Delphi有关系。
值得参考的文件刊物:由於空间的关系,这份文件不能放置太多的范例程式,因此参考其它文件是十分需要的。这不是一份教材式的文件,我不会做太多条理式的说明,但会试著将最具有参考价值的文献列出。
如果你有任何意见或建"",欢迎来信告诉我。
--------------------------------------------------------------------------------
第二部份 ""合环境
2.1 在""合环境中如何找出元件所产生的问题?
我发现唯一能找出问题的方法只有:
在 Delphi ""合环境的 Tools|Options 对话框的 Library 页中将『Compile with debug info』选项打勾。
选 Component|Rebuild Library 重新编译元件库。
从 Turbo Debugger 中执行 Delphi。
选File|Change Dir移至包含元件程式码的目录下。
如果你的元件发生GPF时就可以检视堆叠然後得知到底是哪些发生问题了。
2.2 如何检视 Delphi 所产生的组合语言码?
Glen Boyd 的回答:
开启登录编辑程式(REGEDIT.EXE),接著到『HKEY_CURRENT_USER\Software\Borland\Delphi\2.0\Debugging』下新增一个字串机码『EnableCPU』,将它的字串值设为『1』。此後Delphi""合环境的View选单下就会多一个『CPU』选项,它会开启一个视窗来检视目前程式指令的记忆体及组合语言。你可以在侦错时利用单步追踪或其它方法来观察它。
2.3 我可以在执行时期动态建立元件,但在设计时期就会发生错误。为什麽?
你的元件必须继承自TComponent类别或其衍生类别。
你的元件建构函式及灭构函式宣告必须看起来像这样:
constructor Create(AOwner: TComponent); override;
destructor Destroy; override ;
所有在published区段宣告的栏位型态必须是ordinal、single、double、extended 、comp、currency、string、small set(译注:指元素编号不超过0..31这个范围的集合;平常的集合可容许的范围为0..255)、method pointer或class其中一种。如果你宣告了其它型态的栏位,Delphi编译器并不会检查出错误。然而当你使用这个元件时依然会得到一个GPF。
如果你想让TMyComponent元件可以在设计时期操作,注意下面的宣告会引发十分严重的问题:
type TComplex = record
RealPart: Double;
ComplexPart: Double;
end;
class TMyComponent = Class(TComponent)
private
F1: TComplex;
published
property P1: TComplex read F1 write F1;
end;
2.4 如何撰写一个无法放置到表格上的元件?
Ray Lischner 的回答:
如果你不想让使用者将元件拉曳至表格上的话,使用 RegisterNoIcon 及 RegisterClass 程序来注册元件。
2.5 在程式码编辑器中快速切换程式区段最简单的方法是什麽?
Ray Konopka 的回答:
在探索 VCL 原始程式码时,强烈建""你最好熟悉程式码编辑器里的书签功能。使用方法很简单:Ctrl-Shift-N,N 是从 0 至 9 的数字,用来设定一个书签。此後就可以使用 Ctrl-N 来跳跃至书签处。(译注:使用这项功能真的可以节省你许多来回卷动程式及找寻函式的时间,别迟疑了,快学吧!)
2.6 如何使我的元件在按下滑鼠右键时出现快速功能选单?
你必须要建立一个元件编辑器。元件编辑器决定了元件在设计时期时对滑鼠键的反应及动作,你可以为元件定义它自己的快速功能选单。
建立元件编辑器的步骤大致如下:
从 TComponentEditor 类别继承一个新的类别。
改写类别的 GetVerbCount、GetVerb及 ExecuteVerb方法。
在 Register 程序中使用 RegisterComponentEditor 程序来注册此元件编辑器。
有关元件编辑器这个主题在『Developing Delphi Components』这本书中有详尽的解说及资讯。
2.7 为什麽元件在设计时期会出现『I/O 103』的错误?
你可能在元件中使用了Writeln这个程序。
2.8 为什麽元件编辑器不会将元件属性的变动储存起来?
我发现有时自制的元件编辑器不会将元件属性储存起来。设计时期一切正常,但是储存起来再重新读入後就有问题了。
原因是你很可能忘了在元件编辑器中呼叫此方法:
Designer.Modified;
如此一来Delphi才会知道你的元件编辑器更改过属性值了。
--------------------------------------------------------------------------------
第三部分在元件中使用其它元件
3.1 如何在元件中加入卷轴元件并让它在设计时期能动作?
你的卷轴元件类别必须处理 CM_DESIGNHITTEST 元件讯息才行。
TMyScrollBar = class (TScrollBar)
procedure CMDesignHitTest
(var Message: TCMDesignHitTest); message CM_DESIGNHITTEST;
end;
procedure TMyScrollBar.CMDesignHitTest( var Message: TCMDesignHitTest);
begin
Message.Result := 1;
end;
你的元件必须以以下方法建立卷轴:
TMyScrollBar.Create(nil);
而不是
TMyScrollBar.Create(Self);
3.2 如何建立Windows95式样的卷轴?
你必须设定卷轴的页面大小。你可以用以下的程式码来做:
procedure SetPageSize(ScrollBar: TScrollBar; PageSize: Integer);
var
ScrollInfo: TScrollInfo;
begin
ScrollInfo.cbSize := Sizeof (ScrollInfo);
ScrollInfo.fMask := SIF_PAGE;
ScrollInfo.nPage := PageSize;
SetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo, True);
end;
要取得目前页面大小可用如下方法:
function GetpageSize (ScrollBar: TScrollBar): Integer;
var
ScrollInfo: TScrollInfo;
begin
if HandleAllocated then
begin
ScrollInfo.cbSize := SizeOf (ScrollInfo);
ScrollInfo.fMask := SIF_PAGE;
GetScrollInfo (ScrollBar.Handle, SB_CTL, ScrollInfo);
Result := ScrollInfo.nPage;
end;
end;
--------------------------------------------------------------------------------
第四部分 Bound Controls
4.1 哪里可以找得到有关 TDataLink 类别的说明文件?
我可以大胆地说全世界有关 TDataLink 的说明文件只有一份,就在这儿:
属性 (Property) 介绍
property Active: Boolean(唯读)
当此 DataLink 连结至一个已开启的 DataSource 时会传回 True。当 Active 状态改变时会 触发ActiveChanged方法。
property ActiveRecord: Integer(可读写)
用来设定或取得 DataLink 缓冲区中目前所指向的记录代码,代码的范围是 0 .. BufferCount - 1。使用它来设定记录代码时必须小心不要超过这个范围,否则可能导致不可预期的错误。
property BufferCount: Integer(可读写)
DataLink 拥有一个资料缓冲区。而 BufferCount 属性即用来设定或取得缓冲区大小,缓冲区大小决定了一个dataset同时可以显视的资料记录笔数。对大部分的资料感知元件来说,BufferCount 的值是 1;但对 TDataGrid 来说,BufferCount 代表它的可视列数目。
property DataSet: TDataSet(唯读)
传回此 DataLink 所连结的 DataSet。其实就是 DataSource.DataSet。
property DataSource: TDataSource(可读写)
传回此DataLink所连结的DataSource。
property DataSourceFixed: Boolean(可读写)
这个属性可用来防止 DataSource 属性被更改。如果此属性设为 True,当我们试著改变 DataSource 属性时会引发一个例外。
property Editing: Boolean(唯读)
如果 DataLink 正处於编辑状态则传回 True。
property ReadOnly: Boolean(可读写)
设定 DataLink 是否为唯读状态。这个属性并不会影响所连结的 DataSet。在唯读状态下这个 DataLink 无法进入编辑状态。
property RecordCount: Integer(唯读)
传回DataSet的资料记录数目。
方法 (Method) 介绍
function Edit: Boolean;
让所连结的DataSet进入编辑状态。传回值: 成功传回 True ,失败传回 False
procedure UpdateRecord;
我们不直接呼叫这个方法,它是提供其它程式来呼叫的。这个方法只有设定一个旗帜然後呼叫 UpdateData 方法。
虚拟方法 ( Virtual Method )
要让 TDataLink 物件与元件沟通必须改写下列这些方法:
procedure ActiveChanged
当连结的 DataSource 开启状态改变时会呼叫此方法。使用 Active 属性可以得知目前是否为开启状态。
procedure CheckBrowseMode
资料库有任何改变之後都会先呼叫这个方法。
procedure DataSetChanged;
当下列任一事件发生时都会呼叫此方法:
移至DataSet的开头
移至DataSet的结尾
在DataSet中插入或新增资料
删除DataSet的资料
取消DataSet的编辑
更新记录
如果不想改写这个方法只要在其中呼叫:
RecordChanged(nil);
procedure DataSetScrolled(Distance: Integer)
每当目前记录变更时会呼叫此方法。Distance 参数代表缓冲区欲卷动的行数。(其值范围皆在 -1 .. 1 之间)。使用 ActiveRecord 属性可以取得缓冲区中目前所指向的记录。我们无法强制让 DataLink 的缓冲区卷动。
procedure FocusControl(Field: TFieldRef)
与TField.FocusControl方法相同。
procedure EditingChanged
当 DataLink 的编辑状态改变时会呼叫此方法。使用 Editing 属性可以得知DataLink 是否 正处於编辑状态。
procedure LayoutChanged
当 DataSet 的 Layout 改变时会呼叫此方法(例如新增一个column)。
procedure RecordChanged(Field: TField)
当下列任一事件发生时都会呼叫此方法:
目前记录进入编辑状态
目前记录内容更动
procedure UpdateData
在一笔记录被更新以前会呼叫此方法。你可以呼叫 Abort 程序来防止资料库更新。
4.2 如何得知一个 dataset 中有几笔记录?
TDateSet 的 RecNo 属性可以传回资料记录的数目,但很不幸地它只适用於 dBase 及 Paradox 的资料表格。若想得知目前资料记录的编号,可以从 TDataLink 类别衍生一个新的类别,然後进行下 列步骤:
改写 DataSetScrolled 方法以取得目前记录是否被卷动。
改写 DataSetChanged 方法来确认目前记录是否跳至最前面或最後面了。
接著你可以将这个新类别的物件连结到TDataSource物件上然後就可以随时得知目前的记录编号了。
--------------------------------------------------------------------------------
第五部分 VCL
5.1 使用""合环境除错时如何追踪检视 VCL元件的程式码?
将你想要追踪的 VCL 原始程式单元拷贝至存放专案的目录中并重新编译元件库,此後你就可以在那些 VCL单元中追踪检视程式码了。
5.2 我的元件参考到其它元件,如何得到参考元件被消灭的讯息?
Max Nilson 的回答:
TComponent 类别提供了 Notification 方法。当一个元件被移除时我们可以利用这个方法得到消息以进行适当的反应。你可以参考『Component Writer's Guide』内有关 Notification 及FreeNotification 这两个方法的说明。
当你的元件参考到另一个元件,例如,你的元件中有一个 TDataSource 型态的属性。那 你必须改写此元件的 Notification 方法,在其中检查被移除的元件是否就是本身所参考的元件。预设情况下,当元件被移除时,所有其它在同一个表格上的元件才会收到消息,如果参考元件位於另一个表格上时,你的元件无法得知这件事情。Delphi 2.0 推出了TDataModule,参考元件位於另一个表格上的机会大幅增加,所以你应该利用 FreeNotification 方法来确定当参考元件移除时,你一定可以得到消息。
如果你不改写 Notification 方法来处理参考元件被移除的讯息,这会让 Delphi""合环境陷入十分不稳定的状态。它可能不会立刻当掉,但你也不能再正常地继续其它工作了。
下面是一个范例,当你的元件参考其它元件时,千万记得要做以下的处理:
TMyComponent = class (TComponent)
private
FDataSource: TDataSource;
procedure SetDataSource(Value: TDataSource);
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
published
property DataSource: TDataSource read FDataSource write SetDataSource;
end;
procedure TMyComponent.SetDataSource(Value: TDataSource);
begin
if Value FDataSource then
begin
FDataSource := Value;
// 告诉参考元件说,当它被移除时记得通知我一声。
if FDataSource nil then FDataSource.FreeNotification(Self)
end;
end;
procedure TMyComponent.Notification(AComponent: TComponent; Operation:TOperation);
begin
inherited Notification(AComponent, Operation);
// 如果被移除的正是参考元件,把FDataSource栏位清除。
if (Operation = opRemove) and (AComponent = FDataSource) then
FDataSource := nil
end;
5.3 什麽是元件讯息?
元件讯息是什麽?它十分类似Windows的视窗讯息,只有一点不同:元件讯息只适用於 VCL 元件;而视窗讯息可以用在系统内所有具有 window handle 的控制项或视窗。如果你有一个具有 Font 属性的元件(例如TLabel元件),当我们更改它的 Font 属性时并没有送出视窗讯息(译注:TLabel 元件不是视窗控制项,根本也没有视窗 Handle可以让我们传送视窗讯息),但是控制项仍然知道字型改变了所以要重画自己,为什麽?因为我们有元件讯息。
元件讯息不可以由虚拟方法来处理,这可能是设计 VCL 时的考量,大概是因为不想让虚 拟方法表格(Virtual Method Table)过於庞大的原因。
『Secrets of Delphi 2.0』这本书对於所有的元件讯息有十分详尽的解说。
接下来我们列出一些比较常见的元件讯息及它们的作用。标示著『Notification Only』 的讯息表示送出这个讯息只是为了通知元件某件消息而己,并不传入任何参数而且也不需要传回值。
CM_ACTIVATE (Notification Only)
当表格成为焦点视窗时会传给本身这个讯息。
CM_CTL3DCHANGED (Notification Only)
当控制项的Ctl3D属性更改时会传给本身这个讯息。
CM_DESIGNHITTEST 参数:TCMDesignHitTest 传回值:0或1
在设计时期当滑鼠移到元件上头时,""合环境会送给此元件这个讯息。此讯息的目的用来决定元件在设计时期是否要处理滑鼠讯息。如果传回值是 1,""合环境就让元件自行处理滑鼠讯息;若传回值是 0,则""合环境会帮你处理滑鼠讯息。如果传回值永远是 1,那麽元件的快速功能选单则永远不会出现;如果元件不处理这个讯息或永远传回 0,那此元件在设计时期将无法对滑鼠讯息做任何反应。
CM_FONTCHANGED (Notification Only)
控制项的字型改变後送给本身此讯息。
CM_FONTCHANGE (Notification Only)
当控制项收到WM_FONTCHANGE视窗讯息时会送给本身这个讯息。
CM_PARENTCTL3DCHANGED (Notification Only)
当元件父控制项的Ctl3D属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTCOLORCHANGED (Notification Only)
当元件父控制项的 Color 属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTFONTCHANGED (Notification Only)
当元件父控制项的Font属性改变或设定新的父控制项时会收到此讯息。
CM_PARENTSHOWHINTCHANGED (Notification Only)
当元件父控制项的ShowHint属性改变或设定新的父控制项时会收到此讯息。
CM_WININICHANGE 参数:TWMWinIniChange 传回值:无
当控制项收到WM_WININICHANGE视窗讯息时会送给本身这个讯息。
5.4 我的元件得到输入焦点後仍不能接受键盘讯息,为什麽?
如果你的元件有 DragMode 属性而且将它设成 dmAutomatic 时,很有可能让你的元件以为它正被拖曳但实际上并没有的情况。在 Controls 单元中有一个区域变数 DragControl 指 向目前正被拖曳的元件。你遇到的情况很可能就是明明没有拖曳的动作但是DragControl 变数却指向你的元件。在 TWinControl 的 WndProc 方法中,当 DragControl 变数指向元件本身时,会忽略所有键盘讯息,这就是原因了!
--------------------------------------------------------------------------------
第六部分 其它资讯
6.1 有哪些书介绍或讲解如何撰写元件?
有关撰写元件的『标准』参考书籍:
『Developing Delphi Components』 作者:Ray Konopka 出版:Coriolis Group
下面这本书并不专注於元件写作,但里面提到许多元件撰写者不可不知的资讯:
『Secrets of Delphi 2』 作者:Ray Lischner 出版:Waite Group
另外一本元件撰写的好书,它有许多在『Developing Delphi Components』里找不到的资讯:
『Programming Delphi Custom Components』 作者:Fred Bulback 出版:M Books
6.2 有哪些Web站台可以取得撰写元件的资讯?
全世界最大的 Delphi Web 站台『Delphi SuperPage』
(译注:亚洲地区使用者可以就近到位於日本的 Mirror Site )
我在下面这些站台中找到许多元件的原始程式码:
Temple of Delphi
Delphi Free Stuff
(译注:『Your Delphi Programming Resource』""理元件也十分用心! )
(译注:台湾地区目前维持最好的 Delphi 站台是『32 Bit Delphi 深度历险』及其 Mirror Site)
你也可以使用一些搜寻引擎来寻找有关 Delphi 的站台:
Yahoo
Alta Vista
(译注:Excite 搜寻引擎也别错过罗!)
--------------------------------------------------------------------------------
第七部分 元件的储存与载入
7.1 如何将包含其它物件的物件一起存入 DFM 档?
我试过许多方法,包括改写元件的 DefineProperties及 WriteComponents方法,但都还是失败了。所以我只能说要解决这个问题的话只有使用 Delphi 原本的方法一途。
将包含其它物件的物件一起储存起来的步骤大致如下:
确定你要储存的所有物件都是从 TComponent 类别衍生下来的。
将所有需要储存的变数宣告在 published 区段。
在元件的 Register 程序中呼叫 RegisterComponents程序来注册所有你要储存起来的类别。
每个拥有子控制项的类别必须改写 GetChildren 方法以储存每个子控制项。(在 Delphi 1.0 中你必须改写 WriteComponents 方法并且为每个子控制项呼叫 WriteComponent方法)。
将物件载入的方法用了点小技巧。你必须改写元件的 GetChildOwner 及 GetChildParent 方法,否则 Delphi会将所有物件的拥有者都设定为元件所在的表格。(在Delphi 1.0 中你必须改写ReadState方法)。
7.2 如何得知元件是否正从资料流中读出?
当元件正从资料流中读出时,它的 ComponentState 属性会包含csLoading 旗帜。
constructor TMyClass.Create(AOwner: TComponent);
begin
if csLoading in AOwner.ComponentState then
begin ... end
else
begin ... end;
end;
7.3 如何确定元件的属性是否被正确地储存?
有许多很简单的方法可以验证属性是否被正确地储存在档案里:
在""合环境中用滑鼠右键点选表格然後选择『View as Text』。然而万一 DFM 档 有任何错误的话,你将什麽也看不到。
开个 DOS 视窗,利用 Delphi 所附的『Convert』程式将 DFM 档转成文字格式。
Stefan Hoffmeister 指出复制或剪下元件後,到任何一个文书编辑器(如记事本)中贴上,你就可以看到此元件的文字表示。你甚至可以编辑这些文字表示後再将它贴回 Delphi""合环境的表格上。
--------------------------------------------------------------------------------
第八部分 Delphi 的工具
8.1 有没有Delphi版本的 YACC 及 LEX?
有。Albert Graef 这位仁兄写了 Turbo Pascal 版本的 YACC 及 LEX,也可以让 Delphi 使用。
你可以在 ftp://ftp.simtel.net/pub/simtelnet/msdos/turbopas 下取得 tply30a1.zip 及 tply30a2.zip 这两个档案,其中还包含这两个工具的原始程式哦!
8.2 如何秀出 JPEG 格式图形档?
Jacques Nomssi Nzali 将 Independent JPEG Group 所发展的免费 JPEG 程式库改写成 Pascal 版本。你可以从下取得:
PASJPG10.ZIP
Independent JPEG Group 的免费 JPEG 函式库 rev 6a 之 Pascal 版本 (1.0 版)。
--------------------------------------------------------------------------------
第九部分 基本程式设计技巧
9.1 如何建立不定数目的物件阵列?
最简单的方法是使用 TList 类别。我发现从 TList 衍生一个新类别很有用处。接下来的程式码示范如何为一个特定型态撰写一个特别的 TList 类别,并且加进基本的错误检查。
TListOfMyObject = class (TList)
private
function GetItems(Index: Ordinal): TMyObject;
public
property Items[Index: Ordinal]: TMyObject read GetItems;
procedure Add(AObject: TMyObject);
end;
function TListOfMyObject.GetItems (Index: Ordinal): TMyObject;
begin
if Index = Count then
raise Exception.CreateFmt('Index(%d) outside range 1..%d', [Index, Count-1]);
Result := inherited Items[Index];
end;
procedure TListOfMyObject.Add (AObject: TmyObject);
begin
inherited Add(AObject);
end;
9.2 Delphi 2.0的 WinCrt单元到哪去了?
Delphi 2.0并没有 WinCrt单元。 先别伤心,这是因为我们可以用其它方法来取代它。在 Project|Options 的 Linker 页次中将『Generate console application』选项打开,你就可以像以前使用 WinCrt 单元一样地写程式了!
9.3 自制元件时该从哪个类别继承?
VCL 中有一些『自订』类别,而且有许多控制项是直接由这些『自订』类别继承下来的。例如 TMemo 直接继承自 TCustomMemo类别。这些自订类别写好了所有该控制项所拥有的功能,只是没有将属性公开出来而己。大部分情形下,你应该从那些自订类别继承而不是控制项类别。
如果你要从头撰写自己的元件,那麽从 TCustomControl 类别继承是个不错的主意。撰 写出来的元件会具有 Window Handle 且可以接受输入焦点。
另外根据你的需要也可以从这些类别继承:
TGraphicControl:视觉元件,但是没有window handle,也不能接受输入焦点。
TComponent:不可视元件,你没办法在执行时期看到它。
TWinControl:将已存在的视窗元件包装起来,如Windows标准控制项或VBX元件。
--------------------------------------------------------------------------------
第十部分 进阶程式设计技巧
10.1 Delphi 有与 C++ 一样的 I/O Stream 类别吗?
答案可以说有也可以说没有。Delphi允许你建立自己的『文字档驱动程式』,它可以让你使用Delphi 标准的 I/O 函式库来处理非标准的 I/O,如处理 UNIX 格式的文字档或处理 Socket 所取得的资料。虽然没有像 C++ 的 I/O Stream 类别那麽强大但应该也足够一般用途使用了。
建立『文字档驱动程式』的方法在『Object Pascal Language Guide』中有明述。此 外你也可以参考 VCL 的 Printer 单元。
Delphi有 TStream 类别,不过是设计用来将物件写入资料流的,不像 C++ 的 I/O Stream 类别那麽具有弹性。
10.2 如何取得列举型态变数的文字表示?
使用 TypInfo单元中的 GetEnumName 函式:
type
TMyType = (Value1, Value2);
var
TypeValue: TMyType;
begin
Writeln (GetEnumName(TypeInfo(TMyType), Ord(TypeValue));
end;
TypInfo单元中还有许多与型别资讯有关的函式。
『Secrets of Delphi 2.0』这本书有许多关於TypInfo单元的资讯,值得参考。
--------------------------------------------------------------------------------
第十一部分 元件虚拟方法
11.1 如何得知元件的window handle是何时建立的?
控制项的 window handle 是在 CreateWnd 方法中建立的。如果你想要在建立 window handle 後接著做某些动作那麽你应该改写 CreateWnd 方法:
procedure TMyClass.CreateWnd;
begin
// 现在还没取得 window handle
inherited CreateWnd;
// 呼叫 inherited 以取得 window handle
// 在这里撰写你想要执行的动作
end;
11.2 如何得知是否表格上所有元件都已载入完成?
Loaded 方法是在载入完成後接著被呼叫的。
procedure TMyClass.Loaded;
begin
inherited Loaded;
// 将ComponentState中的 csLoading 状态清除
// 在这里撰写你想要执行的动作
end;
11.3 在哪里绘制元件最适合?
你应该拦截 WM_PAINT 视窗讯息然後利用 Canvas 来绘制元件。然而 VCL 己经帮你拦 截好了,你只须改写元件的 Paint 方法即可。
procedure TMyClass.Paint;
begin
// 如果你的元件是己存在的元件继承下来的,那麽必须在这里呼叫 inherited Paint
inherited Paint ;
// 在这里撰写你想要执行的动作
end;
11.4 如何改变元件的视窗式样?
CreateParams方法用来设定元件的视窗式样及其它必须传递至 CreateWindowEx API 的 参数。要改变元件的视窗式样,例如增加或拿掉元件的垂直卷轴只要改写 CreateParams 方法:
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
if IWantAScrollBar then
Params.Style := Params.Style or WS_VSCROLL
else
Params.Style := Params.Style and not WS_VSCROLL;
end;
--------------------------------------------------------------------------------
第十二部分 Windows API
12.1 元件卷动时闪动的很厉害,如何克服这种情况?
要卷动元件本身最简单的方法就是改变它的座标然後重画元件,但是这方法会导致元件闪动的很厉害。
比较好的方法是呼叫 ScrollWindow 或 ScrollWindowEx Windows API。
闪动的另一个原因可能来自於 WM_PAINT 及 WM_ERASEBKGND。你可以试著拦截 WM_ERASEBKGND 及 WM_PAINT 讯息然後自己处理绘图动作,包括绘制背景的动作,或许可以改善闪动的情况。
12.2 如何重新启动Windows?
使用 ExitWindowsEx Windows API。
12.3 如何快速大量地更改元件资料?
在进行大量资料更改前後,利用 WM_SETREDRAW 讯息来控制你的元件暂时不要重画,这不但可以使资料设定速度增快也防止元件闪烁的情况。
--------------------------------------------------------------------------------
第十三部分 控制项边框
13.1 为什麽我的元件的 Ctl3D 属性设为 True 之後,它依然没有 3D 的边框呢?
如果 ControlStyle 属性内没有包含 csFramed 旗帜那麽 Ctl3D 属性就会没有作用。在元件 的建构函式内加上:
ControlStyle := ControlStyle + [csFramed];
13.2 如何实作 BorderStyle 属性?
在控制项设定有没有边框之後要重新建立 window handle:
FBorderStyle: TBorderStyle;
procedure SetBorderStyle(Style: TBorderStyle);
property BorderStyle: TBorderStyle read FBorderStyle write SetBorderStyle;
procedure CreateParams(var Params: TCreateParams); override;
procedure TMyControl.CreateParams(var Params: TCreateParams);
begin
inherited CreateParams(Params);
if FBorderStyle = bsSingle then
Params.Style := Params.Style or WS_BORDER
else
Params.Style := Params.Style and not WS_BORDER;
end;
procedure TMyControl.SetBorderStyle(Style: TBorderStyle);
begin
if Style FBorderStyle then
begin
FBorderStyle := Style;
// 重新建立window handle
RecreateWnd;
end;
end;
--------------------------------------------------------------------------------
第十四部分 控制项式样
14.1 当元件重绘时如何防止闪动的情况?
如果元件的 ComponentStyle 属性没有包含 csOpaque 旗帜的话,呼叫 Invalidate方法时 会导致元件的背景先被擦掉再重绘。如果你在 Paint 方法中绘制背景,那你应该在元件的建构函式中加上:
ComponentStyle := ComponentStyle + [csOpaque];
Max Nilson的回答:
引起闪动另一个原因可能是 WM_ERASEBKGND 讯息的处理。当 VCL 控制项收到一个 WM_ERASEBKGND 讯息时,它会将元件的背景擦掉然後设定成预设的颜色。如果你的元件衍生自 TWinControl,而且元件的颜色与背景颜色不同(例如图形),每次重画以前都会将元件先清成背景颜色再重绘,这就是造成闪动的原因了!
解决的方法不难,你必须告诉 Windows 你要自行解决『所有的』绘图动作。不过有一个前提是,你一定要确定你的 Paint 方法将""个元件都画过,如果你漏了什麽地方忘了画,那个部分的资料会由乱数组成,你能想见这情况吗?使用这个方法可以加速你的元件绘制动作(稍微快一点点),因为少了一个填满背景颜色的动作。
type
TMyComponent = class (TWinControl)
...
protected
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
...
end;
procedure TBMyComponent.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
// 不要重绘背景,这会造成元件闪动
Message.Result := 0
end;
--------------------------------------------------------------------------------
第十五部分 视窗讯息
15.1 为什麽我的元件得不到方向键的讯息?
你必须拦截 WM_GETDLGCODE 才能处理方向键的讯息,在 WM_GETDLGCODE 的讯息处理 者中传回 DLGC_WANTARROWS。如果你不这样做,那方向键的功用就只能用来移动视窗焦点而己。
Max Nilson 的回答:
想要你的元件能够处理方向键,你必须要拦截 CM_WANTSPECIALKEY 元件讯息。 CM_WANTSPECIALKEY 元件讯息提供你比拦截 WM_GETDLGCODE 视窗讯息更容易且灵活的判断方法来决定是否需要某些特殊键的讯息。当控制项收到任何一个特殊键时就会送出CM_WANTSPECIALKEY 元件讯息给控制项。
特殊键包括:VK_TAB、VK_LEFT、VK_RIGHT、VK_UP、VK_DOWN、VK_RETURN、VK_EXECUTE 、VK_ESCAPE 及 VK_CANCEL。如果讯息传回值是非零值,这个键就会被送至 KeyPress 方法以供处理,否则这个键的讯息会被送至元件的父控制项,以预设方式来处理。
一个简单的范例:
type
TMyComponent = class (TWinControl)
...
protected
procedure CMWantSpecialKey(var Message: TCMWantSpecialKey); message CM_WANTSPECIALKEY;
...
end;
procedure TMyComponent.CMWantSpecialKey(var Message: TCMWantSpecialKey); begin
inherited;
// 我们只想处理向左方向键,其它的特殊键都给 Windows 处理
if Message.CharCode = VK_LEFT then
Message.Result := 1;
end;
CM_WANTSPECIALKEY 元件讯息比 WM_GETDLGCODE 讯息更具有弹性的地方在这儿。我们甚至可以根据是按下的是哪个特殊键才决定是否处理这个键。例如,你的控制项有三张影像,你可以让使用者利用左右方向键来回检视它们,如果翻到最後一张影像再按向右键时,焦点就让它离开元件,剩下的全部都让 Delphi 来处理。
15.2 有没有与 Visual Basic『DoEvents』同样功能的函式?
有。Application.ProcessMessages方法。