Delphi 组件渐进开发浅谈——双简合璧

Posted 癫狂编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Delphi 组件渐进开发浅谈——双简合璧相关的知识,希望对你有一定的参考价值。

2.双简合璧
2.1.带有T[x]Label的T[x]Edit组件
  请允许我用[x]的书写方式来表示不同的对象。因为随后将大量提及TLabeledEdit与TTntLabeledEdit、TCustomLabeledEdit与TTntCustomLabeledEdit这样及其雷同的类。
2.2.分析T[x]LabeledEdit组件结构
  现在要设计一个类似TLabeledEdit的组件,查看ExtCtrls的TLabeledEdit定义如下:
  TLabeledEdit = class(TCustomLabeledEdit)
  TLabeledEdit从TCustomLabeledEdit继承并开放属性,TCustomLabeledEdit部分定义如下:
  TCustomLabeledEdit = class(TCustomEdit)
  private
    FEditLabel: TBoundLabel;
  Public
    property EditLabel: TBoundLabel read FEditLabel;
  可以看出来,TCustomLabeledEdit继承自TCustomEdit,并构造了一个TBoundLabel对象。
TBoundLabel对象定义如下:
  TBoundLabel = class(TCustomLabel)
  TBoundLabel与TLabel都是从TCustomLabel继承,两者溯本逐源,是兄弟关系。
  TLabeledEdit的源头是TCustomEdit和TCustomLabel,而TTntLabeledEdit的源头是TTntCustomEdit和TTntCustomLabel。T[x]CustomEdit和T[x]CustomLabel结合而形成了T[x]CustomLabeledEdit。
  在这里,T[x]CustomEdit和T[x]CustomLabel如同夫妻,两者与T[x]CustomLabeledEdit如同父(母)子关系。
  上一章设计的TGcxCustomEdit和TGcxCustomIntEdit可以替代T[x]CustomEdit,现在我们需要一个新的T[x]BoundLabel对象。
2.3.设计TGcxCustomLabel和TGcxBoundLabel
2.3.1.设计TGcxCustomLabel

  为了求简单,我们先简化TGcxCustomLabel设计,直接从TTntCustomLabel派生,不做任何修改。
  TGcxCustomLabel = class(TTntCustomLabel)
  end;
2.3.2.从TBoundLabel、TTntBoundLabel到TGcxBoundLabel
  TBoundLabel的部分定义:
  TBoundLabel = class(TCustomLabel)
  private
    function GetTop: Integer;
    function GetLeft: Integer;
    function GetWidth: Integer;
    function GetHeight: Integer;
    procedure SetHeight(const Value: Integer);
    procedure SetWidth(const Value: Integer);
  protected
    procedure AdjustBounds; override;
  public
    constructor Create(AOwner: TComponent); override;
  TTntBoundLabel的部分定义:
  TTntBoundLabel = class(TTntCustomLabel)
  private
    function GetTop: Integer;
    function GetLeft: Integer;
    function GetWidth: Integer;
    function GetHeight: Integer;
    procedure SetHeight(const Value: Integer);
    procedure SetWidth(const Value: Integer);
  protected
    procedure AdjustBounds; override;
  public
    constructor Create(AOwner: TComponent); override;
  那么,TGcxBoundLabel应如下定义:
  TGcxBoundLabel = class(TGcxCustomLabel)
  private
    function GetTop: Integer;
    function GetLeft: Integer;
    function GetWidth: Integer;
    function GetHeight: Integer;
    procedure SetHeight(const Value: Integer);
    procedure SetWidth(const Value: Integer);
  protected
    procedure AdjustBounds; override;
  public
    constructor Create(AOwner: TComponent); override;
  代码部分基本剽窃TTntBoundLabel,但AdjustBounds方法略有差异,为什么呢?这需要阅读TBoundLabel.AdjustBounds和TTntBoundLabel.AdjustBounds代码。
2.3.3.AdjustBounds的变化
  TTntBoundLabel.AdjustBounds代码如下:
procedure TTntBoundLabel.AdjustBounds;
begin
  inherited AdjustBounds;
  if Owner is TTntCustomLabeledEdit then
    with Owner as TTntCustomLabeledEdit do
      SetLabelPosition(LabelPosition);
end;
  可以看到,TTntBoundLabel检查它的所有者(Owner)是否为TTntCustomLabeledEdit,并调用Owner的SetLabelPosition。
  这样一来,就局限了TTntBoundLabel的Owner必须为TTntCustomLabeledEdit,限制了TTntBoundLabel的应用范围,这是一个缺陷,违背了OOP的基本原则。
  如果按照这种设计思路,当我们想把T[x]BoundLabel绑定在其它对象上的时候,就需要重新从T[x]CustomLabel或者T[x]BoundLabel继承,并重写AdjustBounds方法。这是一个很臃肿的设计思想,会导致代码和维护量增加,这是我们不愿意看到的。
  Delphi的类只能从一个基础类继承,我们如何改变这个局面呢?对,就是接口。
  接口的概念最早是微软从COM的思想提出的,Delphi引入并延伸了这部分定义。
  我们可以让类拥有一个基础类,并拥有多个接口,并用SysUtils.Supports函数判断该类是否支持某接口。
  好了,为了让TGcxBoundLabel能够为更多的类服务,我们最终的代码如下修改:
procedure TGcxBoundLabel.AdjustBounds;
begin
  inherited AdjustBounds;
  if Supports(Owner, IBoundLabelOwner) then
    with Owner as IBoundLabelOwner do
      SetLabelPosition(GetLabelPosition);
end;
  这样一来,原本是TTntCustomLabeledEdit类的SetLabelPosition方法和LabelPosition属性,被修改成了IBoundLabelOwner接口的SetLabelPosition和GetLabelPosition方法。
2.3.4.IBoundLabelOwner接口定义
  因为接口定义的成员列表memberList只能包括方法和属性。接口中不允许含有域。因为接口中没有域,所以属性的read和write说明符都必须是方法。
  IBoundLabelOwner = interface
  [‘{0056AA66-CCD0-4D56-9555-2DE908E89F8A}‘]
    function GetLabelPosition: TLabelPosition;
    procedure SetLabelPosition(const Value: TLabelPosition);
    property LabelPosition: TLabelPosition read GetLabelPosition write SetLabelPosition;
  end;
  这个定义很简单,就是两个函数方法声明,该接口间接应用于TGcxBoundLabel。
2.3.5.最后一个重要属性Bind
  定义如下:
  TGcxBoundLabel = class(TGcxCustomLabel)
  private
    FBind: TWinControl;
  protected
    procedure SetBind(ABind: TWinControl);
  public
    property Bind: TWinControl read FBind;
  代码如下:
procedure TGcxBoundLabel.SetBind(ABind: TWinControl);
begin
  Self.FBind := ABind;
end;
  这个属性是TBoundLabel和TTntBoundLabel没有的,它是做什么的呢?
  参考TCustomLabeledEdit和TTntCustomLabeledEdit的SetLabelPosition方法,在计算T[x]BoundLabel对象的位置时,计算公式中的Left、Top、Height、Width都是基于当前对象Self的,这样就有了一些麻烦。为什么呢?
  当T[x]BoundLabel由T[x]CustomLabeledEdit构造,并且T[x]CustomLabeledEdit是最终组件时,这不是问题,当T[x]CustomLabeledEdit是一个新组件的部分时,T[x]BoundLabel的位置计算将很痛苦。
  参考T[x]CustomLabeledEdit.SetParent方法,由于T[x]CustomLabeledEdit对象在构造T[x]BoundLabel的时候,会将自身的Parent复制给T[x]BoundLabel的Parent,这就是T[x]CustomLabeledEdit的Height、Width并不包含T[x]BoundLabel的Height、Width的原因。T[x]CustomLabeledEdit是在它的Parent对象中管理并安置T[x]BoundLabel对象。
  TTntCustomLabeledEdit与TCustomLabeledEdit在此处是一样处理的,代码及其雷同。
  所以,我们需要一个属性去修正TGcxBoundLabel的位置,Bind属性就是为了这个目的出现的。对于SetLabelPosition方法的说明,将在后面的TGcxCustomLabeledEdit和TGcxCustomIntLabeledEdit中叙述;对于SetParent方法的说明,将在后面的TGcxCustomValueInfoEdit中叙述。
2.4.合成TGcxCustom[x]LabeledEdit
  TGcxCustomLabeledEdit、TGcxCustomIntLabeledEdit是仿照T[x]CustomLabeledEdit建立的。前者从TGcxCustomEdit继承,用于文本输入;后者从TGcxCustomIntEdit继承,用于数值输入。
  参照前面“2.1.2.3.AdjustBounds的变化”以及“2.1.2.4IBoundLabelOwner接口定义”,TGcxCustomLabeledEdit与TGcxCustomIntLabeledEdit的出场是有些与众不同的,定义如下:
  TGcxCustomLabeledEdit = class(TGcxCustomEdit, IBoundLabelOwner)
  TGcxCustomIntLabeledEdit = class(TGcxCustomIntEdit, IBoundLabelOwner)
  可以看到他们引入了一个接口IBoundLabelOwner。
  下面的定义就基本一致了,唯一与T[x]CustomLabeledEdit略有区别的地方,就是增加了GetLabelPosition方法,修改了LabelPosition属性的read定义部分,原因在前面已经提到了,不在重述。
  private
    { Private declarations }
    FEditLabel: TGcxBoundLabel;
    FLabelPosition: TLabelPosition;
    FLabelSpacing: Integer;
    function GetLabelPosition: TLabelPosition;
    procedure SetLabelPosition(const Value: TLabelPosition);
    procedure SetLabelSpacing(const Value: Integer);

  protected
    { Protected declarations }
    procedure SetParent(AParent: TWinControl); override;
    procedure Notification(AComponent: TComponent;
      Operation: TOperation); override;
    procedure SetName(const Value: TComponentName); override;
    procedure CMVisibleChanged(var Message: TMessage); message CM_VISIBLECHANGED;
    procedure CMEnabledChanged(var Message: TMessage); message CM_ENABLEDCHANGED;
    procedure CMBidimodeChanged(var Message: TMessage); message CM_BIDIMODECHANGED;
  public
    { Public declarations }
    constructor Create(AOwner: TComponent); override;
    procedure SetBounds(ALeft: Integer; ATop: Integer;
      AWidth: Integer; AHeight: Integer); override;
    procedure SetupInternalLabel;
    property EditLabel: TGcxBoundLabel read FEditLabel;
    property LabelPosition: TLabelPosition
      read GetLabelPosition write SetLabelPosition default lpLeft;
    property LabelSpacing: Integer
      read FLabelSpacing write SetLabelSpacing default 3;
2.4.1.GetLabelPosition函数和SetLabelPosition方法
  由于引入IBoundLabelOwner接口,必须为LabelPosition属性提供新的read函数,代码如下:
function TGcxCustomLabeledEdit.GetLabelPosition: TLabelPosition;
begin
  Result := FLabelPosition;
end;
  原来的SetLabelPosition代码很长,而且只能相对自身(Self)计算T[x]BoundLabel的位置。考虑到TGcxBoundLabel与T[x]BoundLabel的差异(前者将为多个类服务),我们将原有的代码重组,设计一个公共的方法UpdateLabelPosition。最后,SetLabelPosition方法简化如下:
procedure TGcxCustomLabeledEdit.SetLabelPosition(
  const Value: TLabelPosition);
begin
  FLabelPosition := Value;
  UpdateLabelPosition(Self, FEditLabel, FLabelPosition, FLabelSpacing);
end;
2.4.2.公共方法UpdateLabelPosition
  这个方法源自SetLabelPosition,提取出来的目的首先是简化单个TGcxBoundLabel子对象TGcxCustom[x]LabeledEdit的代码量,再者就是代码的维护工作量。
procedure UpdateLabelPosition(AOwner: TWinControl;
  AEditLabel: TCustomLabel;
  const NewLabelPosition: TLabelPosition;
  const ALabelSpacing: Integer);
var
  P: TPoint;
  obj: TWinControl;
begin
  if AEditLabel = nil then Exit;

  obj := AOwner;
  if (AEditLabel is TGcxBoundLabel) then
  begin
    with (AEditLabel as TGcxBoundLabel) do
      if Assigned(Bind) then
        obj := Bind;
  end;
  if not Assigned(obj) then
    Exit;

  with obj do
    case NewLabelPosition of
    lpAbove: P := Point(Left, Top - AEditLabel.Height - ALabelSpacing);
    lpBelow: P := Point(Left, Top + Height + ALabelSpacing);
    lpLeft : P := Point(Left - AEditLabel.Width - ALabelSpacing,
                    Top + ((Height - AEditLabel.Height) div 2));
    lpRight: P := Point(Left + Width + ALabelSpacing,
                    Top + ((Height - AEditLabel.Height) div 2));
    end;

  AEditLabel.SetBounds(P.x, P.y, AEditLabel.Width, AEditLabel.Height);
end;
  这段代码中,与T[x]CustomLabeledEdit. SetLabelPosition不同的地方如下:
  obj := AOwner;
  if (AEditLabel is TGcxBoundLabel) then
  begin
    with (AEditLabel as TGcxBoundLabel) do
      if Assigned(Bind) then
        obj := Bind;
  end;
  if not Assigned(obj) then
    Exit;

  with obj do
  因为该方法中AEditLabel的类型定义为TCustomLabel,所以代码首先会检查AEditLabel是否为TGcxBoundLabel,然后判断Bind属性是否有效,如果以上判断不成立,则选择缺省的AOwner对象,并根据判断结果去计算AEditLabel的位置。
2.5.珠联璧合
  写完这一段的时候,仔细检查了一下代码,发现TGcxCustomLabeledEdit和TGcxCustomIntLabeledEdit的代码竟然一摸一样。而且SetLabelSpacing、SetParent、Notification、SetName、CMVisibleChanged、CMEnabledChanged、CMBidimodeChanged、Create、SetBounds、SetupInternalLabel与T[x]CustomLabeledEdit的代码一摸一样。
  原来就是这么简单,如果说T[x]CustomLabeledEdit是周伯通的双手互搏之术,TGcxCustomLabeledEdit与TGcxCustomIntLabeledEdit就像小龙女与杨过、云蕾与张丹枫的双剑合璧。

以上是关于Delphi 组件渐进开发浅谈——双简合璧的主要内容,如果未能解决你的问题,请参考以下文章

浅谈针对DELPHI的界面和业务的分离开发模式和MVC模式

浅谈敏捷开发

如何在 Delphi Rio 上设置日期格式 - 未声明的标识符:'shortdateformat' [重复]

波卡与 Wasm 合约双剑合璧

用于包/组件开发的 Delphi 环境设置

Delphi透明组件开发 good