Delphi 组件渐进开发浅谈(三)——锦上添花

  转载请指明出处来源。       

3.锦上添花
3.1.对齐属性Alignment
  T[x]CustomEdit类没有提供左右对齐属性,这令人很不爽。也许在我们现在的生活中,文字基本都是左对齐的,但数值输入输出如果是右对齐的话就更理想了。
  阅读StdCtrls文件,TCustomLabel、TCustomCheckBox、TRadioButton、TCustomMemo、TCustomStaticText均具有Alignment属性。
  其中TCustomCheckBox、TRadioButton的Alignment属性是TLeftRight类型;其它类的Alignment属性是TAlignment类型,我们将选择TAlignment类型。
3.1.1.添加属性定义
  好了,我们开始增加这个属性,定义如下:
  private
    FAlignment: TAlignment;
    procedure SetAlignment(Value: TAlignment);
  protected
    property Alignment: TAlignment

      read FAlignment write SetAlignment default taLeftJustify;
3.1.2.实现Alignment的方式
  实现Alignment的方式有两种:
  第一种是利用Windows API函数SetWindowLong设置GWL_STYLE;
  第二种是利用Windows API函数CreateWindowEx设置Style。
  TCustomMemo和TCustomStaticText使用的是后者,我们仿照这两个类,也选择后者作为我们的实现方式。
3.1.3.书写SetAlignment代码
  现在添加SetAlignment代码:
procedure TGcxCustomEdit.SetAlignment(const Value: TAlignment);
begin
  if FAlignment <> Value then
  begin
    FAlignment := Value;
    RecreateWnd;
  end;
end;
  在TCustomMemo、TCustomStaticText中,代码都是这么书写的。由于TCustomLabel从TGraphicControl继承,与前两者的设计理念不一样,所以我们不参考它的代码。
  现在我们看一看SetAlignment的实现过程,在SetAlignment中调用了TWinControl的RecreateWnd方法。
3.1.4.RecreateWnd方法的说明
  RecreateWnd源于TWinControl,代码如下:
procedure TWinControl.RecreateWnd;
begin
  if FHandle <> 0 then Perform(CM_RECREATEWND, 0, 0);
end;
  可以看到,它向自身Self发送了一个CM_RECREATEWND消息,引发了CMRecreateWnd函数。
3.1.5.CMRecreateWnd函数
  CMRecreateWnd由CM_RECREATEWND消息引发,它的定义如下:
  TWinControl = class(TControl)
  private
    procedure CMRecreateWnd(var Message: TMessage); message CM_RECREATEWND;
  实现代码如下:
procedure TWinControl.CMRecreateWnd(var Message: TMessage);
var
  WasFocused: Boolean;
begin
  WasFocused := Focused;
  DestroyHandle;
  UpdateControlState;
  if WasFocused and (FHandle <> 0) then Windows.SetFocus(FHandle);
end;
  在CMRecreateWnd函数中,执行了一系列的代码,其中包含保护的DestroyHandle方法以及公共的UpdateControlState方法。
  DestroyHandle用于销毁句柄;UpdateControlState用于更新控件状态。
  现在我们尝试分析一下这两组代码的书写思路,其它代码我们暂时不做分析。
  如果你不想关注细节,只想知道如何做,那么,请你跳过下面这段,直接去阅读“3.1.8.覆盖CreateParams方法”。
3.1.6.销毁句柄的工作
  这个工作从TWinControl的DestroyHandle方法开始,至Windows的DestroyWindow结束。我们一步一步分析。
3.1.6.1.DestroyHandle方法
  这是一个位于保护(protected)的代码。
procedure TWinControl.DestroyHandle;
var
  I: Integer;
begin
  if FHandle <> 0 then
  begin
    if FWinControls <> nil then
      for I := 0 to FWinControls.Count - 1 do
        TWinControl(FWinControls[I]).DestroyHandle;
    DestroyWnd;
  end;
end;
  这里遍历自身的子对象,并调用子对象的DestroyHandle方法进行句柄销毁,最后调用DestroyWnd方法销毁自己的句柄。
3.1.6.2.DestroyWnd方法
  这是一个保护的虚拟方法,可在继承类中覆盖。
  protected
    procedure DestroyWnd; virtual;
  代码如下:
procedure TWinControl.DestroyWnd;
var
  Len: Integer;
begin
  Len := GetTextLen;
  if Len < 1 then FText := StrNew('') else
  begin
    FText := StrAlloc(Len + 1);
    GetTextBuf(FText, StrBufSize(FText));
  end;
  FreeDeviceContexts;
  DestroyWindowHandle;
end;
  忽略大部分代码之后,我们看到了DestroyWindowHandle。
3.1.6.3.DestroyWindowHandle方法
  DestroyWindowHandle依旧是一个被保护的虚拟方法。
  protected
    procedure DestroyWindowHandle; virtual;
  代码如下:
procedure TWinControl.DestroyWindowHandle;
begin
  Include(FControlState, csDestroyingHandle);
  try
    if not Windows.DestroyWindow(FHandle) then
      RaiseLastOSError;
  finally
    Exclude(FControlState, csDestroyingHandle);
  end;
  FHandle := 0;
end;
  看到这里,终于到达了一个我们可以认为的终点——
Windows.DestroyWindow(FHandle)
  到达Windows API的时候,剩下的就是操作系统的工作了,那不是我们现在能够关心的问题。
好了,我们掉头转身,去看下一个话题——重新建立句柄并更新状态。
3.1.7.更新状态的工作
3.1.7.1.UpdateControlState方法
  UpdateControlState是一个公共方法,它的定义如下:
  public
    procedure UpdateControlState;
  代码实现如下:
procedure TWinControl.UpdateControlState;
var
  Control: TWinControl;
begin
  Control := Self;
  while Control.Parent <> nil do
  begin
    Control := Control.Parent;
    if not Control.Showing then Exit;
  end;
  if (Control is TCustomForm) or (Control.FParentWindow <> 0) then UpdateShowing;
end;
  这个代码依靠while循环,一直找到它的最上端“窗口”(很抱歉,此“窗口”非彼“窗口”,这个概念定义应该是C语言的,不是VB或者Delphi里面所谓的Form,如果用Delphi的思想描述,我想应该叫“容器”吧。在VC的定义里面,到处都是“窗口”,没有什么可以称之为“容器”;而在Delphi的概念里面,很多东西都有“容器”属性,却仅仅将Form称之为“窗口”)。
  这段代码的终点就是调用了UpdateShowing。
3.1.7.2.UpdateShowing方法
  这是一段私有代码,它也很复杂。
  TWinControl = class(TControl)
  private
    procedure UpdateShowing
;
  代码如下:
procedure TWinControl.UpdateShowing;
var
  ShowControl: Boolean;
  I: Integer;
begin
  ……
  if ShowControl then
  begin
    if FHandle = 0 then CreateHandle;
    if FWinControls <> nil then
      for I := 0 to FWinControls.Count - 1 do
        TWinControl(FWinControls[I]).UpdateShowing;
  end;
  if FHandle <> 0 then
    if FShowing <> ShowControl then
    begin
      FShowing := ShowControl;
      try
        Perform(CM_SHOWINGCHANGED, 0, 0);
      except
        ……
      end;
    end;
end;
  代码有些长,我们不要被迷惑,关键的地方,它执行了一句:
    if FHandle = 0 then CreateHandle;
  然后他遍历自身的子对象,并调用子对象的UpdateShowing。
  最终它发送了一个CM_SHOWINGCHANGED消息给自己,引发了CMShowingChanged方法。
    procedure CMShowingChanged(var Message: TMessage); message CM_SHOWINGCHANGED;
3.1.7.3.CreateHandle方法
  又是一个被保护的虚拟方法。
  protected
    procedure CreateHandle; virtual;
代码很长,我们贴一段出来:
procedure TWinControl.CreateHandle;
var
  I: Integer;
begin
  if FHandle = 0 then
  begin
    CreateWnd;
    ……
  end;
end;
  好了,我们在这里看到了CreateWnd,这个方法就是我们要找的支点。通过它,只要我们有一根足够长的撬杆,我们可以把一切掌握在自己手中。
3.1.7.4.CreateWnd方法
  protected
    procedure CreateWnd; virtual;
  这段代码依然很长,我们就贴前几行:
procedure TWinControl.CreateWnd;
var
  Params: TCreateParams;
  TempClass: TWndClass;
  ClassRegistered: Boolean;
begin
  CreateParams(Params);
  ……
    CreateWindowHandle(Params);
  ……
  在这里可以看到,CreateWnd所作的第一件事,就是调用了CreateParams方法建立基本的外观风格。随后,它调用了CreateWindowHandle方法创建窗口句柄(这个“窗口”还是VC里面的那个定义)。
  CreateWnd和CreateParams都是被保护的虚拟方法,可以在继承中覆盖。
3.1.7.5.CreateParams方法
  protected
    procedure CreateParams(var Params: TCreateParams); virtual;
  这是一个被TWinControl保护的虚拟方法,代码太长,就不贴了。
  这个方法的主要工作就是设置该TWinControl的外观风格等属性,以参数Params返回。
  在TWinControl中,CreateParams方法仅由CreateWnd调用。所以,由CreateParams返回的Params属性参数将供CreateWnd方法使用。
  因为它是虚拟的,所以可以在继承中覆盖该方法,以实现不同的外观风格。
3.1.7.6.CreateWindowHandle方法
  在CreateWindowHandle方法中,就是利用被传入的Params参数,通过Windows API函数CreateWindowEx建立一个窗口句柄。
  同“3.1.5销毁句柄的工作”一样,到达这里,也就代表一个相对的终点,除非你想深入探讨Windows核心。
  protected
    procedure CreateWindowHandle(const Params: TCreateParams); virtual;

 

procedure TWinControl.CreateWindowHandle(const Params: TCreateParams);
begin
  with Params do
    FHandle := CreateWindowEx(ExStyle, WinClassName, Caption, Style,
      X, Y, Width, Height, WndParent, 0, WindowClass.hInstance, Param);
end;
  从CreateHandle到CreateWindowHandle,一路走来,都是被保护的虚拟方法,我们可以选择任意处进行覆盖,创建出自己独特风格的组件。
3.1.8.覆盖CreateParams方法
  参考TCustomMemo和TCustomStaticText的设计思想,我们书写如下代码:
procedure TGcxCustomEdit.CreateParams(var Params: TCreateParams);
const
  Alignments: array[Boolean, TAlignment] of DWORD =
    ((ES_LEFT, ES_RIGHT, ES_CENTER),(ES_RIGHT, ES_LEFT, ES_CENTER));
begin
  inherited CreateParams(Params);
  with Params do
  begin
    ExStyle := Exstyle and not WS_EX_Transparent;
    Style := Style and not WS_BORDER or
      Alignments[UseRightToLeftAlignment, FAlignment];
  end;
end;
  如果我们不考虑中东(MiddleEast)代码,可以不考虑UseRightToLeftAlignment,但增加对它的支持并不是坏处。
3.1.9.修改构造函数Create
  在TGcxCustomEdit中,我们选择默认左对齐,所以代码如下:
constructor TGcxCustomEdit.Create(AOwner: TComponent);
begin
  inherited;
  ……
  FAlignment := taLeftJustify;
  ……
  在TGcxCustomIntEdit中,我们选择默认右对齐,所以代码如下:
constructor TGcxCustomIntEdit.Create(AOwner: TComponent);
begin
  inherited;
  ……
  FAlignment := taRightJustify;
  ……
  好了,我们看一下TGcxIntEdit现在的效果:
 

 

  它现在是右对齐的。满意了吗?至少我还没有。路漫漫其修远兮,吾将上下而求索。
3.2.边界属性Margin
  上一节我们为T[x]CustomEdit提供了对齐属性Alignment,但是它看起来并不是很舒服。
  为什么?我的看法是它太靠边了。用过Word的人都知道,Word是有页边距设置的,而且中国的书画中也有一个词——留白,这就是我想说的。
  想法有了,怎么实现呢?
3.2.1.添加属性定义
  TGcxCustomEdit = class(TTntCustomEdit)
  private
    FMargin: Integer;
  protected
    property Margin: Integer read FMargin write SetMargin default 5;
3.2.2.实现Margin的方式
  利用Windows API函数SendMessage可以实现Margin的设置。
  想一想谁有类似的实现? CnPack VCL组件库的TCnButtonEdit;还有Raize Components的TRzButtonEdit。
  为什么这么说呢?看起来他们并没有边界属性Margin,但是你只要认真看一下它们的结构,就能想明白。
  这两个组件都有Alignment属性,右侧也都有一个或者两个按钮,而且按钮是覆盖在它们的空间中。而当它们选择右对齐的时候,它们Text属性的文字将在按钮的左侧显示。
  TCnButtonEdit看起来很像TRzButtonEdit,但它的实现方式却不一样。
  很不幸,TCnButtonEdit的设计有缺陷,当ButtonVisible设置为False时,Text显示出现了偏差。因此我们选择TRzButtonEdit的实现方式。
  阅读RzBtnEdt.pas文件,TRzButtonEdit利用Windows API函数SendMessage,并传递EM_SETMARGINS消息给组件句柄实现该功能。
3.2.3.书写SetMargin代码
  现在添加SetMargin代码:
procedure TGcxCustomEdit.SetMargin(const Value: Integer);
begin
  if FMargin <> Value then
  begin
    FMargin := Value;
    RecreateWnd;
  end;
end;
  这段代码与SetAlignment的实现方式很相近,我没有在这里判断Value是否小于0,不过这不是什么问题,因为小于0的值对系统来说没有什么意义,与0的结果是一样的。
  如果你喜欢,可以自行添加代码对它修正。
3.2.4.覆盖CreateWnd方法
  protected
    procedure CreateWnd; override;

 

procedure TGcxCustomEdit.CreateWnd;
begin
  inherited;
  SetEditRect;
end;
  这段代码没有什么值得说明的,就是调用了一个自定义的方法SetEditRect,这么做是为了代码重用的目的,便于以后覆盖。
3.2.5.建立SetEditRect方法
  protected
    procedure SetEditRect; virtual;

 

procedure TGcxCustomEdit.SetEditRect;
begin
  if (Handle = 0) or (not Assigned(Self.Parent)) then
    Exit;
  SendMessage(Handle, EM_SetMargins, EC_LeftMargin, MakeLong(FMargin, 0));
  SendMessage(Handle, EM_SetMargins, EC_RightMargin, MakeLong(0, FMargin));
end;
  在这里调用了两次SendMessage函数,实际上用一个调用也可以,如:
  SendMessage(Handle, EM_SetMargins,
    EC_LeftMargin or EC_RightMargin,
    MakeLong(FMargin, FMargin));
  为什么写两行?实际上就是为了自己阅读方便。在这里,左右两侧的边界Margin是相同的,如果按照后面的写法,当我们想修改成左右边界不同时,对新手来说会有点迷茫。
  在这里提一下最后一个参数,MakeLong函数就是将两个Word值合并成一个LongWord,高16位代表右侧边界,低16位代表左侧边界。
3.2.6.修改构造函数Create
constructor TGcxCustomEdit.Create(AOwner: TComponent);
begin
  inherited;
  ……
  FMargin := 5;
end;
  好了,我们看一下效果:
 

  怎么样?看起来比刚才顺眼了一些吧。
3.3.注意事项
  在构造函数Create中只可以对FAlignment、FMargin这样的私有变量赋值,不可以使用Alignment、Margin这样的属性,因为后者会调用Set方法,从而引发RecreateWnd。
  你需要了解,在构造函数Create中,它仅仅是构造了对象,却没有设置Parent和Name,当然,更没有句柄。
  组件构造成对象时,类成员函数的执行次序如下:
Create  –>  SetParent  ->  RecreateWnd  ->  SetName
  而RecreateWnd引发成员函数的执行次序如下:
RecreateWnd  ->  CreateWnd  ->  CreateParams 
  所以,不要把需要窗口句柄的代码放在Create中,而是由RecreateWnd去引发。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值