代码复用和界面复用

面向对象的编程思想强调代码的可复用。而Delphi的精髓实际上就是Object Pascal语言,Object Pascal语言是一个非常强大的面向对象的编程语言,可以通过对象的继承实现代码复用。同时Delphi作为一个强大的RAD开发工具,不仅可以实现代码复用,还可以实现可视化界面的复用。

基于复制粘贴的界面重用

Delphi最早提出的复用不是面向对象的,而是类似于代码库的重用,比如在执行窗体右键菜单的Add To Repository命令,可以将一些常用的窗体如关于对话框添加到Delphi的代码库中,以后可以在新建窗体时,直接创建一个完全的一样的对话框。其实这种复用无法是帮助我们简化了复制粘贴的过程而已,会带来很多后续维护的问题,过多的使用这种方式编程,会导致大量重复的代码,大量重复的错误。而现代的编程思想如XP,则认为不允许复制粘贴代码,一旦遇到这种情况,就要进行重构。

 

可视化窗体继承(Visual Form Inheritance)

 

可视化窗体继承,以下我们简称其为VFI是Delphi2开始出现的一种软件复用技术。允许我们创建一个基类窗体,并从这个基类窗体派生新的窗体。它在标准的以代码重用为目的类继承的基础上实现了对可视化界面元素的重用。让我们做个试验,假设我们现在编写一组系统配置管理界面,为了统计界面样式,规定所有的配置管理界面都应该有一个容器面板,一个确定和一个取消按钮,由于这样的界面非常多,为了界面的统一,我们就来创建这样一个基类界面,首先新建一个项目起名VFI,然后使用 File | New Form菜单命令新建一个界面,起名为TBaseOptionDlg,界面示意图如下:

 

然后下面创建一个派生类窗体,用来配置数据库连接的参数,选择 File | New …菜单,调出New Items 对话框中,切换到当前的项目VFI下,选中刚才创建的基类BaseOptionDlg,注意在界面的下边inherit的单选框处于选中的状态。点击确定,就会创建一个新的派生配置管理界面了。

 

可以看到我们的派生类自动就继承了父窗体所有的按钮和面板等界面元素。下面,在主窗体上添加一个数据库连接参数菜单,添加我们的DB参数配置界面,

procedure TFormMain.N4Click(Sender: TObject);
var
  AForm:TDBOptionDlg;
begin
  AForm:=TDBOptionDlg.Create(Application);
  try
    AForm.ShowModal;
  finally
    AForm.Free;
  end;
end;

 

运行一下后,我突然想起来,一般配置管理界面都会有一个默认值的按钮,可以用来恢复默认配置参数的值,而刚才设计界面时忽略了这个问题。打开基类窗体,在窗体上放置一个新的默认设置按钮,保存后。回过头来,可以发现我们的数据库配置界面也神奇增加了一个新的按钮。想像一下,如果你的工程中需要编写几十个配置管理窗体,如果不使用窗体继承的方式来编写的话,在程序已经进入测试阶段时候,客户突然发现上面这个问题,要求修改,那么修改的工作量就会非常大,而且很难保证不会因为疏忽而忘记修改某个配置界面。而使用窗体继承的方式,我们只要修改基类窗体就可以保证修改对所有的派生类都生效。

 

除了界面继承之外,VFI也可以实现代码继承,在基类窗体的OnCreate事件中显示一个提示信息对话框:

procedure TBaseOptionDlg.FormCreate(Sender: TObject);
begin
  ShowMessage('配置参数界面');
end;

 

运行程序后,你会发现虽然我们没有编写派生窗体的OnCreate事件处理过程,但是显示界面时,仍然会弹出消息对话框。

 

同时,由于窗体的属性通过VFI被共用,可以有效减少占用的系统资源,比如有时我们可能会在界面上放上一个大的图片进行界面美化,如果这个图片被放在多个界面中,而这些界面之间没有继承关系的话,图片就会被多次编译进资源中,在我们不知不觉中文件大小可能会翻了几倍。而将图片放在基类窗体中,无论图片被多少个子窗体共用,资源都只被编译一次,因此可以极大的减少生成的可执行文件尺寸和加载速度。

 

VFI窗体属性及代码重载

 

VFI支持继承,使我们可以重用一些有共性的代码,但是每个界面又有它特性的一面,这可以通过重载来实现。

 

比如这回我觉得配置管理窗体上面板的颜色有些单调,想调整为淡黄色的,但是我又不确定其它人是否会赞同我的审美眼光,所以我不打算修改基类的窗体面板颜色属性,而只是修改派生的数据库配置界面上面板的颜色为clInfoback,可以看到我们的派生类窗体上面板的颜色变成了淡黄色,但是基类的面板仍然保持不变,也就是说我在子类窗体中对父类窗体的属性进行了重载。如果修改之后,客户不满意我的颜色搭配,而喜欢基类的颜色搭配,有一个简便的办法可以恢复继承的父类属性,那就是选中面板,然后执行右键菜单中的Revert to inherited命令就可以了。见下图:

 

除了属性重载外,Delphi还支持事件重载。接下来,在数据库参数配置界面上添加一个编辑框,用来指定数据库名,当显示界面的时候,需要在编辑框中给用户展现当前配置的数据库名,因此要在窗体的OnCreate事件中进行编辑框内容的初始化。双击窗体,创建OnCreate事件处理函数,你会发现新建的OnCreate事件不同于普通的OnCreate事件,窗体设计器自动在代码中加了一句Inherited语句,代码示意:

 

procedure TDBOptionDlg.FormCreate(Sender: TObject);
begin
  inherited;
 
end;

 

新加的inherited语句调用的其实就是基类的OnCreate事件处理过程。前面基类的OnCreate事件中只是简单的显示一个消息对话框。如果在派生的TDBOptionDlg中不想显示那个愚蠢的消息框,只要把inherited注释掉就可以了。下面是修改后的初始化代码:

procedure TDBOptionDlg.FormCreate(Sender: TObject);
begin
  //inherited;
  edtDB.Text:='c:\hubdog.db';
end;

 

VFI的局限

 

在好的技术都有它的局限性,不能包制百病,VFI同样如此。第一个限制就是在派生类的窗体中,我们不能删除从父类继承的组件,同时我们也不能象代码继承那样,使用protected等关键字降低某些界面组件的保护级别,使其对于子类不可见,从这一点上来说VFI不能实现界面元素信息的隐藏,这不符合面向对象的封装要求。那么这就产生一个问题,在某些配置界面中,可能我们不想提供基类界面要求的恢复默认值的操作功能,一个比较丑陋的办法就是修改默认值按钮的visible属性为false。与此相关的一个问题是,如果基类中定义了一个面板,而在派生类中在面板上放了一个按钮,如果修改基类界面时将面板删除的话,则派生类中的按钮也将被删除,这类问题处理不好的话,有时会产生很大的混乱。

 

另外,虽然VFI允许重载属性和事件,这就会产生另外一个问题,平时很多人习惯了使用事件来实现界面初始化,响应按钮点击实现参数配置更新等操作。但是由于VFI默认情况会调用基类继承的事件,而如果窗体的继承层次很多,并且在不同层次上都使用事件处理函数来实现业务逻辑,那么很有可能会出现,在不同继承层次上的事件处理函数实现了互相矛盾的业务处理,比如在继承树的中间的某个界面在初始化时,向界面上某个列表框添加了很多字符串,而后面的派生类界面的作者不清楚这个问题,在初始化时先清空列表框的字符串,然后又添加了一些字符串,这就造成了原来信息的丢失。因此使用VFI的时候,建议继承的层次不要太多,同时尽量使用虚方法来代替事件的使用,对于需要某些强制派生类实现的方法,要用定义纯虚方法。

 

回到我们的参数配置基类,对于一般参数配置过程,可以抽象出以下一些共性的必须执行的方法:

1、   显示界面时,初始化参数值。

2、   输入参数的有效性校验,如果无效,禁止点击确定按钮。

3、   执行参数的修改,如果成功,则关闭窗体,如果失败,则等待用户重新输入。

4、   点击默认设置时,恢复参数默认设置。

 

为了强制实现上面的四个业务逻辑,修改后的基类代码如下:

type
  TBaseOptionDlg = class(TForm)
  public
    { Public declarations }
    constructor Create(AOwner:TComponent);override;
    procedure InitUI;virtual;abstract;
    function ParamsValid:Boolean;virtual;abstract;
    function UpdateParams:Boolean;virtual;abstract;
    procedure DefaultParams;virtual;abstract;
  end;
procedure TBaseOptionDlg.ActionOKUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled:=ParamsValid;
end;
 
procedure TBaseOptionDlg.ActionOKExecute(Sender: TObject);
begin
  if UpdateParams then
  begin
    ModalResult:=mrOk;
  end;
end;
 
procedure TBaseOptionDlg.ActionCancelExecute(Sender: TObject);
begin
  ModalResult:=mrCancel;
end;
constructor TBaseOptionDlg.Create(AOwner: TComponent);
begin
  inherited;
  initUI;
end;
 
procedure TBaseOptionDlg.ActionDefaultExecute(Sender: TObject);
begin
  DefaultParams;
end;
 
end.

 

为了实现前面定义的业务逻辑,我们在窗体构造方法中调用InitUI过程来初始化界面,而在Action的OnUpdate事件中不停调用ParamsValid函数判断当前输入的参数是否有效,如果有效,则允许点击确定按钮。点击确定按钮后,会调用UpdateParams方法来更新参数,如果更新成功,则关闭界面,否则等待用户重新输入。最后,在用户点击恢复默认参数值按钮时,调用DefaultParams方法来完成。要注意的是,基类中的initUI,ParamValid,UpdateParams和DefaultParams过程都被定义为纯虚的抽象方法。这是因为对于基类来说,由于没有具体的参数输入界面元素,实现这些方法是没有意义的,只有到了具体的参数配置界面,才需要实现这些方法,同时定义为Abstract抽象方法可以强迫派生类必须实现这几个方法来完成业务逻辑,否则编译后无法正确运行。

 

下面是TDBOptionDlg实现的示意性代码:

procedure TDBOptionDlg.DefaultParams;
begin
  edtDB.Text:='c:\hubdog.db';
end;
 
procedure TDBOptionDlg.InitUI;
begin
  edtDB.Text:='c:\hubdog.db';
end;
 
function TDBOptionDlg.ParamsValid: Boolean;
begin
  Result:=trim(edtDB.Text)<>'';
end;
 
function TDBOptionDlg.UpdateParams: Boolean;
begin
  Result:=True;
end;
 
end.

 

另外,窗体继承不支持窗口嵌套,也就是不支持窗口的组合复用,因此为了扩充窗口的功能,无形中鼓励人们使用继承机制,而不是组合机制。这加深了窗口之间的偶合度,不利于灵活性和扩展性。

 

窗体继承还有一个很大的问题就是它只支持窗口级别的组件复用,但是更多的时候,我们想要的只是粒度更小的界面中某一部分显示区域或者组件级别的复用。Borland也考虑到了这个问题,因此在Delphi4中给出了Component Template的解决方案。

 

组件模版模板

组件模版模板技术相当简单,比如下图所示意的双列表框组合界面是很常用的一类界面,可以用来从一个列表框向另一个列表框移动对象。

 

那么我们就可以选择列表框和按钮,然后执行 Component | Create Component Template...命令来创建组件模版模板,示意图如下:


设定组件模版模板名称为TDualListBox,并将其作为一个组件放到Templates组件面板上,组件面板除了能保存各个组件的属性外,还可以保存相关组件的事件代码。还有一点限制是它不能将窗体设定为组件模版模板

生成好的组件模版模板可以象其它组件一样从组件面板上拖放到窗体,并生成模版副本。组件模版模板同VFI本质上的不同在于它不能实现继承,当把TDualListBox作为一个组件放到窗体上之后,它只相当于原来所有的组件的一个拷贝,我们可以随意地将一个按钮删除,而VFI的派生类则不允许我们删除从父类继承的组件。因此,组件模版模板不能算是一个面向对象的界面复用解决方案,即便我们随后修改了组件模版模板的设计,这些设计的变化并不会影响到先前的被实例化了的组件。因此可以认为,组件模版模板只是一个简化我们操作的复制粘贴工具。

另外,组件模版模板还有一个很致命的问题就是,它很难被共享,因为组件模版模板信息是被统一保存到Delphi.dct文件中的,而不是保存在pas文件中的,因此要想在不同机器间共享模版,必须将Delphi.dct拷贝到其它机器上,或者将Delphi.dct进行共享,而Delphi.dct又是一个单独的文件,你无法从中抽取单独的一些模版模板出来分发。所以,Component Template不太适用于多人开发的项目中。

针对VFI和Component Template等技术暴露出来的问题,Borland进一步的提出了TFrame的解决方案。

基于TFrame的复用

TFrame比较像一个综合了Component Template和VFI优点的产物,本身的实现机制同VFI非常类似,都是面向对象的,对基类的可视化变更会立刻反映到派生类,支持代码共享和界面共享,很容易分发。同时,它又像Component Template那样支持小粒度的组件复用,支持子窗口嵌套,是一个比VFI更为轻量级的解决方案。

对于那些非常复杂有很多输入选项的独立的界面编程,TFrame是非常适合的,因为利用TFrame我们可以将一个复杂的界面分解为多个简单的模块的编程。假设这回客户要求我们编写一个客户资源信息录入界面。客户资源信息包括很多内容,比如Email,电话,地址几十项信息。

分析一下客户对象这个实体的属性,一个客户可以对应多个Email地址 ,多个电话号码,对于这种一对多的关系,可以采用一个输入框,一个列表框和添加,删除按钮来完成信息的编辑修改的,同时考虑到电话和Email地址同客户的关系都为一对多,那么就需要编辑组件和大同小异的功能实现。对于这种情况,我们就可以使用TFrame来复用这样的一对多信息输入界面。

首先,新建项目,在主界面上添加一个面板,然后在窗体上放上确定和取消按钮。然后,使用菜单命令 File | New Frame新建一个TFrame。在新建的Frame上添加列表框等输入组件,完成的界面示意图如下:

为Frame添加信息编辑的代码

type
  TFrameList = class(TFrame)
  protected
    function CanAdd:Boolean;virtual;
  end;
procedure TFrameList.ActionAddUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled:=CanAdd;
end;
 
function TFrameList.CanAdd: Boolean;
begin
  Result:=(trim(edtInput.Text)<>'') and (ListBox.Items.IndexOf(trim(edtInput.text))>-1);
end;
 
 
procedure TFrameList.ActionAddExecute(Sender: TObject);
begin
  listbox.Items.Add(trim(edtInput.text));
end;
 
procedure TFrameList.ActionDelUpdate(Sender: TObject);
begin
  (Sender as TAction).Enabled:=ListBox.ItemIndex>-1;
end;
 
procedure TFrameList.ActionDelExecute(Sender: TObject);
begin
  ListBox.Items.Delete(ListBox.ItemIndex);
end;

 

FrameList使用Action对添加删除动作会进行有效性判断,添加时调用函数CanAdd判断是否可以添加,FrameList基类的CanAdd函数只是简单的判断当前输入框中文本是否为空,以及当前输入框中文本是否已经被添加进了列表框,如果不满足,禁止添加按钮。注意为了派生类扩展的需要,这里CanAdd声明为虚方法,后面我们的Email和电话输入Frame要想对Email和电话的有效性进行校验的话,可以重载这个函数。删除前只是简单的判断ListBox中是否有选中的要被删除的信息。

 

接下来就是从我们的基类派生出Email和电话的编辑框架,其中电话的编辑框架只是修改的按钮和标签的Caption,显示添加电话,删除电话,以及电话列表等信息,同时重载了CanAdd函数,提供了对电话号码的简单判断。而Email除了修改显示信息和重载CanAdd函数外,还为列表框增加了双击列表框,激活向Email地址发送邮件的功能。

 

//判断是否可以添加电话号码

function TFrameTele.CanAdd: Boolean;
var
  I, code:Integer;
begin
  if inherited CanAdd then
  begin
    Val(trim(edtInput.Text), I, code);
    Result:=code=0;
  end;
end;
 

//判断是否可以添加Email

function TFrameEmail.CanAdd: Boolean;
begin
  if inherited CanAdd then
  begin
    result:=Pos('@', trim(edtInput.Text))>0;
  end;
end;
//激活Email客户端,收件人为当前Email账户
procedure TFrameEmail.ListBoxDblClick(Sender: TObject);
begin
  inherited;
  if ListBox.ItemIndex>-1 then
    ShellExecute(Handle,'open', PChar('mailto:'+trim(edtInput.text)),nil,nil,SW_NORMAL);
end;
 

注意由于本例子只是演示Frame的用法,所以我只是简单的进行输入有效性判断,真正完备的判断应该是基于正则表达式的,虽然VCL库中没有提供正则表达式的支持,但是有一些免费的第三方库,比如TRegExpr可以使用,这里就不详述了。

 

接下来选中面板,然后点击组件面板Standard页面上的Frames图标,调出Frames列表框,

 

在面板上添加FrameEmail和FrameTele,

接下来是编写界面初始化代码来加载,这时你会发现TFrame不同于TForm,它没有提供OnCreate和OnDestroy事件(不知道是什么原因,我猜测Borland的R&D Team一定也研究这个问题,不知是出于什么考虑从Delphi5到Delphi7一直没有实现这一显而易见的需求),所以要想在TFrame创建时对其进行初始化,只能是在TFrame的OnCreate事件中进行初始化。

 

constructor TFrameEmail.Create(AOwner: TComponent);
begin
  inherited;
  ListBox.Items.Add('hubdog@263.net');
  ListBox.Items.Add('hubcat@263.net');
end;
constructor TFrameTele.Create(AOwner: TComponent);
begin
  inherited;
  ListBox.Items.Add('861088888888');
  ListBox.Items.Add('861066666666');
end;

 

可以看到,使用TFrame后,原来需要集中在主界面完成的代码,现在全都分散到各个单元来实现,同时TFrame可以嵌套在主界面中实现可视化修改,甚至TFrame中也可以继续嵌套TFrame,将TFrame想像成建筑中砖头,工人可以通过砖头的堆砌和组合建立起摩天大楼,同样的我们通过TFrame的组件组合的复用模式,也可以实现操作复杂的交互界面

 

TFrame的局限性

 

虽然TFrame有着很多的好处,但是也一样有它的缺点,比如它和VFI一样,无法实现信息隐藏,因为界面上所有的组件默认都是published属性,并且无法像代码那样通过protected等保护级别关键字进行隐藏,它暴露了内部的太多的实现细节,不满足面向对象思想中窄接口的封装原则。要想实现真正的信息隐藏,必须通过纯代码方式编写的组件来实现,但是编写组件虽然能满足封装的原则,但是无法像TFrame那样无须编译注册,就可以在设计时可以通过窗体设计器随时修改可视化设计,做到所见即所得,正所谓鱼和熊掌不可得兼。在这方面Sergey Orlik给出了一个比较好的解决方案,他编写的Custom Containers Pack可以做到可视化设计复合组件,并通过编译来实现封装,这里限于篇幅的原因,我就不加以介绍了。

 

 

另外,对于某些属性复合组件,在TFrame使用时,经过修改后可能会产生异常的效果。举例说明,新建一个Frame1,在Frame上放上一个TTreeView,在树视图中建立一个标题为1的节点。然后从TFrame1派生一个新的Frame,然后在派生Frame中向TTreeView再添加一个标题为2的节点。回过头去,在基类TFrame1中,向树视图添加新的标题为3的节点,再打开派生的TFrame2,你会发现基类中添加的节点3并没有被继承到TFrame2中,这是因为树视图对树节点信息进行编码后是保存在Data属性中,见下表:

 

  object TreeView1: TTreeView

    Left = 40

    Top = 16

    Width = 265

    Height = 121

    Indent = 19

    TabOrder = 0

    Items.Data = {

      010000001A0000000000000000000000FFFFFFFFFFFFFFFF0000000001000000

      01311A0000000000000000000000FFFFFFFFFFFFFFFF00000000010000000132

      1A0000000000000000000000FFFFFFFFFFFFFFFF00000000000000000133}

  end

 

而窗体设计器还无法智能到理解这种自定义流属性的意义,所以设计器不对这类属性的修改进行继承,导致了上面的古怪现象。不仅TTreeView会出现这种问题,包括TListView等包含复合属性的控件都有可能出现这类问题。所以,在设计TFrame时,一定要谋定而后动,尽量在实现TFrame基类时把事情想周全之后再动手,因为后面如果对基类TFrame进行大的改动,会牵一发而动全身,可能会造成对所有基类的修改,这时就不是节省工作量,而是增加工作量了。

总结

VFI和TFrame可以用来创建复合组件容器,进行所见即所得的可视化编辑,并支持可视化继承和重载。使用TFrame,我们可以构造内部紧密偶合,而同外部松散偶合可重用的界面构造的Block。可以使我们的界面设计更灵活,更容易扩展维护。

posted on 2009-08-28 22:46  on_road  阅读(1820)  评论(0编辑  收藏  举报