转载请指明出处来源。
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去引发。