delphi(17)
时间:2006-06-10 来源:许我一个信仰
第18章 创建Windows程序
第18章将对已经介绍过的技术进行巩固,并示范如何使用这些技术来开发实际的程序。本章中的第一个程序是Rich Text编辑器。之所以选择这个例子,是因为在第14章中您已经熟悉了RichEdit控件;这里将着重强调Delphi为开发Windows程序所提供的各种支持。
本章中的应用程序并不难于开发,但却足以用于讨论应用程序开发的许多方面。而且Delphi 6引入了TAction组件,可以有效地减少所需编写代码的数量。由于前面的章节并未涵盖TActionList和TAction组件,本章将介绍这两个新的组件。另外,在示例程序中使用了MDI(多文档界面),前面的章节也没有涉及到。
为演示如何建立RichEditor.exe例子程序,我们在本章中将涵盖如下内容:分析与设计、工程的准备工作、MDI的使用、Windows注册表的管理、添加帮助文档,以及部署程序的准备工作。读完本章后,您可以全面了解Delphi所提供的工具,以及一些可用于支持开发具有专业水准应用程序的工具。
18.1 准 备 工 作
工程的准备工作有许多种形式。如果要建立原型,那么只要启动Delphi并了解要建立什么原型即可。在开发不太复杂的应用程序时,如本章中的例子,对工作进行一下综述也就足够了。如果开发的应用程序只供自己使用,可以跳过这一步。如果程序是供内部合作使用或为客户开发的,那么对工作陈述一下也就可以了。
比本章中的RichEditor更为复杂的程序,都至少需要进行文档化的、正式的分析与设计。不幸的是,事实刚好相反。许多程序都只是由管理者和程序员开发的,其中缺少一些必要的角色,如体系结构设计师、分析人员、设计人员、产品经理、工程经理、测试人员、质量保证负责人、文档专家、工具建立者、以及库管理人员。想像一下,如果航班没有行李管理人员、旅行代理人、售票代理人、保安人员、飞行员、副驾驶员、领航员、以及服务人员,那会出现什么样的混乱。当开发复杂软件时,每个人可能会担任多个角色,但是像体系结构设计师和工程经理这样的角色,其责任实在重大,所以要确保每个这样的角色至少由一个人来担任。对于体系结构设计师来说,由一个人建立的单一而一致的概念化模型可能是最好的。
程序员开发RichEditor程序的复杂程度,与飞行员驾驶单引擎小飞机相似。大多数情况下,一个单人小组即可完成工作。但是要谨慎,组织和计划即使对于单人小组也是很重要的。
对于我们的简单例子程序,单个人就能够担任所有的角色。对工作进行一下陈述也就足够了。可以把工作描述为:
“实现文本编辑器,能够同时读写多个Rich Text或DOS Text格式的文档。”
注意:如果您所在的工程不只一个人,那么使用版本控制产品并形成一致的目录结构是很重要的,这样可以有效地减少协作者之间的问题,还可以加快新人融入到团队的转变过程。随着时间过去,任何可以减轻工作负担的措施都会得到回报,即使非常简单的应用程序也是如此。
对于我们的简单程序来说,最后一点就是要组装有用的目录结构,并实现版本控制机制。基本的目录结构对于工程的组装是很有帮助的。
18.1.1 大有帮助的简单工作
当与其他开发者一同工作时,或同时开发多个工程时,基本的目录结构可以有效地减少迷惑。RichEditor的目录结构以目录树的形式如下列出。
-RichEditor
-Bin
-Documents
-Help
-Output
-Source
-VCL
当然,您可以使用任何目录结构,只要适合需求即可。但选定一种目录结构并保证一致的使用,可以减少源代码文件的混乱,而且Delphi还支持在多个目录进行不同的输出。Bin目录在RichEditor工程中用于编译过的可执行文件,本例中RichEditor.exe在编译后将写入到Bin子目录。Output目录用于存储编译过的单元,即.DCU文件,将其与源文件隔离开来。源代码文件将存储在Source子目录中。
18.1.2 版本控制
版本控制机制会跟踪源代码相对于时间的演化。您可以将单独的文件或整个的应用程序回复到以前的版本。有些产品还可以将缺陷与源代码关联起来,维护问题出现和解决时的线程信息。如果没有版本控制程序,您可以买一个。有许多可以选择的产品。高端产品包括Harvest、Clearcase和PVCS。低端和中级产品包括SourceSafe和StarTeam。所有这些产品都提供了基本的能力,可以将您的工作随时间的演化存储为多个版本。
注意:Starbase公司的StarTeam是相对较新的产品,包括服务器、桌面客户、Web界面,提供了源代码管理、缺陷跟踪和线程信息功能。
对于RichEditor,使用了Microsoft的SourceSafe产品。它相对较为便宜,并容易得到。SourceSafe的工作方式与Windows文件系统非常相似。可以将程序中的所有目录和文件都添加到SourceSafe。然后SourceSafe将对源代码(或其他检入的文件)进行写保护。当您打算修改文件时,可以检出这些文件。本质上,检出文件将把文件复制到本地驱动器并使之可写。
注意:对于您所使用的产品,请查询用户指南。不幸的是,即使是版本控制机制也并未一致采用。它需要软件过程管理机制的支持,许多开发团队尚未使用源代码控制机制。因此,关于如何使用源代码控制程序的书籍也相对较少。实际上,只要人需要在计算机上进行脑力工作,那么任何行业都需要版本控制产品,如律师、法律书记人员和图形艺术家。
通过使用诸如StarTeam和SourceSafe之类的版本控制产品,可以避免在不注意的情况下修改或删除文件,并阻止可能出现的并发修改,排除代价高昂的改动丢失情况。
18.2 开发中的Delphi工程选项
在开发过程中,Delphi的IDE中的工程选项可以进行限制性最强的设置。这使得Delphi能够尽早的帮助发现和解决问题。Project Option对话框包括许多属性页,其中有许多有用的选项,有助于工程的一般组织并可以向每个应用程序添加特定于工程的细节。
18.2.1 应用设置
Project Option对话框的Application属性页可用于指定应用程序的标题、帮助文件的位置,以及工程的图标。应用程序的标题是Rich Text Editor,并选定了一个合适的图标。可以使用Browse按钮来指定帮助文件。这些选项被编码到工程的.DPR文件中,如下所示。
Application.HelpFile := 'E:\Books\Osborne\Delphi 6 Developer''s
Guide\Chapter
18\Examples\RichEditor\Help\RichEditor\RICHEDITOR.HLP';
请记住,设计时的环境可能无法映像到部署环境。实际上部署环境是无法预测的,因此诸如帮助文件路径的信息必须通过编程动态确定。在主窗体的FormCreate事件中添加下面的代码,即可解决该问题。
Application.HelpFile := ExtractFilePath(Application.EXEName) +
'RichEditor.hlp';
除了要编写代码来动态解析帮助文件的路径之外,需要把动态信息存储到注册表或.INI文件中。RichEditor.exe程序使用了注册表。
18.2.2 设置运行时错误
在Project Option对话框的Compiler属性页上选定所有的运行时错误。虽然这些选项——范围检查、I/O检查、溢出检查——增加了编译后代码的开销,但它们在开发期间有助于发现错误,而发布程序时可以关闭这些选项。
范围检查
当修改Compiler Project选项后,必须重新编译程序才能使新的设置发挥作用。范围检查可以捕捉与索引过界有关的错误。例如,使用小于下界或大于上界的索引值索引数组A将引用未初始化的内存或其他数据的内存,从而导致未定义的行为。如果编译时打开范围检查选项,编译器将添加代码,用于捕捉索引过界错误。
范围检查对应于{$R+}编译器开关。如果启用了范围检查,将检查所有的数组和字符串索引表达式。如果索引过界,将引发ERangeError异常。范围检查增加了程序的开销,降低了程序的速度,最后的、发布之前的编译要关闭该选项。
输入/输出检查
I/O检查将检测所有文件输入输出操作的返回值。如果结果是非零的,将引发EInOutError异常。如果取消I/O检查选项,则需要调用IOResult来检查I/O函数的返回值。在调试过程中,打开这个开关,把编译器生成的I/O检查与IOResult代码合并起来,因为在取消I/O检查进行最后一次编译时,仍然需要对输入输出调用进行确认。
注意:调用IOResult进行检查不如捕捉EInOutError异常。上面一段表明了编译器开关所采取的措施。
溢出检查
当赋值给变量的数据包含了比变量的类型更多的比特位时,将发生溢出。例如,把大于2147483647的值赋值给整数变量时,有些位就会丢失。
溢出检查与{$Q+}编译器开关是等效的。如果溢出检查对于算术操作如+、-、*、Abs、Sqr、Succ、Pred、Inc和Dec失败,将引发EIntOverflow异常。
18.2.3 调试选项
当建立应用程序时,请打开Compiler属性页上的所有Debugging选项,如果希望跟踪到VCL的内部,还可以包括Use Debug DCUs选项。图18.1示范了开发过程中的Compiler选项设置。
图18.1 程序开发过程中的Compiler选项设置图示
一项很好的标准是解决所有以前编译器提示、警告或错误的代码。
另外,可以注意到(见图18.1)Message选项Show Hints和Show Warnings均被选中。在发布程序或VCL组件之前解决所有的编译器提示和警告是个好习惯。编译器提示和警告越早越容易解决。提示和警告实际上是潜在的错误,在其演变成错误之前较为容易解决。
18.2.4 加入版本信息
Project Option对话框的Version Info属性页可用于向应用程序加入内部版本信息。选中Include version information in project复选框以及Auto-increment build number选项,每次选择Project Build菜单项时,编译器将自动更新程序的Build号码并存储版本信息。
Version info属性页底部的Key和Value表格可以在程序中对版权、商标、版本信息以及自定义数据等进行编码。在File Properties对话框的Version属性页上可以看到该信息。Major、Minor、Release、Build号码是由About对话框上的TVersionLabel控件使用的,用于自动更新程序的版本和建立信息(使用TVersionLabel组件的源代码,请参见第10章)。
18.2.5 在RichEditor工程中指定目录和条件选项
协调工程目录、源代码控制目录、文件物理位置最简单的方法是对三者使用相同的路径。有时候懒一点也是有好处的。
注意:也许您听说过关于某些罕见的程序员的神话,他们编程的速度比平均速度快上十倍。这种人确实存在。他们像西部片中的枪手一样傲慢地迈着方步,仿佛有某些秘密一样。他们成功的秘密在于习惯。超级程序员所做的大部分事情都是纯粹、几乎不经大脑的习惯。他们有一种内部的风格指南,你可以认为这是一种地图,大部分时候都可以告诉他们做什么。如果还有余下的时间,可能是用于缩短并改进代码。代码越少,意味着错误越少,交互越简单,而用于注释的时间越少。
设置工程目录
如果对所有的应用程序都定义了同样的文件系统、源代码控制、以及目录结构,那确实就无须浪费头脑了。快速的程序员早已了解这一点,他们的习惯已经可以进行组织、计划、编码、部署以及测试,而无须花费很多时间了解为什么或怎么做。至于为什么,可能是某些激励因素使得他们希望快速完成工作,而怎么做,只要沿着困难最下的方向走下去就行了。要想编程快速,形成一些简单好用的习惯是一条好的途径。
按照这些指导性的原则,Project Options对话框的Directories/Conditionals属性页中指出了相关的物理目录的位置。Output编辑域指向Bin目录,存储编译后的程序;Unit域指向Output目录,存储.DCU文件,Search域为Source路径,可以添加路径。例如,如果需要调试创建的VCL组件,可以将VCL路径也添加到Search域。对于RichEditor.dpr工程,路径分别是\RichEditor\Bin、\RichEditor\Output和\RichEdiotr\Source。
所用的命名惯例
由于我们认为按照通常的原则好的习惯将快速产生好的结果,我们对RichEditor使用了Delphi命名惯例的一个简单的扩展。Delphi对字段使用F前缀,去掉F前缀将得到特性名。Delphi对类使用T前缀,去掉T前缀将得到对象的名字。扩展就是对单元名使用U前缀,用U替换类的T前缀将得到单元名。因此,单元如果包含了FormMain的定义,则其文件名是UFormMain,而类名则是TFormMain。该单元的片断如下所示:
unit UFormMain;
interface
…
uses
…
type
TFormMain = …
var
FormMain : TFormMain;
这样,确定在哪个文件中包含哪个类就不需要什么思考了。尽管可以使用Search,Find Files菜单项,但建立这种关系使得查找包含特定类的单元更为直接。
对于简单的RichEditor程序来说,以上是我们所需的准备工作。迄今为止,我们已经对工作进行了陈述,并准备好了源代码控制机制,新工程的选项也已经设置好,就等着创建源代码了。下面,我们要使该工程稳定下来,向它添加主窗体,这是Windows程序的起点。
18.3 建立主窗体
对工作的描述表明了RichEditor必须能够同时编辑多个Rich Text或文本文件。对于多个文档的编辑,较为理想的是多文档界面(MDI)协议,我们将在稍后讨论MDI。该程序是Windows程序,而且是编辑器。因此,它需要一个主菜单。大部分用户都需要工具栏和状态栏,因此还要向程序添加工具栏和状态栏。TTimer组件的OnTimer事件用于按固定的时间间隔更新状态栏,而且将使用新的TAction组件来提供基本的文件输入/输出、编辑菜单、字体格式化等功能。
注意:当程序随时间演化时,请记住开发者的首要目标是尽可能少写代码。由于我是个喜欢编写代码的人,因此我必须在编写代码的乐趣与经常存在的业务方面的需求进行协调,以避免编写太多的代码。“请记住,成功的开发者只需编写少量的高质量代码,而不是大量普通的代码(Booch,1996)。”
到现在为止,我们还没有讨论过MDI程序,本节我们将从创建窗体开始,自顶向下进行工作,最后分别以状态栏和TApplication组件结束。
18.3.1 多文档界面
MDI是指可以在一个父窗口中打开多个子窗口。它与单文档界面(SDI)程序相对,后者是基本的Windows风格的程序。MDI多用于字处理和电子表格程序,其中用户需要同时在多个文档上工作,对于程序员来说,子窗体是与用户每次工作的文档或工作表相对应的。
Delphi支持MDI程序非常容易,只需将主窗体的FormStyle特性设置为fsMDIForm或将子窗体的FormStyle特性设置为fsMDIChild即可。我们对工作所作的陈述表明,该程序需要读写多个文档,因此使用MDI是很直接的方法。在创建RichEditor.dpr工程后,就已经准备好建立该程序了。
完成的应用程序如图18.2所示。从图中可以看到主菜单、工具栏、前台包含一些Rich Text文本的MDI子窗口、以及位于窗体底部的状态栏。上述的每个元素都将在后面的小节中单独进行讨论。为准备好MDI程序的主窗体,可以按下列步骤进行。
图18.2 已完成的应用程序图示,其中包含一个
MDI子窗口,窗口中有一些Rich Text文本
1.在窗体的Caption特性中键入Rich Text Editor。
2.将主窗体的FormStyle特性值改为fsMDIForm。
3.键入FormMain作为窗体的Name特性。
4.使用我们已经采用的命名惯例,将主窗体保存为UFormMain.pas(UFormMain.dfm将与窗体单元一同自动保存)。
5.在窗体单元的顶部键入版权信息,如图18.3所示(这是个好习惯,您可能需要与公司的法律部门进行协调,以确定所需的特定文本)。
6.向主窗体添加FormCreate事件处理程序。该事件将调用Initialize方法。在主窗体单元的私有声明部分添加一个没有参数的空的Initialize方法。
7.向主窗体添加FormClose事件处理程序。该事件将显式关闭任何打开的MDI子窗体,并向用户提供机会来保存文档中正在进行的工作。
FormClose事件的代码如下所示。其他各部件的代码将在加入那些部件时逐次添加。
图18.3 示例版权信息。向每个单元都添加该信息,
表示文件名、作者以及版权标记等信息
procedure TFormMain.FormClose(Sender: TObject; var Action:
TCloseAction);
var
I : Integer;
begin
for I := MDIChildCount - 1 downto 0 do
MDIChildren[I].Close;
end;
MDI程序的主窗体维护列表MDIChildren,包含了对所用实例化的MDI子窗体的引用。该列表确保调用了每个已经打开的MDI子窗体的Close方法。如果没有该代码,关闭主窗体可能会不注意关闭子窗体,从而导致已修改的文本丢失。
18.3.2 添加TMainMenu组件
TMainMenu组件包含了一个TMenuItem组件的集合,您可以从Menu Designer(TMainMenu的组件编辑器或菜单模板)添加TMenuItem组件。从组件面板的Standard属性页向FormMain添加一个TMainMenu组件。在TMainMenu组件上双击,即可启动其组件编辑器。图18.4显示了TMainMenu组件的编辑器,其中包含了Rich Text Editor程序的菜单项。
图18.4 RichEditor.exe程序的菜单,在TMainMenu的组件编辑器中如图所示
要添加菜单项,单击任一被虚线包围的区域,该区域表示尚未初始化的菜单项,单击该区域使得该组件成为当前焦点。按键F11转到Object Inspector。如果为空白菜单项添加了标题,那么Delphi将创建菜单项组件并将其声明添加到类定义。另外,可以将TAction组件赋值给菜单项组件的Action特性,TAction对象具有在设计时填写附加特性的能力,如Caption、Shortcuts、Hints等(TActionList和TAction组件的细节,请参见使用TActionList的章节)。
提示:要添加菜单项,在已有的菜单项上单击要插入菜单项的前一个位置,并按键Insert即可。
提示:删除菜单项,单击相应的菜单项,按键Delete即可。
TAction组件和手工编辑菜单项可用于为RichEditor创建主菜单。主菜单有两方面可帮助您定义菜单:首先是显示菜单和子菜单名字的菜单视图,其次是UFormMain.dfm文件的片断。由于某些菜单项是使用TAction组件创建的,您可以将主菜单的完成推迟到阅读完本节和使用TActionList的一节,这样您就可以了解如何使用TAction创建菜单项。
提示:将&符号放置在菜单标题中每个字符之后,可以创建该菜单项的快捷键。例如,&Edit菜单项显示为Edit,按键Alt+E即可触发该菜单项。要触发子菜单项,当菜单打开时,对下划线字符按键即可。
提示:如果要在菜单标题中显示&符号,使用&&即可。
&File &Edit &Tools &Windows &Help
&New &Undo &Options &Cascade &Contents
&Open - &Tile Vertically &Topic Search
- Cu&t &Arrange -
Print Set&up… &Copy &Minimize All &About
- &Paste C&lose
E&xit Select &All
object MainMenu1: TMainMenu
Left = 16
Top = 40
object File1: TMenuItem
Caption = '&File'
object New1: TMenuItem
Action = FileNew
end
object Open1: TMenuItem
Action = FileOpen1
end
object N2: TMenuItem
Caption = '-'
end
object PrintSetup1: TMenuItem
Action = FilePrintSetup1
end
object N1: TMenuItem
Caption = '-'
end
object Exit1: TMenuItem
Action = FileExit1
end
end
object Edit1: TMenuItem
Caption = '&Edit'
GroupIndex = 1
object Undo1: TMenuItem
Action = EditUndo1
end
object N4: TMenuItem
Caption = '-'
end
object Cut1: TMenuItem
Action = EditCut1
end
object Copy1: TMenuItem
Action = EditCopy1
end
object Paste1: TMenuItem
Action = EditPaste1
end
object SelectAll1: TMenuItem
Action = EditSelectAll1
end
end
object Tools1: TMenuItem
Caption = '&Tools'
GroupIndex = 7
object Options1: TMenuItem
Caption = '&Options'
OnClick = Options1Click
end
end
object Window1: TMenuItem
Caption = '&Window'
GroupIndex = 8
object Cascade1: TMenuItem
Action = WindowCascade1
end
object Tile1: TMenuItem
Action = WindowTileVertical1
end
object ArrangeAll1: TMenuItem
Action = WindowArrange1
end
object TileVertically1: TMenuItem
Action = WindowMinimizeAll1
end
object Close1: TMenuItem
Action = WindowClose1
end
object N6: TMenuItem
Caption = '-'
end
end
object Help1: TMenuItem
Caption = '&Help'
GroupIndex = 9
object Contents1: TMenuItem
Action = HelpContents1
end
object SearchforHelpOn1: TMenuItem
Action = HelpTopicSearch1
end
object About1: TMenuItem
Caption = '&About...'
OnClick = About1Click
end
end
end
提示:使用减号符(-)作为标题,即可创建子菜单项之间的分隔符。
每个顶层的菜单项在DFM文件中都有一行定义,以关键字object开头;该菜单项的流化特性信息以关键字end结束。例如,对象Tools1:TMenuItem标题特性为‘&Tools’,GroupIndex为7(过一会儿,我们还要讨论GroupIndex特性)。Tools1的特性信息以关键字end结束,其前面的end用于嵌套菜单项Options1(参见列出的代码)。正是DFM文件中的特性嵌套表示了所有权,它可以用于确定菜单间的关系。所有使用TAction组件初始化的菜单项都列出了Action特性;所有通过手工键入标题创建的菜单项都具有Caption特性。
现在能够完成具有Caption特性的菜单项,可以使用菜单的层次和列出的DFM文件作为向导。如果GroupIndex存在,请确认已添加了该特性。如果菜单项具有OnClick事件处理程序,例如About1菜单项,然后您需要在Object Inspector中单击该菜单项的OnClick事件特性来创建事件处理程序。
添加About菜单项
为示范如何添加菜单项,我们来完成About1菜单项。About菜单项将显示About对话框,是使用第10章创建的TAboutBoxDialog组件实现的。
假定TMainMenu组件已经添加到主窗体,在TMainMenu组件上双击,即可触发TMainMenu组件的编辑器。按下列步骤即可完成About菜单。
1.如果尚未添加Help顶层菜单,可以在已有的顶层菜单最右侧的虚线矩形上单击(如果已经存在,该菜单项在Menu Designer上位于较左的位置)。选定矩形后,按键F11转到Object Inspector(如图18.5所示,在Object Inspector右侧可以看到选定的顶层空白菜单插入点)。输入菜单项标题&Help。在Help菜单的下方可以看到空白的子菜单项。
图18.5 Object Inspector的当前焦点位于某个顶层菜单项的Caption特性上,该菜
单项在Menu Designer中处于当前焦点,恰好位于Object Inspector的右侧
2.Help菜单的最后一个菜单项通常是About菜单。要添加About菜单,单击Help菜单末尾的空白菜单项,然后转到Object Inspector。
3.在Caption特性中键入&About,Delphi将自动的在类定义中插入TMenuItem组件并将其命名为About1(如果已存在名为About1的组件,Delphi会自动增加组件名的后缀,以确保组件名是惟一的)。
4.转到Object Inspector,确认在对象选择器中已选定About菜单项。单击Events属性页,双击OnClick事件特性来生成事件方法。
5.在事件方法的begin与end之间添加ShowAbout方法。
6.向窗体类的私有部分添加一个方法,即ShowAbout过程。按键Shift+Ctrl+C(自动完成类定义)即可添加空方法体,也可以手工键入方法体。
注意:尽早的建立接口并使之稳定化,是一个值得赞许的目的。通常这可以使你的程序稳定下来,同时其他的开发者也可以使用这些方法接口。请记住,使用你所编写代码的人,包括你自己在内,只会关心代码是否可以工作。另外,如果开发者需要扩展类,他可能还会关注保护权限的接口。
添加空的方法相当于建立了一个支点,从而可以将实现推迟到较为方便的时候,或者是已经选定了实际实现的时候。由于我们知道About行为是怎样实现的,因此现在就可以添加代码。
使用第10章的TAboutBoxDialog组件,从组件面板上双击该组件将其添加到主窗体。对话框只要调用Execute方法即可工作,而无须进行修改。下面列出了About行为的完整代码。
procedure TFormMain.ShowAbout;
begin
AboutBoxDialog1.Execute;
end;
procedure TFormMain.About1Click(Sender: TObject);
begin
ShowAbout;
end;
注意:请记住,所有的编程风格都是主观的。上面的代码使用了较短的过程,有些开发者可能会强烈反对这种风格。您可以根据几个基本的原则对编程风格进行选择,首先要考虑特定的代码片断是否易于调试,其次要考虑是否易于重用。事实上,方法与代码之间存在一种一一对应关系,这种关系主要是由编译器来处理,而与程序员的关系不大。要记住,开发者一次只能思考一件事,这件事越简单越好。
提示:记住,在添加新的行为时,要对该行为进行单元测试,以确保其工作正确,与预期符合。要避免直到添加了多个行为才开始测试。单元测试符合分而治之的原则。
我们来讨论一下由上面的代码引出的两个风格方面的问题。首先是是否应该使用Delphi提供的默认名字。从主观来说,可以这样做。假定是在某个特定的接口之内使用默认名字,那么不存在任何问题。可以花费时间来定义接口,但却只有一个About组件。因此默认的名字就足够了。反过来,如果有几个同一类型的组件,这些组件用于重要的代码中,或者默认的名字容易引起歧义,就需要为组件指定一个更好的名字。对于本例来说,默认的名字就行了。进一步的考虑编程风格,我们致力于使代码最小化。有些开发者可能比较极端,以至于轻视简单的代码。但如果所有的代码都这样简单,那么会减少很多错误。简明的代码是有益的,因为它引入错误的可能性非常小,易于编写,易于理解。如果代码看起来非常直接,那么已经基本达到了改进代码的目的。
使用GroupIndex特性
TMenuItem具有GroupIndex特性。GroupIndex特性用作自动合并时菜单(参见下一节)的虚拟占位符。考虑Tools菜单,其GroupIndex为7。Delphi可利用GroupIndex确定新菜单的相对位置。当我们创建MDI子窗体时,定义了相关行为的菜单可直接添加到该窗体。利用GroupIndex和AutoMerge特性,Delphi能够确定在主菜单的什么位置显示合并后的菜单。继续这个例子,任何GroupIndex小于7的菜单在主菜单上的位置都在Tools菜单前面。
如果被合并的菜单与已存在的菜单GroupIndex值相同,那么前者将完全取代后者。GroupIndex特性对于MDI程序特别有用。GroupIndex可用于将子窗体上的菜单项经过特定的改变,来替换主窗体上的同名菜单项。任意特定的子窗体都可以获得焦点,如果它具有主菜单,那么它的菜单可以自动合并到主窗体的菜单中。当它失去焦点时,将自动重新显示原来的与上下文相关的菜单。
考虑主菜单,其GroupIndex值对应于正确的索引值(在列出的DFM文件中指定),以确保可以正确地合并子窗体菜单。
自动合并菜单
对子窗体来说,TMainMenu的AutoMerge特性非常重要。考虑一个程序,可能具有多个并发的子窗体,如同Rich Editor的例子。子窗体的行为对主窗体没有意义。例如,当没有打开文档时,Save菜单项没有意义。保存什么呢?但从用户的角度看来,MDI子窗体的内容就是要保存的文档。当打开包含Rich Text的文档时,必须提供保存修改的功能。向文档添加Save行为是有意义的,这样也易于对子窗体的Save行为进行编码。
在子窗体中编码Save行为,这是面向对象主义者希望Rich Text文档所具有的功能。另外,如果在主窗体中加入该行为,将引入额外的复杂性,会造成一些问题,如:有活动窗体吗?哪个窗体是活动的?Save行为是作用于哪个窗体?
TMainMenu的自动合并特性协调了这种两难状况。向子窗体添加TMainMenu组件后,子窗体的菜单可以自动合并到主窗体的菜单,这样程序的行为就变得非常一致。考虑File菜单的例子。如果子窗体具有TMainMenu组件以及相同的菜单名和GroupIndex,则子窗体的菜单可以无缝地替换主窗体的菜单。参考稍后“建立编辑器窗体”一节中有关创建合并菜单的内容。
18.3.3 添加工具栏
TToolbar组件相对较新。也可以将TAction组件赋值给工具栏按钮,这大大的简化了创建工具栏的过程。对于添加工具栏来说,最重要的是定义所需要的行为,以及找到一些合适的组件以改善程序的外观。
设计RichEditor程序的工具栏组件
为确保与其他编辑器的一致性,其他的文本编辑器和字处理器通常所具有的特征都添加到了RichEditor程序的工具栏上。一般来说,工具栏按钮包括了在主菜单上已有的功能,使得这些常用的功能更加容易使用。由于RichEditor程序的工具栏上按钮较多,包括了创建新文档、打开现存文档、保存当前文档、打印、修改文档字体等功能,因此工具栏变得较大。
TToolbar具有自身的编辑器。只需从工具栏组件的上下文菜单上选择正确的操作,即可向工具栏添加按钮或分隔符(RichEditor程序的工具栏可以从图18.2看到)。要向工具栏添加按钮,使用右键单击工具栏并单击New Button菜单项即可;要添加分隔符只需单击New Separator菜单项(这两个菜单项如图18.6所示)。
图18.6 TToolbar组件的上下文菜单包括了New Button和
New Separator菜单项,使得设计工具栏非常方便
提示:单独的按钮也是组件。因此,在设计时它们可以获得焦点,在Object Inspector中分别进行修改。
从RichEditor的工具栏可以看到,从左到右分别是三个按钮和一个分隔符,一个按钮和一个分隔符,四个按钮和一个分隔符,三个按钮和一个分隔符,最后是一个按钮。如果主菜单上对应的操作存在相关联的TAction组件,那么这些TAction组件也用于相应的工具栏按钮。这样,所有的工具栏按钮都具有TAction组件,足以完成所需的功能。因此每个工具栏组件(不包括分隔符)的Action特性都赋值为某个TAction组件。
工具栏按钮与TAction组件如何协作
工具栏、工具栏按钮以及TAction都是组件。因此,这意味着三者都具有状态和行为。工具栏和工具栏按钮需要显示图像和提示,并对单击事件进行响应。TAction组件的行为包括初始化菜单项和工具栏按钮、显示图像和文字,并响应单击事件。如果把TAction组件分配给工具栏按钮或菜单项,它将负责显示文字和图像,并响应单击事件,与其原来的行为相同。
为创建RichEditor程序的工具栏,像上一节那样添加按钮和分隔符。TAction组件将负责行为特征,并显示按钮图标和提示。RichEditor的每个按钮都是Delphi 6中的TAction组件,从而无需寻找适当的图标并手工添加合适的行为。如果手工来做,就必须从头开始定义按钮,可以考虑两种方法:子类化TAction组件,或者修改按钮的Caption、Hint和OnClick特性来初始化所需的行为。而手工初始化的按钮图像还需要一些额外的工作。
您需要从组件面板的Win32属性页向窗体添加一个TImageList组件,使得工具栏按钮可以使用图像。将图像添加到图像列表,然后把TImageList组件赋值给TToolbar.Images特性,这样工具栏按钮就可以使用图像了。接着,将特定按钮所需的图像的索引赋值给TToolButton.ImageIndex特性,即可在按钮上显示相应的图像。
18.3.4 TActionList和TAction组件
使代码呈现内聚特性是代码复杂性管理的重要方面。粗糙的程序往往直接在事件处理程序中添加代码。而当程序的其他部分需要该事件的行为时,程序员可以调用事件处理程序,也可将事件特性指向同一处理程序,或者复制并粘贴代码。新手倾向于复制并粘贴代码,而有经验的开发者会调用事件处理程序,或者将多个事件特性指向同一事件方法。进一步的改善可以使用一个有名字的方法,在事件处理程序中调用该方法。
有名字的方法减少了注释而且易于使用,但指导原则是相同的:使代码内聚以管理复杂性。反过来,发散的代码增加了复杂性。
TActionList是非可视化控件,位于组件面板的Standard属性页上,该组件用于调整对同样的行为引入分散代码路径的潜在可能性。TActionList定义了ImageList特性以及TAction组件的集合。ImageList存储图标,用于分配给可以显示图像的可视化组件;而TAction集合存储了非可视化的TAction组件的列表,或可用的响应方式。TActionList具有设计时组件编辑器(见图18.7,图中Object Inspector的当前焦点是选定的TAction组件),可以用来管理动作。TActionList和TAction是组件,而且可以在运行时动态创建。我们将采取自顶向下的方法来示范如何向窗体添加TActionList组件、添加新的和标准的TAction组件、以及将TAction分配到合适的组件。
使用TActionList
在组件面板的Standard属性页上选定TActionList组件,然后即可将其添加到窗体。在Standard属性页上,TActionList组件位于最右侧。要在设计时使用TActionList的组件编辑器,将鼠标移动到该组件并单击右键,这时将显示TActionList的上下文菜单。从上下文菜单中选择Action List Editor菜单项即可。
提示:在TActionList组件上双击鼠标也可以显示编辑器。在设计时双击任何有编辑器的组件,都会显示相应的组件编辑器。
图18.7 TActionList组件编辑器用于在设计时维护所包含的TAction组件
添加新的动作 在打开动作编辑器(即TActionList的组件编辑器)后,按Insert键。将在编辑器右侧的动作列表中插入一个新的具有默认特性的TAction组件,它在左侧的Categories列表中属于(NO Category)。为便于组织,TAction组件可以与某个种类相关联(在下面的“定义动作”一段中,会涉及更多有关种类方面的知识)。
添加标准动作 TAction是非可视化组件。这意味着它们在运行时没有可视化的外观,而组件的作者也可以创建自定义的TAction组件。特别的,子类化的TAction组件也可以添加到VCL中(参见“创建自定义的标准动作组件”一节,其中有一个关于创建标准动作的例子)。已经存在的动作可称之为标准动作。
要向TActionList对象实例添加标准动作,按键Ctrl+Insert,然后从标准动作的分类列表中选择一个动作即可。Delphi 6引入了几个新的各种动作,可以节省相当的工作量(更多的信息,请参考“Delphi 6中新的标准TAction组件”一节)。
删除动作 动作编辑器的工作方式非常直观。Insert和Ctrl+Insert分别用于添加新的和标准的动作。Delete键可以从列表中删除一个TAction组件。另外,也可以使用动作编辑器的上下文菜单来完成这些操作。
定义动作 将TAction组件插入到动作列表中,就定义了一个新的动作。单击选定TAction组件,然后按键F11转到Object Inspector,新的TAction组件已成为当前焦点(再检查一次,确认已在对象选择器中选定了要修改的TAction组件,对象选择器是Object Inspector顶部的组合框)。TAction组件与其他组件类似,分配了一个默认名字,如Action1。由于动作将定义行为,最好使用动词和名词命名这些组件,分别表示其引发的行为和操纵的对象。例如,标准动作Cut是一个TEditAction组件;该组件可以命名为EditCut。在这里,Edit命名了组件的类别,而Cut描述了相应的行为。
可以想到,在TAction组件中存储了足够的细节,可用于初始化可视化组件,如菜单项或按钮,并对用户初始化的事件进行响应。按钮和菜单项都具有Caption、Name、GroupIndex、Hint、Shortcut和Image特性(TAction组件的图像是由ImageIndex图像代表的)。按钮和菜单项可以被选定,可以使之有效或失效,也可以对事件进行响应。表18.1列出了公开的动作特性和事件。
表18.1 TAction组件的特性和事件
属性名 |
类型 |
描述 |
Caption |
特性 |
动作的标题,将赋值给相关联组件的Caption特性 |
Category |
特性 |
用于在动作编辑器中对TAction组件进行组织 |
Checked |
特性 |
将赋值给相关联组件的Checked特性;改变TAction组件的Checked特性将同时改变相关联组件的Checked特性 |
Enabled |
特性 |
赋值给相关联组件的Enabled特性;改变TAction.Enabled特性将在相关联的组件中反映出来 |
GroupIndex |
特性 |
赋值给相关联组件的GroupIndex特性;改变将反映到相关联的组件 |
HelpContext |
特性 |
若有多个组件关联到同一TAction组件,将共享HelpContext |
Hint |
特性 |
用于初始化组件的Hint特性 |
ImageIndex |
特性 |
如果相关联的组件可以显示图像,将使用ImageIndex索引相应的TActionList的ImageList特性,从而得到图像 |
Name |
特性 |
动作组件的名字,使用名词和动词以保持简明 |
ShortCut |
特性 |
用于初始化加入组件的ShortCut特性 |
Tag |
特性 |
用于初始化加入组件的Tag特性 |
Visible |
特性 |
初始化并更新加入组件的Visible状态 |
OnExecute |
事件 |
当发生可执行的行为时,将调用赋值给该特性的事件方法;例如,如果按钮与TAction组件相关联,那么按钮单击时将调用OnExecute |
OnHint |
事件 |
将要显示加入组件的Hint字符串时,调用该事件 |
OnUpdate |
事件 |
当加入组件的状态需要进行更新时,调用该事件 |
为进行演示,我们把About1菜单项和OnClick事件方法转换为指向动作项。按下列步骤创建HelpAbout动作项,并将其用于RichEditor程序的Help,About菜单。
1.如果RichEditor的主窗体FormMain上没有TActionList组件,则添加该组件(TActionList位于Standard属性页的最右侧)。
2.双击TActionList组件,启动动作编辑器。
3.当动作编辑器获得当前焦点时(标题栏变蓝),按键Insert。这将在动作列表中创建新的动作项。
4.按键F11,转到Object Inspector。
5.将Caption特性改为‘&About…’(省略号是惯例,用于像About菜单项那样显示对话框的菜单项。)
6.将Category特性改为Help(也可从Category的特性编辑器中选择Help,如果该类别在列表中不存在,可以手工键入)。
7.将动作命名为HelpAbout(在Name特性键入‘HelpAbout’)。
8.单击Events属性页。双击OnExecute事件来创建HelpAboutExecute事件方法。在新的事件方法中加入对ShowAbout的调用。
9.保存工作。关闭动作编辑器,双击TMainMenu组件启动其编辑器。
10.在菜单设计器中选定About菜单项。
11.转到Object Inspector,对Action特性选择HelpAbout。
12.现在可以删除OnClick事件处理程序(只要删除代码,保存文件即可。Delphi将自动清除空的事件处理程序)。
这就是所要做的工作。当单击Help,About菜单项时,将调用OnExecute处理程序。下面的VCL代码来自Menus.pas,示范了如何将OnExecute与OnClick事件联系起来。当拥有TMenuItem组件的TMenu组件接收到单击事件时,它将直接调用相应菜单项的Click方法,从而将单击事件分派给正确的菜单项。
procedure TMenuItem.Click;
begin
if Enabled then
begin
{ Call OnClick if assigned and not equal to associated
action's OnExecute.
If associated action's OnExecute assigned then call it,
otherwise, call
OnClick. }
if Assigned(FOnClick) and (Action <> nil) and (@FOnClick <>
@Action.OnExecute) then
FOnClick(Self)
else if not (csDesigning in ComponentState) and (ActionLink
<> nil) then
FActionLink.Execute
else if Assigned(FOnClick) then
FOnClick(Self);
end;
end;
注意:可以像填写按钮或菜单的特性一样填写动作的特性,用OnExecute事件替换OnClick事件即可。TAction组件只是缺乏可视化的外观而已。像按钮或菜单这样的控件具有Action特性。将TAction组件赋值给需要属性和响应功能的控件的Action特性,这样TAction组件即替代了这些特性。
TMenuItem.Click过程将调用OnClick事件处理程序(如果有的话),而TAction的OnExecute和菜单项的OnClick并不是同一方法。这样,如果OnClick和Action.OnExecute都存在,而且是不同的过程,将调用OnClick事件方法。否则,如果处于运行时而且存在相关联的Execute事件处理程序,将调用该处理程序。在最后一个else条件中,如果上面的else条件都失败了,将调用FOnClick事件方法。
标准动作
标准动作是一些预定义的、并使用RegisterActions过程进行了注册的组件。类似于RegisterComponents,RegisterPropertyEditor和RegisterComponentEditor会执行一些必要的步骤,以便在VCL中定位正确的TAction组件。这些标准的TAction组件是与Delphi一同发行的,定义在StdActns.pas单元中。
提示:SetSubComponent是在Delphi 6中引入的,可以方便地设置组件的所有权。
通过定义和使用标准动作,可以在多个程序之间节省很多重复的工作,并且可以在程序之内和之间标准化各种行为。由于在Delphi 6中引入了组件的所有权,TAction组件可以拥有并在Object Inspector中显示子组件。这对于应用开发者是很有用的,而且可以定义很复杂的动作组件。
要向TActionList添加标准动作,在动作编辑器处于当前焦点时按键Ctrl+Insert即可。Delphi 6引入了几种标准动作组件,包括TFileOpen、TFilePrintSetup等,足以完成RichEditor中大部分菜单项的功能(新的标准动作组件的介绍请参见下一节)。
Delphi 6中新的动作组件
对于系统化常见的响应行为,标准动作组件跨出了有益的一大步。Delphi 6引入了多个新的标准动作组件,在RichEditor中可以用到其中的许多组件,减少了为这些行为编写代码的需求。表18.2中给出了RichEditor的菜单项与标准的动作组件之间的联系。
提示:参考Delphi 6帮助上下文菜单中的New VCL Feature一项。其中包括了新的动作组件的详细列表。
表18.2 有许多新的标准动作组件,这些组件对于RichEditor非常有用
(完整的列表请参考Delphi帮助中的“New VCL Features”部分)
菜单项 |
标准动作 |
描述 |
File,Open |
TFileOpen |
显示TOpenDialog对话框,使用户可以选择文件;可以用TFileOpen.Dialog特性修改TOpenDialog的特性 |
File,Print Setup |
TFilePrintSetup |
显示TPrinterSetupDialog对话框;可通过TFilePrintSetup.Dialog特性访问TPrinterSetupDialog |
File,Exit |
TFileExit |
结束程序 |
Edit,Undo |
TEditUndo |
向ActiveControl发送WM_UNDO消息 |
Edit,Cut |
TEditCut |
向ActiveControl发送WM_CUT消息 |
Edit,Copy |
TEditCopy |
向ActiveControl发送WM_COPY消息 |
Edit,Paste |
TEditPaste |
向ActiveControl发送WM_PASTE消息 |
Edit,Select All |
TEditSelectAll |
向ActiveControl发送WM_SETTEXT消息 |
Edit,Delete |
TEditDelete |
发送WM_CLEAR消息以删除选定的文本 |
Format,Edit |
TFontEdit |
显示TFontDialog对话框(参见表后面的示例代码,其中示范了如何响应字体的改变) |
Format,Bold |
TRichEditBold |
设置字体样式,包括fsBold |
(续表)
菜单项 |
标准动作 |
描述 |
Format,Italic |
TRichEditItalic |
设置字体样式,包括fsItalic |
Format,Underline |
TRichEditUnderline |
更新字体样式,包括fsUnderline |
Format,Strikeout |
TRichEditStrikeout |
更新字体样式,包括fsStrikeout |
Format,Align Left |
TRichEditAlignLeft |
将Alignment特性赋值为taLeftJustify |
Format,Align Right |
TRichEditAlignRight |
将Alignment特性赋值为taRightJustify |
Format,Align Center |
TRichEditAlignCenter |
将Alignment特性赋值为taCenter |
Format,Bullets |
TRichEditNumbering |
将TCustomRichEdit.Paragraph.Numbering改为nsBullet |
Window,Cascade |
TWindowsCascade |
调用TForm.Cascade方法 |
Window,Tile Vertically |
TWindowTileVertical |
如果窗体风格为fsMDIForm,向窗体发送WM_MDITILE消息 |
Windows,Arrange |
TWindowArrange |
调用TForm.ArrangeIcons方法 |
Window, Minimize All |
TWindowMinimizeAll |
对所有的MDI子窗体迭代,将WindowState设置为wsMinimized |
Window,Close |
TWindowClose |
调用ActiveMDIChild.Close |
Help,Contents |
THelpContents |
调用Application.HelpCommand |
Help,Topic Search |
THelpTopicSearch |
调用Application.HelpCommand,使用参数HELP_PARTIALKEY |
当尝试对各种实现途径进行选择时,所有新的标准动作组件都会让您省去许多努力。快速地看一下上面的列表,可以看到Windows API被用作执行其中的一些动作,如对话框操作、Application实例的一些操作以及精确的特性值等。
动作组件如TFontEdit等包括一个对话框。Execute行为就负责显示对话框。当用户单击OK(或等效的按钮)时,您需要编写一些代码来响应OnAccept动作。下面的代码示范了RichEditor如何对Font特性的改变进行响应。
procedure TFormEditor.FontEdit1Accept(Sender: TObject);
begin
with Sender As TFontEdit do
if( RichEdit.SelText = '' ) then
RichEdit.DefAttributes.Assign( Dialog.Font )
else
RichEdit.SelAttributes.Assign( Dialog.Font );
end;
警告:如果在对象赋值时不使用Assign方法,那么将进行引用赋值,这一点您需要注意。使用赋值操作符(:=)与C++中的引用赋值相似,而Assign则进行深复制。
上面的代码将分派给TFontEdit.OnAccept事件处理程序,因此使用动态类型转换将Sender参数转换为TFontEdit对象是安全的。如果没有选定文本,将修改默认的字体属性;否则将修改选定文本的属性。Dialog.Font特性指向TFont对象;在进行对象赋值时请记住要使用Assign方法。
创建自定义的标准动作组件
当我们讨论建立自定义动作组件时,有两点是非常重要的,要牢记在心。首先,组件只是一些可重用代码组成的程序包,实际上只是一些类;其次,TAction组件仍然只是一些组件。它们与非动作组件的不同之处在于,不需要使用New Component对话框,因为动作组件不会安装到组件面板,而注册动作组件需要使用RegisterActions过程而不是RegisterComponents过程。其他的都是相同的。
定义动作组件 增长式的修订可以提供健壮的解决方案,因为需要测试的东西很少,出错的可能性也较小。虽然有时候必须从零开始,但对于我们的目的而言,对TFileExit稍微修改一下即可达到目的。
我们将加入一个提示,让用户确认是否退出。TFileExit动作组件的ExecuteTarget方法实际上直接关闭了主窗体。因此,我们重载该方法,并只在用户确认退出的情况下调用继承方法。安装下列步骤可以创建自定义的TFileQueryExit动作组件。
1.单击File | New | Other菜单项,从New Items对话框的New属性页中选择Package,创建一个新的包(打开已有的包也可以)。
2.在Delphi中,单击File | New | Unit菜单项创建新的单元。
3.将单元保存为UFileQueryExit.pas。
4.相应的类可如下定义(下面列出了代码)。
5.添加接口Register过程的声明(见列出的代码)。
6.将Register过程定义为调用RegisterActions(见代码)。
7.定义类的实现(见代码)。
8.保存该单元。对包进行编译,并使用包编辑器进行安装。
下面列出了TFileQueryExit的代码。
unit UFileQueryExit;
// UFileQueryExit.pas - Extends TFileExit Action to include query
dialog
// Copyright (c) 2000. All Rights Reserved.
// By Software Conceptions, Inc. http://www.softconcepts.com
// Written by Paul Kimmel. Okemos, MI USA
interface
uses
Classes, Dialogs, StdActns, Controls;
type
TFileQueryExit = class(TFileExit)
private
FMessage : String;
function CanClose : Boolean;
public
constructor Create(AOwner : TComponent); override;
procedure ExecuteTarget(Target: TObject); override;
published
property Message : String read FMessage write FMessage;
end;
procedure Register;
implementation
uses
ActnList, UPKActions;
resourcestring
Prompt = 'Are you sure?';
procedure Register;
begin
RegisterActions( 'File', [TFileQueryExit], Nil);
end;
constructor TFileQueryExit.Create(AOwner : TComponent);
begin
inherited Create(AOwner);
FMessage := Prompt;
end;
function TFileQueryExit.CanClose : Boolean;
begin
result := (MessageDlg( Message, mtConfirmation, [mbYes, mbNo],
0) = mrYes);
end;
procedure TFileQueryExit.ExecuteTarget(Target: TObject);
begin
if( CanClose ) then inherited ExecuteTarget(Target);
end;
end.
该类添加了Message特性(在构造函数中初始化),以及资源字符串Prompt。私有方法CanClose,显示消息对话框,将用户响应作为布尔值返回。构造函数进行了重载以初始化新的Message特性,在ExecuteTarget方法中定义了扩展行为。扩展行为即首先显示提示,如果用户确认要关闭程序,则使用继承行为来关闭主窗体。
注册动作组件 当安装包时,将调用接口部分的全局过程Register。为注册动作组件,Register过程定义为调用RegisterActions。
RegisterActions( 'File', [TFileQueryExit], Nil);
第一个参数是动作组件所属类别的名字,用于对动作进行组织。第二个参数是要注册的类的数组,而第三个参数在这里是Nil(请参考下一段,其中有一个使用RegisterActions的第三个参数的例子)。
在注册动作组件后,可以使用动作编辑器将其插入到TActionList组件。要插入新的标准动作组件,可以使用Ctrl+Insert。RichEditor程序使用了新的标准动作组件TFileQueryExit,将该组件作为主窗体的Exit菜单项的动作。
创建初始化数据模块 RegisterActions过程的第三个参数是TDataModule子类的类。创建TDataModule,然后向其添加TActionList组件,并向TActionList添加一个TAction组件。请确认所添加的TAction组件的类与要初始化并修改特性的类是相同的。然后,当把数据模块的类名作为RegisterActions的第三个参数传递时,Delphi将创建该数据模块的一个实例,以及相应的TActionList和TAction组件,并使用数据模块中定义的TAction组件来初始化对应的TAction组件的新实例。
起先这看起来像是先有鸡还是先有蛋的问题。怎样创建并不存在的组件的实例呢?只要使用代码创建一个对象实例,其中包含组件即可。TFileQueryExit控件的代码是存在的。因此,您只需要创建一个新的数据模块,将UFileQueryExit单元添加到其uses子句,声明一个TFileQueryExit的公有实例,并在数据模块的DataModuleCreate事件方法对其进行初始化。
unit UPKActions;
// UPKActions.pas - Contains initializing actions for
RegisterActions procedure
// Copyright (c) 2000. All Rights Reserved.
// By Software Conceptions, Inc. http://www.softconcepts.com
// Written by Paul Kimmel. Okemos, MI USA
interface
uses
SysUtils, Classes, ActnList, StdActns, UFileQueryExit;
type
TPKActions = class(TDataModule)
procedure DataModuleCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
FileQueryExit : TFileQueryExit;
end;
var
PKActions: TPKActions;
implementation
{$R *.DFM}
procedure TPKActions.DataModuleCreate(Sender: TObject);
begin
FileQueryExit := TFileQueryExit.Create(Self);
FileQueryExit.Caption := 'E&xit';
FileQueryExit.Hint := 'Exit|Quits the application';
FileQueryExit.Name := 'FileQueryExit';
end;
end.
为注册用于初始化动作组件的数据模块,可以将数据模块的类名添加到包含动作组件的包中,修改RegisterActions过程调用,并且安装包。修改后的过程如下所示:
procedure Register;
begin
RegisterActions( 'File', [TFileQueryExit], TPKActions);
end;
从上面的代码可以注意到,第三个参数是数据模块的类名。
为完成本阶段的开发工作,可以将TFileQueryExit标准动作添加到主窗体的TActionList组件,并赋值给File | Exit菜单项的Action特性。
18.3.5 建立状态栏
状态栏是个很方便的地方,可用于放置一些表示程序的状态和动作的额外信息。显示的信息因应用程序的类型而异。用户可能认为显示键盘信息很有用。因为键盘灯显示了Num Lock和Caps Lock状态,但并未显示Insert或Overwrite状态。除了键盘状态信息之外,RichEditor的状态栏(如图18.8所示)上还显示了活动编辑窗口中光标的行列位置,以及当前时间信息。
图18.8 RichEditor的状态栏显示了编辑状态、光标位置、键盘状态,以及系统时间
首先我们需要从组件面板的Win32属性页向主窗体添加一个TStatusBar组件。默认情况下,状态栏是向窗体的底部对齐的。其次,双击状态栏启动Panels Editor。在Panels Editor中按Insert键六次,添加六个面板(结束时,分别是面板0到面板5)。根据表18.3的指导分别修改TStatusBar控件和各个面板。
表18.3 RichEditor程序中TStatusBar组件和六个面板的特性设置
组件 |
特性设置 |
StatusBar |
SimplePanel = False;SizeGrip = False;创建OnOwnerPanel和OnResize事件处理程序 |
(续表)
组件 |
特性设置 |
Panel 0,5 |
Width = 150 |
Panel 1 |
Width = 200 |
Panel 2,3,4 |
Alignment = taCenter;Style = psOwnerDraw;Width = 50 |
OnOwnerDraw和Style = psOwnerDraw用于创建键盘状态的可视化效果。OnResize事件处理程序用于为各个面板维护合理且足够的大小。下面是OnResize事件的代码:
procedure TFormMain.StatusBar1Resize(Sender: TObject);
var
I : Integer;
begin
StatusBar1.Panels[0].Width := ClientWidth;
For I := 1 to StatusBar1.Panels.Count - 1 do
StatusBar1.Panels[0].Width := StatusBar1.Panels[0].Width -
StatusBar1.Panels[I].Width;
end;
从代码可以看出,除面板0外的五个面板的宽度是固定的,面板0的宽度是浮动的,等于ClientWidth减去其余五个面板的宽度之和。
更新Modified状态
程序中的MDI子窗体属于同一类。在RichEditor中可能会同时打开几个编辑窗体。活动的编辑窗体将把一些文本发送到主窗体的状态栏,用来表示Rich Edit控件的修改状态。为限制编辑窗体过多了解主窗体的细节,将使用消息处理程序来发送该状态。
为便于发送消息,在一个单独的单元中定义了新的消息常数WM_UPDATETEXTSTATUS以及一个消息记录TWMUpdateTextStatus。
const
WM_UPDATETEXTSTATUS = WM_USER + 1;
type
TWMUpdateTextStatus = TWMSetText;
在主窗体中定义了一个消息处理程序来接收WM_UPDATETEXTSTATUS消息。并另外定义了一个方法来完成相应的工作。
procedure TFormMain.UpdateStatus( const Text : String );
begin
StatusBar1.Panels[0].Text := Text;
end;
procedure TFormMain.WMUpdateTextStatus( var Message :
TWMUpdateTextStatus );
begin
UpdateStatus( PChar(Message.Text) );
Message.Result := -1;
end;
使用消息处理程序的好处在于,编辑窗体无需了解主窗体的过多细节。它可以发送消息,然后不再关心。这样产生的代码是松耦合的,从而把主窗体实现的修改对编辑窗体的影响降低到最小。
注意:当在一个组件中进行了许多工作后,您可能希望把它保存为模板。Component | Create Component Template菜单项可以把组件放置到组件模板的Template属性页上,包括所有的属性以及事件处理程序的代码。可以把全功能的组件实现推迟到以后。
编辑器窗体定义了一个SetModified方法,可以更新Rich Edit控件的Modified特性,并调用该控件的UpdateStatus方法。TFormEditor.UpdateStatus向Application.MainForm所引用的窗体发送一个消息。这里的好处在于,编辑器窗体甚至无需了解主窗体的实际对象名,或其他关于主窗体的状态栏行为的精确的实现信息。
procedure TFormEditor.SetModified( const Value : Boolean );
begin
RichEdit.Modified := Value;
if( Value ) then
UpdateStatus( Modified )
else
UpdateStatus( '' );
end;
procedure TFormEditor.UpdateStatus( const Text : String );
begin
SendNotifyMessage( Application.MainForm.Handle,
WM_UPDATETEXTSTATUS, 0, Integer(PChar(Text)));
end;
您可以在主窗体中创建UpdateStatus方法,并将上面的代码编写为FormMain.UpdateStatus( Text )。但这意味着FormEditor必须维护FormMain对象的引用。这样的实现也还算不错,如果主窗体实例化为另一个对象,就需要修改代码。
最不可取的实现是直接修改StatusBaar。
procedure TFormEditor.UpdateStatus( const Text : String );
begin
FormMain.StatusBar.Panels[0].Text := Text;
end;
应该尽可能少像上面这样编写代码。它是紧耦合的,会导致脆弱的程序。如果窗体的对象名、状态栏控件、状态的实现方式、面板的数目或序号等发生了改变,那么就需要修改编辑器窗体的代码。即使存在上面那样僵硬的紧耦合代码,也绝不会是个好的选择。
更新行列信息
从编辑器窗体向主窗体发送行列信息也会引起类似的问题:编辑器了解行列位置,而主窗体显示状态。这里也使用消息机制来实现行列信息的显示。主窗体有一个信息处理程序来响应更新,并调用UpdateCursorPosition来更新状态栏。
procedure TFormMain.UpdateCursorPosition( const Text : String );
begin
StatusBar1.Panels[1].Text := Text;
end;
procedure TFormMain.WMUpdateCursorPosition( var Message :
TWMUpdateCursorPosition );
begin
UpdateCursorPosition( PChar(Message.Text) );
Message.Result := -1;
end;
当光标位置改变时,编辑器窗体通过消息通知主窗体。当发生TFormEditor.FormActivate或Rich Edit控件的SelectionChanged事件时,将调用TFormEditor.UpdateCursorPosition。该过程的实现如下:
procedure TFormEditor.UpdateCursorPosition;
var
CharPos: TPoint;
Text : String;
begin
CharPos.Y := SendMessage(RichEdit.Handle, EM_EXLINEFROMCHAR, 0,
RichEdit.SelStart);
CharPos.X := (RichEdit.SelStart -SendMessage(
RichEdit.Handle, EM_LINEINDEX, CharPos.Y, 0));
Inc(CharPos.Y);
Inc(CharPos.X);
Text := Format( CursorPosition, [CharPos.Y, CharPos.X] );
SendNotifyMessage( Application.MainForm.Handle,
WM_UPDATECURSORPOSITION, 0, Integer(PChar(Text)));
end;
Windows API过程SendMessage用于将SelStart特性转换为表示光标位置的X和Y坐标。使用资源字符串CursorPosition将光标位置格式化为文本,然后通过SendNotifyMessage方法通知主窗体。该代码使用基本的API调用来完成任务,属于相当底层的代码;其好处在于编辑器窗体和主窗体可以分别开发,任一窗体的修改都不会有什么不利的后果。前面的代码片断FormMain.StatusBar.Panels[0].Text := Text;在FormMain发生改变的情况下,可能导致访问违例、无效状态信息、或无法编译通过。
更新键盘状态和系统时间
系统时间可以使用TApplication.OnIdle事件,但TTimer.OnTimer事件的频率可以更加精确的控制。将定时器添加到主窗体,并把间隔设置为750毫秒(四分之三秒)。当OnTimer事件发生时,调用UpdateDateTime。第五个面板显示格式化的日期和时间,调用StatusBar.InvalidDate来重新绘制状态栏。
警告:请记住定时器资源是有限的,应节约使用。
procedure TFormMain.UpdateDateTime;
begin
StatusBar1.Panels[StatusBar1.Panels.Count - 1].Text :=
FormatDateTime( 'h:mm:ss AMPM', Now );
StatusBar1.Invalidate;
end;
回忆一下,面板2、3、4都具有psOwnerDraw风格,而状态栏具有OnDrawPanel事件处理程序。当整个状态栏都无效时(见上面的代码)更新整个面板。包括自定义的键盘状态面板。RichEditor的键盘信息面板是动态定义的,使用阴影字体来更新键盘字体,文本看起来是凸出或凹入的(见图18.8)。
procedure DrawShadow( const Text : String; Canvas : TCanvas;
Index : Integer; Rect : TRect;
Alignment : TAlignment; ForeColor, BackColor : TColor );
const
Alignments: array[TAlignment] of Word = (DT_LEFT, DT_RIGHT,
DT_CENTER);
var
Flags : Word;
begin
SetBkMode( Canvas.Handle, Windows.Transparent );
Canvas.Font.Color := BackColor;
Flags := DT_EXPANDTABS or Alignments[Alignment];
DrawText(Canvas.Handle, PChar(Text), Length(Text), Rect, Flags);
Rect.Left := Rect.Left - 1;
Rect.Top := Rect.Top -1;
Canvas.Font.Color := ForeColor;
DrawText( Canvas.Handle, PChar(Text), Length(Text), Rect, Flags
);
end;
procedure TFormMain.StatusBar1DrawPanel(StatusBar: TStatusBar;
Panel: TStatusPanel; const Rect: TRect);
procedure DrawPanel( const Text : String; ForeColor,
BackColor : TColor );
begin
DrawShadow( Text, StatusBar.Canvas, Panel.Index, Rect,
Panel.Alignment, ForeColor, BackColor );
end;
begin
// update statuskeys
case Panel.Index of
2:if ( GetKeyState( VK_INSERT ) <> 0 )then
DrawPanel( 'INS', clGray, clWhite )
else DrawPanel( 'OVR', clBlack, clWhite );
3:if( GetKeyState( VK_NUMLOCK ) = 0 ) then
DrawPanel( 'NUM', clGray, clWhite )
else DrawPanel( 'NUM', clBlack, clWhite );
4:if( GetKeyState( VK_CAPITAL ) =0 ) then
DrawPanel( 'CAPS', clGray, clWhite )
else DrawPanel( 'CAPS', clBlack, clWhite );
end;
end;
提示:虚拟键,如VK_NUMLOCK等,是在Windows.pas单元中定义的,该单元与Delphi一同发布。
第一个过程DrawShadow在前面已经看到过,它被用于第9章的扩展字体标签。通过使用两种颜色在两个具有很小偏移的位置绘制文本两次,从而形成了阴影效果。OnDrawPanel事件处理程序使用了嵌套过程DrawPanel,该过程调用DrawShadow,并根据每个面板的索引向DrawShadow传递文本、面板的前景色和背景色。GetKeyState有一个参数,表示虚拟键,返回表示状态的整数。
18.4 建立编辑器窗体
编辑器窗体是MDI子窗体。而MDI允许在父窗体中同时打开多个同类型的文档子窗体。编辑器窗体只需将RichEdit控件对齐到窗体。简要叙述一下,将窗体命名为FormEditor;文件保存为UFormEditor;将窗体风格设置为fsMDIChild;向窗体添加一个RichEdit控件,将其Align特性设置为alClient。
我们还有一些困难需要解决。如果将特定的编辑行为集成到主窗体中,那么主窗体在进行操作前需要确定当前的编辑器窗体。例如修改字体,主窗体将显示字体对话框,但必须确定当前的子窗体是哪一个。另外,如果我们把字体编辑行为放到编辑窗体中(从语义来说,该行为是属于子窗体的),那我们必须解决如何将该行为合并到主菜单中。幸运的是,主菜单知道如何来做。还记得AutoMerge特性吗?
通过向编辑器窗体添加TMainMenu组件,可以在编辑器窗体上定义特定于编辑器的行为;而且当每个特定的窗体获得焦点时,它的菜单可以自动合并到主菜单中。除了需要把AutoMerge特性设置为True之外,GroupIndex特性可以帮助确定把特定的菜单和菜单项合并到何处的问题。
18.4.1 自动合并Format菜单
Delphi 6已经定义了RichEditor所需的新的动作组件。因此我们可以使用标准的动作组件,并能够以最小的代价在程序中引入由菜单和工具栏驱动的丰富的字体编辑功能。要添加具有字体编辑功能的Format菜单,按照下列步骤即可:
1.向FormEditor添加TMainMenu组件。
2.插入新的&Format菜单。
3.向编辑器窗体添加TActionList组件。
4.如图18.9所示,将Rich Edit控件相关的编辑动作组件添加到TActionList组件的Format类别,并将这些动作组件分派到Format菜单的各个菜单项。
图18.9 使用动作编辑器和如图所示的Format菜单
来建立RichEditor编辑器的Format菜单
5.将FontEdit标准动作组件添加到Dialogs类别,并将其分派到图中所示的Select Font…菜单项。
这就是我们所需要做的。由于TAction组件已经定义了菜单项的行为和外观,因此只需将动作组件分派到空白的菜单项即可,而不需要编写代码。但TFontEdit动作组件确实需要“Delphi 6中新的动作组件”一节中演示的OnAccept处理程序。
要记得把TMainMenu.AutoMerge特性设置为True,并将Format菜单的GroupIndex特性设置为主窗体上某两个相邻菜单之间的索引值,具体的值依赖于您希望该菜单所放置的位置。这里使用的GroupIndex值为3。
所有其他的菜单项都合并了一些对编辑窗体有用的行为,但所用的技术是相同的。使用GroupIndex来确定菜单合并的位置。出于节省空间的考虑,程序的完整代码放在本书的CD-ROM上,并未收录在这里。
18.4.2 创建一个惟一的临时文件
当单击File | New菜单项时,可使用两个Windows API函数来创建惟一的文件名。即GetTempPath和GetTempFileName。这两个函数可以返回位于\Temp目录、扩展名为.TMP的文件。RichEditor程序对这两个函数进行了包裹,可以返回位于指定目录、扩展名为.RTF的文件。
function GetTempPath : string;
begin
result := StringOfChar( #0, MAX_PATH );
Windows.GetTempPath( MAX_PATH, PChar(result));
result := TrimRight(result);
end;
function GetTempFileName( Directory : String = '';
Extension : String = '.tmp' ) : string;
var
FileName : String;
begin
if( Directory = EmptyStr ) then
Directory := GetTempPath;
FileName := StringOfChar( #0, MAX_PATH );
Windows.GetTempFileName( PChar(Directory), 'doc', 0,
PChar(FileName));
FileName := TrimRight(FileName);
if( Extension <> '' ) and (Extension <> '.tmp' ) then
begin
result := StringReplace(FileName, '.tmp', Extension,
[rfIgnoreCase] );
RenameFile( FileName, result );
end;
end;
注意:很明显,上面的GetTempFileName可以进行简化,只返回具有.rtf扩展名的文件,这样将无需传递并测试Extension参数了。但这里却定义了一个更为一般的函数,其目的在于适应可能在以后出现的需求。经验证明,在全局过程设计中多一点前瞻性的眼光,就可以省掉以后很多的工作。
GetTempPath通常返回‘C:\Temp’。GetTempPath的结果初始化为包含了足够的空间,可以容纳最大的路径长度。GetTempPath只有在GetTempFileName的Directory参数为空字符串时,才会被调用。局部变量FileName初始化为包含了MAX_PATH大小的空间,在调用API过程GetTempFileName时使用了‘doc’前缀,因此结果文件名会形如doc23.tmp。如果Extension参数不是空字符串或‘.tmp’,将使用Extension参数替换扩展名‘.tmp’。FormEditor传递的Extension参数为‘.rtf’。最后,重新命名得到的文件。
18.5 永久保存注册表中应用程序的设置
应用程序选项保存了帮助文件的名字和路径、默认的工作目录、以及表示是否备份文件的复选框,这些在Options窗体上可以看到。Delphi的Project | Options窗体可以用作模型。即使在非常简单的程序中,Options窗体及其从注册表存储和获取数据项的能力都是用单独的对象实现的。如果把这些对象合并起来,那么获取注册表设置时将必须实例化窗体,而有时候可能不想或无法这样做。
这里创建了两个单独的类TAppRegistry和TFormOptions来实现程序设置的持久化。Options窗体使用了编辑域和标签,在创建窗体时将注册表设置读入到控件中,但用户单击OK时把设置写回到注册表。TAppRegistry由Delphi中的TRegistry子类化而来。由于注册表实际上只有一个实例,我们在程序中将使用TAppRegistry类的单对象实例。这意味着,要访问注册表,必须使用该对象实例。
function AppRegistry : TAppRegistry;
begin
if( Not Assigned(FAppRegistry)) then
FAppRegistry := TAppRegistry.Create;
result := FAppRegistry;
end;
函数AppRegistry在接口部分声明。对于程序的其他部分来说,它与对象实例相似。函数的实现确保了在每次调用函数时,有且只有一个有效的对象实例存在。本地变量FAppRegistry用作该类的惟一实例,分别在单元的initialization和finalization部分初始化和清除。
initialization
FAppRegistry := Nil;
finalization
FreeAndNil(FAppRegistry);
在TAppRegistry中定义了一些基本的方法来满足RichEditor程序在读写注册表方面的特定需求。
function TAppRegistry.GetString( const Key, Name : String ) :
String;
begin
result := EmptyStr;
if( OpenKey( AppKey + Key, True )) then
begin
result := ReadString(Name);
CloseKey;
end;
end;
procedure TAppRegistry.SetString( const Key, Name, Value : String
);
begin
if( OpenKey( AppKey + Key, True )) then
begin
WriteString( Name, Value );
CloseKey;
end;
end;
function TAppRegistry.GetBool( const Key, Name : String ) :
Boolean;
begin
result := False;
if( OpenKey( AppKey + Key, True )) then
begin
try
result := ReadBool( Name );
except
on ERegistryException do
result := False;
end;
CloseKey;
end;
end;
procedure TAppRegistry.SetBool( const Key, Name : String ;
Value : Boolean );
begin
if( OpenKey( AppKey + Key, True )) then
begin
WriteBool( Name, Value );
CloseKey;
end;
end;
构造函数设置了从TRegistry继承而来的RootKey特性。SetString、GetString、SetBool、以及GetBool分别定义了一些基本的行为。读写注册表的顺序是先调用OpenKey,然后使用读写方法,最后调用CloseKey。使用上述的基本方法,还为RichEditor程序实现了一些特定的功能,在这里没有列出。对于TAppRegistry和TFormOptions的完整代码,可以参考本书CD-ROM上的代码。
//title here : fit and finish
18.6 使程序合乎需要
程序员编写代码的能力都很强,但他们作为设计人员、分析人员、质量保证人员却表现不佳。这些职位需要对软件开发的某个特定方面负责。但程序员很少有这种时间。不幸的是,许多公司都处于CMM 1级别,即所谓的初始级。“如果一个组织处于初始级,开发过程是临时而无秩序的,开发的成功依赖于少数特别专注的开发人员的英雄式的努力。”(Booch,1996)。如果您所工作的组织并非上述情况,那么您就太幸运了。另一方面,如果情况不怎么样,也不要失望。有许多可用的工具和资源可以帮助你有效地利用时间。
18.6.1 调试与测试
如果您是独立开发者,那么大部分的调试和测试工作都需要您自己来完成。如果有正式的测试过程,那么本节仍然适合您。
程序员必须对所有的代码进行单元测试,更强一些的是白盒测试。在把算法、类、单元集成到程序中之前,您需要在单元测试中测试这些部件。除去有意定义了聚集关系的地方之外,代码应该可以在算法、类、单元一级独立而无错的运行。创建一个简单的工作台,即所谓的测试程序,即可对代码进行测试。白盒测试更为严格,它需要测试所有可能的代码路径。在第9章的TDebug组件中所创建的Trap行为可以确保测试了每个代码路径。
使用断言(assertion)可以发现很多有害的错误、无用或不必要的代码、以及有缺陷的解决方案。不一定要找到所有的错误,但必须知道这些错误是存在的,还要确定必须在发布程序之前解决掉哪些错误。理想情况下,所有的软件在发布之前都必须是无错的,但这并不是现实。
18.6.2 质量保证
质量保证就是使程序合乎需要的过程。产品的行为是一致的吗?窗体是对称的吗?文字的拼写是否正确?由专人直接负责质量保证工作,可以极大地提高用户的满意度和产品的可靠性。
QA(即质量保证)应包括黑盒测试,即从外部对系统进行测试,而无需知道系统内部是如何实现的。由于程序员非常熟悉代码,他们通常知道如何避开缺陷。较为出色的产品,如Starbase公司的StarTeam等,就包括了缺陷的估价与跟踪。这使得开发者可以进行合作、跟踪并解决各种缺陷。其显著效果远远超过了所需的成本,即使对于独立工作的开发者来说也是如此。
18.6.3 文档
许多现存产品的帮助文档都是仓促创建的。良好的开发过程需要有职业技术作家来创建帮助文档,包括在线帮助、用户指南、安装与配置信息、以及技术手册。RichEditor程序的帮助文件是使用RoboHelp工具创建的。该工具可以使任何作者都能写出职业水准的帮助文档。
注意:文档是软件开发外包的一个重要部分。外包提高了以固定成本完成文档编写的可能性,而无需长期雇用职业的技术作家。通常,技术文档写作可以与开发过程并行进行,特别是在分析与设计文档中包括了用例、书面描述、以及原型的情况下。
18.7 工程部署选项
祝贺你!您现在已经建立了自己的Windows程序。再来一次编译即可发行。最后一次建立工程包括设置工程选项、最后一轮测试以及质量保证工作,建立安装用CD母盘,再烧制100000片拷贝。
对于Delphi程序来说,要关闭所有的运行时错误和调试选项。在Project菜单上选择Build来建立RichEditor程序。然后进行一轮用户测试和质量保证过程。自动化工具,如Rational公司的SQA套件中的Robot,可以使这一切都非常快速。接着建立CD母盘。可以使用Delphi自带的InstallShield Express工具进行。要记住再测试一次安装过程,并对从CD母盘进行的安装作黑盒测试。如果一切都满足预期要求,那么您的产品已经完成,可以发行它了。
18.8 小 结
第18章涵盖了许多材料,大部分都与建立Delphi程序并且使用任意语言建立和部署软件相关。过程是关键性的,但根据要建立的软件和涉及的人员而使用不同级别的过程是可以接受的。本章的讨论是最低程度的,并在建立Windows MDI程序RichEditor时对其进行了示范。除了建立程序之外,您还学习了Delphi 6中引入的TActionList和TAction组件。
编程是一种全职工作。如果您在程序设计中担任了许多角色,那么您的不利地位是显然的。创建自定义类,如TFileQueryAction组件,可以使您避免重复工作。与程序设计的最小化方法结合,自动化工具可以使一两个开发者完成一支军队的任务。Delphi是极好的软件开发工具,但只依赖于Delphi是不经济的,那样你将在战术上处于显然的不利地位。