4.5定义公共体系结构:使用对象库
Delphi使应用程序的开发变得容易,以前,要花费很大的精力用于建立应用程序的体系结构,但现在可以轻松多了。问题是,很多开发者往往急于写代码而很少考虑应用程序的结构,这使得一个项目往往以失败而告终。
4.5.1考虑应用程序的体系结构
本书不打算专门讲述体系结构或面向对象的分析和设计。不过,我们认为这是非常重要的。附录C"参考读物"列出了一些关于面向对象的主题。在开始编写代码之前最好先阅读附录C的内容。
下面列出了一些应当考虑的问题:
1. 体系结构支持代码重用吗?
2. 应用程序中的模块、对象等能够本地化吗?
3. 修改体系结构非常容易吗?
4. 用户界面和后端可以本地化吗?
5. 体系结构支持团队开发吗?或者说,团队的成员可以工作于各自的模块吗?
上面这几个问题其实只是开发过程中要考虑的一部分问题。
关于相关内容的书籍很多,我们无意与它们竞争。下面将举例说明怎样设计一个数据库应用程序的通用用户界面。
4.5.2Delphi固有的体系结构
你可能经常听到这样一句话,即作为一个Delphi开发者,没必要是一个组件编写者。尽管这句话是正确的,但下面这句话也是正确的:如果你是一个组件编写者,就一定是一个更优秀的Delphi开发者。
这是因为,组件编写者清楚地知道对象模式和Delphi应用程序的体系结构,这意味着组件编写者能够更好地发挥它们的优势。你可能已经听说过,事实上Delphi本身就是用组件编写的。Delphi本身就是一个运用体系结构的例子。
即使并不想编写一个组件,但掌握体系结构还是有好处的。应当像熟悉Win32操作系统那样熟悉VCL和Object Pascal模型。
4.5.3体系结构的例子
为了证明窗体继承以及对象库的能力,下面将定义一个通用的应用程序体系结构。重点是代码重用性、修改的灵活性、一致性和易于团队开发。
窗体继承,更准确地说是框架,它们的典型应用是在数据库应用程序中。窗体应当对数据库的操作(编辑、添加或浏览)具有感知能力。窗体还应当包含一些通用控件,例如工具栏和状态栏,以便对数据库表进行操作。这些控件随窗体状态变化。另外,这些窗体还应当提供事件,以便跟踪窗体模式的变化。
应用程序的框架应当允许团队开发,每个成员可以各自工作于应用程序的一部分,而不至于出现重复和覆盖。
框架分为3个层次,后面将详细介绍这3个层次。表4-4描述了框架中每个窗体的用途。表4-4框架中的数据库窗体
4.5.4子窗体TChildForm
TChildForm是那些能够被单独打开的模式窗口无模式窗体并能成为其他窗口的子窗口的基类。
TChildForm支持团队开发,每个成员可以工作于应用程序的一部分。同时,TChildForm也实现了漂亮的用户界面,用户可以在应用程序内打开一个窗体,作为一个单独的实体。清单4-3是TChildForm的源代码。清单4-3TChildForm的源代码
上述代码演示了下列技术:首先是重载,这是对Object Pascal语言的扩展;其次是怎样使一个窗体成为另一个窗口的子窗口。
1.提供第二个构造器
你可能注意到了,上述代码中声明了两个构造器(constructor)。第一个构造器用于创建一个普通的窗体,它需要传递一个参数。第二个构造器需要传递两个参数,它重载了第一个构造器。如果要使窗体成为子窗口,应当使用第二个构造器。其中,AParent参数用于传递父窗口。注意,这里用了reintroduce指示符,这样编译器就不会发出警告了。
第一个构造器只是简单地把FAsChild变量设为False,以保证创建的是一个普通的窗体。第二个构造器把这个变量设为True,并且把FTempParent设为AParent参数的值,这个值将在Loaded()方法中作为父窗口。
2.使一个窗体成为子窗口
要使一个窗体成为子窗口,有几件事情需要做。首先,要确保窗体的属性已经正确设置,正如在TChildForm.Loaded()中看到的那样。清单4-3的代码能保证窗体变成一个子窗口而不是一个对话框,这是通过把边框隐去来实现的。如果这个窗体只用做子窗口,可以在设计时设置这些属性。如果这个窗体有可能要用作一个普通的窗体,应在FAsChild变量设为True的情况下才设置这些属性。
还要覆盖CreateParams(),以告诉Windows把窗体作为子窗口。要实现这一点,需要把Params.Style属性设为WS_CHILD风格。
TChildForm并不只限于数据库应用程序。事实上,可以把它用在任何需要把窗体作为子窗口的场合。
4.5.5数据库基础模式窗体TDBModeForm
TDBModeForm是从TChildForm继承下来的。它能够感知数据库的状态(浏览、插入、编辑)。TDBModeForm还提供了一个事件,以跟踪数据库状态的变化。
清单4-4列出了TDBModeForm的源代码。清单4-4TDBModeForm
TDBModeForm的实现比较简单。尽管这里使用了一些目前还没有介绍的技术,但你应当能看出它的作用。首先,这里声明了一个枚举类型TFormMode,用于表示窗体的状态。其次,TDBModeForm提供了一个FormMode属性以及它的读写方法。关于属性和读写方法将在第21章"编写自定义组件"中详细介绍。
4.5.6数据库导航/状态窗体TDBNavstatForm
TDBNavstatForm具有框架的许多功能。TDBNavstatForm中包含了一些用于数据库应用程序的通用组件。特别是,它包含一个导航栏和状态栏,能够随数据库的状态而发生变化。例如,当数据库处于fsBrowse状态,导航栏上的Accept按钮和Cancel按钮就被禁止。当用户使数据库进入fsInsert状态或fsEdit状态,这两个按钮将生效。状态栏上将显示数据库的状态。
下面的清单4-5列出了TDBNavstatForm的源代码。注意,这里去掉了组件的列表。当打开这个范例项目时会看到这些列表。
这里主要处理了一些TToolButton组件的事件,用于设置窗体的当前状态。这实际上是调用覆盖SetFormMode(),再由SetFormMode()调用SetButtons()和SetStatusBar()实现的。SetButtons()能够根据窗体的状态来决定按钮的可用或不可用。清单4-5TDBNavstatForm
你可能注意到了,上面的代码中有两个过程用于修改TToolBar组件和TStatusBar组件的父窗体。当窗体作为子窗口打开时,应当把TToolBar组件和TStatusBar组件的父窗体设为主窗体。当运行随书附带光盘上\FormFramework目录中的项目时,就会知道这样做的意义。
正如前面提到的那样,TDBNavStatForm既可以是一个独立的窗体,也可以是一个子窗口。下面的代码演示了怎样把TDBNavstatForm作为一个独立的窗体调用:
下面的代码演示了怎样把TDBNavStatForm作为子窗口调用:
上面这个过程不仅把TDBNavStatForm作为pnlParent(TPanel组件)的子窗口,同时还使主窗体成为TToolBar组件和TStatusBar组件的父窗体。另外,TMainForm.mmMainMenu.Merge()这一行的作用是把TDBNavstatForm实例上的菜单合并到主窗体的菜单中。当然,当释放TDBNavStatForm的实例时,必须这样调用TMainForm.mmMainMenu.UnMerge():
看一看随书光盘中的范例。图4-l显示了TDBNavStatForm作为一个独立的窗体和作为一个子窗口的情况。实际上,这里可以用TImage组件来代替子窗口。
以后,我们将把这个框架扩展为功能齐全的数据库应用程序。图4-1TDBNavstatForm作为一个独立的窗体和作为一个子窗口
4.5.7使用框架进行应用程序结构设计
Delphi5提供框架功能,可以创建能被嵌入到其他窗体中的组件容器。这一点和我们用TChildForm进行的演示类似。然而,框架允许在设计时使用组件容器,并且可以把这些组件容器加到组件板中,以便将来重用。清单4-6演示了框架的功能。清单4-6框架演示
在清单4-6的代码中,我们显示了一个主窗体,它包含两个由独立的面板构成的长方块。右边的面板用于包容框架。我们已经定义了两个独立的框架。在private段,FFrame被定义为TFrame类型。由于两个框架都是直接来自TFrame,所以FFrame能够直接访问它们。位于主窗体上的两个按钮,各自创建一个不同的TFrame并且将其赋给FFrame。这和TChildForm的效果相同。
4.6一些项目管理的功能
下面将介绍一些项目管理的功能,这对许多使用Delphi5的开发者是有帮助的。
4.6.1在项目中添加资源
前面讲过.res文件是应用程序的资源文件,以及什么是Windows资源。要在项目中添加资源,可以创建一个单独的.res来存储要加到应用程序中的位图、图标、光标等资源。
必须使用专门的资源编辑器来创建.res文件。创建了.res后,只要在项目文件中加上下面这行语句,就能使资源链接到应用程序中:
上面这行语句可以紧接在下面这行语句的后面。下面这行语句的作用是,把一个与项目文件同名的资源文件链接到应用程序中:
如果已经这样做了,这时可以通过TBitmap.LoadFromResourceName()或TBitmap.LoadFromReourceID()来调入资源文件中的资源。清单4-7演示了怎样从资源文件中调入一个位图、图标和光标。注意,这里用到了一些WindowsAPI函数,例如LoadIcon()和LoadCursor(),可以在WindowsAPI的帮助中找到它们的说明。
注意:WindowsAPI中有一个LoadBitmap()函数,它可以调入一个位图,但它不能返回调色板,也就是说,它无法调入256色的位图。因此,建议使用TBitmap.LoadFromResourceName()或TBitmap.LoadFromResourceID()。清单4-7从资源文件中调入资源的例子
4.6.2改变屏幕光标
Cursor属性可能是TScreen最常用的属性之一,它的作用是改变应用程序的光标。例如,下面的代码把光标改为砂漏状,表示现在正在进行一个较长时间的操作。
crHourGlass是一个预定义的常量,其他预定义的常量有crBeam和crSize等。这些常量值的范围是从0到-20(crDefault到crHelp)。可以从在线帮助中查找Cursors属性的详细说明,那里列出所有的光标常量。要改变光标形状,只要把一个常量赋值给Screen.Cursor。
也可以创建一个自定义的光标,然后把它加到Cursors数组中。为此,必须声明一个光标常量,这个光标常量的值不能与已有的光标常量重复。预定义的光标常量的值是从0到-20,而自定义的光标常量最好用正数,负数是Borland保留的。例如:
可以使用资源编辑器(例如Delphi5附带的ImageEditor)来创建自定义的光标。创建的光标必须保存到一个资源文件中。要注意的是,Delphi5会为一个项目自动创建一个资源文件。因此,自定义的资源文件不能与项目文件原有的资源文件重名。另外,自定义的资源文件要放在与项目文件相同的目录中,这样编译器才会找到这个资源文件。要使Delphi5能够把资源文件链接到应用程序中,可以参照下面这行语句:
最后,可以参照下面的代码把自定义的光标调入,加到Cursors数组中,并指定使用这个光标:
这里使用了LoadCursor()函数来调入光标。LoadCursor()需要传递两个参数:一个是需要使用这个光标的模块的句柄,另一个是在.res文件中指定的光标的名字(必须全部大写)。
hInstance代表当前运行的应用程序。接着将从FormCreate返回的值赋给Cursor属性中由crCrossHair指定的位置。最后将当前光标赋给Screen.Cursor。
如果需要的话,可以使用Toolsl|ImageEditor菜单命令打开Image Editor,然后打开资源文件CrossHairRes.res,看看这个光标到底是怎么创建的的。
4.6.3避免创建一个窗体的多个实例
如果使用Application.CreateForm()或TForm.Create()来创建窗体的实例,最好确保当前没有相同的实例存在。下面的代码演示了这一点:
上面的代码中,必须在释放SomeForm变量后把它赋值为nil,否则,Assigned()函数将无法正常工作。不过,上面的代码不适用于无模式窗体,因为对于无模式窗体来说,程序代码并不知道什么时候删除窗体实例。因此,必须在处理onDestroy事件的处理方法中把窗体的实例赋值为nil。本章前面介绍过这种方法。
4.6.4在DPR文件中增加代码
可以主窗体创建之前向项目文件中增加一些代码。这常用于做一些初始化工作,也可以根据需要终止应用程序。清单4-8列出了一个项目文件,它要求用户输入一个口令。清单4-8演示项目初始化的Initialize.dpr文件
4.6.5覆盖应用程序的异常处理
Win32系统具有强大的异常处理能力。缺省情况下,当一个异常发生时,应用程序会自动处理,并显示一个标准的错误框。
当开发一个大型的应用程序时,可能需要定义自己的异常。Delphi5默认的异常处理不能满足需要,因为应用程序往往需要对异常进行特殊的处理。这种情况下,需要覆盖TApplication的默认异常处理,用自己的方法来代替默认的异常处理方法。
TApplication提供了一个OnException事件,可以响应这个事件并加入代码。当一个异常发生时,就会触发这个事件,这样就可以进行特殊的处理,同时,原有的标准错误框不会出现。
但是,由于TApplication的属性和事件都无法在Object Inspector上列出来,必须在应用程序中使用TApplicationEvents组件增加指定的异常处理方法。
清单4-9演示了怎样覆盖应用程序的默认异常处理。清单4-9演示覆盖异常处理的主窗体
在清单4-9中,appevnMainException()方法对TApplicationEvent组件的OnException事件进行处理。首先使用RTTI检查异常的类型,然后各自进行特殊的处理。代码中的注释介绍了处理的过程。
提示:如果选中DebuggerOptions对话框(通过Options|Debugger Options菜单项进入)的Language Exceptions页上的Stop on Delphi Exceptions复选框,当一个程序在调试运行时,如果出现异常,调试器将先报告这个异常,应用程序中的异常处理再起作用。尽管对于调试这很有用,但在查看自定义的异常处理时这很烦人。关闭这个选项,让项目正常运行。
4.6.6显示一个封面
假设要为项目创建一个封面。封面能够在应用程序启动时显示,并在应用程序初始化期间一直停留在屏幕上。显示封面是一件很简单的事情。
下面是创建一个封面的基本步骤:
1)在创建了主窗体后,再创建一个窗体来作为封面。把这个窗体叫做SplashForm。
2)使用Project|Options菜单命令,确保SplashForm没有出现在Auto-Create列表中。
3)把SplashForm的BorderStyle属性设为bsNone,BorderIcons属性设为[]。
4)把一个TImage组件放到SplashForm上,把它的Align属性设为alClient。
5)选择Picture属性,在TImage组件中调入一个位图。
现在已经设计好这个封面了,只要在项目文件中加入代码来显示它。清单4-10列出了一个项目文件,其中包含了显示封面的代码。清单4-10一个带有封面的项目文件
注意代码中有这样一个循环:
这个循环是为了造成延时。窗体上有一个TTimer组件,它的Interval属性设为3000。当TTimer组件的OnTimer事件发生时,就执行下面这行代码:
上面这行代码使while循环的条件为False,并结束循环。
4.6.7使窗体尺寸最小
为了说明怎样改变窗体的尺寸,下面将介绍一个项目,它的主窗体具有蓝色的背景,上面放有一个面板。当用户改变窗体的尺寸时,这个面板总是位于窗体的中心,并且不允许用户把窗体的尺寸设得比面板还要小。清单4-11列出了有关代码。清单4-11模板窗体的源代码
上面的代码演示了怎样捕捉Windows消息,特别是WM_WINDOWPOSCHANGING消息。这个消息是当窗口的尺寸将要改变时发生的,这样就有机会阻止尺寸的改变。第5章"理解Windows消息"将进一步介绍Windows消息。
4.6.8运行没有窗体的项目
窗体是Delphi5应用程序的焦点。不过,完全可以创建一个没有窗体的应用程序。为此要创建一个新的项目,然后使用Project|Remove From Project菜单命令把主窗体移走。此时的项目文件如下所示:
事实上,甚至可以把uses子句、Application.Initialize和Application.Run都删掉:
当然,这么简单的项目肯定是没有任何实际意义的,但要记住,可以在begin..end之间加入代码,这将成为Win32控制台程序的起点。
4.6.9退出Windows
有些情况下往往需要退出Windows。例如,应用程序可能修改了系统配置,而这些配置要在Windows重新启动后才有效。当然可以提示用户自己去重新启动Windows,但常规的做法是询问用户要不要重新启动Windows;如果要的话,就通过程序重新启动Windows。不过要记住,系统重启不是很好的行为,应当尽量避免。
要退出Windows,需要用到下面两个API函数中的一个:ExitWindows()或ExitWindowsEx()。
ExitWindows()函数是从16位Wndows移植过来的。在16位Wndows中,需要设置有关选项,以允许退出Windows后再重新启动Windows。不过,在Win32中,这个函数只是注销当前用户,然后让其他用户登录。
现在最好用ExitWindowsEx()函数,这个函数能够注销当前用户、关闭Windows,或者在关闭Windows后重新启动它。清单4-12演示上述函数的用法。清单4-12用ExitWindows()或ExitWindowsEx()退出Windows
上面的代码用一组单选按钮来让用户选择退出Windows的方式。第一个选项将调用ExitWindows()注销当前用户,然后以另外一个用户身份登录。剩下的两个选项都是使用ExitWindowsEx()函数。第二个选项退出Windows然后重新启动计算机。第三个选项退出并关闭系统,这样用户就可以断开电源。第四个选项与第一个选项相同,但它使用的是ExitWindowsEx()函数。
不论是ExitWindows()还是ExitWindowsEx()函数,如果调用成功就返回True,否则就返回False。
可以使用SysUtils.pas中的Win32Check()函数,它将调用Win32API函数GetLastError()来显示错误信息。
注意:如果运行在WindowsNT环境下,使用ExitWindowsEx()函数并不会关闭系统;这需要特殊的权限。必须使用Win32API函数AdjustTokenPrivleges()授予SE_SHUTDOWN_NAME权限。有关这方面内容的详细介绍请查找Win32的在线帮助。
4.6.10防止关闭Windows
有时候不允许关闭Windows。例如,假设一个应用程序正在编辑一个文件而且还没有存盘,这时如果另一个应用程序调用了ExitWindowsEx(),则会关闭Windows,从而导致数据丢失,除非应用程序知道Windows将要退出。这其实很简单,只要响应主窗体的OnCloseQuery事件,然后参照下面的代码进行处理:
如果把CanClose设为False,表示不允许关闭Windows。如果把CanClose设为True,将提示用户保存文件。可以在光盘中找到一个示例程序NoClose.dpr。
提示:如果运行的是一个无窗体的程序,那么应当捕捉WM_QUERYENDSESSION消息。只要有一个应用程序调用了ExitWindows()或ExitWindowsEx(),每个应用程序就会收到WM_QUERYENDSESSION消息。如果应用程序从这个消息返回非零值,表示允许Windows关闭。如果返回零,表示不允许Windows关闭。在第5章"理解Windows消息"中将进一步讲解有关Windows的消息处理。
4.7总结
本章重点介绍了项目管理技术和体系结构。主要讨论了组成Delphi5项目的关键要素:TForm、TApplication和TScreen。我们通过建立一个通用的体系结构来演示怎样开始设计一个应用程序。这一章还介绍了一些其他有用的例程。